mirror of
https://github.com/Rafostar/clapper.git
synced 2025-08-31 16:31:58 +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@',
|
libdir: '@libdir@',
|
||||||
datadir: '@datadir@',
|
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('libdir', join_paths(get_option('prefix'), get_option('libdir')))
|
||||||
bin_conf.set('datadir', join_paths(get_option('prefix'), get_option('datadir')))
|
bin_conf.set('datadir', join_paths(get_option('prefix'), get_option('datadir')))
|
||||||
|
|
||||||
configure_file(
|
clapper_apps = [ '', 'Remote' ]
|
||||||
input: 'com.github.rafostar.Clapper.in',
|
|
||||||
output: 'com.github.rafostar.Clapper',
|
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,
|
configuration: bin_conf,
|
||||||
install: true,
|
install: true,
|
||||||
install_dir: get_option('bindir'),
|
install_dir: get_option('bindir'),
|
||||||
install_mode: 'rwxr-xr-x'
|
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 { HeaderBar } = imports.clapper_src.headerbar;
|
||||||
const { Widget } = imports.clapper_src.widget;
|
const { Widget } = imports.clapper_src.widget;
|
||||||
const Debug = imports.clapper_src.debug;
|
const Debug = imports.clapper_src.debug;
|
||||||
const Menu = imports.clapper_src.menu;
|
|
||||||
const Misc = imports.clapper_src.misc;
|
|
||||||
|
|
||||||
let { debug } = Debug;
|
let { debug } = Debug;
|
||||||
let { settings } = Misc;
|
|
||||||
|
|
||||||
var App = GObject.registerClass(
|
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()
|
vfunc_startup()
|
||||||
{
|
{
|
||||||
super.vfunc_startup();
|
super.vfunc_startup();
|
||||||
|
|
||||||
let window = new Gtk.ApplicationWindow({
|
this.active_window.isClapperApp = true;
|
||||||
application: this,
|
this.active_window.add_css_class('nobackground');
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
let clapperWidget = new Widget();
|
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;
|
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]}`);
|
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)
|
_onWindowShow(window)
|
||||||
{
|
{
|
||||||
window.disconnect(this.windowShowSignal);
|
super._onWindowShow(window);
|
||||||
this.windowShowSignal = null;
|
|
||||||
|
|
||||||
if(this.playlist.length) {
|
if(this.playlist.length) {
|
||||||
let { player } = window.get_child();
|
let { player } = window.get_child();
|
||||||
player.set_playlist(this.playlist);
|
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)
|
openUri(uri)
|
||||||
{
|
{
|
||||||
let { player } = this.get_transient_for().get_child();
|
let { player } = this.get_transient_for().get_child();
|
||||||
player.set_media(uri);
|
player.set_playlist([uri]);
|
||||||
|
|
||||||
this.close();
|
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(
|
var HeaderBar = GObject.registerClass(
|
||||||
class ClapperHeaderBar extends Gtk.HeaderBar
|
class ClapperHeaderBar extends HeaderBarBase
|
||||||
{
|
{
|
||||||
_init(window, models)
|
_init(window)
|
||||||
{
|
{
|
||||||
super._init({
|
super._init(window);
|
||||||
can_focus: false,
|
|
||||||
});
|
|
||||||
this.add_css_class('noborder');
|
|
||||||
|
|
||||||
this.set_title_widget(this._createWidgetForWindow(window));
|
|
||||||
let clapperWidget = window.get_child();
|
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',
|
clapperWidget.controls.unfloatButton.bind_property('visible', this, 'visible',
|
||||||
GObject.BindingFlags.INVERT_BOOLEAN
|
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()
|
_onFloatButtonClicked()
|
||||||
@@ -103,24 +26,3 @@ class ClapperHeaderBar extends Gtk.HeaderBar
|
|||||||
window.fullscreen();
|
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;
|
const Debug = imports.clapper_src.debug;
|
||||||
|
|
||||||
var appName = 'Clapper';
|
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)
|
function inhibitForState(state, window)
|
||||||
{
|
{
|
||||||
let isInhibited = false;
|
let isInhibited = false;
|
||||||
|
@@ -251,6 +251,21 @@ class ClapperPlayer extends PlayerBase
|
|||||||
this[action]();
|
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()
|
_setHideCursorTimeout()
|
||||||
{
|
{
|
||||||
this._clearTimeout('hideCursor');
|
this._clearTimeout('hideCursor');
|
||||||
@@ -337,6 +352,7 @@ class ClapperPlayer extends PlayerBase
|
|||||||
_onStateChanged(player, state)
|
_onStateChanged(player, state)
|
||||||
{
|
{
|
||||||
this.state = state;
|
this.state = state;
|
||||||
|
this.emitWs('state_changed', state);
|
||||||
|
|
||||||
if(state !== GstPlayer.PlayerState.BUFFERING) {
|
if(state !== GstPlayer.PlayerState.BUFFERING) {
|
||||||
let root = player.widget.get_root();
|
let root = player.widget.get_root();
|
||||||
@@ -364,7 +380,9 @@ class ClapperPlayer extends PlayerBase
|
|||||||
|
|
||||||
_onStreamEnded(player)
|
_onStreamEnded(player)
|
||||||
{
|
{
|
||||||
debug('stream ended');
|
debug(`end of stream: ${this._trackId}`);
|
||||||
|
this.emitWs('end_of_stream', this._trackId);
|
||||||
|
|
||||||
this._trackId++;
|
this._trackId++;
|
||||||
|
|
||||||
if(this._trackId < this._playlist.length)
|
if(this._trackId < this._playlist.length)
|
||||||
|
@@ -1,27 +1,13 @@
|
|||||||
const { Gio, GLib, GObject, Gst, GstPlayer, Gtk } = imports.gi;
|
const { Gio, GLib, GObject, Gst, GstPlayer, Gtk } = imports.gi;
|
||||||
const Debug = imports.clapper_src.debug;
|
const Debug = imports.clapper_src.debug;
|
||||||
const Misc = imports.clapper_src.misc;
|
const Misc = imports.clapper_src.misc;
|
||||||
|
const { WebApp } = imports.clapper_src.webApp;
|
||||||
/* 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,
|
|
||||||
};
|
|
||||||
|
|
||||||
let { debug } = Debug;
|
let { debug } = Debug;
|
||||||
let { settings } = Misc;
|
let { settings } = Misc;
|
||||||
|
|
||||||
|
let WebServer;
|
||||||
|
|
||||||
var PlayerBase = GObject.registerClass(
|
var PlayerBase = GObject.registerClass(
|
||||||
class ClapperPlayerBase extends GstPlayer.Player
|
class ClapperPlayerBase extends GstPlayer.Player
|
||||||
{
|
{
|
||||||
@@ -66,6 +52,9 @@ class ClapperPlayerBase extends GstPlayer.Player
|
|||||||
this.state = GstPlayer.PlayerState.STOPPED;
|
this.state = GstPlayer.PlayerState.STOPPED;
|
||||||
this.visualization_enabled = false;
|
this.visualization_enabled = false;
|
||||||
|
|
||||||
|
this.webserver = null;
|
||||||
|
this.webapp = null;
|
||||||
|
|
||||||
this.set_all_plugins_ranks();
|
this.set_all_plugins_ranks();
|
||||||
this.set_initial_config();
|
this.set_initial_config();
|
||||||
this.set_and_bind_settings();
|
this.set_and_bind_settings();
|
||||||
@@ -89,6 +78,7 @@ class ClapperPlayerBase extends GstPlayer.Player
|
|||||||
'audio-offset',
|
'audio-offset',
|
||||||
'subtitle-offset',
|
'subtitle-offset',
|
||||||
'play-flags',
|
'play-flags',
|
||||||
|
'webserver-enabled'
|
||||||
];
|
];
|
||||||
|
|
||||||
for(let key of settingsToSet)
|
for(let key of settingsToSet)
|
||||||
@@ -175,6 +165,19 @@ class ClapperPlayerBase extends GstPlayer.Player
|
|||||||
this.widget.queue_render();
|
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)
|
_onSettingsKeyChanged(settings, key)
|
||||||
{
|
{
|
||||||
let root, value, action;
|
let root, value, action;
|
||||||
@@ -267,6 +270,48 @@ class ClapperPlayerBase extends GstPlayer.Player
|
|||||||
this.pipeline.flags = settingsFlags;
|
this.pipeline.flags = settingsFlags;
|
||||||
debug(`changed play flags: ${initialFlags} -> ${settingsFlags}`);
|
debug(`changed play flags: ${initialFlags} -> ${settingsFlags}`);
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
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;
|
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(
|
var GeneralPage = GObject.registerClass(
|
||||||
class ClapperGeneralPage extends PrefsBase.Grid
|
class ClapperGeneralPage extends PrefsBase.Grid
|
||||||
{
|
{
|
||||||
@@ -97,6 +114,13 @@ class ClapperNetworkPage extends PrefsBase.Grid
|
|||||||
|
|
||||||
this.addTitle('Client');
|
this.addTitle('Client');
|
||||||
this.addPlayFlagCheckButton('Progressive download buffering', Gst.PlayFlags.DOWNLOAD);
|
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
|
}, class ClapperWidget extends Gtk.Grid
|
||||||
{
|
{
|
||||||
_init(opts)
|
_init()
|
||||||
{
|
{
|
||||||
Debug.gstVersionCheck();
|
Debug.gstVersionCheck();
|
||||||
|
|
||||||
super._init();
|
super._init();
|
||||||
|
|
||||||
let clapperPath = Misc.getClapperPath();
|
/* load CSS here to allow using this class
|
||||||
let defaults = {
|
* separately as a pre-made GTK widget */
|
||||||
cssPath: `${clapperPath}/css/styles.css`,
|
Misc.loadCustomCss();
|
||||||
};
|
|
||||||
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
|
|
||||||
);
|
|
||||||
|
|
||||||
this.windowSize = JSON.parse(settings.get_string('window-size'));
|
this.windowSize = JSON.parse(settings.get_string('window-size'));
|
||||||
this.floatSize = JSON.parse(settings.get_string('float-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>
|
<summary>The subtitles font description</summary>
|
||||||
</key>
|
</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 -->
|
<!-- Tweaks -->
|
||||||
<key name="dark-theme" type="b">
|
<key name="dark-theme" type="b">
|
||||||
<default>true</default>
|
<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('clapper_src', install_dir : installdir)
|
||||||
install_subdir('css', install_dir : installdir)
|
install_subdir('css', install_dir : installdir)
|
||||||
install_subdir('ui', 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')
|
meson.add_install_script('build-aux/meson/postinstall.py')
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
"buildsystem": "meson",
|
"buildsystem": "meson",
|
||||||
"config-opts": [
|
"config-opts": [
|
||||||
"--wrap-mode=nofallback",
|
"--wrap-mode=nofallback",
|
||||||
|
"-Dbroadway-backend=true",
|
||||||
"-Dwin32-backend=false",
|
"-Dwin32-backend=false",
|
||||||
"-Dmacos-backend=false",
|
"-Dmacos-backend=false",
|
||||||
"-Dmedia-ffmpeg=disabled",
|
"-Dmedia-ffmpeg=disabled",
|
||||||
@@ -10,7 +11,8 @@
|
|||||||
"-Dprint-cloudprint=disabled",
|
"-Dprint-cloudprint=disabled",
|
||||||
"-Dintrospection=enabled",
|
"-Dintrospection=enabled",
|
||||||
"-Ddemos=false",
|
"-Ddemos=false",
|
||||||
"-Dbuild-examples=false"
|
"-Dbuild-examples=false",
|
||||||
|
"-Dbuild-tests=false"
|
||||||
],
|
],
|
||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user