mirror of
https://github.com/Rafostar/clapper.git
synced 2025-08-30 16:02:00 +02:00
Recent GTK4 git is way too buggy. We cannot update the GTK version above 3.99.4 yet. So add compatibility with newer versions without dropping support for older ones.
656 lines
19 KiB
JavaScript
656 lines
19 KiB
JavaScript
const { Gdk, Gio, GLib, GObject, Gst, GstPlayer, Gtk } = imports.gi;
|
|
const ByteArray = imports.byteArray;
|
|
const Debug = imports.clapper_src.debug;
|
|
const Misc = imports.clapper_src.misc;
|
|
const { PlayerBase } = imports.clapper_src.playerBase;
|
|
|
|
let { debug } = Debug;
|
|
let { settings } = Misc;
|
|
|
|
var Player = GObject.registerClass(
|
|
class ClapperPlayer extends PlayerBase
|
|
{
|
|
_init()
|
|
{
|
|
super._init();
|
|
|
|
this.cursorInPlayer = false;
|
|
this.is_local_file = false;
|
|
this.seek_done = true;
|
|
this.dragAllowed = false;
|
|
this.isWidgetDragging = false;
|
|
this.doneStartup = false;
|
|
|
|
this.playOnFullscreen = false;
|
|
this.quitOnStop = false;
|
|
|
|
this.posX = 0;
|
|
this.posY = 0;
|
|
this.keyPressCount = 0;
|
|
|
|
this._maxVolume = Misc.getLinearValue(Misc.maxVolume);
|
|
|
|
this._playlist = [];
|
|
this._trackId = 0;
|
|
|
|
this._hideCursorTimeout = null;
|
|
this._hideControlsTimeout = null;
|
|
this._updateTimeTimeout = null;
|
|
|
|
let clickGesture = new Gtk.GestureClick();
|
|
clickGesture.set_button(0);
|
|
clickGesture.connect('pressed', this._onWidgetPressed.bind(this));
|
|
this.widget.add_controller(clickGesture);
|
|
|
|
let dragGesture = new Gtk.GestureDrag();
|
|
dragGesture.connect('drag-update', this._onWidgetDragUpdate.bind(this));
|
|
this.widget.add_controller(dragGesture);
|
|
|
|
let 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);
|
|
|
|
let scrollController = new Gtk.EventControllerScroll();
|
|
scrollController.set_flags(Gtk.EventControllerScrollFlags.BOTH_AXES);
|
|
scrollController.connect('scroll', this._onScroll.bind(this));
|
|
this.widget.add_controller(scrollController);
|
|
|
|
let 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);
|
|
|
|
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_media(source)
|
|
{
|
|
if(!Gst.uri_is_valid(source))
|
|
source = Gst.filename_to_uri(source);
|
|
|
|
if(!source)
|
|
return debug('parsing source to URI failed');
|
|
|
|
debug(`parsed source to URI: ${source}`);
|
|
|
|
if(Gst.Uri.get_protocol(source) !== 'file') {
|
|
this.is_local_file = false;
|
|
return this.set_uri(source);
|
|
}
|
|
|
|
let file = Gio.file_new_for_uri(source);
|
|
|
|
if(!file.query_exists(null)) {
|
|
debug(`file does not exist: ${source}`, 'LEVEL_WARNING');
|
|
this._trackId++;
|
|
|
|
if(this._playlist.length <= this._trackId)
|
|
return debug('set media reached end of playlist');
|
|
|
|
return this.set_media(this._playlist[this._trackId]);
|
|
}
|
|
|
|
if(file.get_path().endsWith('.claps'))
|
|
return this.load_playlist_file(file);
|
|
|
|
this.is_local_file = true;
|
|
this.set_uri(source);
|
|
}
|
|
|
|
load_playlist_file(file)
|
|
{
|
|
let stream = new Gio.DataInputStream({
|
|
base_stream: file.read(null)
|
|
});
|
|
let listdir = file.get_parent();
|
|
let 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)) {
|
|
let lineFile = listdir.resolve_relative_path(line);
|
|
if(!lineFile)
|
|
continue;
|
|
|
|
line = lineFile.get_path();
|
|
}
|
|
debug(`new playlist item: ${line}`);
|
|
playlist.push(line);
|
|
}
|
|
stream.close(null);
|
|
this.set_playlist(playlist);
|
|
}
|
|
|
|
set_playlist(playlist)
|
|
{
|
|
if(!Array.isArray(playlist) || !playlist.length)
|
|
return;
|
|
|
|
this._trackId = 0;
|
|
this._playlist = playlist;
|
|
|
|
this.set_media(this._playlist[0]);
|
|
}
|
|
|
|
get_playlist()
|
|
{
|
|
return this._playlist;
|
|
}
|
|
|
|
set_subtitles(uri)
|
|
{
|
|
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)
|
|
{
|
|
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(position)
|
|
{
|
|
this.seek(position * 1000000000);
|
|
}
|
|
|
|
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;
|
|
|
|
let { controls } = this.widget.get_ancestor(Gtk.Grid);
|
|
let max = controls.positionAdjustment.get_upper();
|
|
let seekingValue = settings.get_int('seeking-value');
|
|
let seekingUnit = settings.get_string('seeking-unit');
|
|
|
|
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)
|
|
{
|
|
let { controls } = this.widget.get_ancestor(Gtk.Grid);
|
|
|
|
let value = (isIncrease) ? 0.05 : -0.05;
|
|
let volume = controls.volumeScale.get_value() + value;
|
|
controls.volumeScale.set_value(volume);
|
|
}
|
|
|
|
toggle_play()
|
|
{
|
|
let action = (this.state === GstPlayer.PlayerState.PLAYING)
|
|
? 'pause'
|
|
: 'play';
|
|
|
|
this[action]();
|
|
}
|
|
|
|
_setHideCursorTimeout()
|
|
{
|
|
this._clearTimeout('hideCursor');
|
|
this._hideCursorTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => {
|
|
this._hideCursorTimeout = null;
|
|
|
|
if(this.cursorInPlayer) {
|
|
let blankCursor = Gdk.Cursor.new_from_name('none', null);
|
|
this.widget.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) {
|
|
let 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');
|
|
let clapperWidget = this.widget.get_ancestor(Gtk.Grid);
|
|
let 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;
|
|
|
|
let clapperWidget = this.widget.get_ancestor(Gtk.Grid);
|
|
if(!clapperWidget.fullscreenMode) {
|
|
let size = (Misc.isOldGtk)
|
|
? window.get_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;
|
|
|
|
if(state !== GstPlayer.PlayerState.BUFFERING) {
|
|
let root = player.widget.get_root();
|
|
Misc.inhibitForState(state, root);
|
|
|
|
if(this.quitOnStop) {
|
|
if(state === GstPlayer.PlayerState.STOPPED)
|
|
root.run_dispose();
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
let clapperWidget = player.widget.get_ancestor(Gtk.Grid);
|
|
if(!clapperWidget) return;
|
|
|
|
if(!this.seek_done && state !== GstPlayer.PlayerState.BUFFERING) {
|
|
clapperWidget.updateTime();
|
|
this.seek_done = true;
|
|
debug('seeking finished');
|
|
}
|
|
|
|
clapperWidget._onPlayerStateChanged(player, state);
|
|
}
|
|
|
|
_onStreamEnded(player)
|
|
{
|
|
debug('stream ended');
|
|
this._trackId++;
|
|
|
|
if(this._trackId < this._playlist.length)
|
|
this.set_media(this._playlist[this._trackId]);
|
|
else 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}`);
|
|
|
|
if(!this.doneStartup) {
|
|
this.doneStartup = true;
|
|
|
|
if(settings.get_boolean('fullscreen-auto')) {
|
|
let root = player.widget.get_root();
|
|
let 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;
|
|
|
|
let 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)
|
|
{
|
|
this.keyPressCount++;
|
|
|
|
let bool = false;
|
|
let clapperWidget = this.widget.get_ancestor(Gtk.Grid);
|
|
|
|
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)
|
|
{
|
|
this.keyPressCount = 0;
|
|
|
|
let value;
|
|
let clapperWidget = this.widget.get_ancestor(Gtk.Grid);
|
|
|
|
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) {
|
|
let root = this.widget.get_root();
|
|
root.unfullscreen();
|
|
}
|
|
break;
|
|
case Gdk.KEY_q:
|
|
case Gdk.KEY_Q:
|
|
let root = this.widget.get_root();
|
|
root.emit('close-request');
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
_onWidgetPressed(gesture, nPress, x, y)
|
|
{
|
|
let button = gesture.get_current_button();
|
|
let isDouble = (nPress % 2 == 0);
|
|
this.dragAllowed = !isDouble;
|
|
|
|
switch(button) {
|
|
case Gdk.BUTTON_PRIMARY:
|
|
if(isDouble) {
|
|
let 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();
|
|
|
|
let 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
|
|
) {
|
|
let defaultCursor = Gdk.Cursor.new_from_name('default', null);
|
|
this.widget.set_cursor(defaultCursor);
|
|
this._setHideCursorTimeout();
|
|
|
|
let clapperWidget = this.widget.get_ancestor(Gtk.Grid);
|
|
|
|
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;
|
|
|
|
let clapperWidget = this.widget.get_ancestor(Gtk.Grid);
|
|
if(clapperWidget.fullscreenMode)
|
|
return;
|
|
|
|
let { gtk_double_click_distance } = this.widget.get_settings();
|
|
|
|
if (
|
|
Math.abs(offsetX) > gtk_double_click_distance
|
|
|| Math.abs(offsetY) > gtk_double_click_distance
|
|
) {
|
|
let [isActive, startX, startY] = gesture.get_start_point();
|
|
if(!isActive) return;
|
|
|
|
let native = this.widget.get_native();
|
|
if(!native) return;
|
|
|
|
let [isShared, winX, winY] = this.widget.translate_coordinates(
|
|
native, startX, startY
|
|
);
|
|
if(!isShared) return;
|
|
|
|
let [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)
|
|
{
|
|
let isHorizontal = Math.abs(dx) >= Math.abs(dy);
|
|
let isIncrease = (isHorizontal) ? dx < 0 : dy < 0;
|
|
|
|
if(isHorizontal) {
|
|
this.adjust_position(isIncrease);
|
|
let { controls } = this.widget.get_ancestor(Gtk.Grid);
|
|
let value = Math.round(controls.positionScale.get_value());
|
|
this.seek_seconds(value);
|
|
}
|
|
else
|
|
this.adjust_volume(isIncrease);
|
|
|
|
return true;
|
|
}
|
|
|
|
_onCloseRequest(window)
|
|
{
|
|
this._performCloseCleanup(window);
|
|
|
|
if(this.state === GstPlayer.PlayerState.STOPPED)
|
|
return window.run_dispose();
|
|
|
|
this.quitOnStop = true;
|
|
this.stop();
|
|
}
|
|
});
|