From 3ba21d42ec26ce8b370938a32d24c3dad1671bd3 Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Tue, 19 Jan 2021 14:40:25 +0100 Subject: [PATCH] Add playlist widget to elapsed time button popover --- clapper_src/app.js | 22 ++-- clapper_src/buttons.js | 96 +++++++++++------- clapper_src/controls.js | 30 ++++-- clapper_src/player.js | 81 ++++++--------- clapper_src/playerBase.js | 2 + clapper_src/playlist.js | 207 ++++++++++++++++++++++++++++++++++++++ clapper_src/widget.js | 17 ++-- css/styles.css | 8 ++ 8 files changed, 345 insertions(+), 118 deletions(-) create mode 100644 clapper_src/playlist.js diff --git a/clapper_src/app.js b/clapper_src/app.js index 3d8a2e58..85d51c8b 100644 --- a/clapper_src/app.js +++ b/clapper_src/app.js @@ -17,7 +17,6 @@ class ClapperApp extends AppBase this.get_flags() | Gio.ApplicationFlags.HANDLES_OPEN ); - this.playlist = []; } vfunc_startup() @@ -42,10 +41,12 @@ class ClapperApp extends AppBase { super.vfunc_open(files, hint); - this.playlist = files; + const { player } = this.active_window.get_child(); - if(this.doneFirstActivate) - this.setWindowPlaylist(this.active_window); + if(!this.doneFirstActivate) + player._preparePlaylist(files); + else + player.set_playlist(files); this.activate(); } @@ -54,15 +55,12 @@ class ClapperApp extends AppBase { super._onWindowShow(window); - this.setWindowPlaylist(window); - } + const { player } = this.active_window.get_child(); + const success = player.playlistWidget.nextTrack(); - setWindowPlaylist(window) - { - if(!this.playlist.length) - return; + if(!success) + debug('playlist is empty'); - const { player } = window.get_child(); - player.set_playlist(this.playlist); + player.widget.grab_focus(); } }); diff --git a/clapper_src/buttons.js b/clapper_src/buttons.js index 2bbe34c5..3653716f 100644 --- a/clapper_src/buttons.js +++ b/clapper_src/buttons.js @@ -85,13 +85,6 @@ class ClapperIconButton extends CustomButton }); this.floatUnaffected = true; } - - setFullscreenMode(isFullscreen) - { - /* Redraw icon after style class change */ - this.set_icon_name(this.icon_name); - super.setFullscreenMode(isFullscreen); - } }); var IconToggleButton = GObject.registerClass( @@ -116,38 +109,13 @@ class ClapperIconToggleButton extends IconButton } }); -var LabelButton = GObject.registerClass( -class ClapperLabelButton extends CustomButton +var PopoverButtonBase = GObject.registerClass( +class ClapperPopoverButtonBase extends CustomButton { - _init(text) + _init() { - super._init({ - margin_start: 0, - margin_end: 0, - }); + super._init(); - this.customLabel = new Gtk.Label({ - label: text, - single_line_mode: true, - }); - this.customLabel.add_css_class('labelbutton'); - this.set_child(this.customLabel); - } - - set_label(text) - { - this.customLabel.set_text(text); - } -}); - -var PopoverButton = GObject.registerClass( -class ClapperPopoverButton extends IconButton -{ - _init(icon) - { - super._init(icon); - - this.floatUnaffected = false; this.popover = new Gtk.Popover({ position: Gtk.PositionType.TOP, }); @@ -203,3 +171,59 @@ class ClapperPopoverButton extends IconButton this.popover.unparent(); } }); + +var IconPopoverButton = GObject.registerClass( +class ClapperIconPopoverButton extends PopoverButtonBase +{ + _init(icon) + { + super._init(); + + this.icon_name = icon; + } +}); + +var LabelPopoverButton = GObject.registerClass( +class ClapperLabelPopoverButton extends PopoverButtonBase +{ + _init(text) + { + super._init(); + + this.customLabel = new Gtk.Label({ + label: text, + single_line_mode: true, + }); + this.customLabel.add_css_class('labelbutton'); + this.set_child(this.customLabel); + } + + set_label(text) + { + this.customLabel.set_text(text); + } +}); + +var ElapsedPopoverButton = GObject.registerClass( +class ClapperElapsedPopoverButton extends LabelPopoverButton +{ + _init(text) + { + super._init(text); + + this.scrolledWindow = new Gtk.ScrolledWindow({ + max_content_height: 150, + min_content_width: 250, + propagate_natural_height: true, + }); + this.popoverBox.append(this.scrolledWindow); + } + + setFullscreenMode(isFullscreen) + { + super.setFullscreenMode(isFullscreen); + + this.scrolledWindow.max_content_height = (isFullscreen) + ? 190 : 150; + } +}); diff --git a/clapper_src/controls.js b/clapper_src/controls.js index f9f271c1..e4362290 100644 --- a/clapper_src/controls.js +++ b/clapper_src/controls.js @@ -42,7 +42,7 @@ class ClapperControls extends Gtk.Box this._addTogglePlayButton(); const elapsedRevealer = new Revealers.ButtonsRevealer('SLIDE_RIGHT'); - this.elapsedButton = this.addLabelButton('00:00/00:00', elapsedRevealer); + this.elapsedButton = this.addElapsedPopoverButton('00:00/00:00', elapsedRevealer); elapsedRevealer.set_reveal_child(true); this.revealersArr.push(elapsedRevealer); this.append(elapsedRevealer); @@ -59,22 +59,22 @@ class ClapperControls extends Gtk.Box const tracksRevealer = new Revealers.ButtonsRevealer( 'SLIDE_LEFT', revealTracksButton ); - this.visualizationsButton = this.addPopoverButton( + this.visualizationsButton = this.addIconPopoverButton( 'display-projector-symbolic', tracksRevealer ); this.visualizationsButton.set_visible(false); - this.videoTracksButton = this.addPopoverButton( + this.videoTracksButton = this.addIconPopoverButton( 'emblem-videos-symbolic', tracksRevealer ); this.videoTracksButton.set_visible(false); - this.audioTracksButton = this.addPopoverButton( + this.audioTracksButton = this.addIconPopoverButton( 'emblem-music-symbolic', tracksRevealer ); this.audioTracksButton.set_visible(false); - this.subtitleTracksButton = this.addPopoverButton( + this.subtitleTracksButton = this.addIconPopoverButton( 'media-view-subtitles-symbolic', tracksRevealer ); @@ -165,17 +165,25 @@ class ClapperControls extends Gtk.Box return button; } - addLabelButton(text, revealer) + addIconPopoverButton(iconName, revealer) { - text = text || ''; - const button = new Buttons.LabelButton(text); + const button = new Buttons.IconPopoverButton(iconName); return this.addButton(button, revealer); } - addPopoverButton(iconName, revealer) + addLabelPopoverButton(text, revealer) { - const button = new Buttons.PopoverButton(iconName); + text = text || ''; + const button = new Buttons.LabelPopoverButton(text); + + return this.addButton(button, revealer); + } + + addElapsedPopoverButton(text, revealer) + { + text = text || ''; + const button = new Buttons.ElapsedPopoverButton(text); return this.addButton(button, revealer); } @@ -363,7 +371,7 @@ class ClapperControls extends Gtk.Box _addVolumeButton() { - this.volumeButton = this.addPopoverButton( + this.volumeButton = this.addIconPopoverButton( 'audio-volume-muted-symbolic' ); this.volumeScale = new Gtk.Scale({ diff --git a/clapper_src/player.js b/clapper_src/player.js index 7a263b7b..58895181 100644 --- a/clapper_src/player.js +++ b/clapper_src/player.js @@ -15,7 +15,6 @@ class ClapperPlayer extends PlayerBase super._init(); this.cursorInPlayer = false; - this.is_local_file = false; this.seek_done = true; this.dragAllowed = false; this.isWidgetDragging = false; @@ -32,9 +31,6 @@ class ClapperPlayer extends PlayerBase this._maxVolume = Misc.getLinearValue(Misc.maxVolume); - this._playlist = []; - this._trackId = 0; - this._hideCursorTimeout = null; this._hideControlsTimeout = null; this._updateTimeTimeout = null; @@ -80,45 +76,24 @@ class ClapperPlayer extends PlayerBase this._realizeSignal = this.widget.connect('realize', this._onWidgetRealize.bind(this)); } - set_media(source) + set_uri(uri) { - let file; - - if(source.get_path) - file = source; - else { - 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); - } - - file = Gio.file_new_for_uri(source); - } + if(Gst.Uri.get_protocol(uri) !== 'file') + return super.set_uri(uri); + let file = Gio.file_new_for_uri(uri); if(!file.query_exists(null)) { debug(`file does not exist: ${file.get_path()}`, 'LEVEL_WARNING'); - this._trackId++; - if(this._playlist.length <= this._trackId) - return debug('set media reached end of playlist'); + if(!this.playlistWidget.nextTrack()) + debug('set media reached end of playlist'); - return this.set_media(this._playlist[this._trackId]); + return; } - - const uri = file.get_uri(); if(uri.endsWith('.claps')) return this.load_playlist_file(file); - this.is_local_file = true; - this.set_uri(uri); + super.set_uri(uri); } load_playlist_file(file) @@ -140,7 +115,7 @@ class ClapperPlayer extends PlayerBase if(!lineFile) continue; - line = lineFile.get_path(); + line = lineFile.get_uri(); } debug(`new playlist item: ${line}`); playlist.push(line); @@ -149,20 +124,29 @@ class ClapperPlayer extends PlayerBase this.set_playlist(playlist); } - set_playlist(playlist) + _preparePlaylist(playlist) { - if(!Array.isArray(playlist) || !playlist.length) - return; + this.playlistWidget.removeAll(); - this._trackId = 0; - this._playlist = playlist; + for(let source of playlist) { + const uri = (source.get_uri != null) + ? source.get_uri() + : Gst.uri_is_valid(source) + ? source + : Gst.filename_to_uri(source); - this.set_media(this._playlist[0]); + this.playlistWidget.addItem(uri); + } } - get_playlist() + set_playlist(playlist) { - return this._playlist; + this._preparePlaylist(playlist); + + const firstTrack = this.playlistWidget.get_row_at_index(0); + if(!firstTrack) return; + + firstTrack.activate(); } set_subtitles(source) @@ -433,14 +417,15 @@ class ClapperPlayer extends PlayerBase _onStreamEnded(player) { - debug(`end of stream: ${this._trackId}`); - this.emitWs('end_of_stream', this._trackId); + const lastTrackId = this.playlistWidget.activeRowId; - this._trackId++; + debug(`end of stream: ${lastTrackId}`); + this.emitWs('end_of_stream', lastTrackId); - if(this._trackId < this._playlist.length) - this.set_media(this._playlist[this._trackId]); - else if(settings.get_boolean('close-auto')) { + if(this.playlistWidget.nextTrack()) + return; + + if(settings.get_boolean('close-auto')) { /* Stop will be automatically called soon afterwards */ this._performCloseCleanup(this.widget.get_root()); this.quitOnStop = true; diff --git a/clapper_src/playerBase.js b/clapper_src/playerBase.js index d1e40a28..d5a2be00 100644 --- a/clapper_src/playerBase.js +++ b/clapper_src/playerBase.js @@ -1,6 +1,7 @@ const { Gio, GLib, GObject, Gst, GstPlayer, Gtk } = imports.gi; const Debug = imports.clapper_src.debug; const Misc = imports.clapper_src.misc; +const { PlaylistWidget } = imports.clapper_src.playlist; const { WebApp } = imports.clapper_src.webApp; const { debug } = Debug; @@ -54,6 +55,7 @@ class ClapperPlayerBase extends GstPlayer.Player this.webserver = null; this.webapp = null; + this.playlistWidget = new PlaylistWidget(); this.set_all_plugins_ranks(); this.set_initial_config(); diff --git a/clapper_src/playlist.js b/clapper_src/playlist.js new file mode 100644 index 00000000..b0ec49d1 --- /dev/null +++ b/clapper_src/playlist.js @@ -0,0 +1,207 @@ +const { Gdk, GLib, GObject, Gst, Gtk, Pango } = imports.gi; + +var PlaylistWidget = GObject.registerClass( +class ClapperPlaylistWidget extends Gtk.ListBox +{ + _init() + { + super._init({ + selection_mode: Gtk.SelectionMode.NONE, + }); + this.activeRowId = -1; + this.connect('row-activated', this._onRowActivated.bind(this)); + } + + addItem(uri) + { + const item = new PlaylistItem(uri); + this.append(item); + } + + removeItem(item) + { + const itemIndex = item.get_index(); + + /* TODO: Handle this case somehow (should app quit?) + * or disable remove button */ + if(itemIndex === this.activeRowId) + return; + + if(itemIndex < this.activeRowId) + this.activeRowId--; + + this.remove(item); + } + + removeAll() + { + let oldItem; + while((oldItem = this.get_row_at_index(0))) + this.remove(oldItem); + + this.activeRowId = -1; + } + + nextTrack() + { + const nextRow = this.get_row_at_index(this.activeRowId + 1); + if(!nextRow) + return false; + + nextRow.activate(); + + return true; + } + + getActiveFilename() + { + const row = this.get_row_at_index(this.activeRowId); + if(!row) return null; + + return row.filename; + } + + /* FIXME: Remove once/if GstPlay(er) gets + * less vague MediaInfo signals */ + getActiveIsLocalFile() + { + const row = this.get_row_at_index(this.activeRowId); + if(!row) return null; + + return row.isLocalFile; + } + + _onRowActivated(listBox, row) + { + const { player } = this.get_ancestor(Gtk.Grid); + + this.activeRowId = row.get_index(); + player.set_uri(row.uri); + } +}); + +let PlaylistItem = GObject.registerClass( +class ClapperPlaylistItem extends Gtk.ListBoxRow +{ + _init(uri) + { + super._init(); + + this.uri = uri; + this.isLocalFile = false; + + let filename; + if(Gst.Uri.get_protocol(uri) === 'file') { + filename = GLib.path_get_basename( + GLib.filename_from_uri(uri)[0] + ); + this.isLocalFile = true; + } + this.filename = filename || uri; + + const box = new Gtk.Box({ + orientation: Gtk.Orientation.HORIZONTAL, + spacing: 6, + margin_start: 6, + margin_end: 6, + height_request: 22, + }); + const icon = new Gtk.Image({ + icon_name: 'open-menu-symbolic', + }); + const label = new Gtk.Label({ + label: filename, + single_line_mode: true, + ellipsize: Pango.EllipsizeMode.END, + width_chars: 5, + hexpand: true, + halign: Gtk.Align.START, + }); + const button = new Gtk.Button({ + icon_name: 'edit-delete-symbolic', + }); + button.add_css_class('flat'); + button.add_css_class('circular'); + button.add_css_class('popoverbutton'); + button.connect('clicked', this._onRemoveClicked.bind(this)); + + box.append(icon); + box.append(label); + box.append(button); + this.set_child(box); + +/* FIXME: D&D inside popover is broken in GTK4 + const dragSource = new Gtk.DragSource({ + actions: Gdk.DragAction.MOVE + }); + dragSource.connect('prepare', this._onDragPrepare.bind(this)); + dragSource.connect('drag-begin', this._onDragBegin.bind(this)); + dragSource.connect('drag-end', this._onDragEnd.bind(this)); + this.add_controller(dragSource); + + const dropTarget = new Gtk.DropTarget({ + actions: Gdk.DragAction.MOVE, + preload: true, + }); + dropTarget.set_gtypes([PlaylistItem]); + dropTarget.connect('enter', this._onEnter.bind(this)); + dropTarget.connect('drop', this._onDrop.bind(this)); + this.add_controller(dropTarget); +*/ + } + + _onRemoveClicked(button) + { + const listBox = this.get_ancestor(Gtk.ListBox); + + listBox.removeItem(this); + } + + _onDragPrepare(source, x, y) + { + const widget = source.get_widget(); + const paintable = new Gtk.WidgetPaintable({ widget }); + const staticImg = paintable.get_current_image(); + + source.set_icon(staticImg, x, y); + + return Gdk.ContentProvider.new_for_value(widget); + } + + _onDragBegin(source, drag) + { + this.child.set_opacity(0.3); + } + + _onDragEnd(source, drag, deleteData) + { + this.child.set_opacity(1.0); + } + + _onEnter(target, x, y) + { + return (target.value) + ? Gdk.DragAction.MOVE + : 0; + } + + _onDrop(target, value, x, y) + { + const destIndex = this.get_index(); + const targetIndex = target.value.get_index(); + + if(destIndex === targetIndex) + return true; + + const listBox = this.get_ancestor(Gtk.ListBox); + + if(listBox && destIndex >= 0) { + listBox.remove(target.value); + listBox.insert(target.value, destIndex); + + return true; + } + + return false; + } +}); diff --git a/clapper_src/widget.js b/clapper_src/widget.js index 940714bf..51633324 100644 --- a/clapper_src/widget.js +++ b/clapper_src/widget.js @@ -1,4 +1,4 @@ -const { Gdk, GLib, GObject, Gst, GstPlayer, Gtk } = imports.gi; +const { Gdk, GLib, GObject, GstPlayer, Gtk } = imports.gi; const { Controls } = imports.clapper_src.controls; const Debug = imports.clapper_src.debug; const Misc = imports.clapper_src.misc; @@ -45,6 +45,8 @@ class ClapperWidget extends Gtk.Grid this.mapSignal = this.connect('map', this._onMap.bind(this)); this.player = new Player(); + this.controls.elapsedButton.scrolledWindow.set_child(this.player.playlistWidget); + this.player.connect('position-updated', this._onPlayerPositionUpdated.bind(this)); this.player.connect('duration-changed', this._onPlayerDurationChanged.bind(this)); @@ -324,17 +326,10 @@ class ClapperWidget extends Gtk.Grid updateTitles(mediaInfo) { let title = mediaInfo.get_title(); - let subtitle = mediaInfo.get_uri(); + let subtitle = this.player.playlistWidget.getActiveFilename(); - if(Gst.Uri.get_protocol(subtitle) === 'file') { - subtitle = GLib.path_get_basename( - GLib.filename_from_uri(subtitle)[0] - ); - } if(!title) { - title = (!subtitle) - ? this.defaultTitle - : (subtitle.includes('.')) + title = (subtitle.includes('.')) ? subtitle.split('.').slice(0, -1).join('.') : subtitle; @@ -461,7 +456,7 @@ class ClapperWidget extends Gtk.Grid this.controls.positionScale.clear_marks(); this.controls.chapters = null; } - if(!player.is_local_file) { + if(!player.playlistWidget.getActiveIsLocalFile()) { this.needsTracksUpdate = true; } break; diff --git a/css/styles.css b/css/styles.css index 1718c214..d8c9c3f8 100644 --- a/css/styles.css +++ b/css/styles.css @@ -22,6 +22,10 @@ radio { min-width: 17px; min-height: 17px; } +/* Adwaita is missing osd ListBox */ +.osd list { + background: none; +} .osd .playercontrols { -gtk-icon-size: 24px; } @@ -70,6 +74,10 @@ radio { font-weight: 600; font-variant-numeric: tabular-nums; } +.popoverbutton { + min-width: 24px; + min-height: 24px; +} /* Position Scale */ .positionscale {