mirror of
https://github.com/Rafostar/clapper.git
synced 2025-08-29 23:32:04 +02:00
494 lines
16 KiB
JavaScript
494 lines
16 KiB
JavaScript
const { GLib, GObject, Gdk, Gtk } = imports.gi;
|
||
const Buttons = imports.src.buttons;
|
||
const Debug = imports.src.debug;
|
||
const Misc = imports.src.misc;
|
||
const Revealers = imports.src.revealers;
|
||
|
||
const { debug } = Debug;
|
||
const { settings } = Misc;
|
||
|
||
var Controls = GObject.registerClass({
|
||
GTypeName: 'ClapperControls',
|
||
},
|
||
class ClapperControls extends Gtk.Box
|
||
{
|
||
_init()
|
||
{
|
||
super._init({
|
||
orientation: Gtk.Orientation.HORIZONTAL,
|
||
valign: Gtk.Align.END,
|
||
can_focus: false,
|
||
});
|
||
|
||
this.minFullViewWidth = 560;
|
||
|
||
this.currentPosition = 0;
|
||
this.isPositionDragging = false;
|
||
this.isMobile = false;
|
||
|
||
this.showHours = false;
|
||
this.durationFormatted = '00∶00';
|
||
this.revealersArr = [];
|
||
this.chapters = null;
|
||
|
||
this.chapterShowId = null;
|
||
this.chapterHideId = null;
|
||
|
||
this.togglePlayButton = new Buttons.IconToggleButton(
|
||
'play-symbolic',
|
||
'pause-symbolic'
|
||
);
|
||
this.togglePlayButton.connect(
|
||
'clicked', this._onTogglePlayClicked.bind(this)
|
||
);
|
||
this.append(this.togglePlayButton);
|
||
|
||
const elapsedRevealer = new Revealers.ButtonsRevealer('SLIDE_RIGHT');
|
||
this.elapsedButton = new Buttons.ElapsedTimeButton();
|
||
elapsedRevealer.append(this.elapsedButton);
|
||
elapsedRevealer.reveal_child = true;
|
||
this.append(elapsedRevealer);
|
||
this.revealersArr.push(elapsedRevealer);
|
||
|
||
this._addPositionScale();
|
||
|
||
const revealTracksButton = new Buttons.IconToggleButton(
|
||
'go-previous-symbolic',
|
||
'go-next-symbolic'
|
||
);
|
||
revealTracksButton.add_css_class('narrowbutton');
|
||
const tracksRevealer = new Revealers.ButtonsRevealer(
|
||
'SLIDE_LEFT', revealTracksButton
|
||
);
|
||
this.visualizationsButton = new Buttons.TrackSelectButton({
|
||
icon_name: 'display-projector-symbolic',
|
||
visible: false,
|
||
});
|
||
tracksRevealer.append(this.visualizationsButton);
|
||
this.videoTracksButton = new Buttons.TrackSelectButton({
|
||
icon_name: 'emblem-videos-symbolic',
|
||
visible: false,
|
||
});
|
||
tracksRevealer.append(this.videoTracksButton);
|
||
this.audioTracksButton = new Buttons.TrackSelectButton({
|
||
icon_name: 'emblem-music-symbolic',
|
||
visible: false,
|
||
});
|
||
tracksRevealer.append(this.audioTracksButton);
|
||
this.subtitleTracksButton = new Buttons.TrackSelectButton({
|
||
icon_name: 'media-view-subtitles-symbolic',
|
||
visible: false,
|
||
});
|
||
tracksRevealer.append(this.subtitleTracksButton);
|
||
|
||
this.revealTracksRevealer = new Revealers.ButtonsRevealer('SLIDE_LEFT');
|
||
this.revealTracksRevealer.append(revealTracksButton);
|
||
this.revealTracksRevealer.set_visible(false);
|
||
this.append(this.revealTracksRevealer);
|
||
|
||
tracksRevealer.set_reveal_child(true);
|
||
this.revealersArr.push(tracksRevealer);
|
||
this.append(tracksRevealer);
|
||
|
||
this.volumeButton = new Buttons.VolumeButton();
|
||
this.append(this.volumeButton);
|
||
|
||
this.unfullscreenButton = new Buttons.CustomButton({
|
||
icon_name: 'view-restore-symbolic',
|
||
});
|
||
this.unfullscreenButton.connect('clicked', this._onUnfullscreenClicked.bind(this));
|
||
this.unfullscreenButton.set_visible(false);
|
||
this.append(this.unfullscreenButton);
|
||
|
||
this.add_css_class('clappercontrols');
|
||
this.realizeSignal = this.connect('realize', this._onRealize.bind(this));
|
||
}
|
||
|
||
setFullscreenMode(isFullscreen, isMobileMonitor)
|
||
{
|
||
/* Allow recheck on next resize */
|
||
this.isMobile = null;
|
||
|
||
this.elapsedButton.setFullscreenMode(isFullscreen, isMobileMonitor);
|
||
this.visualizationsButton.setFullscreenMode(isFullscreen, isMobileMonitor);
|
||
this.videoTracksButton.setFullscreenMode(isFullscreen, isMobileMonitor);
|
||
this.audioTracksButton.setFullscreenMode(isFullscreen, isMobileMonitor);
|
||
this.subtitleTracksButton.setFullscreenMode(isFullscreen, isMobileMonitor);
|
||
|
||
this.unfullscreenButton.visible = isFullscreen;
|
||
}
|
||
|
||
setLiveMode(isLive, isSeekable)
|
||
{
|
||
if(isLive)
|
||
this.elapsedButton.label = 'LIVE';
|
||
|
||
this.positionScale.visible = isSeekable;
|
||
}
|
||
|
||
setInitialState()
|
||
{
|
||
this.currentPosition = 0;
|
||
this.positionScale.set_value(0);
|
||
this.positionScale.visible = false;
|
||
|
||
this.elapsedButton.setInitialState();
|
||
this.togglePlayButton.setPrimaryIcon();
|
||
|
||
for(let type of ['video', 'audio', 'subtitle'])
|
||
this[`${type}TracksButton`].visible = false;
|
||
|
||
this.visualizationsButton.visible = false;
|
||
}
|
||
|
||
updateElapsedLabel(value)
|
||
{
|
||
value = value || 0;
|
||
|
||
const elapsed = Misc.getFormattedTime(value, this.showHours)
|
||
+ '∕' + this.durationFormatted;
|
||
|
||
this.elapsedButton.label = elapsed;
|
||
}
|
||
|
||
addCheckButtons(box, array, activeId)
|
||
{
|
||
let group = null;
|
||
let child = box.get_first_child();
|
||
let i = 0;
|
||
|
||
while(child || i < array.length) {
|
||
if(i >= array.length) {
|
||
if(child.visible) {
|
||
debug(`hiding unused ${child.type} checkButton nr: ${i}`);
|
||
child.visible = false;
|
||
}
|
||
i++;
|
||
child = child.get_next_sibling();
|
||
continue;
|
||
}
|
||
|
||
const el = array[i];
|
||
let checkButton;
|
||
|
||
if(child) {
|
||
checkButton = child;
|
||
debug(`reusing ${el.type} checkButton nr: ${i}`);
|
||
}
|
||
else {
|
||
debug(`creating new ${el.type} checkButton nr: ${i}`);
|
||
checkButton = new Gtk.CheckButton({
|
||
group: group,
|
||
});
|
||
checkButton.connect(
|
||
'toggled',
|
||
this._onCheckButtonToggled.bind(this)
|
||
);
|
||
box.append(checkButton);
|
||
}
|
||
|
||
checkButton.label = el.label;
|
||
debug(`checkButton label: ${checkButton.label}`);
|
||
checkButton.type = el.type;
|
||
debug(`checkButton type: ${checkButton.type}`);
|
||
checkButton.activeId = el.activeId;
|
||
debug(`checkButton id: ${checkButton.activeId}`);
|
||
checkButton.visible = true;
|
||
|
||
if(checkButton.activeId === activeId) {
|
||
checkButton.set_active(true);
|
||
debug(`activated ${el.type} checkButton nr: ${i}`);
|
||
}
|
||
if(!group)
|
||
group = checkButton;
|
||
|
||
i++;
|
||
if(child)
|
||
child = child.get_next_sibling();
|
||
}
|
||
}
|
||
|
||
_handleTrackChange(checkButton)
|
||
{
|
||
const clapperWidget = this.get_ancestor(Gtk.Grid);
|
||
|
||
if(checkButton.activeId < 0) {
|
||
return clapperWidget.player[
|
||
`set_${checkButton.type}_track_enabled`
|
||
](false);
|
||
}
|
||
|
||
const setTrack = `set_${checkButton.type}_track`;
|
||
|
||
clapperWidget.player[setTrack](checkButton.activeId);
|
||
clapperWidget.player[`${setTrack}_enabled`](true);
|
||
}
|
||
|
||
_handleVisualizationChange(checkButton)
|
||
{
|
||
const clapperWidget = this.get_ancestor(Gtk.Grid);
|
||
const isEnabled = clapperWidget.player.get_visualization_enabled();
|
||
|
||
if(!checkButton.activeId) {
|
||
if(isEnabled) {
|
||
clapperWidget.player.set_visualization_enabled(false);
|
||
debug('disabled visualizations');
|
||
}
|
||
return;
|
||
}
|
||
|
||
const currVis = clapperWidget.player.get_current_visualization();
|
||
|
||
if(currVis === checkButton.activeId)
|
||
return;
|
||
|
||
debug(`set visualization: ${checkButton.activeId}`);
|
||
clapperWidget.player.set_visualization(checkButton.activeId);
|
||
|
||
if(!isEnabled) {
|
||
clapperWidget.player.set_visualization_enabled(true);
|
||
debug('enabled visualizations');
|
||
}
|
||
}
|
||
|
||
_addPositionScale()
|
||
{
|
||
this.positionScale = new Gtk.Scale({
|
||
orientation: Gtk.Orientation.HORIZONTAL,
|
||
value_pos: Gtk.PositionType.LEFT,
|
||
draw_value: false,
|
||
hexpand: true,
|
||
valign: Gtk.Align.CENTER,
|
||
can_focus: false,
|
||
visible: false,
|
||
});
|
||
const scrollController = new Gtk.EventControllerScroll();
|
||
scrollController.set_flags(Gtk.EventControllerScrollFlags.BOTH_AXES);
|
||
scrollController.connect('scroll', this._onPositionScaleScroll.bind(this));
|
||
this.positionScale.add_controller(scrollController);
|
||
|
||
this.positionScale.add_css_class('positionscale');
|
||
this.positionScaleValueSignal = this.positionScale.connect(
|
||
'value-changed', this._onPositionScaleValueChanged.bind(this)
|
||
);
|
||
|
||
/* GTK4 is missing pressed/released signals for GtkRange/GtkScale.
|
||
* We cannot add controllers, cause it already has them, so we
|
||
* workaround this by observing css classes it currently has */
|
||
this.positionScaleDragSignal = this.positionScale.connect(
|
||
'notify::css-classes', this._onPositionScaleDragging.bind(this)
|
||
);
|
||
|
||
this.positionAdjustment = this.positionScale.get_adjustment();
|
||
this.positionAdjustment.set_page_increment(0);
|
||
this.positionAdjustment.set_step_increment(8);
|
||
|
||
const chapterLabel = new Gtk.Label();
|
||
chapterLabel.add_css_class('chapterlabel');
|
||
|
||
this.chapterPopover = new Gtk.Popover({
|
||
position: Gtk.PositionType.TOP,
|
||
autohide: false,
|
||
child: chapterLabel,
|
||
});
|
||
const box = new Gtk.Box({
|
||
orientation: Gtk.Orientation.VERTICAL,
|
||
hexpand: true,
|
||
valign: Gtk.Align.CENTER,
|
||
can_focus: false,
|
||
});
|
||
|
||
box.append(this.chapterPopover);
|
||
box.append(this.positionScale);
|
||
this.append(box);
|
||
}
|
||
|
||
_setChapterVisible(isVisible)
|
||
{
|
||
const type = (isVisible) ? 'Show' : 'Hide';
|
||
const anti = (isVisible) ? 'Hide' : 'Show';
|
||
|
||
if(this[`chapter${anti}Id`]) {
|
||
GLib.source_remove(this[`chapter${anti}Id`]);
|
||
this[`chapter${anti}Id`] = null;
|
||
}
|
||
|
||
if(
|
||
this[`chapter${type}Id`]
|
||
|| (!isVisible && this.chapterPopover.visible === isVisible)
|
||
)
|
||
return;
|
||
|
||
debug(`changing chapter visibility to: ${isVisible}`);
|
||
|
||
this[`chapter${type}Id`] = GLib.idle_add(
|
||
GLib.PRIORITY_DEFAULT_IDLE + 20,
|
||
() => {
|
||
if(isVisible) {
|
||
const [start, end] = this.positionScale.get_slider_range();
|
||
const controlsHeight = this.parent.get_height();
|
||
const scaleBoxHeight = this.positionScale.parent.get_height();
|
||
const [isShared, destX, destY] = this.positionScale.translate_coordinates(
|
||
this.positionScale.parent, 0, 0
|
||
);
|
||
const clapperWidget = this.get_ancestor(Gtk.Grid);
|
||
|
||
/* Half of slider width, values are defined in CSS */
|
||
const sliderOffset = (
|
||
clapperWidget.isFullscreenMode && !clapperWidget.isMobileMonitor
|
||
) ? 10 : 9;
|
||
|
||
this.chapterPopover.set_pointing_to(new Gdk.Rectangle({
|
||
x: destX + end - sliderOffset,
|
||
y: -(controlsHeight - scaleBoxHeight) / 2,
|
||
width: 0,
|
||
height: 0,
|
||
}));
|
||
}
|
||
|
||
this.chapterPopover.visible = isVisible;
|
||
this[`chapter${type}Id`] = null;
|
||
|
||
debug(`chapter visible: ${isVisible}`);
|
||
|
||
return GLib.SOURCE_REMOVE;
|
||
}
|
||
);
|
||
}
|
||
|
||
_onRealize()
|
||
{
|
||
this.disconnect(this.realizeSignal);
|
||
this.realizeSignal = null;
|
||
|
||
const clapperWidget = this.get_ancestor(Gtk.Grid);
|
||
const scrollController = new Gtk.EventControllerScroll();
|
||
scrollController.set_flags(
|
||
Gtk.EventControllerScrollFlags.VERTICAL
|
||
| Gtk.EventControllerScrollFlags.DISCRETE
|
||
);
|
||
scrollController.connect('scroll', clapperWidget._onScroll.bind(clapperWidget));
|
||
this.volumeButton.add_controller(scrollController);
|
||
|
||
const initialVolume = (settings.get_boolean('volume-custom'))
|
||
? settings.get_int('volume-value') / 100
|
||
: settings.get_double('volume-last');
|
||
|
||
clapperWidget.player.volume = initialVolume;
|
||
clapperWidget.player.bind_property('mute', this.volumeButton, 'muted',
|
||
GObject.BindingFlags.DEFAULT
|
||
);
|
||
}
|
||
|
||
_onPlayerResize(width, height)
|
||
{
|
||
const isMobile = (width < this.minFullViewWidth);
|
||
if(this.isMobile === isMobile)
|
||
return;
|
||
|
||
for(let revealer of this.revealersArr)
|
||
revealer.set_reveal_child(!isMobile);
|
||
|
||
this.revealTracksRevealer.set_reveal_child(isMobile);
|
||
this.isMobile = isMobile;
|
||
}
|
||
|
||
_onUnfullscreenClicked(button)
|
||
{
|
||
const root = button.get_root();
|
||
root.unfullscreen();
|
||
}
|
||
|
||
_onCheckButtonToggled(checkButton)
|
||
{
|
||
if(!checkButton.get_active())
|
||
return;
|
||
|
||
switch(checkButton.type) {
|
||
case 'video':
|
||
case 'audio':
|
||
case 'subtitle':
|
||
this._handleTrackChange(checkButton);
|
||
break;
|
||
case 'visualization':
|
||
this._handleVisualizationChange(checkButton);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
_onTogglePlayClicked()
|
||
{
|
||
/* Parent of controls changes, so get ancestor instead */
|
||
const { player } = this.get_ancestor(Gtk.Grid);
|
||
player.toggle_play();
|
||
}
|
||
|
||
_onPositionScaleScroll(controller, dx, dy)
|
||
{
|
||
const clapperWidget = this.get_ancestor(Gtk.Grid);
|
||
clapperWidget._onScroll(controller, dx || dy, 0);
|
||
}
|
||
|
||
_onPositionScaleValueChanged(scale)
|
||
{
|
||
const scaleValue = scale.get_value();
|
||
const positionSeconds = Math.round(scaleValue);
|
||
|
||
this.currentPosition = positionSeconds;
|
||
this.updateElapsedLabel(positionSeconds);
|
||
|
||
if(this.chapters && this.isPositionDragging) {
|
||
const chapter = this.chapters[scaleValue];
|
||
const isChapter = (chapter != null);
|
||
|
||
if(isChapter)
|
||
this.chapterPopover.child.label = chapter;
|
||
|
||
this._setChapterVisible(isChapter);
|
||
}
|
||
}
|
||
|
||
_onPositionScaleDragging(scale)
|
||
{
|
||
const isPositionDragging = scale.has_css_class('dragging');
|
||
|
||
if(this.isPositionDragging === isPositionDragging)
|
||
return;
|
||
|
||
const clapperWidget = this.get_ancestor(Gtk.Grid);
|
||
if(!clapperWidget) return;
|
||
|
||
if(clapperWidget.isFullscreenMode) {
|
||
clapperWidget.revealControls();
|
||
|
||
if(isPositionDragging)
|
||
clapperWidget._clearTimeout('hideControls');
|
||
}
|
||
|
||
if((this.isPositionDragging = isPositionDragging))
|
||
return;
|
||
|
||
const scaleValue = scale.get_value();
|
||
const isChapterSeek = this.chapterPopover.visible;
|
||
|
||
if(!isChapterSeek) {
|
||
const positionSeconds = Math.round(scaleValue);
|
||
clapperWidget.player.seek_seconds(positionSeconds);
|
||
}
|
||
else {
|
||
clapperWidget.player.seek_chapter(scaleValue);
|
||
this._setChapterVisible(false);
|
||
}
|
||
}
|
||
|
||
_onCloseRequest()
|
||
{
|
||
debug('controls close request');
|
||
|
||
this.positionScale.disconnect(this.positionScaleValueSignal);
|
||
this.positionScale.disconnect(this.positionScaleDragSignal);
|
||
}
|
||
});
|