mirror of
https://github.com/Rafostar/clapper.git
synced 2025-08-30 16:02:00 +02:00
Merge pull request #29 from Rafostar/remote-controller
Control player remotely
This commit is contained in:
@@ -9,4 +9,4 @@ Package.init({
|
||||
libdir: '@libdir@',
|
||||
datadir: '@datadir@',
|
||||
});
|
||||
Package.run(imports.main);
|
||||
Package.run(imports.clapper_src.main);
|
||||
|
12
bin/com.github.rafostar.ClapperRemote.in
Normal file
12
bin/com.github.rafostar.ClapperRemote.in
Normal file
@@ -0,0 +1,12 @@
|
||||
#!@GJS@
|
||||
|
||||
const Package = imports.package;
|
||||
|
||||
Package.init({
|
||||
name: '@PACKAGE_NAME@',
|
||||
version: '@PACKAGE_VERSION@',
|
||||
prefix: '@prefix@',
|
||||
libdir: '@libdir@',
|
||||
datadir: '@datadir@',
|
||||
});
|
||||
Package.run(imports.clapper_src.mainRemote);
|
@@ -6,11 +6,15 @@ bin_conf.set('prefix', get_option('prefix'))
|
||||
bin_conf.set('libdir', join_paths(get_option('prefix'), get_option('libdir')))
|
||||
bin_conf.set('datadir', join_paths(get_option('prefix'), get_option('datadir')))
|
||||
|
||||
configure_file(
|
||||
input: 'com.github.rafostar.Clapper.in',
|
||||
output: 'com.github.rafostar.Clapper',
|
||||
configuration: bin_conf,
|
||||
install: true,
|
||||
install_dir: get_option('bindir'),
|
||||
install_mode: 'rwxr-xr-x'
|
||||
)
|
||||
clapper_apps = [ '', 'Remote' ]
|
||||
|
||||
foreach id_postfix : clapper_apps
|
||||
configure_file(
|
||||
input: 'com.github.rafostar.Clapper' + id_postfix + '.in',
|
||||
output: 'com.github.rafostar.Clapper' + id_postfix,
|
||||
configuration: bin_conf,
|
||||
install: true,
|
||||
install_dir: get_option('bindir'),
|
||||
install_mode: 'rwxr-xr-x'
|
||||
)
|
||||
endforeach
|
||||
|
@@ -1,136 +1,39 @@
|
||||
const { Gio, GObject, Gtk } = imports.gi;
|
||||
const { GObject } = imports.gi;
|
||||
const { AppBase } = imports.clapper_src.appBase;
|
||||
const { HeaderBar } = imports.clapper_src.headerbar;
|
||||
const { Widget } = imports.clapper_src.widget;
|
||||
const Debug = imports.clapper_src.debug;
|
||||
const Menu = imports.clapper_src.menu;
|
||||
const Misc = imports.clapper_src.misc;
|
||||
|
||||
let { debug } = Debug;
|
||||
let { settings } = Misc;
|
||||
|
||||
var App = GObject.registerClass(
|
||||
class ClapperApp extends Gtk.Application
|
||||
class ClapperApp extends AppBase
|
||||
{
|
||||
_init(opts)
|
||||
{
|
||||
super._init({
|
||||
application_id: Misc.appId
|
||||
});
|
||||
|
||||
let defaults = {
|
||||
playlist: [],
|
||||
};
|
||||
Object.assign(this, defaults, opts);
|
||||
|
||||
this.doneFirstActivate = false;
|
||||
}
|
||||
|
||||
vfunc_startup()
|
||||
{
|
||||
super.vfunc_startup();
|
||||
|
||||
let window = new Gtk.ApplicationWindow({
|
||||
application: this,
|
||||
title: Misc.appName,
|
||||
});
|
||||
window.isClapperApp = true;
|
||||
window.add_css_class('nobackground');
|
||||
|
||||
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) {
|
||||
let simpleAction = new Gio.SimpleAction({
|
||||
name: action
|
||||
});
|
||||
simpleAction.connect(
|
||||
'activate', () => Menu.actions[action](this.active_window)
|
||||
);
|
||||
this.add_action(simpleAction);
|
||||
}
|
||||
this.active_window.isClapperApp = true;
|
||||
this.active_window.add_css_class('nobackground');
|
||||
|
||||
let clapperWidget = new Widget();
|
||||
window.set_child(clapperWidget);
|
||||
this.active_window.set_child(clapperWidget);
|
||||
|
||||
let headerBar = new HeaderBar(this.active_window);
|
||||
this.active_window.set_titlebar(headerBar);
|
||||
|
||||
let size = clapperWidget.windowSize;
|
||||
window.set_default_size(size[0], size[1]);
|
||||
this.active_window.set_default_size(size[0], size[1]);
|
||||
debug(`restored window size: ${size[0]}x${size[1]}`);
|
||||
|
||||
let clapperPath = Misc.getClapperPath();
|
||||
let uiBuilder = Gtk.Builder.new_from_file(
|
||||
`${clapperPath}/ui/clapper.ui`
|
||||
);
|
||||
let models = {
|
||||
addMediaMenu: uiBuilder.get_object('addMediaMenu'),
|
||||
settingsMenu: uiBuilder.get_object('settingsMenu'),
|
||||
};
|
||||
let headerBar = new HeaderBar(window, models);
|
||||
window.set_titlebar(headerBar);
|
||||
}
|
||||
|
||||
vfunc_activate()
|
||||
{
|
||||
super.vfunc_activate();
|
||||
|
||||
if(!this.doneFirstActivate)
|
||||
this._onFirstActivate();
|
||||
|
||||
this.active_window.present();
|
||||
}
|
||||
|
||||
run(arr)
|
||||
{
|
||||
super.run(arr || []);
|
||||
}
|
||||
|
||||
_onFirstActivate()
|
||||
{
|
||||
let 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;
|
||||
super._onWindowShow(window);
|
||||
|
||||
if(this.playlist.length) {
|
||||
let { player } = window.get_child();
|
||||
player.set_playlist(this.playlist);
|
||||
}
|
||||
}
|
||||
|
||||
_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}`);
|
||||
}
|
||||
});
|
||||
|
112
clapper_src/appBase.js
Normal file
112
clapper_src/appBase.js
Normal file
@@ -0,0 +1,112 @@
|
||||
const { Gio, GObject, Gtk } = imports.gi;
|
||||
const Debug = imports.clapper_src.debug;
|
||||
const Menu = imports.clapper_src.menu;
|
||||
const Misc = imports.clapper_src.misc;
|
||||
|
||||
let { debug } = Debug;
|
||||
let { settings } = Misc;
|
||||
|
||||
var AppBase = GObject.registerClass(
|
||||
class ClapperAppBase extends Gtk.Application
|
||||
{
|
||||
_init(opts)
|
||||
{
|
||||
opts = opts || {};
|
||||
|
||||
let defaults = {
|
||||
idPostfix: '',
|
||||
playlist: [],
|
||||
};
|
||||
Object.assign(this, defaults, opts);
|
||||
|
||||
super._init({
|
||||
application_id: Misc.appId + this.idPostfix
|
||||
});
|
||||
|
||||
this.doneFirstActivate = false;
|
||||
}
|
||||
|
||||
vfunc_startup()
|
||||
{
|
||||
super.vfunc_startup();
|
||||
|
||||
let 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) {
|
||||
let 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();
|
||||
}
|
||||
|
||||
run(arr)
|
||||
{
|
||||
super.run(arr || []);
|
||||
}
|
||||
|
||||
_onFirstActivate()
|
||||
{
|
||||
let 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
clapper_src/appRemote.js
Normal file
21
clapper_src/appRemote.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const { GObject } = imports.gi;
|
||||
const { AppBase } = imports.clapper_src.appBase;
|
||||
const { HeaderBarBase } = imports.clapper_src.headerbarBase;
|
||||
const { WidgetRemote } = imports.clapper_src.widgetRemote;
|
||||
|
||||
var AppRemote = GObject.registerClass(
|
||||
class ClapperAppRemote extends AppBase
|
||||
{
|
||||
vfunc_startup()
|
||||
{
|
||||
super.vfunc_startup();
|
||||
|
||||
let clapperWidget = new WidgetRemote();
|
||||
this.active_window.set_child(clapperWidget);
|
||||
|
||||
let headerBar = new HeaderBarBase(this.active_window);
|
||||
this.active_window.set_titlebar(headerBar);
|
||||
|
||||
this.active_window.maximize();
|
||||
}
|
||||
});
|
@@ -133,7 +133,7 @@ class ClapperUriDialog extends Gtk.Dialog
|
||||
openUri(uri)
|
||||
{
|
||||
let { player } = this.get_transient_for().get_child();
|
||||
player.set_media(uri);
|
||||
player.set_playlist([uri]);
|
||||
|
||||
this.close();
|
||||
}
|
||||
|
@@ -1,94 +1,17 @@
|
||||
const { GObject, Gtk, Pango } = imports.gi;
|
||||
const { GObject } = imports.gi;
|
||||
const { HeaderBarBase } = imports.clapper_src.headerbarBase;
|
||||
|
||||
var HeaderBar = GObject.registerClass(
|
||||
class ClapperHeaderBar extends Gtk.HeaderBar
|
||||
class ClapperHeaderBar extends HeaderBarBase
|
||||
{
|
||||
_init(window, models)
|
||||
_init(window)
|
||||
{
|
||||
super._init({
|
||||
can_focus: false,
|
||||
});
|
||||
this.add_css_class('noborder');
|
||||
super._init(window);
|
||||
|
||||
this.set_title_widget(this._createWidgetForWindow(window));
|
||||
let clapperWidget = window.get_child();
|
||||
|
||||
let addMediaButton = new Gtk.MenuButton({
|
||||
icon_name: 'list-add-symbolic',
|
||||
});
|
||||
let addMediaPopover = new HeaderBarPopover(models.addMediaMenu);
|
||||
addMediaButton.set_popover(addMediaPopover);
|
||||
this.pack_start(addMediaButton);
|
||||
|
||||
let openMenuButton = new Gtk.MenuButton({
|
||||
icon_name: 'open-menu-symbolic',
|
||||
});
|
||||
let settingsPopover = new HeaderBarPopover(models.settingsMenu);
|
||||
openMenuButton.set_popover(settingsPopover);
|
||||
this.pack_end(openMenuButton);
|
||||
|
||||
let buttonsBox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
});
|
||||
buttonsBox.add_css_class('linked');
|
||||
|
||||
let floatButton = new Gtk.Button({
|
||||
icon_name: 'preferences-desktop-remote-desktop-symbolic',
|
||||
});
|
||||
floatButton.connect('clicked', this._onFloatButtonClicked.bind(this));
|
||||
clapperWidget.controls.unfloatButton.bind_property('visible', this, 'visible',
|
||||
GObject.BindingFlags.INVERT_BOOLEAN
|
||||
);
|
||||
buttonsBox.append(floatButton);
|
||||
|
||||
let 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)
|
||||
{
|
||||
let 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()
|
||||
@@ -103,24 +26,3 @@ class ClapperHeaderBar extends Gtk.HeaderBar
|
||||
window.fullscreen();
|
||||
}
|
||||
});
|
||||
|
||||
var HeaderBarPopover = GObject.registerClass(
|
||||
class ClapperHeaderBarPopover extends Gtk.PopoverMenu
|
||||
{
|
||||
_init(model)
|
||||
{
|
||||
super._init({
|
||||
menu_model: model,
|
||||
});
|
||||
|
||||
this.connect('closed', this._onClosed.bind(this));
|
||||
}
|
||||
|
||||
_onClosed()
|
||||
{
|
||||
let root = this.get_root();
|
||||
let clapperWidget = root.get_child();
|
||||
|
||||
clapperWidget.player.widget.grab_focus();
|
||||
}
|
||||
});
|
||||
|
135
clapper_src/headerbarBase.js
Normal file
135
clapper_src/headerbarBase.js
Normal file
@@ -0,0 +1,135 @@
|
||||
const { GObject, Gtk, Pango } = imports.gi;
|
||||
const Misc = imports.clapper_src.misc;
|
||||
|
||||
var HeaderBarBase = GObject.registerClass(
|
||||
class ClapperHeaderBarBase extends Gtk.HeaderBar
|
||||
{
|
||||
_init(window)
|
||||
{
|
||||
super._init({
|
||||
can_focus: false,
|
||||
});
|
||||
|
||||
let clapperPath = Misc.getClapperPath();
|
||||
let uiBuilder = Gtk.Builder.new_from_file(
|
||||
`${clapperPath}/ui/clapper.ui`
|
||||
);
|
||||
let models = {
|
||||
addMediaMenu: uiBuilder.get_object('addMediaMenu'),
|
||||
settingsMenu: uiBuilder.get_object('settingsMenu'),
|
||||
};
|
||||
|
||||
this.add_css_class('noborder');
|
||||
this.set_title_widget(this._createWidgetForWindow(window));
|
||||
|
||||
let addMediaButton = new Gtk.MenuButton({
|
||||
icon_name: 'list-add-symbolic',
|
||||
});
|
||||
let addMediaPopover = new HeaderBarPopover(models.addMediaMenu);
|
||||
addMediaButton.set_popover(addMediaPopover);
|
||||
this.pack_start(addMediaButton);
|
||||
|
||||
let openMenuButton = new Gtk.MenuButton({
|
||||
icon_name: 'open-menu-symbolic',
|
||||
});
|
||||
let settingsPopover = new HeaderBarPopover(models.settingsMenu);
|
||||
openMenuButton.set_popover(settingsPopover);
|
||||
this.pack_end(openMenuButton);
|
||||
|
||||
let buttonsBox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
});
|
||||
buttonsBox.add_css_class('linked');
|
||||
|
||||
let floatButton = new Gtk.Button({
|
||||
icon_name: 'preferences-desktop-remote-desktop-symbolic',
|
||||
});
|
||||
floatButton.connect('clicked', this._onFloatButtonClicked.bind(this));
|
||||
buttonsBox.append(floatButton);
|
||||
|
||||
let 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)
|
||||
{
|
||||
let 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()
|
||||
{
|
||||
let root = this.get_root();
|
||||
let clapperWidget = root.get_child();
|
||||
|
||||
if(
|
||||
!clapperWidget
|
||||
|| !clapperWidget.player
|
||||
|| !clapperWidget.player.widget
|
||||
)
|
||||
return;
|
||||
|
||||
clapperWidget.player.widget.grab_focus();
|
||||
}
|
||||
});
|
17
clapper_src/mainRemote.js
Normal file
17
clapper_src/mainRemote.js
Normal file
@@ -0,0 +1,17 @@
|
||||
imports.gi.versions.Gdk = '4.0';
|
||||
imports.gi.versions.Gtk = '4.0';
|
||||
|
||||
const { AppRemote } = imports.clapper_src.appRemote;
|
||||
const Misc = imports.clapper_src.misc;
|
||||
|
||||
const opts = {
|
||||
idPostfix: 'Remote',
|
||||
};
|
||||
|
||||
Misc.clapperPath = pkg.datadir + '/' +
|
||||
pkg.name.substring(0, pkg.name.lastIndexOf(opts.idPostfix));
|
||||
|
||||
function main()
|
||||
{
|
||||
new AppRemote(opts).run();
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
const { Gio, GstAudio, GstPlayer, Gtk } = imports.gi;
|
||||
const { Gio, GstAudio, GstPlayer, Gdk, Gtk } = imports.gi;
|
||||
const Debug = imports.clapper_src.debug;
|
||||
|
||||
var appName = 'Clapper';
|
||||
@@ -35,6 +35,19 @@ function getClapperVersion()
|
||||
: '';
|
||||
}
|
||||
|
||||
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;
|
||||
|
@@ -251,6 +251,21 @@ class ClapperPlayer extends PlayerBase
|
||||
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');
|
||||
@@ -337,6 +352,7 @@ class ClapperPlayer extends PlayerBase
|
||||
_onStateChanged(player, state)
|
||||
{
|
||||
this.state = state;
|
||||
this.emitWs('state_changed', state);
|
||||
|
||||
if(state !== GstPlayer.PlayerState.BUFFERING) {
|
||||
let root = player.widget.get_root();
|
||||
@@ -364,7 +380,9 @@ class ClapperPlayer extends PlayerBase
|
||||
|
||||
_onStreamEnded(player)
|
||||
{
|
||||
debug('stream ended');
|
||||
debug(`end of stream: ${this._trackId}`);
|
||||
this.emitWs('end_of_stream', this._trackId);
|
||||
|
||||
this._trackId++;
|
||||
|
||||
if(this._trackId < this._playlist.length)
|
||||
|
@@ -1,27 +1,13 @@
|
||||
const { Gio, GLib, GObject, Gst, GstPlayer, Gtk } = imports.gi;
|
||||
const Debug = imports.clapper_src.debug;
|
||||
const Misc = imports.clapper_src.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,
|
||||
};
|
||||
const { WebApp } = imports.clapper_src.webApp;
|
||||
|
||||
let { debug } = Debug;
|
||||
let { settings } = Misc;
|
||||
|
||||
let WebServer;
|
||||
|
||||
var PlayerBase = GObject.registerClass(
|
||||
class ClapperPlayerBase extends GstPlayer.Player
|
||||
{
|
||||
@@ -66,6 +52,9 @@ class ClapperPlayerBase extends GstPlayer.Player
|
||||
this.state = GstPlayer.PlayerState.STOPPED;
|
||||
this.visualization_enabled = false;
|
||||
|
||||
this.webserver = null;
|
||||
this.webapp = null;
|
||||
|
||||
this.set_all_plugins_ranks();
|
||||
this.set_initial_config();
|
||||
this.set_and_bind_settings();
|
||||
@@ -89,6 +78,7 @@ class ClapperPlayerBase extends GstPlayer.Player
|
||||
'audio-offset',
|
||||
'subtitle-offset',
|
||||
'play-flags',
|
||||
'webserver-enabled'
|
||||
];
|
||||
|
||||
for(let key of settingsToSet)
|
||||
@@ -175,6 +165,19 @@ class ClapperPlayerBase extends GstPlayer.Player
|
||||
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;
|
||||
@@ -267,6 +270,48 @@ class ClapperPlayerBase extends GstPlayer.Player
|
||||
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.clapper_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.startRemoteApp();
|
||||
}
|
||||
else
|
||||
this.webapp.stopRemoteApp();
|
||||
}
|
||||
else if(this.webserver) {
|
||||
/* remote app will close too when connection is lost */
|
||||
this.webserver.stopListening();
|
||||
}
|
||||
break;
|
||||
case 'webserver-port':
|
||||
if(!this.webserver)
|
||||
break;
|
||||
|
||||
this.webserver.setListeningPort(settings.get_int(key));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
21
clapper_src/playerRemote.js
Normal file
21
clapper_src/playerRemote.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const { GObject } = imports.gi;
|
||||
const { WebClient } = imports.clapper_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
|
||||
});
|
||||
}
|
||||
});
|
@@ -4,6 +4,23 @@ const PrefsBase = imports.clapper_src.prefsBase;
|
||||
|
||||
let { 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
|
||||
{
|
||||
@@ -97,6 +114,13 @@ class ClapperNetworkPage extends PrefsBase.Grid
|
||||
|
||||
this.addTitle('Client');
|
||||
this.addPlayFlagCheckButton('Progressive download buffering', Gst.PlayFlags.DOWNLOAD);
|
||||
|
||||
this.addTitle('Server');
|
||||
let webServer = this.addCheckButton('Control player remotely', 'webserver-enabled');
|
||||
let serverPort = this.addSpinButton('Listening port', 1024, 65535, 'webserver-port');
|
||||
webServer.bind_property('active', serverPort, 'visible', GObject.BindingFlags.SYNC_CREATE);
|
||||
//let webApp = this.addCheckButton('Run built-in web application', 'webapp-enabled');
|
||||
//webServer.bind_property('active', webApp, 'visible', GObject.BindingFlags.SYNC_CREATE);
|
||||
}
|
||||
});
|
||||
|
||||
|
64
clapper_src/webApp.js
Normal file
64
clapper_src/webApp.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const { Gio, GObject } = imports.gi;
|
||||
const Debug = imports.clapper_src.debug;
|
||||
const Misc = imports.clapper_src.misc;
|
||||
|
||||
let { 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.remoteApp = null;
|
||||
this.isRemoteClosing = false;
|
||||
|
||||
this.setenv('GDK_BACKEND', 'broadway', true);
|
||||
}
|
||||
|
||||
startRemoteApp()
|
||||
{
|
||||
if(this.remoteApp)
|
||||
return;
|
||||
|
||||
this.remoteApp = this.spawnv([Misc.appId + 'Remote']);
|
||||
this.remoteApp.wait_async(null, this._onRemoteClosed.bind(this));
|
||||
|
||||
debug('remote app started');
|
||||
}
|
||||
|
||||
stopRemoteApp()
|
||||
{
|
||||
if(!this.remoteApp || this.isRemoteClosing)
|
||||
return;
|
||||
|
||||
this.isRemoteClosing = true;
|
||||
this.remoteApp.force_exit();
|
||||
|
||||
debug('send stop signal to remote app');
|
||||
}
|
||||
|
||||
_onRemoteClosed(proc, result)
|
||||
{
|
||||
let hadError;
|
||||
|
||||
try {
|
||||
hadError = proc.wait_finish(result);
|
||||
}
|
||||
catch(err) {
|
||||
debug(err);
|
||||
}
|
||||
|
||||
this.remoteApp = null;
|
||||
this.isRemoteClosing = false;
|
||||
|
||||
if(hadError)
|
||||
debug('remote app exited with error');
|
||||
|
||||
debug('remote app closed');
|
||||
}
|
||||
});
|
88
clapper_src/webClient.js
Normal file
88
clapper_src/webClient.js
Normal file
@@ -0,0 +1,88 @@
|
||||
const { Gio, GObject, Soup } = imports.gi;
|
||||
const Debug = imports.clapper_src.debug;
|
||||
const Misc = imports.clapper_src.misc;
|
||||
const WebHelpers = imports.clapper_src.webHelpers;
|
||||
|
||||
let { debug } = Debug;
|
||||
let { 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
clapper_src/webHelpers.js
Normal file
30
clapper_src/webHelpers.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const { Soup } = imports.gi;
|
||||
const ByteArray = imports.byteArray;
|
||||
const Debug = imports.clapper_src.debug;
|
||||
|
||||
let { 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
clapper_src/webServer.js
Normal file
139
clapper_src/webServer.js
Normal file
@@ -0,0 +1,139 @@
|
||||
const { Soup, GObject } = imports.gi;
|
||||
const Debug = imports.clapper_src.debug;
|
||||
const WebHelpers = imports.clapper_src.webHelpers;
|
||||
|
||||
let { 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;
|
||||
}
|
||||
});
|
@@ -16,25 +16,15 @@ var Widget = GObject.registerClass({
|
||||
}
|
||||
}, class ClapperWidget extends Gtk.Grid
|
||||
{
|
||||
_init(opts)
|
||||
_init()
|
||||
{
|
||||
Debug.gstVersionCheck();
|
||||
|
||||
super._init();
|
||||
|
||||
let clapperPath = Misc.getClapperPath();
|
||||
let defaults = {
|
||||
cssPath: `${clapperPath}/css/styles.css`,
|
||||
};
|
||||
Object.assign(this, defaults, opts);
|
||||
|
||||
let cssProvider = new Gtk.CssProvider();
|
||||
cssProvider.load_from_path(this.cssPath);
|
||||
Gtk.StyleContext.add_provider_for_display(
|
||||
Gdk.Display.get_default(),
|
||||
cssProvider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||
);
|
||||
/* 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'));
|
||||
|
29
clapper_src/widgetRemote.js
Normal file
29
clapper_src/widgetRemote.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const { GObject, Gtk } = imports.gi;
|
||||
const Misc = imports.clapper_src.misc;
|
||||
const { PlayerRemote } = imports.clapper_src.playerRemote;
|
||||
|
||||
var WidgetRemote = GObject.registerClass(
|
||||
class ClapperWidgetRemote extends Gtk.Grid
|
||||
{
|
||||
_init(opts)
|
||||
{
|
||||
super._init();
|
||||
|
||||
Misc.loadCustomCss();
|
||||
|
||||
this.player = new PlayerRemote();
|
||||
this.player.webclient.passMsgData = this.receiveWs.bind(this);
|
||||
}
|
||||
|
||||
receiveWs(action, value)
|
||||
{
|
||||
switch(action) {
|
||||
case 'close':
|
||||
let root = this.get_root();
|
||||
root.run_dispose();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
@@ -49,6 +49,20 @@
|
||||
<summary>The subtitles font description</summary>
|
||||
</key>
|
||||
|
||||
<!-- Network -->
|
||||
<key name="webserver-enabled" type="b">
|
||||
<default>false</default>
|
||||
<summary>Enable WebSocket server for remote playback control</summary>
|
||||
</key>
|
||||
<key name="webserver-port" type="i">
|
||||
<default>6446</default>
|
||||
<summary>Listening port to use for incoming WebSocket connections</summary>
|
||||
</key>
|
||||
<key name="webapp-enabled" type="b">
|
||||
<default>false</default>
|
||||
<summary>Run built-in broadway based web application</summary>
|
||||
</key>
|
||||
|
||||
<!-- Tweaks -->
|
||||
<key name="dark-theme" type="b">
|
||||
<default>true</default>
|
||||
|
@@ -23,6 +23,5 @@ installdir = join_paths(get_option('prefix'), 'share', meson.project_name())
|
||||
install_subdir('clapper_src', install_dir : installdir)
|
||||
install_subdir('css', install_dir : installdir)
|
||||
install_subdir('ui', install_dir : installdir)
|
||||
install_data('main.js', install_dir : installdir)
|
||||
|
||||
meson.add_install_script('build-aux/meson/postinstall.py')
|
||||
|
@@ -3,6 +3,7 @@
|
||||
"buildsystem": "meson",
|
||||
"config-opts": [
|
||||
"--wrap-mode=nofallback",
|
||||
"-Dbroadway-backend=true",
|
||||
"-Dwin32-backend=false",
|
||||
"-Dmacos-backend=false",
|
||||
"-Dmedia-ffmpeg=disabled",
|
||||
@@ -10,7 +11,8 @@
|
||||
"-Dprint-cloudprint=disabled",
|
||||
"-Dintrospection=enabled",
|
||||
"-Ddemos=false",
|
||||
"-Dbuild-examples=false"
|
||||
"-Dbuild-examples=false",
|
||||
"-Dbuild-tests=false"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
|
Reference in New Issue
Block a user