mirror of
https://github.com/Rafostar/clapper.git
synced 2025-08-30 07:42:23 +02:00
Move "clapper_src" dir to "src"
The "clapper_src" directory name was unusual. This was done to make it work as a widget for other apps. Now that this functionality got removed it can be named simply "src" as recommended by guidelines.
This commit is contained in:
66
src/app.js
Normal file
66
src/app.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const { Gio, GObject } = imports.gi;
|
||||
const { AppBase } = imports.src.appBase;
|
||||
const { HeaderBar } = imports.src.headerbar;
|
||||
const { Widget } = imports.src.widget;
|
||||
const Debug = imports.src.debug;
|
||||
|
||||
const { debug } = Debug;
|
||||
|
||||
var App = GObject.registerClass(
|
||||
class ClapperApp extends AppBase
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init();
|
||||
|
||||
this.set_flags(
|
||||
this.get_flags()
|
||||
| Gio.ApplicationFlags.HANDLES_OPEN
|
||||
);
|
||||
}
|
||||
|
||||
vfunc_startup()
|
||||
{
|
||||
super.vfunc_startup();
|
||||
|
||||
this.active_window.isClapperApp = true;
|
||||
this.active_window.add_css_class('nobackground');
|
||||
|
||||
const clapperWidget = new Widget();
|
||||
this.active_window.set_child(clapperWidget);
|
||||
|
||||
const headerBar = new HeaderBar(this.active_window);
|
||||
this.active_window.set_titlebar(headerBar);
|
||||
|
||||
const size = clapperWidget.windowSize;
|
||||
this.active_window.set_default_size(size[0], size[1]);
|
||||
debug(`restored window size: ${size[0]}x${size[1]}`);
|
||||
}
|
||||
|
||||
vfunc_open(files, hint)
|
||||
{
|
||||
super.vfunc_open(files, hint);
|
||||
|
||||
const { player } = this.active_window.get_child();
|
||||
|
||||
if(!this.doneFirstActivate)
|
||||
player._preparePlaylist(files);
|
||||
else
|
||||
player.set_playlist(files);
|
||||
|
||||
this.activate();
|
||||
}
|
||||
|
||||
_onWindowShow(window)
|
||||
{
|
||||
super._onWindowShow(window);
|
||||
|
||||
const { player } = this.active_window.get_child();
|
||||
const success = player.playlistWidget.nextTrack();
|
||||
|
||||
if(!success)
|
||||
debug('playlist is empty');
|
||||
|
||||
player.widget.grab_focus();
|
||||
}
|
||||
});
|
105
src/appBase.js
Normal file
105
src/appBase.js
Normal file
@@ -0,0 +1,105 @@
|
||||
const { Gio, GLib, GObject, Gtk } = imports.gi;
|
||||
const Debug = imports.src.debug;
|
||||
const Menu = imports.src.menu;
|
||||
const Misc = imports.src.misc;
|
||||
|
||||
const { debug } = Debug;
|
||||
const { settings } = Misc;
|
||||
|
||||
var AppBase = GObject.registerClass(
|
||||
class ClapperAppBase extends Gtk.Application
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init({
|
||||
application_id: Misc.appId,
|
||||
});
|
||||
|
||||
this.doneFirstActivate = false;
|
||||
}
|
||||
|
||||
vfunc_startup()
|
||||
{
|
||||
super.vfunc_startup();
|
||||
|
||||
const window = new Gtk.ApplicationWindow({
|
||||
application: this,
|
||||
title: Misc.appName,
|
||||
});
|
||||
|
||||
if(!settings.get_boolean('render-shadows'))
|
||||
window.add_css_class('gpufriendly');
|
||||
|
||||
if(
|
||||
settings.get_boolean('dark-theme')
|
||||
&& settings.get_boolean('brighter-sliders')
|
||||
)
|
||||
window.add_css_class('brightscale');
|
||||
|
||||
for(let action in Menu.actions) {
|
||||
const simpleAction = new Gio.SimpleAction({
|
||||
name: action
|
||||
});
|
||||
simpleAction.connect(
|
||||
'activate', () => Menu.actions[action](this.active_window)
|
||||
);
|
||||
this.add_action(simpleAction);
|
||||
}
|
||||
}
|
||||
|
||||
vfunc_activate()
|
||||
{
|
||||
super.vfunc_activate();
|
||||
|
||||
if(!this.doneFirstActivate)
|
||||
this._onFirstActivate();
|
||||
|
||||
this.active_window.present_with_time(
|
||||
Math.floor(GLib.get_monotonic_time() / 1000)
|
||||
);
|
||||
}
|
||||
|
||||
run(arr)
|
||||
{
|
||||
super.run(arr || []);
|
||||
}
|
||||
|
||||
_onFirstActivate()
|
||||
{
|
||||
const gtkSettings = Gtk.Settings.get_default();
|
||||
settings.bind(
|
||||
'dark-theme', gtkSettings,
|
||||
'gtk-application-prefer-dark-theme',
|
||||
Gio.SettingsBindFlags.GET
|
||||
);
|
||||
this._onThemeChanged(gtkSettings);
|
||||
gtkSettings.connect('notify::gtk-theme-name', this._onThemeChanged.bind(this));
|
||||
|
||||
this.windowShowSignal = this.active_window.connect(
|
||||
'show', this._onWindowShow.bind(this)
|
||||
);
|
||||
this.doneFirstActivate = true;
|
||||
}
|
||||
|
||||
_onWindowShow(window)
|
||||
{
|
||||
window.disconnect(this.windowShowSignal);
|
||||
this.windowShowSignal = null;
|
||||
}
|
||||
|
||||
_onThemeChanged(gtkSettings)
|
||||
{
|
||||
const theme = gtkSettings.gtk_theme_name;
|
||||
debug(`user selected theme: ${theme}`);
|
||||
|
||||
if(!theme.endsWith('-dark'))
|
||||
return;
|
||||
|
||||
/* We need to request a default theme with optional dark variant
|
||||
to make the "gtk_application_prefer_dark_theme" setting work */
|
||||
const parsedTheme = theme.substring(0, theme.lastIndexOf('-'));
|
||||
|
||||
gtkSettings.gtk_theme_name = parsedTheme;
|
||||
debug(`set theme: ${parsedTheme}`);
|
||||
}
|
||||
});
|
21
src/appRemote.js
Normal file
21
src/appRemote.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const { GObject } = imports.gi;
|
||||
const { AppBase } = imports.src.appBase;
|
||||
const { HeaderBarBase } = imports.src.headerbarBase;
|
||||
const { WidgetRemote } = imports.src.widgetRemote;
|
||||
|
||||
var AppRemote = GObject.registerClass(
|
||||
class ClapperAppRemote extends AppBase
|
||||
{
|
||||
vfunc_startup()
|
||||
{
|
||||
super.vfunc_startup();
|
||||
|
||||
const clapperWidget = new WidgetRemote();
|
||||
this.active_window.set_child(clapperWidget);
|
||||
|
||||
const headerBar = new HeaderBarBase(this.active_window);
|
||||
this.active_window.set_titlebar(headerBar);
|
||||
|
||||
this.active_window.maximize();
|
||||
}
|
||||
});
|
233
src/buttons.js
Normal file
233
src/buttons.js
Normal file
@@ -0,0 +1,233 @@
|
||||
const { GObject, Gtk } = imports.gi;
|
||||
|
||||
var CustomButton = GObject.registerClass(
|
||||
class ClapperCustomButton extends Gtk.Button
|
||||
{
|
||||
_init(opts)
|
||||
{
|
||||
opts = opts || {};
|
||||
|
||||
const defaults = {
|
||||
margin_top: 4,
|
||||
margin_bottom: 4,
|
||||
margin_start: 2,
|
||||
margin_end: 2,
|
||||
can_focus: false,
|
||||
};
|
||||
Object.assign(opts, defaults);
|
||||
|
||||
super._init(opts);
|
||||
|
||||
this.floatUnaffected = false;
|
||||
this.wantedVisible = true;
|
||||
this.isFullscreen = false;
|
||||
this.isFloating = false;
|
||||
|
||||
this.add_css_class('flat');
|
||||
}
|
||||
|
||||
setFullscreenMode(isFullscreen)
|
||||
{
|
||||
if(this.isFullscreen === isFullscreen)
|
||||
return;
|
||||
|
||||
this.margin_top = (isFullscreen) ? 5 : 4;
|
||||
this.margin_start = (isFullscreen) ? 3 : 2;
|
||||
this.margin_end = (isFullscreen) ? 3 : 2;
|
||||
this.can_focus = isFullscreen;
|
||||
|
||||
/* Redraw icon after style class change */
|
||||
if(this.icon_name)
|
||||
this.set_icon_name(this.icon_name);
|
||||
|
||||
this.isFullscreen = isFullscreen;
|
||||
}
|
||||
|
||||
setFloatingMode(isFloating)
|
||||
{
|
||||
if(this.isFloating === isFloating)
|
||||
return;
|
||||
|
||||
this.isFloating = isFloating;
|
||||
|
||||
if(this.floatUnaffected)
|
||||
return;
|
||||
|
||||
if(isFloating)
|
||||
super.set_visible(false);
|
||||
else
|
||||
super.set_visible(this.wantedVisible);
|
||||
}
|
||||
|
||||
set_visible(isVisible)
|
||||
{
|
||||
this.wantedVisible = isVisible;
|
||||
|
||||
if(this.isFloating && !this.floatUnaffected)
|
||||
super.set_visible(false);
|
||||
else
|
||||
super.set_visible(isVisible);
|
||||
}
|
||||
|
||||
vfunc_clicked()
|
||||
{
|
||||
if(!this.isFullscreen)
|
||||
return;
|
||||
|
||||
const { player } = this.get_ancestor(Gtk.Grid);
|
||||
player._setHideControlsTimeout();
|
||||
}
|
||||
});
|
||||
|
||||
var IconButton = GObject.registerClass(
|
||||
class ClapperIconButton extends CustomButton
|
||||
{
|
||||
_init(icon)
|
||||
{
|
||||
super._init({
|
||||
icon_name: icon,
|
||||
});
|
||||
this.floatUnaffected = true;
|
||||
}
|
||||
});
|
||||
|
||||
var IconToggleButton = GObject.registerClass(
|
||||
class ClapperIconToggleButton extends IconButton
|
||||
{
|
||||
_init(primaryIcon, secondaryIcon)
|
||||
{
|
||||
super._init(primaryIcon);
|
||||
|
||||
this.primaryIcon = primaryIcon;
|
||||
this.secondaryIcon = secondaryIcon;
|
||||
}
|
||||
|
||||
setPrimaryIcon()
|
||||
{
|
||||
this.icon_name = this.primaryIcon;
|
||||
}
|
||||
|
||||
setSecondaryIcon()
|
||||
{
|
||||
this.icon_name = this.secondaryIcon;
|
||||
}
|
||||
});
|
||||
|
||||
var PopoverButtonBase = GObject.registerClass(
|
||||
class ClapperPopoverButtonBase extends CustomButton
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init();
|
||||
|
||||
this.popover = new Gtk.Popover({
|
||||
position: Gtk.PositionType.TOP,
|
||||
});
|
||||
this.popoverBox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
});
|
||||
|
||||
this.popover.set_child(this.popoverBox);
|
||||
this.popover.set_offset(0, -this.margin_top);
|
||||
|
||||
if(this.isFullscreen)
|
||||
this.popover.add_css_class('osd');
|
||||
|
||||
this.popover.connect('closed', this._onClosed.bind(this));
|
||||
this.popover.set_parent(this);
|
||||
}
|
||||
|
||||
setFullscreenMode(isFullscreen)
|
||||
{
|
||||
if(this.isFullscreen === isFullscreen)
|
||||
return;
|
||||
|
||||
super.setFullscreenMode(isFullscreen);
|
||||
|
||||
this.popover.set_offset(0, -this.margin_top);
|
||||
|
||||
const cssClass = 'osd';
|
||||
if(isFullscreen === this.popover.has_css_class(cssClass))
|
||||
return;
|
||||
|
||||
const action = (isFullscreen) ? 'add' : 'remove';
|
||||
this.popover[action + '_css_class'](cssClass);
|
||||
}
|
||||
|
||||
vfunc_clicked()
|
||||
{
|
||||
super.vfunc_clicked();
|
||||
|
||||
this.set_state_flags(Gtk.StateFlags.CHECKED, false);
|
||||
this.popover.popup();
|
||||
}
|
||||
|
||||
_onClosed()
|
||||
{
|
||||
const { player } = this.get_ancestor(Gtk.Grid);
|
||||
player.widget.grab_focus();
|
||||
|
||||
this.unset_state_flags(Gtk.StateFlags.CHECKED);
|
||||
}
|
||||
|
||||
_onCloseRequest()
|
||||
{
|
||||
this.popover.unparent();
|
||||
}
|
||||
});
|
||||
|
||||
var IconPopoverButton = GObject.registerClass(
|
||||
class ClapperIconPopoverButton extends PopoverButtonBase
|
||||
{
|
||||
_init(icon)
|
||||
{
|
||||
super._init();
|
||||
|
||||
this.icon_name = icon;
|
||||
}
|
||||
});
|
||||
|
||||
var LabelPopoverButton = GObject.registerClass(
|
||||
class ClapperLabelPopoverButton extends PopoverButtonBase
|
||||
{
|
||||
_init(text)
|
||||
{
|
||||
super._init();
|
||||
|
||||
this.customLabel = new Gtk.Label({
|
||||
label: text,
|
||||
single_line_mode: true,
|
||||
});
|
||||
this.customLabel.add_css_class('labelbutton');
|
||||
this.set_child(this.customLabel);
|
||||
}
|
||||
|
||||
set_label(text)
|
||||
{
|
||||
this.customLabel.set_text(text);
|
||||
}
|
||||
});
|
||||
|
||||
var ElapsedPopoverButton = GObject.registerClass(
|
||||
class ClapperElapsedPopoverButton extends LabelPopoverButton
|
||||
{
|
||||
_init(text)
|
||||
{
|
||||
super._init(text);
|
||||
|
||||
this.scrolledWindow = new Gtk.ScrolledWindow({
|
||||
max_content_height: 150,
|
||||
min_content_width: 250,
|
||||
propagate_natural_height: true,
|
||||
});
|
||||
this.popoverBox.append(this.scrolledWindow);
|
||||
}
|
||||
|
||||
setFullscreenMode(isFullscreen)
|
||||
{
|
||||
super.setFullscreenMode(isFullscreen);
|
||||
|
||||
this.scrolledWindow.max_content_height = (isFullscreen)
|
||||
? 190 : 150;
|
||||
}
|
||||
});
|
676
src/controls.js
vendored
Normal file
676
src/controls.js
vendored
Normal file
@@ -0,0 +1,676 @@
|
||||
const { GLib, GObject, Gdk, Gtk } = imports.gi;
|
||||
const Buttons = imports.src.buttons;
|
||||
const Debug = imports.src.debug;
|
||||
const Misc = imports.src.misc;
|
||||
const Revealers = imports.src.revealers;
|
||||
|
||||
const CONTROLS_MARGIN = 2;
|
||||
const CONTROLS_SPACING = 0;
|
||||
|
||||
const { debug } = Debug;
|
||||
const { settings } = Misc;
|
||||
|
||||
var Controls = GObject.registerClass(
|
||||
class ClapperControls extends Gtk.Box
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
margin_start: CONTROLS_MARGIN,
|
||||
margin_end: CONTROLS_MARGIN,
|
||||
spacing: CONTROLS_SPACING,
|
||||
valign: Gtk.Align.END,
|
||||
can_focus: false,
|
||||
});
|
||||
|
||||
this.currentVolume = 0;
|
||||
this.currentPosition = 0;
|
||||
this.currentDuration = 0;
|
||||
this.isPositionDragging = false;
|
||||
this.isMobile = false;
|
||||
|
||||
this.showHours = false;
|
||||
this.durationFormatted = '00:00';
|
||||
this.buttonsArr = [];
|
||||
this.revealersArr = [];
|
||||
this.chapters = null;
|
||||
|
||||
this.chapterShowId = null;
|
||||
this.chapterHideId = null;
|
||||
|
||||
this._addTogglePlayButton();
|
||||
this._addElapsedButton();
|
||||
this._addPositionScale();
|
||||
|
||||
const revealTracksButton = new Buttons.IconToggleButton(
|
||||
'go-previous-symbolic',
|
||||
'go-next-symbolic'
|
||||
);
|
||||
revealTracksButton.floatUnaffected = false;
|
||||
revealTracksButton.add_css_class('narrowbutton');
|
||||
this.buttonsArr.push(revealTracksButton);
|
||||
const tracksRevealer = new Revealers.ButtonsRevealer(
|
||||
'SLIDE_LEFT', revealTracksButton
|
||||
);
|
||||
this.visualizationsButton = this.addIconPopoverButton(
|
||||
'display-projector-symbolic',
|
||||
tracksRevealer
|
||||
);
|
||||
this.visualizationsButton.set_visible(false);
|
||||
this.videoTracksButton = this.addIconPopoverButton(
|
||||
'emblem-videos-symbolic',
|
||||
tracksRevealer
|
||||
);
|
||||
this.videoTracksButton.set_visible(false);
|
||||
this.audioTracksButton = this.addIconPopoverButton(
|
||||
'emblem-music-symbolic',
|
||||
tracksRevealer
|
||||
);
|
||||
this.audioTracksButton.set_visible(false);
|
||||
this.subtitleTracksButton = this.addIconPopoverButton(
|
||||
'media-view-subtitles-symbolic',
|
||||
tracksRevealer
|
||||
);
|
||||
this.subtitleTracksButton.set_visible(false);
|
||||
|
||||
this.revealTracksRevealer = new Revealers.ButtonsRevealer('SLIDE_LEFT');
|
||||
this.revealTracksRevealer.append(revealTracksButton);
|
||||
this.revealTracksRevealer.set_visible(false);
|
||||
this.append(this.revealTracksRevealer);
|
||||
|
||||
tracksRevealer.set_reveal_child(true);
|
||||
this.revealersArr.push(tracksRevealer);
|
||||
this.append(tracksRevealer);
|
||||
|
||||
this._addVolumeButton();
|
||||
this.unfullscreenButton = this.addButton(
|
||||
'view-restore-symbolic'
|
||||
);
|
||||
this.unfullscreenButton.connect('clicked', this._onUnfullscreenClicked.bind(this));
|
||||
this.unfullscreenButton.set_visible(false);
|
||||
|
||||
this.unfloatButton = this.addButton(
|
||||
'preferences-desktop-remote-desktop-symbolic'
|
||||
);
|
||||
this.unfloatButton.connect('clicked', this._onUnfloatClicked.bind(this));
|
||||
this.unfloatButton.set_visible(false);
|
||||
|
||||
const keyController = new Gtk.EventControllerKey();
|
||||
keyController.connect('key-pressed', this._onControlsKeyPressed.bind(this));
|
||||
keyController.connect('key-released', this._onControlsKeyReleased.bind(this));
|
||||
this.add_controller(keyController);
|
||||
|
||||
this.add_css_class('playercontrols');
|
||||
this.realizeSignal = this.connect('realize', this._onRealize.bind(this));
|
||||
}
|
||||
|
||||
setFullscreenMode(isFullscreen)
|
||||
{
|
||||
/* Allow recheck on next resize */
|
||||
this.isMobile = null;
|
||||
|
||||
for(let button of this.buttonsArr)
|
||||
button.setFullscreenMode(isFullscreen);
|
||||
|
||||
this.unfullscreenButton.set_visible(isFullscreen);
|
||||
this.set_can_focus(isFullscreen);
|
||||
}
|
||||
|
||||
setFloatingMode(isFloating)
|
||||
{
|
||||
this.isMobile = null;
|
||||
|
||||
for(let button of this.buttonsArr)
|
||||
button.setFloatingMode(isFloating);
|
||||
}
|
||||
|
||||
setLiveMode(isLive, isSeekable)
|
||||
{
|
||||
if(isLive)
|
||||
this.elapsedButton.set_label('LIVE');
|
||||
|
||||
this.positionScale.visible = isSeekable;
|
||||
}
|
||||
|
||||
updateElapsedLabel(value)
|
||||
{
|
||||
value = value || 0;
|
||||
|
||||
const elapsed = Misc.getFormattedTime(value, this.showHours)
|
||||
+ '/' + this.durationFormatted;
|
||||
|
||||
this.elapsedButton.set_label(elapsed);
|
||||
}
|
||||
|
||||
addButton(buttonIcon, revealer)
|
||||
{
|
||||
const button = (buttonIcon instanceof Gtk.Button)
|
||||
? buttonIcon
|
||||
: new Buttons.IconButton(buttonIcon);
|
||||
|
||||
if(!revealer)
|
||||
this.append(button);
|
||||
else
|
||||
revealer.append(button);
|
||||
|
||||
this.buttonsArr.push(button);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
addIconPopoverButton(iconName, revealer)
|
||||
{
|
||||
const button = new Buttons.IconPopoverButton(iconName);
|
||||
|
||||
return this.addButton(button, revealer);
|
||||
}
|
||||
|
||||
addLabelPopoverButton(text, revealer)
|
||||
{
|
||||
text = text || '';
|
||||
const button = new Buttons.LabelPopoverButton(text);
|
||||
|
||||
return this.addButton(button, revealer);
|
||||
}
|
||||
|
||||
addElapsedPopoverButton(text, revealer)
|
||||
{
|
||||
text = text || '';
|
||||
const button = new Buttons.ElapsedPopoverButton(text);
|
||||
|
||||
return this.addButton(button, revealer);
|
||||
}
|
||||
|
||||
addCheckButtons(box, array, activeId)
|
||||
{
|
||||
let group = null;
|
||||
let child = box.get_first_child();
|
||||
let i = 0;
|
||||
|
||||
while(child || i < array.length) {
|
||||
if(i >= array.length) {
|
||||
child.hide();
|
||||
debug(`hiding unused ${child.type} checkButton nr: ${i}`);
|
||||
i++;
|
||||
child = child.get_next_sibling();
|
||||
continue;
|
||||
}
|
||||
|
||||
const el = array[i];
|
||||
let checkButton;
|
||||
|
||||
if(child) {
|
||||
checkButton = child;
|
||||
debug(`reusing ${el.type} checkButton nr: ${i}`);
|
||||
}
|
||||
else {
|
||||
debug(`creating new ${el.type} checkButton nr: ${i}`);
|
||||
checkButton = new Gtk.CheckButton({
|
||||
group: group,
|
||||
});
|
||||
checkButton.connect(
|
||||
'toggled',
|
||||
this._onCheckButtonToggled.bind(this)
|
||||
);
|
||||
box.append(checkButton);
|
||||
}
|
||||
|
||||
checkButton.label = el.label;
|
||||
debug(`checkButton label: ${checkButton.label}`);
|
||||
checkButton.type = el.type;
|
||||
debug(`checkButton type: ${checkButton.type}`);
|
||||
checkButton.activeId = el.activeId;
|
||||
debug(`checkButton id: ${checkButton.activeId}`);
|
||||
|
||||
if(checkButton.activeId === activeId) {
|
||||
checkButton.set_active(true);
|
||||
debug(`activated ${el.type} checkButton nr: ${i}`);
|
||||
}
|
||||
if(!group)
|
||||
group = checkButton;
|
||||
|
||||
i++;
|
||||
if(child)
|
||||
child = child.get_next_sibling();
|
||||
}
|
||||
}
|
||||
|
||||
_handleTrackChange(checkButton)
|
||||
{
|
||||
const clapperWidget = this.get_ancestor(Gtk.Grid);
|
||||
|
||||
/* Reenabling audio is slow (as expected),
|
||||
* so it is better to toggle mute instead */
|
||||
if(checkButton.type === 'audio') {
|
||||
if(checkButton.activeId < 0)
|
||||
return clapperWidget.player.set_mute(true);
|
||||
|
||||
if(clapperWidget.player.get_mute())
|
||||
clapperWidget.player.set_mute(false);
|
||||
|
||||
return clapperWidget.player[
|
||||
`set_${checkButton.type}_track`
|
||||
](checkButton.activeId);
|
||||
}
|
||||
|
||||
if(checkButton.activeId < 0) {
|
||||
if(checkButton.type === 'video')
|
||||
clapperWidget.player.draw_black(true);
|
||||
|
||||
return clapperWidget.player[
|
||||
`set_${checkButton.type}_track_enabled`
|
||||
](false);
|
||||
}
|
||||
|
||||
const setTrack = `set_${checkButton.type}_track`;
|
||||
|
||||
clapperWidget.player[setTrack](checkButton.activeId);
|
||||
clapperWidget.player[`${setTrack}_enabled`](true);
|
||||
|
||||
if(checkButton.type === 'video')
|
||||
clapperWidget.player.draw_black(false);
|
||||
}
|
||||
|
||||
_handleVisualizationChange(checkButton)
|
||||
{
|
||||
const clapperWidget = this.get_ancestor(Gtk.Grid);
|
||||
const isEnabled = clapperWidget.player.get_visualization_enabled();
|
||||
|
||||
if(!checkButton.activeId) {
|
||||
if(isEnabled) {
|
||||
clapperWidget.player.set_visualization_enabled(false);
|
||||
debug('disabled visualizations');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const currVis = clapperWidget.player.get_current_visualization();
|
||||
|
||||
if(currVis === checkButton.activeId)
|
||||
return;
|
||||
|
||||
debug(`set visualization: ${checkButton.activeId}`);
|
||||
clapperWidget.player.set_visualization(checkButton.activeId);
|
||||
|
||||
if(!isEnabled) {
|
||||
clapperWidget.player.set_visualization_enabled(true);
|
||||
debug('enabled visualizations');
|
||||
}
|
||||
}
|
||||
|
||||
_addTogglePlayButton()
|
||||
{
|
||||
this.togglePlayButton = new Buttons.IconToggleButton(
|
||||
'media-playback-start-symbolic',
|
||||
'media-playback-pause-symbolic'
|
||||
);
|
||||
this.togglePlayButton.child.add_css_class('playbackicon');
|
||||
this.togglePlayButton.connect(
|
||||
'clicked', this._onTogglePlayClicked.bind(this)
|
||||
);
|
||||
this.addButton(this.togglePlayButton);
|
||||
}
|
||||
|
||||
_addElapsedButton()
|
||||
{
|
||||
const elapsedRevealer = new Revealers.ButtonsRevealer('SLIDE_RIGHT');
|
||||
this.elapsedButton = this.addElapsedPopoverButton('00:00/00:00', elapsedRevealer);
|
||||
elapsedRevealer.set_reveal_child(true);
|
||||
this.revealersArr.push(elapsedRevealer);
|
||||
|
||||
const speedScale = new Gtk.Scale({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
value_pos: Gtk.PositionType.BOTTOM,
|
||||
draw_value: false,
|
||||
round_digits: 2,
|
||||
hexpand: true,
|
||||
valign: Gtk.Align.CENTER,
|
||||
});
|
||||
|
||||
this.speedAdjustment = speedScale.get_adjustment();
|
||||
this.speedAdjustment.set_lower(0.01);
|
||||
this.speedAdjustment.set_upper(2);
|
||||
this.speedAdjustment.set_value(1);
|
||||
|
||||
speedScale.add_mark(0.25, Gtk.PositionType.BOTTOM, '0.25x');
|
||||
speedScale.add_mark(1, Gtk.PositionType.BOTTOM, 'normal');
|
||||
speedScale.add_mark(2, Gtk.PositionType.BOTTOM, '2x');
|
||||
|
||||
this.elapsedButton.popoverBox.append(speedScale);
|
||||
this.append(elapsedRevealer);
|
||||
}
|
||||
|
||||
_addPositionScale()
|
||||
{
|
||||
this.positionScale = new Gtk.Scale({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
value_pos: Gtk.PositionType.LEFT,
|
||||
draw_value: false,
|
||||
hexpand: true,
|
||||
valign: Gtk.Align.CENTER,
|
||||
can_focus: false,
|
||||
visible: false,
|
||||
});
|
||||
const scrollController = new Gtk.EventControllerScroll();
|
||||
scrollController.set_flags(Gtk.EventControllerScrollFlags.BOTH_AXES);
|
||||
scrollController.connect('scroll', this._onPositionScaleScroll.bind(this));
|
||||
this.positionScale.add_controller(scrollController);
|
||||
|
||||
this.positionScale.add_css_class('positionscale');
|
||||
this.positionScaleValueSignal = this.positionScale.connect(
|
||||
'value-changed', this._onPositionScaleValueChanged.bind(this)
|
||||
);
|
||||
|
||||
/* GTK4 is missing pressed/released signals for GtkRange/GtkScale.
|
||||
* We cannot add controllers, cause it already has them, so we
|
||||
* workaround this by observing css classes it currently has */
|
||||
this.positionScaleDragSignal = this.positionScale.connect(
|
||||
'notify::css-classes', this._onPositionScaleDragging.bind(this)
|
||||
);
|
||||
|
||||
this.positionAdjustment = this.positionScale.get_adjustment();
|
||||
this.positionAdjustment.set_page_increment(0);
|
||||
this.positionAdjustment.set_step_increment(8);
|
||||
|
||||
const box = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
hexpand: true,
|
||||
valign: Gtk.Align.CENTER,
|
||||
can_focus: false,
|
||||
});
|
||||
this.chapterPopover = new Gtk.Popover({
|
||||
position: Gtk.PositionType.TOP,
|
||||
autohide: false,
|
||||
});
|
||||
const chapterLabel = new Gtk.Label();
|
||||
chapterLabel.add_css_class('chapterlabel');
|
||||
this.chapterPopover.set_child(chapterLabel);
|
||||
this.chapterPopover.set_parent(box);
|
||||
|
||||
box.append(this.positionScale);
|
||||
this.append(box);
|
||||
}
|
||||
|
||||
_addVolumeButton()
|
||||
{
|
||||
this.volumeButton = this.addIconPopoverButton(
|
||||
'audio-volume-muted-symbolic'
|
||||
);
|
||||
this.volumeScale = new Gtk.Scale({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
inverted: true,
|
||||
value_pos: Gtk.PositionType.TOP,
|
||||
draw_value: false,
|
||||
vexpand: true,
|
||||
});
|
||||
this.volumeScale.add_css_class('volumescale');
|
||||
this.volumeAdjustment = this.volumeScale.get_adjustment();
|
||||
|
||||
this.volumeAdjustment.set_upper(Misc.maxVolume);
|
||||
this.volumeAdjustment.set_step_increment(0.05);
|
||||
this.volumeAdjustment.set_page_increment(0.05);
|
||||
|
||||
for(let i of [0, 1, Misc.maxVolume]) {
|
||||
const text = (!i) ? '0%' : (i % 1 === 0) ? `${i}00%` : `${i * 10}0%`;
|
||||
this.volumeScale.add_mark(i, Gtk.PositionType.LEFT, text);
|
||||
}
|
||||
|
||||
this.volumeScale.connect(
|
||||
'value-changed', this._onVolumeScaleValueChanged.bind(this)
|
||||
);
|
||||
this.volumeButton.popoverBox.append(this.volumeScale);
|
||||
}
|
||||
|
||||
_updateVolumeButtonIcon(volume)
|
||||
{
|
||||
const icon = (volume <= 0)
|
||||
? 'muted'
|
||||
: (volume <= 0.3)
|
||||
? 'low'
|
||||
: (volume <= 0.7)
|
||||
? 'medium'
|
||||
: (volume <= 1)
|
||||
? 'high'
|
||||
: 'overamplified';
|
||||
|
||||
const iconName = `audio-volume-${icon}-symbolic`;
|
||||
if(this.volumeButton.icon_name === iconName)
|
||||
return;
|
||||
|
||||
this.volumeButton.set_icon_name(iconName);
|
||||
debug(`set volume icon: ${icon}`);
|
||||
}
|
||||
|
||||
_setChapterVisible(isVisible)
|
||||
{
|
||||
const type = (isVisible) ? 'Show' : 'Hide';
|
||||
const anti = (isVisible) ? 'Hide' : 'Show';
|
||||
|
||||
if(this[`chapter${anti}Id`]) {
|
||||
GLib.source_remove(this[`chapter${anti}Id`]);
|
||||
this[`chapter${anti}Id`] = null;
|
||||
}
|
||||
|
||||
if(
|
||||
this[`chapter${type}Id`]
|
||||
|| (!isVisible && this.chapterPopover.visible === isVisible)
|
||||
)
|
||||
return;
|
||||
|
||||
debug(`changing chapter visibility to: ${isVisible}`);
|
||||
|
||||
this[`chapter${type}Id`] = GLib.idle_add(
|
||||
GLib.PRIORITY_DEFAULT_IDLE,
|
||||
() => {
|
||||
if(isVisible) {
|
||||
const [start, end] = this.positionScale.get_slider_range();
|
||||
const controlsHeight = this.parent.get_height();
|
||||
const scaleHeight = this.positionScale.parent.get_height();
|
||||
|
||||
this.chapterPopover.set_pointing_to(new Gdk.Rectangle({
|
||||
x: 2,
|
||||
y: -(controlsHeight - scaleHeight) / 2,
|
||||
width: 2 * end,
|
||||
height: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
this.chapterPopover.visible = isVisible;
|
||||
this[`chapter${type}Id`] = null;
|
||||
|
||||
debug(`chapter visible: ${isVisible}`);
|
||||
|
||||
return GLib.SOURCE_REMOVE;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_onRealize()
|
||||
{
|
||||
this.disconnect(this.realizeSignal);
|
||||
this.realizeSignal = null;
|
||||
|
||||
const { player } = this.get_ancestor(Gtk.Grid);
|
||||
const scrollController = new Gtk.EventControllerScroll();
|
||||
scrollController.set_flags(
|
||||
Gtk.EventControllerScrollFlags.VERTICAL
|
||||
| Gtk.EventControllerScrollFlags.DISCRETE
|
||||
);
|
||||
scrollController.connect('scroll', player._onScroll.bind(player));
|
||||
this.volumeButton.add_controller(scrollController);
|
||||
|
||||
const initialVolume = (settings.get_string('volume-initial') === 'custom')
|
||||
? settings.get_int('volume-value') / 100
|
||||
: Misc.getCubicValue(settings.get_double('volume-last'));
|
||||
|
||||
this.volumeScale.set_value(initialVolume);
|
||||
player.widget.connect('resize', this._onPlayerResize.bind(this));
|
||||
}
|
||||
|
||||
_onPlayerResize(widget, width, height)
|
||||
{
|
||||
const isMobile = (width < 560);
|
||||
if(this.isMobile === isMobile)
|
||||
return;
|
||||
|
||||
for(let revealer of this.revealersArr)
|
||||
revealer.set_reveal_child(!isMobile);
|
||||
|
||||
this.revealTracksRevealer.set_reveal_child(isMobile);
|
||||
this.isMobile = isMobile;
|
||||
}
|
||||
|
||||
_onUnfullscreenClicked(button)
|
||||
{
|
||||
const root = button.get_root();
|
||||
root.unfullscreen();
|
||||
}
|
||||
|
||||
_onUnfloatClicked(button)
|
||||
{
|
||||
const clapperWidget = this.get_ancestor(Gtk.Grid);
|
||||
clapperWidget.setFloatingMode(false);
|
||||
}
|
||||
|
||||
_onCheckButtonToggled(checkButton)
|
||||
{
|
||||
if(!checkButton.get_active())
|
||||
return;
|
||||
|
||||
switch(checkButton.type) {
|
||||
case 'video':
|
||||
case 'audio':
|
||||
case 'subtitle':
|
||||
this._handleTrackChange(checkButton);
|
||||
break;
|
||||
case 'visualization':
|
||||
this._handleVisualizationChange(checkButton);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_onTogglePlayClicked()
|
||||
{
|
||||
/* Parent of controls changes, so get ancestor instead */
|
||||
const { player } = this.get_ancestor(Gtk.Grid);
|
||||
player.toggle_play();
|
||||
}
|
||||
|
||||
_onPositionScaleScroll(controller, dx, dy)
|
||||
{
|
||||
const { player } = this.get_ancestor(Gtk.Grid);
|
||||
player._onScroll(controller, dx || dy, 0);
|
||||
}
|
||||
|
||||
_onPositionScaleValueChanged(scale)
|
||||
{
|
||||
const scaleValue = scale.get_value();
|
||||
const positionSeconds = Math.round(scaleValue);
|
||||
|
||||
this.currentPosition = positionSeconds;
|
||||
this.updateElapsedLabel(positionSeconds);
|
||||
|
||||
if(this.chapters && this.isPositionDragging) {
|
||||
const chapter = this.chapters[scaleValue];
|
||||
const isChapter = (chapter != null);
|
||||
|
||||
if(isChapter)
|
||||
this.chapterPopover.child.label = chapter;
|
||||
|
||||
this._setChapterVisible(isChapter);
|
||||
}
|
||||
}
|
||||
|
||||
_onVolumeScaleValueChanged(scale)
|
||||
{
|
||||
const volume = scale.get_value();
|
||||
const linearVolume = Misc.getLinearValue(volume);
|
||||
const { player } = this.get_ancestor(Gtk.Grid);
|
||||
|
||||
player.set_volume(linearVolume);
|
||||
|
||||
/* FIXME: All of below should be placed in 'volume-changed'
|
||||
* event once we move to message bus API */
|
||||
const cssClass = 'overamp';
|
||||
const hasOveramp = (scale.has_css_class(cssClass));
|
||||
|
||||
if(volume > 1) {
|
||||
if(!hasOveramp)
|
||||
scale.add_css_class(cssClass);
|
||||
}
|
||||
else {
|
||||
if(hasOveramp)
|
||||
scale.remove_css_class(cssClass);
|
||||
}
|
||||
|
||||
this._updateVolumeButtonIcon(volume);
|
||||
}
|
||||
|
||||
_onPositionScaleDragging(scale)
|
||||
{
|
||||
const isPositionDragging = scale.has_css_class('dragging');
|
||||
|
||||
if((this.isPositionDragging = isPositionDragging))
|
||||
return;
|
||||
|
||||
const isChapterSeek = this.chapterPopover.visible;
|
||||
|
||||
if(!isPositionDragging)
|
||||
this._setChapterVisible(false);
|
||||
|
||||
const clapperWidget = this.get_ancestor(Gtk.Grid);
|
||||
if(!clapperWidget) return;
|
||||
|
||||
const scaleValue = scale.get_value();
|
||||
|
||||
if(!isChapterSeek) {
|
||||
const positionSeconds = Math.round(scaleValue);
|
||||
clapperWidget.player.seek_seconds(positionSeconds);
|
||||
}
|
||||
else
|
||||
clapperWidget.player.seek_chapter(scaleValue);
|
||||
}
|
||||
|
||||
/* Only happens when navigating through controls panel */
|
||||
_onControlsKeyPressed(controller, keyval, keycode, state)
|
||||
{
|
||||
const { player } = this.get_ancestor(Gtk.Grid);
|
||||
player._setHideControlsTimeout();
|
||||
}
|
||||
|
||||
_onControlsKeyReleased(controller, keyval, keycode, state)
|
||||
{
|
||||
switch(keyval) {
|
||||
case Gdk.KEY_space:
|
||||
case Gdk.KEY_Return:
|
||||
case Gdk.KEY_Escape:
|
||||
case Gdk.KEY_Right:
|
||||
case Gdk.KEY_Left:
|
||||
break;
|
||||
default:
|
||||
const { player } = this.get_ancestor(Gtk.Grid);
|
||||
player._onWidgetKeyReleased(controller, keyval, keycode, state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_onCloseRequest()
|
||||
{
|
||||
debug('controls close request');
|
||||
|
||||
this.positionScale.disconnect(this.positionScaleValueSignal);
|
||||
this.positionScale.disconnect(this.positionScaleDragSignal);
|
||||
|
||||
for(let button of this.buttonsArr) {
|
||||
if(!button._onCloseRequest)
|
||||
continue;
|
||||
|
||||
button._onCloseRequest();
|
||||
}
|
||||
|
||||
this.chapterPopover.unparent();
|
||||
}
|
||||
});
|
68
src/daemon.js
Normal file
68
src/daemon.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const { Gio, GLib, GObject } = imports.gi;
|
||||
const Debug = imports.src.debug;
|
||||
|
||||
const { debug } = Debug;
|
||||
|
||||
var Daemon = GObject.registerClass(
|
||||
class ClapperDaemon extends Gio.SubprocessLauncher
|
||||
{
|
||||
_init()
|
||||
{
|
||||
const port = ARGV[0] || 8080;
|
||||
|
||||
/* FIXME: show output when debugging is on */
|
||||
const flags = Gio.SubprocessFlags.STDOUT_SILENCE
|
||||
| Gio.SubprocessFlags.STDERR_SILENCE;
|
||||
|
||||
super._init({ flags });
|
||||
|
||||
this.errMsg = 'exited with error or was forced to close';
|
||||
this.loop = GLib.MainLoop.new(null, false);
|
||||
|
||||
this.broadwayd = this.spawnv(['gtk4-broadwayd', '--port=' + port]);
|
||||
this.broadwayd.wait_async(null, this._onBroadwaydClosed.bind(this));
|
||||
|
||||
this.setenv('GDK_BACKEND', 'broadway', true);
|
||||
|
||||
const remoteApp = this.spawnv(['com.github.rafostar.Clapper.Remote']);
|
||||
remoteApp.wait_async(null, this._onRemoteClosed.bind(this));
|
||||
|
||||
this.loop.run();
|
||||
}
|
||||
|
||||
_checkProcResult(proc, result)
|
||||
{
|
||||
let hadError = false;
|
||||
|
||||
try {
|
||||
hadError = proc.wait_finish(result);
|
||||
}
|
||||
catch(err) {
|
||||
debug(err);
|
||||
}
|
||||
|
||||
return hadError;
|
||||
}
|
||||
|
||||
_onBroadwaydClosed(proc, result)
|
||||
{
|
||||
const hadError = this._checkProcResult(proc, result);
|
||||
|
||||
if(hadError)
|
||||
debug(`broadwayd ${this.errMsg}`);
|
||||
|
||||
this.broadwayd = null;
|
||||
this.loop.quit();
|
||||
}
|
||||
|
||||
_onRemoteClosed(proc, result)
|
||||
{
|
||||
const hadError = this._checkProcResult(proc, result);
|
||||
|
||||
if(hadError)
|
||||
debug(`remote app ${this.errMsg}`);
|
||||
|
||||
if(this.broadwayd)
|
||||
this.broadwayd.force_exit();
|
||||
}
|
||||
});
|
41
src/debug.js
Normal file
41
src/debug.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const { GLib } = imports.gi;
|
||||
const { Debug } = imports.extras.debug;
|
||||
const { Ink } = imports.extras.ink;
|
||||
|
||||
const G_DEBUG_ENV = GLib.getenv('G_MESSAGES_DEBUG');
|
||||
|
||||
const clapperDebugger = new Debug.Debugger('Clapper', {
|
||||
name_printer: new Ink.Printer({
|
||||
font: Ink.Font.BOLD,
|
||||
color: Ink.Color.MAGENTA
|
||||
}),
|
||||
time_printer: new Ink.Printer({
|
||||
color: Ink.Color.ORANGE
|
||||
}),
|
||||
high_precision: true,
|
||||
});
|
||||
clapperDebugger.enabled = (
|
||||
clapperDebugger.enabled
|
||||
|| G_DEBUG_ENV != null
|
||||
&& G_DEBUG_ENV.includes('Clapper')
|
||||
);
|
||||
const clapperDebug = clapperDebugger.debug;
|
||||
|
||||
function debug(msg, levelName)
|
||||
{
|
||||
levelName = levelName || 'LEVEL_DEBUG';
|
||||
|
||||
if(msg.message) {
|
||||
levelName = 'LEVEL_CRITICAL';
|
||||
msg = msg.message;
|
||||
}
|
||||
|
||||
if(levelName !== 'LEVEL_CRITICAL')
|
||||
return clapperDebug(msg);
|
||||
|
||||
GLib.log_structured(
|
||||
'Clapper', GLib.LogLevelFlags[levelName], {
|
||||
MESSAGE: msg,
|
||||
SYSLOG_IDENTIFIER: 'clapper'
|
||||
});
|
||||
}
|
291
src/dialogs.js
Normal file
291
src/dialogs.js
Normal file
@@ -0,0 +1,291 @@
|
||||
const { Gio, GObject, Gtk, Gst } = imports.gi;
|
||||
const Debug = imports.src.debug;
|
||||
const Misc = imports.src.misc;
|
||||
const Prefs = imports.src.prefs;
|
||||
const PrefsBase = imports.src.prefsBase;
|
||||
|
||||
const { debug } = Debug;
|
||||
|
||||
var FileChooser = GObject.registerClass(
|
||||
class ClapperFileChooser extends Gtk.FileChooserNative
|
||||
{
|
||||
_init(window)
|
||||
{
|
||||
super._init({
|
||||
transient_for: window,
|
||||
modal: true,
|
||||
select_multiple: true,
|
||||
});
|
||||
|
||||
const filter = new Gtk.FileFilter({
|
||||
name: 'Media Files',
|
||||
});
|
||||
filter.add_mime_type('video/*');
|
||||
filter.add_mime_type('audio/*');
|
||||
filter.add_mime_type('application/claps');
|
||||
this.subsMimes = [
|
||||
'application/x-subrip',
|
||||
];
|
||||
this.subsMimes.forEach(mime => filter.add_mime_type(mime));
|
||||
this.add_filter(filter);
|
||||
|
||||
this.responseSignal = this.connect('response', this._onResponse.bind(this));
|
||||
|
||||
/* File chooser closes itself when nobody is holding its ref */
|
||||
this.ref();
|
||||
this.show();
|
||||
}
|
||||
|
||||
_onResponse(filechooser, response)
|
||||
{
|
||||
debug('closing file chooser dialog');
|
||||
|
||||
this.disconnect(this.responseSignal);
|
||||
this.responseSignal = null;
|
||||
|
||||
if(response === Gtk.ResponseType.ACCEPT) {
|
||||
const files = this.get_files();
|
||||
const playlist = [];
|
||||
|
||||
let index = 0;
|
||||
let file;
|
||||
let subs;
|
||||
|
||||
while((file = files.get_item(index))) {
|
||||
const filename = file.get_basename();
|
||||
const [type, isUncertain] = Gio.content_type_guess(filename, null);
|
||||
|
||||
if(this.subsMimes.includes(type)) {
|
||||
subs = file;
|
||||
files.remove(index);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
playlist.push(file);
|
||||
index++;
|
||||
}
|
||||
|
||||
const { player } = this.get_transient_for().get_child();
|
||||
|
||||
if(playlist.length)
|
||||
player.set_playlist(playlist);
|
||||
|
||||
/* add subs to single selected video
|
||||
or to already playing file */
|
||||
if(subs && !files.get_item(1))
|
||||
player.set_subtitles(subs);
|
||||
}
|
||||
|
||||
this.unref();
|
||||
}
|
||||
});
|
||||
|
||||
var UriDialog = GObject.registerClass(
|
||||
class ClapperUriDialog extends Gtk.Dialog
|
||||
{
|
||||
_init(window)
|
||||
{
|
||||
super._init({
|
||||
transient_for: window,
|
||||
destroy_with_parent: true,
|
||||
modal: true,
|
||||
title: 'Open URI',
|
||||
default_width: 460,
|
||||
});
|
||||
|
||||
const box = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
valign: Gtk.Align.CENTER,
|
||||
spacing: 6,
|
||||
});
|
||||
box.add_css_class('uridialogbox');
|
||||
|
||||
const linkEntry = new Gtk.Entry({
|
||||
activates_default: true,
|
||||
truncate_multiline: true,
|
||||
width_request: 220,
|
||||
height_request: 36,
|
||||
hexpand: true,
|
||||
});
|
||||
linkEntry.set_placeholder_text("Enter or drop URI here");
|
||||
linkEntry.connect('notify::text', this._onTextNotify.bind(this));
|
||||
box.append(linkEntry);
|
||||
|
||||
const openButton = new Gtk.Button({
|
||||
label: "Open",
|
||||
halign: Gtk.Align.END,
|
||||
sensitive: false,
|
||||
});
|
||||
openButton.connect('clicked', this._onOpenButtonClicked.bind(this));
|
||||
box.append(openButton);
|
||||
|
||||
const area = this.get_content_area();
|
||||
area.append(box);
|
||||
|
||||
this.closeSignal = this.connect('close-request', this._onCloseRequest.bind(this));
|
||||
|
||||
this.ref();
|
||||
this.show();
|
||||
}
|
||||
|
||||
openUri(uri)
|
||||
{
|
||||
const { player } = this.get_transient_for().get_child();
|
||||
player.set_playlist([uri]);
|
||||
|
||||
this.close();
|
||||
}
|
||||
|
||||
_onTextNotify(entry)
|
||||
{
|
||||
const isUriValid = (entry.text.length)
|
||||
? Gst.uri_is_valid(entry.text)
|
||||
: false;
|
||||
|
||||
const button = entry.get_next_sibling();
|
||||
button.set_sensitive(isUriValid);
|
||||
}
|
||||
|
||||
_onOpenButtonClicked(button)
|
||||
{
|
||||
const entry = button.get_prev_sibling();
|
||||
this.openUri(entry.text);
|
||||
}
|
||||
|
||||
_onCloseRequest(dialog)
|
||||
{
|
||||
debug('closing URI dialog');
|
||||
|
||||
dialog.disconnect(this.closeSignal);
|
||||
this.closeSignal = null;
|
||||
}
|
||||
});
|
||||
|
||||
var PrefsDialog = GObject.registerClass(
|
||||
class ClapperPrefsDialog extends Gtk.Dialog
|
||||
{
|
||||
_init(window)
|
||||
{
|
||||
super._init({
|
||||
transient_for: window,
|
||||
destroy_with_parent: true,
|
||||
modal: true,
|
||||
title: 'Preferences',
|
||||
default_width: 460,
|
||||
default_height: 400,
|
||||
});
|
||||
|
||||
const pages = [
|
||||
{
|
||||
title: 'Player',
|
||||
pages: [
|
||||
{
|
||||
title: 'General',
|
||||
widget: Prefs.GeneralPage,
|
||||
},
|
||||
{
|
||||
title: 'Behaviour',
|
||||
widget: Prefs.BehaviourPage,
|
||||
},
|
||||
{
|
||||
title: 'Audio',
|
||||
widget: Prefs.AudioPage,
|
||||
},
|
||||
{
|
||||
title: 'Subtitles',
|
||||
widget: Prefs.SubtitlesPage,
|
||||
},
|
||||
{
|
||||
title: 'Network',
|
||||
widget: Prefs.NetworkPage,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Advanced',
|
||||
pages: [
|
||||
{
|
||||
title: 'GStreamer',
|
||||
widget: Prefs.GStreamerPage,
|
||||
},
|
||||
{
|
||||
title: 'Tweaks',
|
||||
widget: Prefs.TweaksPage,
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const prefsNotebook = new PrefsBase.Notebook(pages);
|
||||
prefsNotebook.add_css_class('prefsnotebook');
|
||||
|
||||
const area = this.get_content_area();
|
||||
area.append(prefsNotebook);
|
||||
|
||||
this.closeSignal = this.connect('close-request', this._onCloseRequest.bind(this));
|
||||
|
||||
this.ref();
|
||||
this.show();
|
||||
}
|
||||
|
||||
_onCloseRequest(dialog)
|
||||
{
|
||||
debug('closing prefs dialog');
|
||||
|
||||
dialog.disconnect(this.closeSignal);
|
||||
this.closeSignal = null;
|
||||
|
||||
const area = dialog.get_content_area();
|
||||
const notebook = area.get_first_child();
|
||||
notebook._onClose();
|
||||
}
|
||||
});
|
||||
|
||||
var AboutDialog = GObject.registerClass(
|
||||
class ClapperAboutDialog extends Gtk.AboutDialog
|
||||
{
|
||||
_init(window)
|
||||
{
|
||||
const gstVer = [
|
||||
Gst.VERSION_MAJOR, Gst.VERSION_MINOR, Gst.VERSION_MICRO
|
||||
].join('.');
|
||||
|
||||
const gtkVer = [
|
||||
Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION, Gtk.MICRO_VERSION
|
||||
].join('.');
|
||||
|
||||
const osInfo = [
|
||||
'GTK version' + ': ' + gtkVer,
|
||||
'GStreamer version' + ': ' + gstVer
|
||||
].join('\n');
|
||||
|
||||
super._init({
|
||||
transient_for: window,
|
||||
destroy_with_parent: true,
|
||||
modal: true,
|
||||
program_name: Misc.appName,
|
||||
comments: 'A GNOME media player powered by GStreamer',
|
||||
version: Misc.getClapperVersion(),
|
||||
authors: ['Rafał Dzięgiel'],
|
||||
artists: ['Rafał Dzięgiel'],
|
||||
license_type: Gtk.License.GPL_3_0,
|
||||
logo_icon_name: 'com.github.rafostar.Clapper',
|
||||
website: 'https://rafostar.github.io/clapper',
|
||||
system_information: osInfo,
|
||||
});
|
||||
|
||||
this.closeSignal = this.connect('close-request', this._onCloseRequest.bind(this));
|
||||
|
||||
this.ref();
|
||||
this.show();
|
||||
}
|
||||
|
||||
_onCloseRequest(dialog)
|
||||
{
|
||||
debug('closing about dialog');
|
||||
|
||||
dialog.disconnect(this.closeSignal);
|
||||
this.closeSignal = null;
|
||||
}
|
||||
});
|
28
src/headerbar.js
Normal file
28
src/headerbar.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const { GObject } = imports.gi;
|
||||
const { HeaderBarBase } = imports.src.headerbarBase;
|
||||
|
||||
var HeaderBar = GObject.registerClass(
|
||||
class ClapperHeaderBar extends HeaderBarBase
|
||||
{
|
||||
_init(window)
|
||||
{
|
||||
super._init(window);
|
||||
|
||||
const clapperWidget = window.get_child();
|
||||
clapperWidget.controls.unfloatButton.bind_property('visible', this, 'visible',
|
||||
GObject.BindingFlags.INVERT_BOOLEAN
|
||||
);
|
||||
}
|
||||
|
||||
_onFloatButtonClicked()
|
||||
{
|
||||
const clapperWidget = this.get_prev_sibling();
|
||||
clapperWidget.setFloatingMode(true);
|
||||
}
|
||||
|
||||
_onFullscreenButtonClicked()
|
||||
{
|
||||
const window = this.get_parent();
|
||||
window.fullscreen();
|
||||
}
|
||||
});
|
134
src/headerbarBase.js
Normal file
134
src/headerbarBase.js
Normal file
@@ -0,0 +1,134 @@
|
||||
const { GObject, Gtk, Pango } = imports.gi;
|
||||
const Misc = imports.src.misc;
|
||||
|
||||
var HeaderBarBase = GObject.registerClass(
|
||||
class ClapperHeaderBarBase extends Gtk.HeaderBar
|
||||
{
|
||||
_init(window)
|
||||
{
|
||||
super._init({
|
||||
can_focus: false,
|
||||
});
|
||||
|
||||
const clapperPath = Misc.getClapperPath();
|
||||
const uiBuilder = Gtk.Builder.new_from_file(
|
||||
`${clapperPath}/ui/clapper.ui`
|
||||
);
|
||||
const models = {
|
||||
addMediaMenu: uiBuilder.get_object('addMediaMenu'),
|
||||
settingsMenu: uiBuilder.get_object('settingsMenu'),
|
||||
};
|
||||
|
||||
this.add_css_class('noborder');
|
||||
this.set_title_widget(this._createWidgetForWindow(window));
|
||||
|
||||
const addMediaButton = new Gtk.MenuButton({
|
||||
icon_name: 'list-add-symbolic',
|
||||
});
|
||||
const addMediaPopover = new HeaderBarPopover(models.addMediaMenu);
|
||||
addMediaButton.set_popover(addMediaPopover);
|
||||
this.pack_start(addMediaButton);
|
||||
|
||||
const openMenuButton = new Gtk.MenuButton({
|
||||
icon_name: 'open-menu-symbolic',
|
||||
});
|
||||
const settingsPopover = new HeaderBarPopover(models.settingsMenu);
|
||||
openMenuButton.set_popover(settingsPopover);
|
||||
this.pack_end(openMenuButton);
|
||||
|
||||
const buttonsBox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
});
|
||||
buttonsBox.add_css_class('linked');
|
||||
|
||||
const floatButton = new Gtk.Button({
|
||||
icon_name: 'preferences-desktop-remote-desktop-symbolic',
|
||||
});
|
||||
floatButton.connect('clicked', this._onFloatButtonClicked.bind(this));
|
||||
buttonsBox.append(floatButton);
|
||||
|
||||
const fullscreenButton = new Gtk.Button({
|
||||
icon_name: 'view-fullscreen-symbolic',
|
||||
});
|
||||
fullscreenButton.connect('clicked', this._onFullscreenButtonClicked.bind(this));
|
||||
|
||||
buttonsBox.append(fullscreenButton);
|
||||
this.pack_end(buttonsBox);
|
||||
}
|
||||
|
||||
updateHeaderBar(title, subtitle)
|
||||
{
|
||||
this.titleLabel.label = title;
|
||||
this.subtitleLabel.visible = (subtitle !== null);
|
||||
|
||||
if(subtitle)
|
||||
this.subtitleLabel.label = subtitle;
|
||||
}
|
||||
|
||||
_createWidgetForWindow(window)
|
||||
{
|
||||
const box = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
valign: Gtk.Align.CENTER,
|
||||
});
|
||||
|
||||
this.titleLabel = new Gtk.Label({
|
||||
halign: Gtk.Align.CENTER,
|
||||
single_line_mode: true,
|
||||
ellipsize: Pango.EllipsizeMode.END,
|
||||
width_chars: 5,
|
||||
});
|
||||
this.titleLabel.add_css_class('title');
|
||||
this.titleLabel.set_parent(box);
|
||||
|
||||
window.bind_property('title', this.titleLabel, 'label',
|
||||
GObject.BindingFlags.SYNC_CREATE
|
||||
);
|
||||
|
||||
this.subtitleLabel = new Gtk.Label({
|
||||
halign: Gtk.Align.CENTER,
|
||||
single_line_mode: true,
|
||||
ellipsize: Pango.EllipsizeMode.END,
|
||||
});
|
||||
this.subtitleLabel.add_css_class('subtitle');
|
||||
this.subtitleLabel.set_parent(box);
|
||||
this.subtitleLabel.visible = false;
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
_onFloatButtonClicked()
|
||||
{
|
||||
}
|
||||
|
||||
_onFullscreenButtonClicked()
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
var HeaderBarPopover = GObject.registerClass(
|
||||
class ClapperHeaderBarPopover extends Gtk.PopoverMenu
|
||||
{
|
||||
_init(model)
|
||||
{
|
||||
super._init({
|
||||
menu_model: model,
|
||||
});
|
||||
|
||||
this.connect('closed', this._onClosed.bind(this));
|
||||
}
|
||||
|
||||
_onClosed()
|
||||
{
|
||||
const { child } = this.get_root();
|
||||
|
||||
if(
|
||||
!child
|
||||
|| !child.player
|
||||
|| !child.player.widget
|
||||
)
|
||||
return;
|
||||
|
||||
child.player.widget.grab_focus();
|
||||
}
|
||||
});
|
12
src/main.js
Normal file
12
src/main.js
Normal file
@@ -0,0 +1,12 @@
|
||||
imports.gi.versions.Gdk = '4.0';
|
||||
imports.gi.versions.Gtk = '4.0';
|
||||
|
||||
const { Gst } = imports.gi;
|
||||
const { App } = imports.src.app;
|
||||
|
||||
Gst.init(null);
|
||||
|
||||
function main(argv)
|
||||
{
|
||||
new App().run(argv);
|
||||
}
|
6
src/mainDaemon.js
Normal file
6
src/mainDaemon.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const { Daemon } = imports.src.daemon;
|
||||
|
||||
function main()
|
||||
{
|
||||
new Daemon();
|
||||
}
|
15
src/mainRemote.js
Normal file
15
src/mainRemote.js
Normal file
@@ -0,0 +1,15 @@
|
||||
imports.gi.versions.Gdk = '4.0';
|
||||
imports.gi.versions.Gtk = '4.0';
|
||||
|
||||
const { AppRemote } = imports.src.appRemote;
|
||||
const Misc = imports.src.misc;
|
||||
|
||||
const ID_POSTFIX = 'Remote';
|
||||
|
||||
Misc.clapperPath = `${pkg.datadir}/${Misc.appId}`;
|
||||
Misc.appId += '.' + ID_POSTFIX;
|
||||
|
||||
function main()
|
||||
{
|
||||
new AppRemote().run();
|
||||
}
|
13
src/menu.js
Normal file
13
src/menu.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const { GObject, Gst, Gtk } = imports.gi;
|
||||
const Dialogs = imports.src.dialogs;
|
||||
|
||||
var actions = {
|
||||
openLocal: (window) => new Dialogs.FileChooser(window),
|
||||
openUri: (window) => new Dialogs.UriDialog(window),
|
||||
prefs: (window) => new Dialogs.PrefsDialog(window),
|
||||
about: (window) => new Dialogs.AboutDialog(window),
|
||||
};
|
||||
|
||||
var accels = [
|
||||
['app.quit', ['q']],
|
||||
];
|
115
src/misc.js
Normal file
115
src/misc.js
Normal file
@@ -0,0 +1,115 @@
|
||||
const { Gio, GstAudio, GstPlayer, Gdk, Gtk } = imports.gi;
|
||||
const Debug = imports.src.debug;
|
||||
|
||||
const { debug } = Debug;
|
||||
|
||||
var appName = 'Clapper';
|
||||
var appId = 'com.github.rafostar.Clapper';
|
||||
|
||||
var clapperPath = null;
|
||||
var clapperVersion = null;
|
||||
|
||||
var settings = new Gio.Settings({
|
||||
schema_id: appId,
|
||||
});
|
||||
|
||||
var maxVolume = 1.5;
|
||||
|
||||
let inhibitCookie;
|
||||
|
||||
function getClapperPath()
|
||||
{
|
||||
return (clapperPath)
|
||||
? clapperPath
|
||||
: (pkg)
|
||||
? `${pkg.datadir}/${pkg.name}`
|
||||
: '.';
|
||||
}
|
||||
|
||||
function getClapperVersion()
|
||||
{
|
||||
return (clapperVersion)
|
||||
? clapperVersion
|
||||
: (pkg)
|
||||
? pkg.version
|
||||
: '';
|
||||
}
|
||||
|
||||
function loadCustomCss()
|
||||
{
|
||||
const clapperPath = getClapperPath();
|
||||
const cssProvider = new Gtk.CssProvider();
|
||||
|
||||
cssProvider.load_from_path(`${clapperPath}/css/styles.css`);
|
||||
Gtk.StyleContext.add_provider_for_display(
|
||||
Gdk.Display.get_default(),
|
||||
cssProvider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||
);
|
||||
}
|
||||
|
||||
function inhibitForState(state, window)
|
||||
{
|
||||
let isInhibited = false;
|
||||
|
||||
if(state === GstPlayer.PlayerState.PLAYING) {
|
||||
if(inhibitCookie)
|
||||
return;
|
||||
|
||||
const app = window.get_application();
|
||||
|
||||
inhibitCookie = app.inhibit(
|
||||
window,
|
||||
Gtk.ApplicationInhibitFlags.IDLE,
|
||||
'video is playing'
|
||||
);
|
||||
if(!inhibitCookie)
|
||||
debug(new Error('could not inhibit session!'));
|
||||
|
||||
isInhibited = (inhibitCookie > 0);
|
||||
}
|
||||
else {
|
||||
if(!inhibitCookie)
|
||||
return;
|
||||
|
||||
const app = window.get_application();
|
||||
app.uninhibit(inhibitCookie);
|
||||
inhibitCookie = null;
|
||||
}
|
||||
|
||||
debug(`set prevent suspend to: ${isInhibited}`);
|
||||
}
|
||||
|
||||
function getFormattedTime(time, showHours)
|
||||
{
|
||||
let hours;
|
||||
|
||||
if(showHours || time >= 3600) {
|
||||
hours = ('0' + Math.floor(time / 3600)).slice(-2);
|
||||
time -= hours * 3600;
|
||||
}
|
||||
const minutes = ('0' + Math.floor(time / 60)).slice(-2);
|
||||
time -= minutes * 60;
|
||||
const seconds = ('0' + Math.floor(time)).slice(-2);
|
||||
|
||||
const parsed = (hours) ? `${hours}:` : '';
|
||||
return parsed + `${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
function getCubicValue(linearVal)
|
||||
{
|
||||
return GstAudio.StreamVolume.convert_volume(
|
||||
GstAudio.StreamVolumeFormat.LINEAR,
|
||||
GstAudio.StreamVolumeFormat.CUBIC,
|
||||
linearVal
|
||||
);
|
||||
}
|
||||
|
||||
function getLinearValue(cubicVal)
|
||||
{
|
||||
return GstAudio.StreamVolume.convert_volume(
|
||||
GstAudio.StreamVolumeFormat.CUBIC,
|
||||
GstAudio.StreamVolumeFormat.LINEAR,
|
||||
cubicVal
|
||||
);
|
||||
}
|
735
src/player.js
Normal file
735
src/player.js
Normal file
@@ -0,0 +1,735 @@
|
||||
const { Gdk, Gio, GLib, GObject, Gst, GstPlayer, Gtk } = imports.gi;
|
||||
const ByteArray = imports.byteArray;
|
||||
const Debug = imports.src.debug;
|
||||
const Misc = imports.src.misc;
|
||||
const { PlayerBase } = imports.src.playerBase;
|
||||
|
||||
const { debug } = Debug;
|
||||
const { settings } = Misc;
|
||||
|
||||
var Player = GObject.registerClass(
|
||||
class ClapperPlayer extends PlayerBase
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init();
|
||||
|
||||
this.cursorInPlayer = false;
|
||||
this.seek_done = true;
|
||||
this.dragAllowed = false;
|
||||
this.isWidgetDragging = false;
|
||||
this.doneStartup = false;
|
||||
this.needsFastSeekRestore = false;
|
||||
|
||||
this.playOnFullscreen = false;
|
||||
this.quitOnStop = false;
|
||||
this.needsTocUpdate = true;
|
||||
|
||||
this.posX = 0;
|
||||
this.posY = 0;
|
||||
this.keyPressCount = 0;
|
||||
|
||||
this._maxVolume = Misc.getLinearValue(Misc.maxVolume);
|
||||
|
||||
this._hideCursorTimeout = null;
|
||||
this._hideControlsTimeout = null;
|
||||
this._updateTimeTimeout = null;
|
||||
|
||||
const clickGesture = new Gtk.GestureClick();
|
||||
clickGesture.set_button(0);
|
||||
clickGesture.connect('pressed', this._onWidgetPressed.bind(this));
|
||||
this.widget.add_controller(clickGesture);
|
||||
|
||||
const dragGesture = new Gtk.GestureDrag();
|
||||
dragGesture.connect('drag-update', this._onWidgetDragUpdate.bind(this));
|
||||
this.widget.add_controller(dragGesture);
|
||||
|
||||
const keyController = new Gtk.EventControllerKey();
|
||||
keyController.connect('key-pressed', this._onWidgetKeyPressed.bind(this));
|
||||
keyController.connect('key-released', this._onWidgetKeyReleased.bind(this));
|
||||
this.widget.add_controller(keyController);
|
||||
|
||||
const scrollController = new Gtk.EventControllerScroll();
|
||||
scrollController.set_flags(Gtk.EventControllerScrollFlags.BOTH_AXES);
|
||||
scrollController.connect('scroll', this._onScroll.bind(this));
|
||||
this.widget.add_controller(scrollController);
|
||||
|
||||
const motionController = new Gtk.EventControllerMotion();
|
||||
motionController.connect('enter', this._onWidgetEnter.bind(this));
|
||||
motionController.connect('leave', this._onWidgetLeave.bind(this));
|
||||
motionController.connect('motion', this._onWidgetMotion.bind(this));
|
||||
this.widget.add_controller(motionController);
|
||||
|
||||
const dropTarget = new Gtk.DropTarget({
|
||||
actions: Gdk.DragAction.COPY,
|
||||
});
|
||||
dropTarget.set_gtypes([GObject.TYPE_STRING]);
|
||||
dropTarget.connect('drop', this._onDataDrop.bind(this));
|
||||
this.widget.add_controller(dropTarget);
|
||||
|
||||
this.connect('state-changed', this._onStateChanged.bind(this));
|
||||
this.connect('uri-loaded', this._onUriLoaded.bind(this));
|
||||
this.connect('end-of-stream', this._onStreamEnded.bind(this));
|
||||
this.connect('warning', this._onPlayerWarning.bind(this));
|
||||
this.connect('error', this._onPlayerError.bind(this));
|
||||
|
||||
this._realizeSignal = this.widget.connect('realize', this._onWidgetRealize.bind(this));
|
||||
}
|
||||
|
||||
set_uri(uri)
|
||||
{
|
||||
/* FIXME: Player does not notify about
|
||||
* rate change after file load */
|
||||
if(this.rate !== 1)
|
||||
this.set_rate(1);
|
||||
|
||||
if(Gst.Uri.get_protocol(uri) !== 'file')
|
||||
return super.set_uri(uri);
|
||||
|
||||
let file = Gio.file_new_for_uri(uri);
|
||||
if(!file.query_exists(null)) {
|
||||
debug(`file does not exist: ${file.get_path()}`, 'LEVEL_WARNING');
|
||||
|
||||
if(!this.playlistWidget.nextTrack())
|
||||
debug('set media reached end of playlist');
|
||||
|
||||
return;
|
||||
}
|
||||
if(uri.endsWith('.claps'))
|
||||
return this.load_playlist_file(file);
|
||||
|
||||
super.set_uri(uri);
|
||||
}
|
||||
|
||||
load_playlist_file(file)
|
||||
{
|
||||
const stream = new Gio.DataInputStream({
|
||||
base_stream: file.read(null)
|
||||
});
|
||||
const listdir = file.get_parent();
|
||||
const playlist = [];
|
||||
|
||||
let line;
|
||||
while((line = stream.read_line(null)[0])) {
|
||||
line = (line instanceof Uint8Array)
|
||||
? ByteArray.toString(line).trim()
|
||||
: String(line).trim();
|
||||
|
||||
if(!Gst.uri_is_valid(line)) {
|
||||
const lineFile = listdir.resolve_relative_path(line);
|
||||
if(!lineFile)
|
||||
continue;
|
||||
|
||||
line = lineFile.get_uri();
|
||||
}
|
||||
debug(`new playlist item: ${line}`);
|
||||
playlist.push(line);
|
||||
}
|
||||
stream.close(null);
|
||||
this.set_playlist(playlist);
|
||||
}
|
||||
|
||||
_preparePlaylist(playlist)
|
||||
{
|
||||
this.playlistWidget.removeAll();
|
||||
|
||||
for(let source of playlist) {
|
||||
const uri = (source.get_uri != null)
|
||||
? source.get_uri()
|
||||
: Gst.uri_is_valid(source)
|
||||
? source
|
||||
: Gst.filename_to_uri(source);
|
||||
|
||||
this.playlistWidget.addItem(uri);
|
||||
}
|
||||
}
|
||||
|
||||
set_playlist(playlist)
|
||||
{
|
||||
this._preparePlaylist(playlist);
|
||||
|
||||
const firstTrack = this.playlistWidget.get_row_at_index(0);
|
||||
if(!firstTrack) return;
|
||||
|
||||
firstTrack.activate();
|
||||
}
|
||||
|
||||
set_subtitles(source)
|
||||
{
|
||||
const uri = (source.get_uri)
|
||||
? source.get_uri()
|
||||
: source;
|
||||
|
||||
this.set_subtitle_uri(uri);
|
||||
this.set_subtitle_track_enabled(true);
|
||||
|
||||
debug(`applied subtitle track: ${uri}`);
|
||||
}
|
||||
|
||||
set_visualization_enabled(value)
|
||||
{
|
||||
if(value === this.visualization_enabled)
|
||||
return;
|
||||
|
||||
super.set_visualization_enabled(value);
|
||||
this.visualization_enabled = value;
|
||||
}
|
||||
|
||||
get_visualization_enabled()
|
||||
{
|
||||
return this.visualization_enabled;
|
||||
}
|
||||
|
||||
seek(position)
|
||||
{
|
||||
/* avoid seek emits when position bar is altered */
|
||||
if(this.needsTocUpdate)
|
||||
return;
|
||||
|
||||
this.seek_done = false;
|
||||
|
||||
if(this.state === GstPlayer.PlayerState.STOPPED)
|
||||
this.pause();
|
||||
|
||||
if(position < 0)
|
||||
position = 0;
|
||||
|
||||
debug(`${this.seekingMode} seeking to position: ${position}`);
|
||||
|
||||
super.seek(position);
|
||||
}
|
||||
|
||||
seek_seconds(seconds)
|
||||
{
|
||||
this.seek(seconds * 1000000000);
|
||||
}
|
||||
|
||||
seek_chapter(seconds)
|
||||
{
|
||||
if(this.seekingMode !== 'fast') {
|
||||
this.seek_seconds(seconds);
|
||||
return;
|
||||
}
|
||||
|
||||
/* FIXME: Remove this check when GstPlay(er) have set_seek_mode function */
|
||||
if(this.set_seek_mode) {
|
||||
this.set_seek_mode(GstPlayer.PlayerSeekMode.DEFAULT);
|
||||
this.seekingMode = 'normal';
|
||||
this.needsFastSeekRestore = true;
|
||||
}
|
||||
|
||||
this.seek_seconds(seconds);
|
||||
}
|
||||
|
||||
set_volume(volume)
|
||||
{
|
||||
if(volume < 0)
|
||||
volume = 0;
|
||||
else if(volume > this._maxVolume)
|
||||
volume = this._maxVolume;
|
||||
|
||||
super.set_volume(volume);
|
||||
debug(`set player volume: ${volume}`);
|
||||
}
|
||||
|
||||
adjust_position(isIncrease)
|
||||
{
|
||||
this.seek_done = false;
|
||||
|
||||
const { controls } = this.widget.get_ancestor(Gtk.Grid);
|
||||
const max = controls.positionAdjustment.get_upper();
|
||||
const seekingUnit = settings.get_string('seeking-unit');
|
||||
|
||||
let seekingValue = settings.get_int('seeking-value');
|
||||
|
||||
switch(seekingUnit) {
|
||||
case 'minute':
|
||||
seekingValue *= 60;
|
||||
break;
|
||||
case 'percentage':
|
||||
seekingValue = max * seekingValue / 100;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if(!isIncrease)
|
||||
seekingValue *= -1;
|
||||
|
||||
let positionSeconds = controls.positionScale.get_value() + seekingValue;
|
||||
|
||||
if(positionSeconds > max)
|
||||
positionSeconds = max;
|
||||
|
||||
controls.positionScale.set_value(positionSeconds);
|
||||
}
|
||||
|
||||
adjust_volume(isIncrease)
|
||||
{
|
||||
const { controls } = this.widget.get_ancestor(Gtk.Grid);
|
||||
const value = (isIncrease) ? 0.05 : -0.05;
|
||||
const volume = controls.volumeScale.get_value() + value;
|
||||
|
||||
controls.volumeScale.set_value(volume);
|
||||
}
|
||||
|
||||
toggle_play()
|
||||
{
|
||||
const action = (this.state === GstPlayer.PlayerState.PLAYING)
|
||||
? 'pause'
|
||||
: 'play';
|
||||
|
||||
this[action]();
|
||||
}
|
||||
|
||||
receiveWs(action, value)
|
||||
{
|
||||
switch(action) {
|
||||
case 'toggle_play':
|
||||
case 'play':
|
||||
case 'pause':
|
||||
case 'set_playlist':
|
||||
this[action](value);
|
||||
break;
|
||||
default:
|
||||
super.receiveWs(action, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_setHideCursorTimeout()
|
||||
{
|
||||
this._clearTimeout('hideCursor');
|
||||
this._hideCursorTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => {
|
||||
this._hideCursorTimeout = null;
|
||||
|
||||
if(this.cursorInPlayer) {
|
||||
const clapperWidget = this.widget.get_ancestor(Gtk.Grid);
|
||||
const blankCursor = Gdk.Cursor.new_from_name('none', null);
|
||||
|
||||
this.widget.set_cursor(blankCursor);
|
||||
clapperWidget.revealerTop.set_cursor(blankCursor);
|
||||
}
|
||||
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
}
|
||||
|
||||
_setHideControlsTimeout()
|
||||
{
|
||||
this._clearTimeout('hideControls');
|
||||
this._hideControlsTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 3, () => {
|
||||
this._hideControlsTimeout = null;
|
||||
|
||||
if(this.cursorInPlayer) {
|
||||
const clapperWidget = this.widget.get_ancestor(Gtk.Grid);
|
||||
if(clapperWidget.fullscreenMode || clapperWidget.floatingMode) {
|
||||
this._clearTimeout('updateTime');
|
||||
clapperWidget.revealControls(false);
|
||||
}
|
||||
}
|
||||
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
}
|
||||
|
||||
_setUpdateTimeInterval()
|
||||
{
|
||||
this._clearTimeout('updateTime');
|
||||
|
||||
const clapperWidget = this.widget.get_ancestor(Gtk.Grid);
|
||||
const nextUpdate = clapperWidget.updateTime();
|
||||
|
||||
if(nextUpdate === null)
|
||||
return;
|
||||
|
||||
this._updateTimeTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, nextUpdate, () => {
|
||||
this._updateTimeTimeout = null;
|
||||
|
||||
if(clapperWidget.fullscreenMode)
|
||||
this._setUpdateTimeInterval();
|
||||
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
}
|
||||
|
||||
_clearTimeout(name)
|
||||
{
|
||||
if(!this[`_${name}Timeout`])
|
||||
return;
|
||||
|
||||
GLib.source_remove(this[`_${name}Timeout`]);
|
||||
this[`_${name}Timeout`] = null;
|
||||
|
||||
if(name === 'updateTime')
|
||||
debug('cleared update time interval');
|
||||
}
|
||||
|
||||
_performCloseCleanup(window)
|
||||
{
|
||||
window.disconnect(this.closeRequestSignal);
|
||||
this.closeRequestSignal = null;
|
||||
|
||||
const clapperWidget = this.widget.get_ancestor(Gtk.Grid);
|
||||
if(!clapperWidget.fullscreenMode) {
|
||||
const size = window.get_default_size();
|
||||
|
||||
if(size[0] > 0 && size[1] > 0)
|
||||
clapperWidget._saveWindowSize(size);
|
||||
}
|
||||
settings.set_double('volume-last', this.volume);
|
||||
|
||||
clapperWidget.controls._onCloseRequest();
|
||||
}
|
||||
|
||||
_onStateChanged(player, state)
|
||||
{
|
||||
this.state = state;
|
||||
this.emitWs('state_changed', state);
|
||||
|
||||
if(state !== GstPlayer.PlayerState.BUFFERING) {
|
||||
const root = player.widget.get_root();
|
||||
|
||||
if(this.quitOnStop) {
|
||||
if(state === GstPlayer.PlayerState.STOPPED)
|
||||
root.run_dispose();
|
||||
|
||||
return;
|
||||
}
|
||||
Misc.inhibitForState(state, root);
|
||||
}
|
||||
|
||||
const clapperWidget = player.widget.get_ancestor(Gtk.Grid);
|
||||
if(!clapperWidget) return;
|
||||
|
||||
if(!this.seek_done && state !== GstPlayer.PlayerState.BUFFERING) {
|
||||
clapperWidget.updateTime();
|
||||
|
||||
if(this.needsFastSeekRestore) {
|
||||
this.set_seek_mode(GstPlayer.PlayerSeekMode.FAST);
|
||||
this.seekingMode = 'fast';
|
||||
this.needsFastSeekRestore = false;
|
||||
}
|
||||
|
||||
this.seek_done = true;
|
||||
debug('seeking finished');
|
||||
}
|
||||
|
||||
clapperWidget._onPlayerStateChanged(player, state);
|
||||
}
|
||||
|
||||
_onStreamEnded(player)
|
||||
{
|
||||
const lastTrackId = this.playlistWidget.activeRowId;
|
||||
|
||||
debug(`end of stream: ${lastTrackId}`);
|
||||
this.emitWs('end_of_stream', lastTrackId);
|
||||
|
||||
if(this.playlistWidget.nextTrack())
|
||||
return;
|
||||
|
||||
if(settings.get_boolean('close-auto')) {
|
||||
/* Stop will be automatically called soon afterwards */
|
||||
this._performCloseCleanup(this.widget.get_root());
|
||||
this.quitOnStop = true;
|
||||
}
|
||||
}
|
||||
|
||||
_onUriLoaded(player, uri)
|
||||
{
|
||||
debug(`URI loaded: ${uri}`);
|
||||
this.needsTocUpdate = true;
|
||||
|
||||
if(!this.doneStartup) {
|
||||
this.doneStartup = true;
|
||||
|
||||
if(settings.get_boolean('fullscreen-auto')) {
|
||||
const root = player.widget.get_root();
|
||||
const clapperWidget = root.get_child();
|
||||
if(!clapperWidget.fullscreenMode) {
|
||||
this.playOnFullscreen = true;
|
||||
root.fullscreen();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.play();
|
||||
}
|
||||
|
||||
_onPlayerWarning(player, error)
|
||||
{
|
||||
debug(error.message, 'LEVEL_WARNING');
|
||||
}
|
||||
|
||||
_onPlayerError(player, error)
|
||||
{
|
||||
debug(error);
|
||||
}
|
||||
|
||||
_onWidgetRealize()
|
||||
{
|
||||
this.widget.disconnect(this._realizeSignal);
|
||||
this._realizeSignal = null;
|
||||
|
||||
const root = this.widget.get_root();
|
||||
if(!root) return;
|
||||
|
||||
this.closeRequestSignal = root.connect(
|
||||
'close-request', this._onCloseRequest.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/* Widget only - does not happen when using controls navigation */
|
||||
_onWidgetKeyPressed(controller, keyval, keycode, state)
|
||||
{
|
||||
const clapperWidget = this.widget.get_ancestor(Gtk.Grid);
|
||||
let bool = false;
|
||||
|
||||
this.keyPressCount++;
|
||||
|
||||
switch(keyval) {
|
||||
case Gdk.KEY_Up:
|
||||
bool = true;
|
||||
case Gdk.KEY_Down:
|
||||
this.adjust_volume(bool);
|
||||
break;
|
||||
case Gdk.KEY_Right:
|
||||
bool = true;
|
||||
case Gdk.KEY_Left:
|
||||
this.adjust_position(bool);
|
||||
this._clearTimeout('hideControls');
|
||||
if(this.keyPressCount > 1) {
|
||||
clapperWidget.revealerBottom.set_can_focus(false);
|
||||
clapperWidget.revealerBottom.revealChild(true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Also happens after using controls navigation for selected keys */
|
||||
_onWidgetKeyReleased(controller, keyval, keycode, state)
|
||||
{
|
||||
const clapperWidget = this.widget.get_ancestor(Gtk.Grid);
|
||||
let value, root;
|
||||
|
||||
this.keyPressCount = 0;
|
||||
|
||||
switch(keyval) {
|
||||
case Gdk.KEY_space:
|
||||
this.toggle_play();
|
||||
break;
|
||||
case Gdk.KEY_Return:
|
||||
if(clapperWidget.fullscreenMode) {
|
||||
clapperWidget.revealControls(true);
|
||||
this._setHideControlsTimeout();
|
||||
}
|
||||
break;
|
||||
case Gdk.KEY_Right:
|
||||
case Gdk.KEY_Left:
|
||||
value = Math.round(
|
||||
clapperWidget.controls.positionScale.get_value()
|
||||
);
|
||||
this.seek_seconds(value);
|
||||
this._setHideControlsTimeout();
|
||||
break;
|
||||
case Gdk.KEY_F11:
|
||||
case Gdk.KEY_f:
|
||||
case Gdk.KEY_F:
|
||||
clapperWidget.toggleFullscreen();
|
||||
break;
|
||||
case Gdk.KEY_Escape:
|
||||
if(clapperWidget.fullscreenMode) {
|
||||
root = this.widget.get_root();
|
||||
root.unfullscreen();
|
||||
}
|
||||
break;
|
||||
case Gdk.KEY_q:
|
||||
case Gdk.KEY_Q:
|
||||
root = this.widget.get_root();
|
||||
root.emit('close-request');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_onWidgetPressed(gesture, nPress, x, y)
|
||||
{
|
||||
const button = gesture.get_current_button();
|
||||
const isDouble = (nPress % 2 == 0);
|
||||
this.dragAllowed = !isDouble;
|
||||
|
||||
switch(button) {
|
||||
case Gdk.BUTTON_PRIMARY:
|
||||
if(isDouble) {
|
||||
const clapperWidget = this.widget.get_ancestor(Gtk.Grid);
|
||||
clapperWidget.toggleFullscreen();
|
||||
}
|
||||
break;
|
||||
case Gdk.BUTTON_SECONDARY:
|
||||
this.toggle_play();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_onWidgetEnter(controller, x, y)
|
||||
{
|
||||
this.cursorInPlayer = true;
|
||||
this.isWidgetDragging = false;
|
||||
|
||||
this._setHideCursorTimeout();
|
||||
|
||||
const clapperWidget = this.widget.get_ancestor(Gtk.Grid);
|
||||
if(clapperWidget.fullscreenMode || clapperWidget.floatingMode)
|
||||
this._setHideControlsTimeout();
|
||||
}
|
||||
|
||||
_onWidgetLeave(controller)
|
||||
{
|
||||
this.cursorInPlayer = false;
|
||||
|
||||
this._clearTimeout('hideCursor');
|
||||
this._clearTimeout('hideControls');
|
||||
}
|
||||
|
||||
_onWidgetMotion(controller, posX, posY)
|
||||
{
|
||||
this.cursorInPlayer = true;
|
||||
|
||||
/* GTK4 sometimes generates motions with same coords */
|
||||
if(this.posX === posX && this.posY === posY)
|
||||
return;
|
||||
|
||||
/* Do not show cursor on small movements */
|
||||
if(
|
||||
Math.abs(this.posX - posX) >= 0.5
|
||||
|| Math.abs(this.posY - posY) >= 0.5
|
||||
) {
|
||||
const clapperWidget = this.widget.get_ancestor(Gtk.Grid);
|
||||
const defaultCursor = Gdk.Cursor.new_from_name('default', null);
|
||||
|
||||
this.widget.set_cursor(defaultCursor);
|
||||
clapperWidget.revealerTop.set_cursor(defaultCursor);
|
||||
this._setHideCursorTimeout();
|
||||
|
||||
if(clapperWidget.floatingMode && !clapperWidget.fullscreenMode) {
|
||||
clapperWidget.revealerBottom.set_can_focus(false);
|
||||
clapperWidget.revealerBottom.revealChild(true);
|
||||
this._setHideControlsTimeout();
|
||||
}
|
||||
else if(clapperWidget.fullscreenMode) {
|
||||
if(!this._updateTimeTimeout)
|
||||
this._setUpdateTimeInterval();
|
||||
|
||||
if(!clapperWidget.revealerTop.get_reveal_child()) {
|
||||
/* Do not grab controls key focus on mouse movement */
|
||||
clapperWidget.revealerBottom.set_can_focus(false);
|
||||
clapperWidget.revealControls(true);
|
||||
}
|
||||
this._setHideControlsTimeout();
|
||||
}
|
||||
else {
|
||||
if(this._hideControlsTimeout)
|
||||
this._clearTimeout('hideControls');
|
||||
if(this._updateTimeTimeout)
|
||||
this._clearTimeout('updateTime');
|
||||
}
|
||||
}
|
||||
|
||||
this.posX = posX;
|
||||
this.posY = posY;
|
||||
}
|
||||
|
||||
_onWidgetDragUpdate(gesture, offsetX, offsetY)
|
||||
{
|
||||
if(!this.dragAllowed)
|
||||
return;
|
||||
|
||||
const clapperWidget = this.widget.get_ancestor(Gtk.Grid);
|
||||
if(clapperWidget.fullscreenMode)
|
||||
return;
|
||||
|
||||
const { gtk_double_click_distance } = this.widget.get_settings();
|
||||
|
||||
if (
|
||||
Math.abs(offsetX) > gtk_double_click_distance
|
||||
|| Math.abs(offsetY) > gtk_double_click_distance
|
||||
) {
|
||||
const [isActive, startX, startY] = gesture.get_start_point();
|
||||
if(!isActive) return;
|
||||
|
||||
const native = this.widget.get_native();
|
||||
if(!native) return;
|
||||
|
||||
let [isShared, winX, winY] = this.widget.translate_coordinates(
|
||||
native, startX, startY
|
||||
);
|
||||
if(!isShared) return;
|
||||
|
||||
const [nativeX, nativeY] = native.get_surface_transform();
|
||||
winX += nativeX;
|
||||
winY += nativeY;
|
||||
|
||||
this.isWidgetDragging = true;
|
||||
native.get_surface().begin_move(
|
||||
gesture.get_device(),
|
||||
gesture.get_current_button(),
|
||||
winX,
|
||||
winY,
|
||||
gesture.get_current_event_time()
|
||||
);
|
||||
|
||||
gesture.reset();
|
||||
}
|
||||
}
|
||||
|
||||
_onScroll(controller, dx, dy)
|
||||
{
|
||||
const isHorizontal = (Math.abs(dx) >= Math.abs(dy));
|
||||
const isIncrease = (isHorizontal) ? dx < 0 : dy < 0;
|
||||
|
||||
if(isHorizontal) {
|
||||
this.adjust_position(isIncrease);
|
||||
const { controls } = this.widget.get_ancestor(Gtk.Grid);
|
||||
const value = Math.round(controls.positionScale.get_value());
|
||||
this.seek_seconds(value);
|
||||
}
|
||||
else
|
||||
this.adjust_volume(isIncrease);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_onDataDrop(dropTarget, value, x, y)
|
||||
{
|
||||
const playlist = value.split(/\r?\n/).filter(uri => {
|
||||
return Gst.uri_is_valid(uri);
|
||||
});
|
||||
|
||||
if(!playlist.length)
|
||||
return false;
|
||||
|
||||
this.set_playlist(playlist);
|
||||
|
||||
const { application } = this.widget.get_root();
|
||||
application.activate();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_onCloseRequest(window)
|
||||
{
|
||||
this._performCloseCleanup(window);
|
||||
|
||||
if(this.state === GstPlayer.PlayerState.STOPPED)
|
||||
return window.run_dispose();
|
||||
|
||||
this.quitOnStop = true;
|
||||
this.stop();
|
||||
}
|
||||
});
|
315
src/playerBase.js
Normal file
315
src/playerBase.js
Normal file
@@ -0,0 +1,315 @@
|
||||
const { Gio, GLib, GObject, Gst, GstPlayer, Gtk } = imports.gi;
|
||||
const Debug = imports.src.debug;
|
||||
const Misc = imports.src.misc;
|
||||
const { PlaylistWidget } = imports.src.playlist;
|
||||
const { WebApp } = imports.src.webApp;
|
||||
|
||||
const { debug } = Debug;
|
||||
const { settings } = Misc;
|
||||
|
||||
let WebServer;
|
||||
|
||||
var PlayerBase = GObject.registerClass(
|
||||
class ClapperPlayerBase extends GstPlayer.Player
|
||||
{
|
||||
_init()
|
||||
{
|
||||
if(!Gst.is_initialized())
|
||||
Gst.init(null);
|
||||
|
||||
const plugin = 'gtk4glsink';
|
||||
const gtk4glsink = Gst.ElementFactory.make(plugin, null);
|
||||
|
||||
if(!gtk4glsink) {
|
||||
debug(new Error(
|
||||
`Could not load "${plugin}".`
|
||||
+ ' Do you have gstreamer-plugins-good-gtk4 installed?'
|
||||
));
|
||||
}
|
||||
|
||||
const glsinkbin = Gst.ElementFactory.make('glsinkbin', null);
|
||||
glsinkbin.sink = gtk4glsink;
|
||||
|
||||
const context = GLib.MainContext.ref_thread_default();
|
||||
const acquired = context.acquire();
|
||||
debug(`default context acquired: ${acquired}`);
|
||||
|
||||
const dispatcher = new GstPlayer.PlayerGMainContextSignalDispatcher({
|
||||
application_context: context,
|
||||
});
|
||||
const renderer = new GstPlayer.PlayerVideoOverlayVideoRenderer({
|
||||
video_sink: glsinkbin
|
||||
});
|
||||
|
||||
super._init({
|
||||
signal_dispatcher: dispatcher,
|
||||
video_renderer: renderer
|
||||
});
|
||||
|
||||
this.widget = gtk4glsink.widget;
|
||||
this.widget.vexpand = true;
|
||||
this.widget.hexpand = true;
|
||||
|
||||
this.state = GstPlayer.PlayerState.STOPPED;
|
||||
this.visualization_enabled = false;
|
||||
|
||||
this.webserver = null;
|
||||
this.webapp = null;
|
||||
this.playlistWidget = new PlaylistWidget();
|
||||
|
||||
this.set_all_plugins_ranks();
|
||||
this.set_initial_config();
|
||||
this.set_and_bind_settings();
|
||||
|
||||
settings.connect('changed', this._onSettingsKeyChanged.bind(this));
|
||||
|
||||
/* FIXME: additional reference for working around GstPlayer
|
||||
* buggy signal dispatcher on self. Remove when ported to BUS API */
|
||||
this.ref();
|
||||
}
|
||||
|
||||
set_and_bind_settings()
|
||||
{
|
||||
const settingsToSet = [
|
||||
'seeking-mode',
|
||||
'audio-offset',
|
||||
'subtitle-offset',
|
||||
'play-flags',
|
||||
'webserver-enabled'
|
||||
];
|
||||
|
||||
for(let key of settingsToSet)
|
||||
this._onSettingsKeyChanged(settings, key);
|
||||
|
||||
const flag = Gio.SettingsBindFlags.GET;
|
||||
settings.bind('subtitle-font', this.pipeline, 'subtitle_font_desc', flag);
|
||||
}
|
||||
|
||||
set_initial_config()
|
||||
{
|
||||
const gstPlayerConfig = {
|
||||
position_update_interval: 1000,
|
||||
user_agent: 'clapper',
|
||||
};
|
||||
|
||||
for(let option of Object.keys(gstPlayerConfig))
|
||||
this.set_config_option(option, gstPlayerConfig[option]);
|
||||
|
||||
this.set_mute(false);
|
||||
|
||||
/* FIXME: change into option in preferences */
|
||||
const pipeline = this.get_pipeline();
|
||||
pipeline.ring_buffer_max_size = 8 * 1024 * 1024;
|
||||
}
|
||||
|
||||
set_config_option(option, value)
|
||||
{
|
||||
const setOption = GstPlayer.Player[`config_set_${option}`];
|
||||
if(!setOption)
|
||||
return debug(`unsupported option: ${option}`, 'LEVEL_WARNING');
|
||||
|
||||
const config = this.get_config();
|
||||
setOption(config, value);
|
||||
const success = this.set_config(config);
|
||||
|
||||
if(!success)
|
||||
debug(`could not change option: ${option}`);
|
||||
}
|
||||
|
||||
set_all_plugins_ranks()
|
||||
{
|
||||
let data = [];
|
||||
|
||||
/* Set empty plugin list if someone messed it externally */
|
||||
try {
|
||||
data = JSON.parse(settings.get_string('plugin-ranking'));
|
||||
if(!Array.isArray(data))
|
||||
throw new Error('plugin ranking data is not an array!');
|
||||
}
|
||||
catch(err) {
|
||||
debug(err);
|
||||
settings.set_string('plugin-ranking', "[]");
|
||||
}
|
||||
|
||||
for(let plugin of data) {
|
||||
if(!plugin.apply || !plugin.name)
|
||||
continue;
|
||||
|
||||
this.set_plugin_rank(plugin.name, plugin.rank);
|
||||
}
|
||||
}
|
||||
|
||||
set_plugin_rank(name, rank)
|
||||
{
|
||||
const gstRegistry = Gst.Registry.get();
|
||||
const feature = gstRegistry.lookup_feature(name);
|
||||
if(!feature)
|
||||
return debug(`plugin unavailable: ${name}`);
|
||||
|
||||
const oldRank = feature.get_rank();
|
||||
if(rank === oldRank)
|
||||
return;
|
||||
|
||||
feature.set_rank(rank);
|
||||
debug(`changed rank: ${oldRank} -> ${rank} for ${name}`);
|
||||
}
|
||||
|
||||
draw_black(isEnabled)
|
||||
{
|
||||
this.widget.ignore_textures = isEnabled;
|
||||
|
||||
if(this.state !== GstPlayer.PlayerState.PLAYING)
|
||||
this.widget.queue_render();
|
||||
}
|
||||
|
||||
emitWs(action, value)
|
||||
{
|
||||
if(!this.webserver)
|
||||
return;
|
||||
|
||||
this.webserver.sendMessage({ action, value });
|
||||
}
|
||||
|
||||
receiveWs(action, value)
|
||||
{
|
||||
debug(`unhandled WebSocket action: ${action}`);
|
||||
}
|
||||
|
||||
_onSettingsKeyChanged(settings, key)
|
||||
{
|
||||
let root, value, action;
|
||||
|
||||
switch(key) {
|
||||
case 'seeking-mode':
|
||||
const isSeekMode = (typeof this.set_seek_mode !== 'undefined');
|
||||
this.seekingMode = settings.get_string('seeking-mode');
|
||||
switch(this.seekingMode) {
|
||||
case 'fast':
|
||||
if(isSeekMode)
|
||||
this.set_seek_mode(GstPlayer.PlayerSeekMode.FAST);
|
||||
else
|
||||
this.set_config_option('seek_fast', true);
|
||||
break;
|
||||
case 'accurate':
|
||||
if(isSeekMode)
|
||||
this.set_seek_mode(GstPlayer.PlayerSeekMode.ACCURATE);
|
||||
else {
|
||||
this.set_config_option('seek_fast', false);
|
||||
this.set_config_option('seek_accurate', true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if(isSeekMode)
|
||||
this.set_seek_mode(GstPlayer.PlayerSeekMode.DEFAULT);
|
||||
else {
|
||||
this.set_config_option('seek_fast', false);
|
||||
this.set_config_option('seek_accurate', false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'render-shadows':
|
||||
root = this.widget.get_root();
|
||||
/* Editing theme of someone else app is taboo */
|
||||
if(!root || !root.isClapperApp)
|
||||
break;
|
||||
|
||||
const gpuClass = 'gpufriendly';
|
||||
const renderShadows = settings.get_boolean(key);
|
||||
const hasShadows = !root.has_css_class(gpuClass);
|
||||
|
||||
if(renderShadows === hasShadows)
|
||||
break;
|
||||
|
||||
action = (renderShadows) ? 'remove' : 'add';
|
||||
root[action + '_css_class'](gpuClass);
|
||||
break;
|
||||
case 'audio-offset':
|
||||
value = Math.round(settings.get_double(key) * -1000000);
|
||||
this.set_audio_video_offset(value);
|
||||
debug(`set audio-video offset: ${value}`);
|
||||
break;
|
||||
case 'subtitle-offset':
|
||||
value = Math.round(settings.get_double(key) * -1000000);
|
||||
this.set_subtitle_video_offset(value);
|
||||
debug(`set subtitle-video offset: ${value}`);
|
||||
break;
|
||||
case 'dark-theme':
|
||||
case 'brighter-sliders':
|
||||
root = this.widget.get_root();
|
||||
if(!root || !root.isClapperApp)
|
||||
break;
|
||||
|
||||
const brightClass = 'brightscale';
|
||||
const isBrighter = root.has_css_class(brightClass);
|
||||
|
||||
if(key === 'dark-theme' && isBrighter && !settings.get_boolean(key)) {
|
||||
root.remove_css_class(brightClass);
|
||||
debug('remove brighter sliders');
|
||||
break;
|
||||
}
|
||||
|
||||
const setBrighter = settings.get_boolean('brighter-sliders');
|
||||
if(setBrighter === isBrighter)
|
||||
break;
|
||||
|
||||
action = (setBrighter) ? 'add' : 'remove';
|
||||
root[action + '_css_class'](brightClass);
|
||||
debug(`${action} brighter sliders`);
|
||||
break;
|
||||
case 'play-flags':
|
||||
const initialFlags = this.pipeline.flags;
|
||||
const settingsFlags = settings.get_int(key);
|
||||
|
||||
if(initialFlags === settingsFlags)
|
||||
break;
|
||||
|
||||
this.pipeline.flags = settingsFlags;
|
||||
debug(`changed play flags: ${initialFlags} -> ${settingsFlags}`);
|
||||
break;
|
||||
case 'webserver-enabled':
|
||||
case 'webapp-enabled':
|
||||
const webserverEnabled = settings.get_boolean('webserver-enabled');
|
||||
|
||||
if(webserverEnabled) {
|
||||
if(!WebServer) {
|
||||
/* Probably most users will not use this,
|
||||
* so conditional import for faster startup */
|
||||
WebServer = imports.src.webServer.WebServer;
|
||||
}
|
||||
|
||||
if(!this.webserver) {
|
||||
this.webserver = new WebServer(settings.get_int('webserver-port'));
|
||||
this.webserver.passMsgData = this.receiveWs.bind(this);
|
||||
}
|
||||
this.webserver.startListening();
|
||||
|
||||
const webappEnabled = settings.get_boolean('webapp-enabled');
|
||||
|
||||
if(!this.webapp && !webappEnabled)
|
||||
break;
|
||||
|
||||
if(webappEnabled) {
|
||||
if(!this.webapp)
|
||||
this.webapp = new WebApp();
|
||||
|
||||
this.webapp.startDaemonApp(settings.get_int('webapp-port'));
|
||||
}
|
||||
}
|
||||
else if(this.webserver) {
|
||||
/* remote app will close when connection is lost
|
||||
* which will cause the daemon to close too */
|
||||
this.webserver.stopListening();
|
||||
}
|
||||
break;
|
||||
case 'webserver-port':
|
||||
if(!this.webserver)
|
||||
break;
|
||||
|
||||
this.webserver.setListeningPort(settings.get_int(key));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
21
src/playerRemote.js
Normal file
21
src/playerRemote.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const { GObject } = imports.gi;
|
||||
const { WebClient } = imports.src.webClient;
|
||||
|
||||
var PlayerRemote = GObject.registerClass(
|
||||
class ClapperPlayerRemote extends GObject.Object
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init();
|
||||
|
||||
this.webclient = new WebClient();
|
||||
}
|
||||
|
||||
set_playlist(playlist)
|
||||
{
|
||||
this.webclient.sendMessage({
|
||||
action: 'set_playlist',
|
||||
value: playlist
|
||||
});
|
||||
}
|
||||
});
|
207
src/playlist.js
Normal file
207
src/playlist.js
Normal file
@@ -0,0 +1,207 @@
|
||||
const { Gdk, GLib, GObject, Gst, Gtk, Pango } = imports.gi;
|
||||
|
||||
var PlaylistWidget = GObject.registerClass(
|
||||
class ClapperPlaylistWidget extends Gtk.ListBox
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init({
|
||||
selection_mode: Gtk.SelectionMode.NONE,
|
||||
});
|
||||
this.activeRowId = -1;
|
||||
this.connect('row-activated', this._onRowActivated.bind(this));
|
||||
}
|
||||
|
||||
addItem(uri)
|
||||
{
|
||||
const item = new PlaylistItem(uri);
|
||||
this.append(item);
|
||||
}
|
||||
|
||||
removeItem(item)
|
||||
{
|
||||
const itemIndex = item.get_index();
|
||||
|
||||
/* TODO: Handle this case somehow (should app quit?)
|
||||
* or disable remove button */
|
||||
if(itemIndex === this.activeRowId)
|
||||
return;
|
||||
|
||||
if(itemIndex < this.activeRowId)
|
||||
this.activeRowId--;
|
||||
|
||||
this.remove(item);
|
||||
}
|
||||
|
||||
removeAll()
|
||||
{
|
||||
let oldItem;
|
||||
while((oldItem = this.get_row_at_index(0)))
|
||||
this.remove(oldItem);
|
||||
|
||||
this.activeRowId = -1;
|
||||
}
|
||||
|
||||
nextTrack()
|
||||
{
|
||||
const nextRow = this.get_row_at_index(this.activeRowId + 1);
|
||||
if(!nextRow)
|
||||
return false;
|
||||
|
||||
nextRow.activate();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getActiveFilename()
|
||||
{
|
||||
const row = this.get_row_at_index(this.activeRowId);
|
||||
if(!row) return null;
|
||||
|
||||
return row.filename;
|
||||
}
|
||||
|
||||
/* FIXME: Remove once/if GstPlay(er) gets
|
||||
* less vague MediaInfo signals */
|
||||
getActiveIsLocalFile()
|
||||
{
|
||||
const row = this.get_row_at_index(this.activeRowId);
|
||||
if(!row) return null;
|
||||
|
||||
return row.isLocalFile;
|
||||
}
|
||||
|
||||
_onRowActivated(listBox, row)
|
||||
{
|
||||
const { player } = this.get_ancestor(Gtk.Grid);
|
||||
|
||||
this.activeRowId = row.get_index();
|
||||
player.set_uri(row.uri);
|
||||
}
|
||||
});
|
||||
|
||||
let PlaylistItem = GObject.registerClass(
|
||||
class ClapperPlaylistItem extends Gtk.ListBoxRow
|
||||
{
|
||||
_init(uri)
|
||||
{
|
||||
super._init();
|
||||
|
||||
this.uri = uri;
|
||||
this.isLocalFile = false;
|
||||
|
||||
let filename;
|
||||
if(Gst.Uri.get_protocol(uri) === 'file') {
|
||||
filename = GLib.path_get_basename(
|
||||
GLib.filename_from_uri(uri)[0]
|
||||
);
|
||||
this.isLocalFile = true;
|
||||
}
|
||||
this.filename = filename || uri;
|
||||
|
||||
const box = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 6,
|
||||
margin_start: 6,
|
||||
margin_end: 6,
|
||||
height_request: 22,
|
||||
});
|
||||
const icon = new Gtk.Image({
|
||||
icon_name: 'open-menu-symbolic',
|
||||
});
|
||||
const label = new Gtk.Label({
|
||||
label: filename,
|
||||
single_line_mode: true,
|
||||
ellipsize: Pango.EllipsizeMode.END,
|
||||
width_chars: 5,
|
||||
hexpand: true,
|
||||
halign: Gtk.Align.START,
|
||||
});
|
||||
const button = new Gtk.Button({
|
||||
icon_name: 'edit-delete-symbolic',
|
||||
});
|
||||
button.add_css_class('flat');
|
||||
button.add_css_class('circular');
|
||||
button.add_css_class('popoverbutton');
|
||||
button.connect('clicked', this._onRemoveClicked.bind(this));
|
||||
|
||||
box.append(icon);
|
||||
box.append(label);
|
||||
box.append(button);
|
||||
this.set_child(box);
|
||||
|
||||
/* FIXME: D&D inside popover is broken in GTK4
|
||||
const dragSource = new Gtk.DragSource({
|
||||
actions: Gdk.DragAction.MOVE
|
||||
});
|
||||
dragSource.connect('prepare', this._onDragPrepare.bind(this));
|
||||
dragSource.connect('drag-begin', this._onDragBegin.bind(this));
|
||||
dragSource.connect('drag-end', this._onDragEnd.bind(this));
|
||||
this.add_controller(dragSource);
|
||||
|
||||
const dropTarget = new Gtk.DropTarget({
|
||||
actions: Gdk.DragAction.MOVE,
|
||||
preload: true,
|
||||
});
|
||||
dropTarget.set_gtypes([PlaylistItem]);
|
||||
dropTarget.connect('enter', this._onEnter.bind(this));
|
||||
dropTarget.connect('drop', this._onDrop.bind(this));
|
||||
this.add_controller(dropTarget);
|
||||
*/
|
||||
}
|
||||
|
||||
_onRemoveClicked(button)
|
||||
{
|
||||
const listBox = this.get_ancestor(Gtk.ListBox);
|
||||
|
||||
listBox.removeItem(this);
|
||||
}
|
||||
|
||||
_onDragPrepare(source, x, y)
|
||||
{
|
||||
const widget = source.get_widget();
|
||||
const paintable = new Gtk.WidgetPaintable({ widget });
|
||||
const staticImg = paintable.get_current_image();
|
||||
|
||||
source.set_icon(staticImg, x, y);
|
||||
|
||||
return Gdk.ContentProvider.new_for_value(widget);
|
||||
}
|
||||
|
||||
_onDragBegin(source, drag)
|
||||
{
|
||||
this.child.set_opacity(0.3);
|
||||
}
|
||||
|
||||
_onDragEnd(source, drag, deleteData)
|
||||
{
|
||||
this.child.set_opacity(1.0);
|
||||
}
|
||||
|
||||
_onEnter(target, x, y)
|
||||
{
|
||||
return (target.value)
|
||||
? Gdk.DragAction.MOVE
|
||||
: 0;
|
||||
}
|
||||
|
||||
_onDrop(target, value, x, y)
|
||||
{
|
||||
const destIndex = this.get_index();
|
||||
const targetIndex = value.get_index();
|
||||
|
||||
if(destIndex === targetIndex)
|
||||
return true;
|
||||
|
||||
const listBox = this.get_ancestor(Gtk.ListBox);
|
||||
|
||||
if(listBox && destIndex >= 0) {
|
||||
listBox.remove(value);
|
||||
listBox.insert(value, destIndex);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
333
src/prefs.js
Normal file
333
src/prefs.js
Normal file
@@ -0,0 +1,333 @@
|
||||
const { GObject, Gst, Gtk, Pango } = imports.gi;
|
||||
const Misc = imports.src.misc;
|
||||
const PrefsBase = imports.src.prefsBase;
|
||||
|
||||
const { settings } = Misc;
|
||||
|
||||
/* PlayFlags are not exported through GI */
|
||||
Gst.PlayFlags = {
|
||||
VIDEO: 1,
|
||||
AUDIO: 2,
|
||||
TEXT: 4,
|
||||
VIS: 8,
|
||||
SOFT_VOLUME: 16,
|
||||
NATIVE_AUDIO: 32,
|
||||
NATIVE_VIDEO: 64,
|
||||
DOWNLOAD: 128,
|
||||
BUFFERING: 256,
|
||||
DEINTERLACE: 512,
|
||||
SOFT_COLORBALANCE: 1024,
|
||||
FORCE_FILTERS: 2048,
|
||||
FORCE_SW_DECODERS: 4096,
|
||||
};
|
||||
|
||||
var GeneralPage = GObject.registerClass(
|
||||
class ClapperGeneralPage extends PrefsBase.Grid
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init();
|
||||
|
||||
this.addTitle('Startup');
|
||||
this.addCheckButton('Auto enter fullscreen', 'fullscreen-auto');
|
||||
|
||||
this.addTitle('Volume');
|
||||
const comboBox = this.addComboBoxText('Initial value', [
|
||||
['restore', "Restore"],
|
||||
['custom', "Custom"],
|
||||
], 'volume-initial');
|
||||
const spinButton = this.addSpinButton('Value (percentage)', 0, 200, 'volume-value');
|
||||
this._onVolumeInitialChanged(spinButton, comboBox);
|
||||
comboBox.connect('changed', this._onVolumeInitialChanged.bind(this, spinButton));
|
||||
|
||||
this.addTitle('Finish');
|
||||
this.addCheckButton('Close after playback', 'close-auto');
|
||||
}
|
||||
|
||||
_onVolumeInitialChanged(spinButton, comboBox)
|
||||
{
|
||||
const value = comboBox.get_active_id();
|
||||
spinButton.set_visible(value === 'custom');
|
||||
}
|
||||
});
|
||||
|
||||
var BehaviourPage = GObject.registerClass(
|
||||
class ClapperBehaviourPage extends PrefsBase.Grid
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init();
|
||||
|
||||
this.addTitle('Seeking');
|
||||
this.addComboBoxText('Mode', [
|
||||
['normal', "Normal"],
|
||||
['accurate', "Accurate"],
|
||||
['fast', "Fast"],
|
||||
], 'seeking-mode');
|
||||
this.addComboBoxText('Unit', [
|
||||
['second', "Second"],
|
||||
['minute', "Minute"],
|
||||
['percentage', "Percentage"],
|
||||
], 'seeking-unit');
|
||||
this.addSpinButton('Value', 1, 99, 'seeking-value');
|
||||
}
|
||||
});
|
||||
|
||||
var AudioPage = GObject.registerClass(
|
||||
class ClapperAudioPage extends PrefsBase.Grid
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init();
|
||||
|
||||
this.addTitle('Synchronization');
|
||||
this.addSpinButton('Offset (milliseconds)', -1000, 1000, 'audio-offset', 25);
|
||||
|
||||
this.addTitle('Processing');
|
||||
this.addPlayFlagCheckButton('Only use native audio formats', Gst.PlayFlags.NATIVE_AUDIO);
|
||||
}
|
||||
});
|
||||
|
||||
var SubtitlesPage = GObject.registerClass(
|
||||
class ClapperSubtitlesPage extends PrefsBase.Grid
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init();
|
||||
|
||||
/* FIXME: This should be moved to subtitles popup and displayed only when
|
||||
external subtitles were added for easier customization per video. */
|
||||
//this.addTitle('Synchronization');
|
||||
//this.addSpinButton('Offset (milliseconds)', -5000, 5000, 'subtitle-offset', 25);
|
||||
|
||||
this.addTitle('External Subtitles');
|
||||
this.addFontButton('Default font', 'subtitle-font');
|
||||
}
|
||||
});
|
||||
|
||||
var NetworkPage = GObject.registerClass(
|
||||
class ClapperNetworkPage extends PrefsBase.Grid
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init();
|
||||
|
||||
this.addTitle('Client');
|
||||
this.addPlayFlagCheckButton('Progressive download buffering', Gst.PlayFlags.DOWNLOAD);
|
||||
|
||||
this.addTitle('Server');
|
||||
const webServer = this.addCheckButton('Control player remotely', 'webserver-enabled');
|
||||
const serverPort = this.addSpinButton('Listening port', 1024, 65535, 'webserver-port');
|
||||
webServer.bind_property('active', serverPort, 'visible', GObject.BindingFlags.SYNC_CREATE);
|
||||
const webApp = this.addCheckButton('Start built-in web application', 'webapp-enabled');
|
||||
webServer.bind_property('active', webApp, 'visible', GObject.BindingFlags.SYNC_CREATE);
|
||||
const webAppPort = this.addSpinButton('Web application port', 1024, 65535, 'webapp-port');
|
||||
webServer.bind_property('active', webAppPort, 'visible', GObject.BindingFlags.SYNC_CREATE);
|
||||
}
|
||||
});
|
||||
|
||||
var GStreamerPage = GObject.registerClass(
|
||||
class ClapperGStreamerPage extends PrefsBase.Grid
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init();
|
||||
|
||||
this.addTitle('Plugin Ranking');
|
||||
const listStore = new Gtk.ListStore();
|
||||
listStore.set_column_types([
|
||||
GObject.TYPE_BOOLEAN,
|
||||
GObject.TYPE_STRING,
|
||||
GObject.TYPE_STRING,
|
||||
]);
|
||||
const treeView = new Gtk.TreeView({
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
enable_search: false,
|
||||
model: listStore,
|
||||
});
|
||||
const treeSelection = treeView.get_selection();
|
||||
|
||||
const apply = new Gtk.TreeViewColumn({
|
||||
title: "Apply",
|
||||
});
|
||||
const name = new Gtk.TreeViewColumn({
|
||||
title: "Plugin",
|
||||
expand: true,
|
||||
});
|
||||
const rank = new Gtk.TreeViewColumn({
|
||||
title: "Rank",
|
||||
min_width: 90,
|
||||
});
|
||||
|
||||
const applyCell = new Gtk.CellRendererToggle();
|
||||
const nameCell = new Gtk.CellRendererText({
|
||||
editable: true,
|
||||
placeholder_text: "Insert plugin name",
|
||||
});
|
||||
const rankCell = new Gtk.CellRendererText({
|
||||
editable: true,
|
||||
weight: Pango.Weight.BOLD,
|
||||
placeholder_text: "Insert plugin rank",
|
||||
});
|
||||
|
||||
apply.pack_start(applyCell, true);
|
||||
name.pack_start(nameCell, true);
|
||||
rank.pack_start(rankCell, true);
|
||||
|
||||
apply.add_attribute(applyCell, 'active', 0);
|
||||
name.add_attribute(nameCell, 'text', 1);
|
||||
rank.add_attribute(rankCell, 'text', 2);
|
||||
|
||||
treeView.insert_column(apply, 0);
|
||||
treeView.insert_column(name, 1);
|
||||
treeView.insert_column(rank, 2);
|
||||
|
||||
const frame = new Gtk.Frame({
|
||||
child: treeView
|
||||
});
|
||||
this.addToGrid(frame);
|
||||
|
||||
const addButton = new Gtk.Button({
|
||||
icon_name: 'list-add-symbolic',
|
||||
halign: Gtk.Align.END,
|
||||
});
|
||||
const removeButton = new Gtk.Button({
|
||||
icon_name: 'list-remove-symbolic',
|
||||
sensitive: false,
|
||||
halign: Gtk.Align.END,
|
||||
});
|
||||
const label = new Gtk.Label({
|
||||
label: 'Changes require player restart',
|
||||
halign: Gtk.Align.START,
|
||||
hexpand: true,
|
||||
ellipsize: Pango.EllipsizeMode.END,
|
||||
});
|
||||
const box = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 6,
|
||||
hexpand: true,
|
||||
});
|
||||
box.append(label);
|
||||
box.append(removeButton);
|
||||
box.append(addButton);
|
||||
this.addToGrid(box);
|
||||
|
||||
applyCell.connect('toggled', this._onApplyCellEdited.bind(this));
|
||||
nameCell.connect('edited', this._onNameCellEdited.bind(this));
|
||||
rankCell.connect('edited', this._onRankCellEdited.bind(this));
|
||||
addButton.connect('clicked', this._onAddButtonClicked.bind(this, listStore));
|
||||
removeButton.connect('clicked', this._onRemoveButtonClicked.bind(this, listStore));
|
||||
treeSelection.connect('changed', this._onTreeSelectionChanged.bind(this, removeButton));
|
||||
|
||||
this.settingsChangedSignal = settings.connect(
|
||||
'changed::plugin-ranking', this.refreshListStore.bind(this, listStore)
|
||||
);
|
||||
|
||||
this.refreshListStore(listStore);
|
||||
}
|
||||
|
||||
refreshListStore(listStore)
|
||||
{
|
||||
const data = JSON.parse(settings.get_string('plugin-ranking'));
|
||||
listStore.clear();
|
||||
|
||||
for(let plugin of data) {
|
||||
listStore.set(
|
||||
listStore.append(),
|
||||
[0, 1, 2], [
|
||||
plugin.apply || false,
|
||||
plugin.name || '',
|
||||
plugin.rank || 0
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
updatePlugin(index, prop, value)
|
||||
{
|
||||
const data = JSON.parse(settings.get_string('plugin-ranking'));
|
||||
data[index][prop] = value;
|
||||
settings.set_string('plugin-ranking', JSON.stringify(data));
|
||||
}
|
||||
|
||||
_onTreeSelectionChanged(removeButton, treeSelection)
|
||||
{
|
||||
const [isSelected, model, iter] = treeSelection.get_selected();
|
||||
this.activeIndex = -1;
|
||||
|
||||
if(isSelected) {
|
||||
this.activeIndex = Number(model.get_string_from_iter(iter));
|
||||
}
|
||||
|
||||
removeButton.set_sensitive(this.activeIndex >= 0);
|
||||
}
|
||||
|
||||
_onAddButtonClicked(listStore, button)
|
||||
{
|
||||
const data = JSON.parse(settings.get_string('plugin-ranking'));
|
||||
data.push({
|
||||
apply: false,
|
||||
name: '',
|
||||
rank: 0,
|
||||
});
|
||||
settings.set_string('plugin-ranking', JSON.stringify(data));
|
||||
}
|
||||
|
||||
_onRemoveButtonClicked(listStore, button)
|
||||
{
|
||||
if(this.activeIndex < 0)
|
||||
return;
|
||||
|
||||
const data = JSON.parse(settings.get_string('plugin-ranking'));
|
||||
data.splice(this.activeIndex, 1);
|
||||
settings.set_string('plugin-ranking', JSON.stringify(data));
|
||||
}
|
||||
|
||||
_onApplyCellEdited(cell, path)
|
||||
{
|
||||
const newState = !cell.active;
|
||||
this.updatePlugin(path, 'apply', newState);
|
||||
}
|
||||
|
||||
_onNameCellEdited(cell, path, newText)
|
||||
{
|
||||
newText = newText.trim();
|
||||
this.updatePlugin(path, 'name', newText);
|
||||
}
|
||||
|
||||
_onRankCellEdited(cell, path, newText)
|
||||
{
|
||||
newText = newText.trim();
|
||||
|
||||
if(isNaN(newText))
|
||||
newText = 0;
|
||||
|
||||
this.updatePlugin(path, 'rank', Number(newText));
|
||||
}
|
||||
|
||||
_onClose()
|
||||
{
|
||||
super._onClose('gstreamer');
|
||||
|
||||
settings.disconnect(this.settingsChangedSignal);
|
||||
this.settingsChangedSignal = null;
|
||||
}
|
||||
});
|
||||
|
||||
var TweaksPage = GObject.registerClass(
|
||||
class ClapperTweaksPage extends PrefsBase.Grid
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init();
|
||||
|
||||
this.addTitle('Appearance');
|
||||
const darkCheck = this.addCheckButton('Enable dark theme', 'dark-theme');
|
||||
const brighterCheck = this.addCheckButton('Make sliders brighter', 'brighter-sliders');
|
||||
darkCheck.bind_property('active', brighterCheck, 'visible', GObject.BindingFlags.SYNC_CREATE);
|
||||
|
||||
this.addTitle('Performance');
|
||||
this.addCheckButton('Render window shadows', 'render-shadows');
|
||||
}
|
||||
});
|
238
src/prefsBase.js
Normal file
238
src/prefsBase.js
Normal file
@@ -0,0 +1,238 @@
|
||||
const { Gio, GObject, Gtk } = imports.gi;
|
||||
const Debug = imports.src.debug;
|
||||
const Misc = imports.src.misc;
|
||||
|
||||
const { debug } = Debug;
|
||||
const { settings } = Misc;
|
||||
|
||||
var Notebook = GObject.registerClass(
|
||||
class ClapperPrefsNotebook extends Gtk.Notebook
|
||||
{
|
||||
_init(pages, isSubpage)
|
||||
{
|
||||
super._init({
|
||||
show_border: false,
|
||||
vexpand: true,
|
||||
hexpand: true,
|
||||
});
|
||||
|
||||
if(isSubpage) {
|
||||
this.set_tab_pos(Gtk.PositionType.LEFT);
|
||||
this.add_css_class('prefssubpage');
|
||||
}
|
||||
|
||||
this.addArrayPages(pages);
|
||||
}
|
||||
|
||||
addArrayPages(array)
|
||||
{
|
||||
for(let obj of array)
|
||||
this.addObjectPages(obj);
|
||||
}
|
||||
|
||||
addObjectPages(item)
|
||||
{
|
||||
const widget = (item.pages)
|
||||
? new Notebook(item.pages, true)
|
||||
: new item.widget();
|
||||
|
||||
this.addToNotebook(widget, item.title);
|
||||
}
|
||||
|
||||
addToNotebook(widget, title)
|
||||
{
|
||||
const label = new Gtk.Label({
|
||||
label: title,
|
||||
});
|
||||
this.append_page(widget, label);
|
||||
}
|
||||
|
||||
_onClose()
|
||||
{
|
||||
const totalPages = this.get_n_pages();
|
||||
let index = 0;
|
||||
|
||||
while(index < totalPages) {
|
||||
const page = this.get_nth_page(index);
|
||||
page._onClose();
|
||||
index++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var Grid = GObject.registerClass(
|
||||
class ClapperPrefsGrid extends Gtk.Grid
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init({
|
||||
row_spacing: 6,
|
||||
column_spacing: 20,
|
||||
});
|
||||
|
||||
this.flag = Gio.SettingsBindFlags.DEFAULT;
|
||||
this.gridIndex = 0;
|
||||
this.widgetDefaults = {
|
||||
width_request: 160,
|
||||
halign: Gtk.Align.END,
|
||||
valign: Gtk.Align.CENTER,
|
||||
};
|
||||
}
|
||||
|
||||
addToGrid(leftWidget, rightWidget)
|
||||
{
|
||||
let spanWidth = 2;
|
||||
|
||||
if(rightWidget) {
|
||||
spanWidth = 1;
|
||||
rightWidget.bind_property('visible', leftWidget, 'visible',
|
||||
GObject.BindingFlags.SYNC_CREATE
|
||||
);
|
||||
this.attach(rightWidget, 1, this.gridIndex, 1, 1);
|
||||
}
|
||||
|
||||
this.attach(leftWidget, 0, this.gridIndex, spanWidth, 1);
|
||||
this.gridIndex++;
|
||||
|
||||
return rightWidget || leftWidget;
|
||||
}
|
||||
|
||||
addTitle(text)
|
||||
{
|
||||
const label = this.getLabel(text, true);
|
||||
|
||||
return this.addToGrid(label);
|
||||
}
|
||||
|
||||
addComboBoxText(text, entries, setting)
|
||||
{
|
||||
const label = this.getLabel(text + ':');
|
||||
const widget = this.getComboBoxText(entries, setting);
|
||||
|
||||
return this.addToGrid(label, widget);
|
||||
}
|
||||
|
||||
addSpinButton(text, min, max, setting, precision)
|
||||
{
|
||||
const label = this.getLabel(text + ':');
|
||||
const widget = this.getSpinButton(min, max, setting, precision);
|
||||
|
||||
return this.addToGrid(label, widget);
|
||||
}
|
||||
|
||||
addCheckButton(text, setting)
|
||||
{
|
||||
const widget = this.getCheckButton(text, setting);
|
||||
|
||||
return this.addToGrid(widget);
|
||||
}
|
||||
|
||||
addPlayFlagCheckButton(text, flag)
|
||||
{
|
||||
const checkButton = this.addCheckButton(text);
|
||||
const playFlags = settings.get_int('play-flags');
|
||||
|
||||
checkButton.active = ((playFlags & flag) === flag);
|
||||
checkButton.connect('toggled', this._onPlayFlagToggled.bind(this, flag));
|
||||
|
||||
return checkButton;
|
||||
}
|
||||
|
||||
addFontButton(text, setting)
|
||||
{
|
||||
const label = this.getLabel(text + ':');
|
||||
const widget = this.getFontButton(setting);
|
||||
|
||||
return this.addToGrid(label, widget);
|
||||
}
|
||||
|
||||
getLabel(text, isTitle)
|
||||
{
|
||||
const marginTop = (isTitle && this.gridIndex > 0) ? 16 : 0;
|
||||
const marginBottom = (isTitle) ? 2 : 0;
|
||||
|
||||
let marginLR = 0;
|
||||
|
||||
if(isTitle)
|
||||
text = '<span font="12"><b>' + text + '</b></span>';
|
||||
else
|
||||
marginLR = 12;
|
||||
|
||||
return new Gtk.Label({
|
||||
label: text,
|
||||
use_markup: true,
|
||||
hexpand: true,
|
||||
halign: Gtk.Align.START,
|
||||
margin_top: marginTop,
|
||||
margin_bottom: marginBottom,
|
||||
margin_start: marginLR,
|
||||
margin_end: marginLR,
|
||||
});
|
||||
}
|
||||
|
||||
getComboBoxText(entries, setting)
|
||||
{
|
||||
const comboBox = new Gtk.ComboBoxText(this.widgetDefaults);
|
||||
|
||||
for(let entry of entries)
|
||||
comboBox.append(entry[0], entry[1]);
|
||||
|
||||
settings.bind(setting, comboBox, 'active-id', this.flag);
|
||||
|
||||
return comboBox;
|
||||
}
|
||||
|
||||
getSpinButton(min, max, setting, precision)
|
||||
{
|
||||
precision = precision || 1;
|
||||
|
||||
const spinButton = new Gtk.SpinButton(this.widgetDefaults);
|
||||
spinButton.set_range(min, max);
|
||||
spinButton.set_digits(precision % 1 === 0 ? 0 : 3);
|
||||
spinButton.set_increments(precision, 1);
|
||||
settings.bind(setting, spinButton, 'value', this.flag);
|
||||
|
||||
return spinButton;
|
||||
}
|
||||
|
||||
getCheckButton(text, setting)
|
||||
{
|
||||
const checkButton = new Gtk.CheckButton({
|
||||
label: text || null,
|
||||
});
|
||||
|
||||
if(setting)
|
||||
settings.bind(setting, checkButton, 'active', this.flag);
|
||||
|
||||
return checkButton;
|
||||
}
|
||||
|
||||
getFontButton(setting)
|
||||
{
|
||||
const fontButton = new Gtk.FontButton({
|
||||
use_font: true,
|
||||
use_size: true,
|
||||
});
|
||||
settings.bind(setting, fontButton, 'font', this.flag);
|
||||
|
||||
return fontButton;
|
||||
}
|
||||
|
||||
_onPlayFlagToggled(flag, button)
|
||||
{
|
||||
let playFlags = settings.get_int('play-flags');
|
||||
|
||||
if(button.active)
|
||||
playFlags |= flag;
|
||||
else
|
||||
playFlags &= ~flag;
|
||||
|
||||
settings.set_int('play-flags', playFlags);
|
||||
}
|
||||
|
||||
_onClose(name)
|
||||
{
|
||||
if(name)
|
||||
debug(`cleanup of prefs ${name} page`);
|
||||
}
|
||||
});
|
301
src/revealers.js
Normal file
301
src/revealers.js
Normal file
@@ -0,0 +1,301 @@
|
||||
const { GLib, GObject, Gtk, Pango } = imports.gi;
|
||||
const Debug = imports.src.debug;
|
||||
|
||||
const REVEAL_TIME = 800;
|
||||
|
||||
const { debug } = Debug;
|
||||
|
||||
var CustomRevealer = GObject.registerClass(
|
||||
class ClapperCustomRevealer extends Gtk.Revealer
|
||||
{
|
||||
_init(opts)
|
||||
{
|
||||
opts = opts || {};
|
||||
|
||||
const defaults = {
|
||||
visible: false,
|
||||
can_focus: false,
|
||||
};
|
||||
Object.assign(opts, defaults);
|
||||
|
||||
super._init(opts);
|
||||
|
||||
this.revealerName = '';
|
||||
}
|
||||
|
||||
revealChild(isReveal)
|
||||
{
|
||||
if(isReveal) {
|
||||
this._clearTimeout();
|
||||
this.set_visible(isReveal);
|
||||
}
|
||||
else
|
||||
this._setHideTimeout();
|
||||
|
||||
/* Restore focusability after we are done */
|
||||
if(!isReveal) this.set_can_focus(true);
|
||||
|
||||
this._timedReveal(isReveal, REVEAL_TIME);
|
||||
}
|
||||
|
||||
showChild(isReveal)
|
||||
{
|
||||
this._clearTimeout();
|
||||
this.set_visible(isReveal);
|
||||
this._timedReveal(isReveal, 0);
|
||||
}
|
||||
|
||||
set_visible(isVisible)
|
||||
{
|
||||
if(this.visible === isVisible)
|
||||
return false;
|
||||
|
||||
super.set_visible(isVisible);
|
||||
debug(`${this.revealerName} revealer visible: ${isVisible}`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_timedReveal(isReveal, time)
|
||||
{
|
||||
this.set_transition_duration(time);
|
||||
this.set_reveal_child(isReveal);
|
||||
}
|
||||
|
||||
/* Drawing revealers on top of video frames
|
||||
* increases CPU usage, so we hide them */
|
||||
_setHideTimeout()
|
||||
{
|
||||
this._clearTimeout();
|
||||
|
||||
this._revealerTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, REVEAL_TIME + 20, () => {
|
||||
this._revealerTimeout = null;
|
||||
this.set_visible(false);
|
||||
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
}
|
||||
|
||||
_clearTimeout()
|
||||
{
|
||||
if(!this._revealerTimeout)
|
||||
return;
|
||||
|
||||
GLib.source_remove(this._revealerTimeout);
|
||||
this._revealerTimeout = null;
|
||||
}
|
||||
});
|
||||
|
||||
var RevealerTop = GObject.registerClass(
|
||||
class ClapperRevealerTop extends CustomRevealer
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init({
|
||||
transition_duration: REVEAL_TIME,
|
||||
transition_type: Gtk.RevealerTransitionType.CROSSFADE,
|
||||
valign: Gtk.Align.START,
|
||||
});
|
||||
this.revealerName = 'top';
|
||||
|
||||
const initTime = GLib.DateTime.new_now_local().format('%X');
|
||||
this.timeFormat = (initTime.length > 8)
|
||||
? '%I:%M %p'
|
||||
: '%H:%M';
|
||||
|
||||
this.revealerGrid = new Gtk.Grid({
|
||||
column_spacing: 8
|
||||
});
|
||||
this.revealerGrid.add_css_class('osd');
|
||||
this.revealerGrid.add_css_class('reavealertop');
|
||||
|
||||
this.mediaTitle = new Gtk.Label({
|
||||
ellipsize: Pango.EllipsizeMode.END,
|
||||
vexpand: true,
|
||||
hexpand: true,
|
||||
margin_top: 14,
|
||||
margin_start: 12,
|
||||
xalign: 0,
|
||||
yalign: 0,
|
||||
});
|
||||
|
||||
const timeLabelOpts = {
|
||||
margin_end: 10,
|
||||
xalign: 1,
|
||||
yalign: 0,
|
||||
};
|
||||
this.currentTime = new Gtk.Label(timeLabelOpts);
|
||||
this.currentTime.add_css_class('osdtime');
|
||||
|
||||
timeLabelOpts.visible = false;
|
||||
this.endTime = new Gtk.Label(timeLabelOpts);
|
||||
this.endTime.add_css_class('osdendtime');
|
||||
|
||||
this.revealerGrid.attach(this.mediaTitle, 0, 0, 1, 1);
|
||||
this.revealerGrid.attach(this.currentTime, 1, 0, 1, 1);
|
||||
this.revealerGrid.attach(this.endTime, 1, 0, 1, 1);
|
||||
|
||||
this.set_child(this.revealerGrid);
|
||||
}
|
||||
|
||||
setMediaTitle(title)
|
||||
{
|
||||
this.mediaTitle.label = title;
|
||||
}
|
||||
|
||||
setTimes(currTime, endTime)
|
||||
{
|
||||
const now = currTime.format(this.timeFormat);
|
||||
const end = endTime.format(this.timeFormat);
|
||||
const endText = `Ends at: ${end}`;
|
||||
|
||||
this.currentTime.set_label(now);
|
||||
this.endTime.set_label(endText);
|
||||
|
||||
/* Make sure that next timeout is always run after clock changes,
|
||||
* by delaying it for additional few milliseconds */
|
||||
const nextUpdate = 60002 - parseInt(currTime.get_seconds() * 1000);
|
||||
debug(`updated current time: ${now}, ends at: ${end}`);
|
||||
|
||||
return nextUpdate;
|
||||
}
|
||||
});
|
||||
|
||||
var RevealerBottom = GObject.registerClass(
|
||||
class ClapperRevealerBottom extends CustomRevealer
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init({
|
||||
transition_duration: REVEAL_TIME,
|
||||
transition_type: Gtk.RevealerTransitionType.SLIDE_UP,
|
||||
valign: Gtk.Align.END,
|
||||
});
|
||||
|
||||
this.revealerName = 'bottom';
|
||||
this.revealerBox = new Gtk.Box();
|
||||
this.revealerBox.add_css_class('osd');
|
||||
|
||||
this.set_child(this.revealerBox);
|
||||
}
|
||||
|
||||
append(widget)
|
||||
{
|
||||
this.revealerBox.append(widget);
|
||||
}
|
||||
|
||||
remove(widget)
|
||||
{
|
||||
this.revealerBox.remove(widget);
|
||||
}
|
||||
|
||||
setFloatingClass(isFloating)
|
||||
{
|
||||
if(isFloating === this.revealerBox.has_css_class('floatingcontrols'))
|
||||
return;
|
||||
|
||||
const action = (isFloating) ? 'add' : 'remove';
|
||||
this.revealerBox[`${action}_css_class`]('floatingcontrols');
|
||||
}
|
||||
|
||||
set_visible(isVisible)
|
||||
{
|
||||
const isChange = super.set_visible(isVisible);
|
||||
if(!isChange || !this.can_focus) return;
|
||||
|
||||
const parent = this.get_parent();
|
||||
const playerWidget = parent.get_first_child();
|
||||
if(!playerWidget) return;
|
||||
|
||||
if(isVisible) {
|
||||
const box = this.get_first_child();
|
||||
if(!box) return;
|
||||
|
||||
const controls = box.get_first_child();
|
||||
if(!controls) return;
|
||||
|
||||
const togglePlayButton = controls.get_first_child();
|
||||
if(togglePlayButton) {
|
||||
togglePlayButton.grab_focus();
|
||||
debug('focus moved to toggle play button');
|
||||
}
|
||||
playerWidget.set_can_focus(false);
|
||||
}
|
||||
else {
|
||||
playerWidget.set_can_focus(true);
|
||||
playerWidget.grab_focus();
|
||||
debug('focus moved to player widget');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var ButtonsRevealer = GObject.registerClass(
|
||||
class ClapperButtonsRevealer extends Gtk.Revealer
|
||||
{
|
||||
_init(trType, toggleButton)
|
||||
{
|
||||
super._init({
|
||||
transition_duration: 500,
|
||||
transition_type: Gtk.RevealerTransitionType[trType],
|
||||
});
|
||||
|
||||
const revealerBox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
});
|
||||
this.set_child(revealerBox);
|
||||
|
||||
if(toggleButton) {
|
||||
toggleButton.connect('clicked', this._onToggleButtonClicked.bind(this));
|
||||
this.connect('notify::reveal-child', this._onRevealChild.bind(this, toggleButton));
|
||||
this.connect('notify::child-revealed', this._onChildRevealed.bind(this, toggleButton));
|
||||
}
|
||||
}
|
||||
|
||||
set_reveal_child(isReveal)
|
||||
{
|
||||
if(this.reveal_child === isReveal)
|
||||
return;
|
||||
|
||||
const grandson = this.child.get_first_child();
|
||||
if(grandson && grandson.isFloating && !grandson.isFullscreen)
|
||||
return;
|
||||
|
||||
super.set_reveal_child(isReveal);
|
||||
}
|
||||
|
||||
append(widget)
|
||||
{
|
||||
this.get_child().append(widget);
|
||||
}
|
||||
|
||||
_setRotateClass(icon, isAdd)
|
||||
{
|
||||
const cssClass = 'halfrotate';
|
||||
const hasClass = icon.has_css_class(cssClass);
|
||||
|
||||
if(!hasClass && isAdd)
|
||||
icon.add_css_class(cssClass);
|
||||
else if(hasClass && !isAdd)
|
||||
icon.remove_css_class(cssClass);
|
||||
}
|
||||
|
||||
_onToggleButtonClicked(button)
|
||||
{
|
||||
this.set_reveal_child(!this.reveal_child);
|
||||
}
|
||||
|
||||
_onRevealChild(button)
|
||||
{
|
||||
this._setRotateClass(button.child, true);
|
||||
}
|
||||
|
||||
_onChildRevealed(button)
|
||||
{
|
||||
if(!this.child_revealed)
|
||||
button.setPrimaryIcon();
|
||||
else
|
||||
button.setSecondaryIcon();
|
||||
|
||||
this._setRotateClass(button.child, false);
|
||||
}
|
||||
});
|
49
src/webApp.js
Normal file
49
src/webApp.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const { Gio, GObject } = imports.gi;
|
||||
const Debug = imports.src.debug;
|
||||
const Misc = imports.src.misc;
|
||||
|
||||
const { debug } = Debug;
|
||||
|
||||
var WebApp = GObject.registerClass(
|
||||
class ClapperWebApp extends Gio.SubprocessLauncher
|
||||
{
|
||||
_init()
|
||||
{
|
||||
const flags = Gio.SubprocessFlags.STDOUT_SILENCE
|
||||
| Gio.SubprocessFlags.STDERR_SILENCE;
|
||||
|
||||
super._init({ flags });
|
||||
|
||||
this.daemonApp = null;
|
||||
}
|
||||
|
||||
startDaemonApp(port)
|
||||
{
|
||||
if(this.daemonApp)
|
||||
return;
|
||||
|
||||
this.daemonApp = this.spawnv([Misc.appId + '.Daemon', String(port)]);
|
||||
this.daemonApp.wait_async(null, this._onDaemonClosed.bind(this));
|
||||
|
||||
debug('daemon app started');
|
||||
}
|
||||
|
||||
_onDaemonClosed(proc, result)
|
||||
{
|
||||
let hadError;
|
||||
|
||||
try {
|
||||
hadError = proc.wait_finish(result);
|
||||
}
|
||||
catch(err) {
|
||||
debug(err);
|
||||
}
|
||||
|
||||
this.daemonApp = null;
|
||||
|
||||
if(hadError)
|
||||
debug('daemon app exited with error or was forced to close');
|
||||
|
||||
debug('daemon app closed');
|
||||
}
|
||||
});
|
88
src/webClient.js
Normal file
88
src/webClient.js
Normal file
@@ -0,0 +1,88 @@
|
||||
const { Gio, GObject, Soup } = imports.gi;
|
||||
const Debug = imports.src.debug;
|
||||
const Misc = imports.src.misc;
|
||||
const WebHelpers = imports.src.webHelpers;
|
||||
|
||||
const { debug } = Debug;
|
||||
const { settings } = Misc;
|
||||
|
||||
var WebClient = GObject.registerClass(
|
||||
class ClapperWebClient extends Soup.Session
|
||||
{
|
||||
_init(port)
|
||||
{
|
||||
super._init({
|
||||
timeout: 3,
|
||||
use_thread_context: true,
|
||||
});
|
||||
|
||||
this.wsConn = null;
|
||||
|
||||
this.connectWebsocket();
|
||||
}
|
||||
|
||||
connectWebsocket()
|
||||
{
|
||||
if(this.wsConn)
|
||||
return;
|
||||
|
||||
const port = settings.get_int('webserver-port');
|
||||
const message = Soup.Message.new('GET', `ws://127.0.0.1:${port}/websocket`);
|
||||
this.websocket_connect_async(message, null, null, null, this._onWsConnect.bind(this));
|
||||
|
||||
debug('connecting WebSocket to Clapper app');
|
||||
}
|
||||
|
||||
sendMessage(data)
|
||||
{
|
||||
if(
|
||||
!this.wsConn
|
||||
|| this.wsConn.state !== Soup.WebsocketState.OPEN
|
||||
)
|
||||
return;
|
||||
|
||||
this.wsConn.send_text(JSON.stringify(data));
|
||||
}
|
||||
|
||||
passMsgData(action, value)
|
||||
{
|
||||
}
|
||||
|
||||
_onWsConnect(session, result)
|
||||
{
|
||||
let connection = null;
|
||||
|
||||
try {
|
||||
connection = this.websocket_connect_finish(result);
|
||||
}
|
||||
catch(err) {
|
||||
debug(err);
|
||||
}
|
||||
|
||||
if(!connection)
|
||||
return this.passMsgData('close');
|
||||
|
||||
connection.connect('message', this._onWsMessage.bind(this));
|
||||
connection.connect('closed', this._onWsClosed.bind(this));
|
||||
|
||||
this.wsConn = connection;
|
||||
|
||||
debug('successfully connected WebSocket');
|
||||
}
|
||||
|
||||
_onWsMessage(connection, dataType, bytes)
|
||||
{
|
||||
const [success, parsedMsg] = WebHelpers.parseData(dataType, bytes);
|
||||
|
||||
if(success)
|
||||
this.passMsgData(parsedMsg.action, parsedMsg.value);
|
||||
}
|
||||
|
||||
_onWsClosed(connection)
|
||||
{
|
||||
debug('closed WebSocket connection');
|
||||
this.wsConn = null;
|
||||
|
||||
this.passMsgData('close');
|
||||
}
|
||||
});
|
30
src/webHelpers.js
Normal file
30
src/webHelpers.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const { Soup } = imports.gi;
|
||||
const ByteArray = imports.byteArray;
|
||||
const Debug = imports.src.debug;
|
||||
|
||||
const { debug } = Debug;
|
||||
|
||||
function parseData(dataType, bytes)
|
||||
{
|
||||
if(dataType !== Soup.WebsocketDataType.TEXT) {
|
||||
debug('ignoring non-text WebSocket message');
|
||||
return [false];
|
||||
}
|
||||
|
||||
let parsedMsg = null;
|
||||
const msg = bytes.get_data();
|
||||
|
||||
try {
|
||||
parsedMsg = JSON.parse(ByteArray.toString(msg));
|
||||
}
|
||||
catch(err) {
|
||||
debug(err);
|
||||
}
|
||||
|
||||
if(!parsedMsg || !parsedMsg.action) {
|
||||
debug('no "action" in parsed WebSocket message');
|
||||
return [false];
|
||||
}
|
||||
|
||||
return [true, parsedMsg];
|
||||
}
|
139
src/webServer.js
Normal file
139
src/webServer.js
Normal file
@@ -0,0 +1,139 @@
|
||||
const { Soup, GObject } = imports.gi;
|
||||
const Debug = imports.src.debug;
|
||||
const WebHelpers = imports.src.webHelpers;
|
||||
|
||||
const { debug } = Debug;
|
||||
|
||||
var WebServer = GObject.registerClass(
|
||||
class ClapperWebServer extends Soup.Server
|
||||
{
|
||||
_init(port)
|
||||
{
|
||||
super._init();
|
||||
|
||||
this.isListening = false;
|
||||
this.listeningPort = null;
|
||||
this.wsConns = [];
|
||||
|
||||
if(port)
|
||||
this.setListeningPort(port);
|
||||
}
|
||||
|
||||
setListeningPort(port)
|
||||
{
|
||||
if(!port)
|
||||
return;
|
||||
|
||||
const wasListening = this.isListening;
|
||||
|
||||
if(wasListening)
|
||||
this.stopListening();
|
||||
|
||||
this.listeningPort = port;
|
||||
|
||||
if(wasListening)
|
||||
this.startListening();
|
||||
}
|
||||
|
||||
startListening()
|
||||
{
|
||||
if(this.isListening || !this.listeningPort)
|
||||
return;
|
||||
|
||||
let isListening = false;
|
||||
|
||||
this.add_handler('/', this._onDefaultAccess.bind(this));
|
||||
this.add_websocket_handler('/websocket', null, null, this._onWsConnection.bind(this));
|
||||
|
||||
try {
|
||||
isListening = this.listen_local(this.listeningPort, Soup.ServerListenOptions.IPV4_ONLY);
|
||||
}
|
||||
catch(err) {
|
||||
debug(err);
|
||||
}
|
||||
|
||||
if(isListening) {
|
||||
const uris = this.get_uris();
|
||||
const usedPort = uris[0].get_port();
|
||||
debug(`WebSocket server started listening on port: ${usedPort}`);
|
||||
}
|
||||
else {
|
||||
debug(new Error('WebSocket server could not start listening'));
|
||||
this._closeCleanup();
|
||||
}
|
||||
|
||||
this.isListening = isListening;
|
||||
}
|
||||
|
||||
stopListening()
|
||||
{
|
||||
if(!this.isListening)
|
||||
return;
|
||||
|
||||
this._closeCleanup();
|
||||
this.disconnect();
|
||||
|
||||
this.isListening = false;
|
||||
}
|
||||
|
||||
sendMessage(data)
|
||||
{
|
||||
for(const connection of this.wsConns) {
|
||||
if(connection.state !== Soup.WebsocketState.OPEN)
|
||||
continue;
|
||||
|
||||
connection.send_text(JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
|
||||
passMsgData(action, value)
|
||||
{
|
||||
}
|
||||
|
||||
_closeCleanup()
|
||||
{
|
||||
while(this.wsConns.length) {
|
||||
const connection = this.wsConns.pop();
|
||||
|
||||
if(connection.state !== Soup.WebsocketState.OPEN)
|
||||
continue;
|
||||
|
||||
connection.close(Soup.WebsocketCloseCode.NORMAL, null);
|
||||
}
|
||||
|
||||
this.remove_handler('/websocket');
|
||||
this.remove_handler('/');
|
||||
}
|
||||
|
||||
_onWsConnection(server, connection)
|
||||
{
|
||||
debug('new WebSocket connection');
|
||||
|
||||
connection.connect('message', this._onWsMessage.bind(this));
|
||||
connection.connect('closed', this._onWsClosed.bind(this));
|
||||
|
||||
this.wsConns.push(connection);
|
||||
debug(`total WebSocket connections: ${this.wsConns.length}`);
|
||||
}
|
||||
|
||||
_onWsMessage(connection, dataType, bytes)
|
||||
{
|
||||
const [success, parsedMsg] = WebHelpers.parseData(dataType, bytes);
|
||||
|
||||
if(success)
|
||||
this.passMsgData(parsedMsg.action, parsedMsg.value);
|
||||
}
|
||||
|
||||
_onWsClosed(connection)
|
||||
{
|
||||
debug('closed WebSocket connection');
|
||||
|
||||
this.wsConns = this.wsConns.filter(conn => conn !== connection);
|
||||
debug(`remaining WebSocket connections: ${this.wsConns.length}`);
|
||||
}
|
||||
|
||||
_onDefaultAccess(server, msg)
|
||||
{
|
||||
msg.status_code = 404;
|
||||
}
|
||||
});
|
574
src/widget.js
Normal file
574
src/widget.js
Normal file
@@ -0,0 +1,574 @@
|
||||
const { Gdk, GLib, GObject, GstPlayer, Gtk } = imports.gi;
|
||||
const { Controls } = imports.src.controls;
|
||||
const Debug = imports.src.debug;
|
||||
const Misc = imports.src.misc;
|
||||
const { Player } = imports.src.player;
|
||||
const Revealers = imports.src.revealers;
|
||||
|
||||
const { debug } = Debug;
|
||||
const { settings } = Misc;
|
||||
|
||||
var Widget = GObject.registerClass(
|
||||
class ClapperWidget extends Gtk.Grid
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init();
|
||||
|
||||
/* load CSS here to allow using this class
|
||||
* separately as a pre-made GTK widget */
|
||||
Misc.loadCustomCss();
|
||||
|
||||
this.windowSize = JSON.parse(settings.get_string('window-size'));
|
||||
this.floatSize = JSON.parse(settings.get_string('float-size'));
|
||||
|
||||
this.fullscreenMode = false;
|
||||
this.floatingMode = false;
|
||||
this.isSeekable = false;
|
||||
|
||||
this.needsTracksUpdate = true;
|
||||
|
||||
this.overlay = new Gtk.Overlay();
|
||||
this.revealerTop = new Revealers.RevealerTop();
|
||||
this.revealerBottom = new Revealers.RevealerBottom();
|
||||
this.controls = new Controls();
|
||||
|
||||
this.controlsBox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
});
|
||||
this.controlsBox.add_css_class('controlsbox');
|
||||
this.controlsBox.append(this.controls);
|
||||
|
||||
this.attach(this.overlay, 0, 0, 1, 1);
|
||||
this.attach(this.controlsBox, 0, 1, 1, 1);
|
||||
|
||||
this.mapSignal = this.connect('map', this._onMap.bind(this));
|
||||
|
||||
this.player = new Player();
|
||||
this.controls.elapsedButton.scrolledWindow.set_child(this.player.playlistWidget);
|
||||
this.controls.speedAdjustment.bind_property(
|
||||
'value', this.player, 'rate', GObject.BindingFlags.BIDIRECTIONAL
|
||||
);
|
||||
|
||||
this.player.connect('position-updated', this._onPlayerPositionUpdated.bind(this));
|
||||
this.player.connect('duration-changed', this._onPlayerDurationChanged.bind(this));
|
||||
|
||||
/* FIXME: re-enable once ported to new GstPlayer API with messages bus */
|
||||
//this.player.connect('volume-changed', this._onPlayerVolumeChanged.bind(this));
|
||||
|
||||
this.overlay.set_child(this.player.widget);
|
||||
this.overlay.add_overlay(this.revealerTop);
|
||||
this.overlay.add_overlay(this.revealerBottom);
|
||||
|
||||
const motionController = new Gtk.EventControllerMotion();
|
||||
motionController.connect('leave', this._onLeave.bind(this));
|
||||
this.add_controller(motionController);
|
||||
|
||||
const topClickGesture = new Gtk.GestureClick();
|
||||
topClickGesture.set_button(0);
|
||||
topClickGesture.connect('pressed', this.player._onWidgetPressed.bind(this.player));
|
||||
this.revealerTop.add_controller(topClickGesture);
|
||||
|
||||
const topMotionController = new Gtk.EventControllerMotion();
|
||||
topMotionController.connect('motion', this.player._onWidgetMotion.bind(this.player));
|
||||
this.revealerTop.add_controller(topMotionController);
|
||||
|
||||
const topScrollController = new Gtk.EventControllerScroll();
|
||||
topScrollController.set_flags(Gtk.EventControllerScrollFlags.BOTH_AXES);
|
||||
topScrollController.connect('scroll', this.player._onScroll.bind(this.player));
|
||||
this.revealerTop.add_controller(topScrollController);
|
||||
}
|
||||
|
||||
revealControls(isReveal)
|
||||
{
|
||||
for(let pos of ['Top', 'Bottom'])
|
||||
this[`revealer${pos}`].revealChild(isReveal);
|
||||
}
|
||||
|
||||
showControls(isShow)
|
||||
{
|
||||
for(let pos of ['Top', 'Bottom'])
|
||||
this[`revealer${pos}`].showChild(isShow);
|
||||
}
|
||||
|
||||
toggleFullscreen()
|
||||
{
|
||||
const root = this.get_root();
|
||||
if(!root) return;
|
||||
|
||||
const un = (this.fullscreenMode) ? 'un' : '';
|
||||
root[`${un}fullscreen`]();
|
||||
}
|
||||
|
||||
setFullscreenMode(isFullscreen)
|
||||
{
|
||||
if(this.fullscreenMode === isFullscreen)
|
||||
return;
|
||||
|
||||
this.fullscreenMode = isFullscreen;
|
||||
|
||||
const root = this.get_root();
|
||||
const action = (isFullscreen) ? 'add' : 'remove';
|
||||
root[action + '_css_class']('gpufriendlyfs');
|
||||
|
||||
if(!this.floatingMode)
|
||||
this._changeControlsPlacement(isFullscreen);
|
||||
else {
|
||||
this._setWindowFloating(!isFullscreen);
|
||||
this.revealerBottom.setFloatingClass(!isFullscreen);
|
||||
this.controls.setFloatingMode(!isFullscreen);
|
||||
this.controls.unfloatButton.set_visible(!isFullscreen);
|
||||
}
|
||||
|
||||
this.controls.setFullscreenMode(isFullscreen);
|
||||
this.showControls(isFullscreen);
|
||||
this.player.widget.grab_focus();
|
||||
|
||||
if(this.player.playOnFullscreen && isFullscreen) {
|
||||
this.player.playOnFullscreen = false;
|
||||
this.player.play();
|
||||
}
|
||||
}
|
||||
|
||||
setFloatingMode(isFloating)
|
||||
{
|
||||
if(this.floatingMode === isFloating)
|
||||
return;
|
||||
|
||||
const root = this.get_root();
|
||||
const size = root.get_default_size();
|
||||
|
||||
this._saveWindowSize(size);
|
||||
|
||||
if(isFloating) {
|
||||
this.windowSize = size;
|
||||
this.player.widget.set_size_request(192, 108);
|
||||
}
|
||||
else {
|
||||
this.floatSize = size;
|
||||
this.player.widget.set_size_request(-1, -1);
|
||||
}
|
||||
|
||||
this.floatingMode = isFloating;
|
||||
|
||||
this.revealerBottom.setFloatingClass(isFloating);
|
||||
this._changeControlsPlacement(isFloating);
|
||||
this.controls.setFloatingMode(isFloating);
|
||||
this.controls.unfloatButton.set_visible(isFloating);
|
||||
this._setWindowFloating(isFloating);
|
||||
|
||||
const resize = (isFloating)
|
||||
? this.floatSize
|
||||
: this.windowSize;
|
||||
|
||||
root.set_default_size(resize[0], resize[1]);
|
||||
debug(`resized window: ${resize[0]}x${resize[1]}`);
|
||||
|
||||
this.revealerBottom.showChild(false);
|
||||
this.player.widget.grab_focus();
|
||||
}
|
||||
|
||||
_setWindowFloating(isFloating)
|
||||
{
|
||||
const root = this.get_root();
|
||||
const cssClass = 'floatingwindow';
|
||||
|
||||
if(isFloating === root.has_css_class(cssClass))
|
||||
return;
|
||||
|
||||
const action = (isFloating) ? 'add' : 'remove';
|
||||
root[action + '_css_class'](cssClass);
|
||||
}
|
||||
|
||||
_saveWindowSize(size)
|
||||
{
|
||||
const rootName = (this.floatingMode)
|
||||
? 'float'
|
||||
: 'window';
|
||||
|
||||
settings.set_string(`${rootName}-size`, JSON.stringify(size));
|
||||
debug(`saved ${rootName} size: ${size[0]}x${size[1]}`);
|
||||
}
|
||||
|
||||
_changeControlsPlacement(isOnTop)
|
||||
{
|
||||
if(isOnTop) {
|
||||
this.controlsBox.remove(this.controls);
|
||||
this.revealerBottom.append(this.controls);
|
||||
}
|
||||
else {
|
||||
this.revealerBottom.remove(this.controls);
|
||||
this.controlsBox.append(this.controls);
|
||||
}
|
||||
|
||||
this.controlsBox.set_visible(!isOnTop);
|
||||
}
|
||||
|
||||
_updateMediaInfo()
|
||||
{
|
||||
const mediaInfo = this.player.get_media_info();
|
||||
if(!mediaInfo)
|
||||
return GLib.SOURCE_REMOVE;
|
||||
|
||||
/* Set titlebar media title and path */
|
||||
this.updateTitles(mediaInfo);
|
||||
|
||||
/* Show/hide position scale on LIVE */
|
||||
const isLive = mediaInfo.is_live();
|
||||
this.isSeekable = mediaInfo.is_seekable();
|
||||
this.controls.setLiveMode(isLive, this.isSeekable);
|
||||
|
||||
if(this.player.needsTocUpdate) {
|
||||
/* FIXME: Remove `get_toc` check after required GstPlay(er) ver bump */
|
||||
if(!isLive && mediaInfo.get_toc)
|
||||
this.updateChapters(mediaInfo.get_toc());
|
||||
|
||||
this.player.needsTocUpdate = false;
|
||||
}
|
||||
|
||||
const streamList = mediaInfo.get_stream_list();
|
||||
const parsedInfo = {
|
||||
videoTracks: [],
|
||||
audioTracks: [],
|
||||
subtitleTracks: []
|
||||
};
|
||||
|
||||
for(let info of streamList) {
|
||||
let type, text, codec;
|
||||
|
||||
switch(info.constructor) {
|
||||
case GstPlayer.PlayerVideoInfo:
|
||||
type = 'video';
|
||||
codec = info.get_codec() || 'Undetermined';
|
||||
text = codec + ', ' +
|
||||
+ info.get_width() + 'x'
|
||||
+ info.get_height();
|
||||
let fps = info.get_framerate();
|
||||
fps = Number((fps[0] / fps[1]).toFixed(2));
|
||||
if(fps)
|
||||
text += `@${fps}`;
|
||||
break;
|
||||
case GstPlayer.PlayerAudioInfo:
|
||||
type = 'audio';
|
||||
codec = info.get_codec() || 'Undetermined';
|
||||
if(codec.includes('(')) {
|
||||
codec = codec.substring(
|
||||
codec.indexOf('(') + 1,
|
||||
codec.indexOf(')')
|
||||
);
|
||||
}
|
||||
text = info.get_language() || 'Undetermined';
|
||||
text += ', ' + codec + ', '
|
||||
+ info.get_channels() + ' Channels';
|
||||
break;
|
||||
case GstPlayer.PlayerSubtitleInfo:
|
||||
type = 'subtitle';
|
||||
text = info.get_language() || 'Undetermined';
|
||||
break;
|
||||
default:
|
||||
debug(`unrecognized media info type: ${info.constructor}`);
|
||||
break;
|
||||
}
|
||||
const tracksArr = parsedInfo[`${type}Tracks`];
|
||||
if(!tracksArr.length)
|
||||
{
|
||||
tracksArr[0] = {
|
||||
label: 'Disabled',
|
||||
type: type,
|
||||
activeId: -1
|
||||
};
|
||||
}
|
||||
tracksArr.push({
|
||||
label: text,
|
||||
type: type,
|
||||
activeId: info.get_index(),
|
||||
});
|
||||
}
|
||||
|
||||
let anyButtonShown = false;
|
||||
|
||||
for(let type of ['video', 'audio', 'subtitle']) {
|
||||
const currStream = this.player[`get_current_${type}_track`]();
|
||||
const activeId = (currStream) ? currStream.get_index() : -1;
|
||||
|
||||
if(currStream && type !== 'subtitle') {
|
||||
const caps = currStream.get_caps();
|
||||
debug(`${type} caps: ${caps.to_string()}`, 'LEVEL_INFO');
|
||||
}
|
||||
if(type === 'video') {
|
||||
const isShowVis = (parsedInfo[`${type}Tracks`].length === 0);
|
||||
this.showVisualizationsButton(isShowVis);
|
||||
}
|
||||
if(!parsedInfo[`${type}Tracks`].length) {
|
||||
debug(`hiding popover button without contents: ${type}`);
|
||||
this.controls[`${type}TracksButton`].set_visible(false);
|
||||
|
||||
continue;
|
||||
}
|
||||
this.controls.addCheckButtons(
|
||||
this.controls[`${type}TracksButton`].popoverBox,
|
||||
parsedInfo[`${type}Tracks`],
|
||||
activeId
|
||||
);
|
||||
debug(`showing popover button with contents: ${type}`);
|
||||
this.controls[`${type}TracksButton`].set_visible(true);
|
||||
|
||||
anyButtonShown = true;
|
||||
}
|
||||
this.controls.revealTracksRevealer.set_visible(anyButtonShown);
|
||||
|
||||
return GLib.SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
updateTitles(mediaInfo)
|
||||
{
|
||||
let title = mediaInfo.get_title();
|
||||
let subtitle = this.player.playlistWidget.getActiveFilename();
|
||||
|
||||
if(!title) {
|
||||
title = (subtitle.includes('.'))
|
||||
? subtitle.split('.').slice(0, -1).join('.')
|
||||
: subtitle;
|
||||
|
||||
subtitle = null;
|
||||
}
|
||||
|
||||
const root = this.get_root();
|
||||
const headerbar = root.get_titlebar();
|
||||
|
||||
if(headerbar && headerbar.updateHeaderBar)
|
||||
headerbar.updateHeaderBar(title, subtitle);
|
||||
|
||||
this.revealerTop.setMediaTitle(title);
|
||||
}
|
||||
|
||||
updateTime()
|
||||
{
|
||||
if(!this.revealerTop.visible)
|
||||
return null;
|
||||
|
||||
const currTime = GLib.DateTime.new_now_local();
|
||||
const endTime = currTime.add_seconds(
|
||||
this.controls.positionAdjustment.get_upper() - this.controls.currentPosition
|
||||
);
|
||||
const nextUpdate = this.revealerTop.setTimes(currTime, endTime);
|
||||
|
||||
return nextUpdate;
|
||||
}
|
||||
|
||||
updateChapters(toc)
|
||||
{
|
||||
if(!toc) return;
|
||||
|
||||
const entries = toc.get_entries();
|
||||
if(!entries) return;
|
||||
|
||||
for(let entry of entries) {
|
||||
const subentries = entry.get_sub_entries();
|
||||
if(!subentries) continue;
|
||||
|
||||
for(let subentry of subentries)
|
||||
this._parseTocSubentry(subentry);
|
||||
}
|
||||
}
|
||||
|
||||
_parseTocSubentry(subentry)
|
||||
{
|
||||
const [success, start, stop] = subentry.get_start_stop_times();
|
||||
if(!success) {
|
||||
debug('could not obtain toc subentry start/stop times');
|
||||
return;
|
||||
}
|
||||
|
||||
const pos = Math.floor(start / 1000000) / 1000;
|
||||
const tags = subentry.get_tags();
|
||||
|
||||
this.controls.positionScale.add_mark(pos, Gtk.PositionType.TOP, null);
|
||||
this.controls.positionScale.add_mark(pos, Gtk.PositionType.BOTTOM, null);
|
||||
|
||||
if(!tags) {
|
||||
debug('could not obtain toc subentry tags');
|
||||
return;
|
||||
}
|
||||
|
||||
const [isString, title] = tags.get_string('title');
|
||||
if(!isString) {
|
||||
debug('toc subentry tag does not have a title');
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this.controls.chapters)
|
||||
this.controls.chapters = {};
|
||||
|
||||
this.controls.chapters[pos] = title;
|
||||
debug(`chapter at ${pos}: ${title}`);
|
||||
}
|
||||
|
||||
showVisualizationsButton(isShow)
|
||||
{
|
||||
if(isShow && !this.controls.visualizationsButton.isVisList) {
|
||||
debug('creating visualizations list');
|
||||
const visArr = GstPlayer.Player.visualizations_get();
|
||||
if(!visArr.length)
|
||||
return;
|
||||
|
||||
const parsedVisArr = [{
|
||||
label: 'Disabled',
|
||||
type: 'visualization',
|
||||
activeId: null
|
||||
}];
|
||||
|
||||
visArr.forEach(vis => {
|
||||
parsedVisArr.push({
|
||||
label: vis.name[0].toUpperCase() + vis.name.substring(1),
|
||||
type: 'visualization',
|
||||
activeId: vis.name,
|
||||
});
|
||||
});
|
||||
|
||||
this.controls.addCheckButtons(
|
||||
this.controls.visualizationsButton.popoverBox,
|
||||
parsedVisArr,
|
||||
null
|
||||
);
|
||||
this.controls.visualizationsButton.isVisList = true;
|
||||
debug(`total visualizations: ${visArr.length}`);
|
||||
}
|
||||
|
||||
if(this.controls.visualizationsButton.visible === isShow)
|
||||
return;
|
||||
|
||||
const action = (isShow) ? 'show' : 'hide';
|
||||
this.controls.visualizationsButton[action]();
|
||||
debug(`show visualizations button: ${isShow}`);
|
||||
}
|
||||
|
||||
_onPlayerStateChanged(player, state)
|
||||
{
|
||||
switch(state) {
|
||||
case GstPlayer.PlayerState.BUFFERING:
|
||||
debug('player state changed to: BUFFERING');
|
||||
if(player.needsTocUpdate) {
|
||||
this.controls._setChapterVisible(false);
|
||||
this.controls.positionScale.clear_marks();
|
||||
this.controls.chapters = null;
|
||||
}
|
||||
if(!player.playlistWidget.getActiveIsLocalFile()) {
|
||||
this.needsTracksUpdate = true;
|
||||
}
|
||||
break;
|
||||
case GstPlayer.PlayerState.STOPPED:
|
||||
debug('player state changed to: STOPPED');
|
||||
this.controls.currentPosition = 0;
|
||||
this.controls.positionScale.set_value(0);
|
||||
this.controls.togglePlayButton.setPrimaryIcon();
|
||||
this.needsTracksUpdate = true;
|
||||
break;
|
||||
case GstPlayer.PlayerState.PAUSED:
|
||||
debug('player state changed to: PAUSED');
|
||||
this.controls.togglePlayButton.setPrimaryIcon();
|
||||
break;
|
||||
case GstPlayer.PlayerState.PLAYING:
|
||||
debug('player state changed to: PLAYING');
|
||||
this.controls.togglePlayButton.setSecondaryIcon();
|
||||
if(this.needsTracksUpdate) {
|
||||
this.needsTracksUpdate = false;
|
||||
GLib.idle_add(
|
||||
GLib.PRIORITY_DEFAULT_IDLE,
|
||||
this._updateMediaInfo.bind(this)
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const isNotStopped = (state !== GstPlayer.PlayerState.STOPPED);
|
||||
this.revealerTop.endTime.set_visible(isNotStopped);
|
||||
}
|
||||
|
||||
_onPlayerDurationChanged(player)
|
||||
{
|
||||
const duration = Math.floor(player.get_duration() / 1000000000);
|
||||
|
||||
/* Sometimes GstPlayer might re-emit
|
||||
* duration changed during playback */
|
||||
if(this.controls.currentDuration === duration)
|
||||
return;
|
||||
|
||||
this.controls.currentDuration = duration;
|
||||
this.controls.showHours = (duration >= 3600);
|
||||
|
||||
this.controls.positionAdjustment.set_upper(duration);
|
||||
this.controls.durationFormatted = Misc.getFormattedTime(duration);
|
||||
this.controls.updateElapsedLabel();
|
||||
}
|
||||
|
||||
_onPlayerPositionUpdated(player, position)
|
||||
{
|
||||
if(
|
||||
!this.isSeekable
|
||||
|| this.controls.isPositionDragging
|
||||
|| !player.seek_done
|
||||
)
|
||||
return;
|
||||
|
||||
const positionSeconds = Math.round(position / 1000000000);
|
||||
if(positionSeconds === this.controls.currentPosition)
|
||||
return;
|
||||
|
||||
this.controls.positionScale.set_value(positionSeconds);
|
||||
}
|
||||
|
||||
_onPlayerVolumeChanged(player)
|
||||
{
|
||||
const volume = player.get_volume();
|
||||
|
||||
/* FIXME: This check should not be needed, GstPlayer should not
|
||||
* emit 'volume-changed' with the same values, but it does. */
|
||||
if(volume === this.controls.currentVolume)
|
||||
return;
|
||||
|
||||
/* Once above is fixed in GstPlayer, remove this var too */
|
||||
this.controls.currentVolume = volume;
|
||||
|
||||
const cubicVolume = Misc.getCubicValue(volume);
|
||||
this.controls._updateVolumeButtonIcon(cubicVolume);
|
||||
}
|
||||
|
||||
_onStateNotify(toplevel)
|
||||
{
|
||||
const isFullscreen = Boolean(
|
||||
toplevel.state & Gdk.ToplevelState.FULLSCREEN
|
||||
);
|
||||
|
||||
if(this.fullscreenMode === isFullscreen)
|
||||
return;
|
||||
|
||||
this.setFullscreenMode(isFullscreen);
|
||||
debug(`interface in fullscreen mode: ${isFullscreen}`);
|
||||
}
|
||||
|
||||
_onLeave(controller)
|
||||
{
|
||||
if(
|
||||
this.fullscreenMode
|
||||
|| !this.floatingMode
|
||||
|| this.player.isWidgetDragging
|
||||
)
|
||||
return;
|
||||
|
||||
this.revealerBottom.revealChild(false);
|
||||
}
|
||||
|
||||
_onMap()
|
||||
{
|
||||
this.disconnect(this.mapSignal);
|
||||
|
||||
const root = this.get_root();
|
||||
if(!root) return;
|
||||
|
||||
const surface = root.get_surface();
|
||||
surface.connect('notify::state', this._onStateNotify.bind(this));
|
||||
}
|
||||
});
|
72
src/widgetRemote.js
Normal file
72
src/widgetRemote.js
Normal file
@@ -0,0 +1,72 @@
|
||||
const { GObject, Gtk, GstPlayer } = imports.gi;
|
||||
const Buttons = imports.src.buttons;
|
||||
const Misc = imports.src.misc;
|
||||
const { PlayerRemote } = imports.src.playerRemote;
|
||||
|
||||
var WidgetRemote = GObject.registerClass(
|
||||
class ClapperWidgetRemote extends Gtk.Grid
|
||||
{
|
||||
_init(opts)
|
||||
{
|
||||
super._init({
|
||||
halign: Gtk.Align.CENTER,
|
||||
valign: Gtk.Align.CENTER,
|
||||
});
|
||||
|
||||
Misc.loadCustomCss();
|
||||
|
||||
this.player = new PlayerRemote();
|
||||
this.player.webclient.passMsgData = this.receiveWs.bind(this);
|
||||
|
||||
/* FIXME: create better way to add buttons for
|
||||
* remote app without duplicating too much code */
|
||||
this.togglePlayButton = new Buttons.IconToggleButton(
|
||||
'media-playback-start-symbolic',
|
||||
'media-playback-pause-symbolic'
|
||||
);
|
||||
this.togglePlayButton.remove_css_class('flat');
|
||||
this.togglePlayButton.child.add_css_class('playbackicon');
|
||||
this.togglePlayButton.connect(
|
||||
'clicked', this.sendWs.bind(this, 'toggle_play')
|
||||
);
|
||||
|
||||
this.attach(this.togglePlayButton, 0, 0, 1, 1);
|
||||
}
|
||||
|
||||
sendWs(action, value)
|
||||
{
|
||||
const data = { action };
|
||||
|
||||
/* do not send "null" or "undefined"
|
||||
* for faster network data transfer */
|
||||
if(value != null)
|
||||
data.value = value;
|
||||
|
||||
this.player.webclient.sendMessage(data);
|
||||
}
|
||||
|
||||
receiveWs(action, value)
|
||||
{
|
||||
switch(action) {
|
||||
case 'state_changed':
|
||||
switch(value) {
|
||||
case GstPlayer.PlayerState.STOPPED:
|
||||
case GstPlayer.PlayerState.PAUSED:
|
||||
this.togglePlayButton.setPrimaryIcon();
|
||||
break;
|
||||
case GstPlayer.PlayerState.PLAYING:
|
||||
this.togglePlayButton.setSecondaryIcon();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'close':
|
||||
const root = this.get_root();
|
||||
root.run_dispose();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
Reference in New Issue
Block a user