mirror of
https://github.com/Rafostar/clapper.git
synced 2025-08-30 16:02:00 +02:00
In order to not end up with random names prefixed with Gjs_, give each class a proper name, so its easier to inspect and allows usage with UI files
379 lines
9.3 KiB
JavaScript
379 lines
9.3 KiB
JavaScript
const { Gdk, GLib, GObject, Gtk, Pango } = imports.gi;
|
|
const Debug = imports.src.debug;
|
|
const Misc = imports.src.misc;
|
|
|
|
const { debug, warn } = Debug;
|
|
|
|
var RepeatMode = {
|
|
NONE: 0,
|
|
TRACK: 1,
|
|
PLAYLIST: 2,
|
|
SHUFFLE: 3,
|
|
};
|
|
|
|
const repeatIcons = [
|
|
'media-playlist-consecutive-symbolic',
|
|
'media-playlist-repeat-song-symbolic',
|
|
'media-playlist-repeat-symbolic',
|
|
'media-playlist-shuffle-symbolic',
|
|
];
|
|
|
|
var PlaylistWidget = GObject.registerClass({
|
|
GTypeName: 'ClapperPlaylistWidget',
|
|
},
|
|
class ClapperPlaylistWidget extends Gtk.ListBox
|
|
{
|
|
_init()
|
|
{
|
|
super._init({
|
|
selection_mode: Gtk.SelectionMode.NONE,
|
|
});
|
|
this.activeRowId = -1;
|
|
this.repeatMode = RepeatMode.NONE;
|
|
this.add_css_class('clapperplaylist');
|
|
|
|
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();
|
|
|
|
if(itemIndex === this.activeRowId) {
|
|
this.activate_action('window.close', null);
|
|
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()
|
|
{
|
|
return this._switchTrack(false);
|
|
}
|
|
|
|
prevTrack()
|
|
{
|
|
return this._switchTrack(true);
|
|
}
|
|
|
|
getActiveRow()
|
|
{
|
|
return this.get_row_at_index(this.activeRowId);
|
|
}
|
|
|
|
getPlaylist(useFilePaths)
|
|
{
|
|
const playlist = [];
|
|
let index = 0;
|
|
let item;
|
|
|
|
while((item = this.get_row_at_index(index))) {
|
|
const path = (useFilePaths && item.isLocalFile)
|
|
? GLib.filename_from_uri(item.uri)[0]
|
|
: item.uri;
|
|
|
|
playlist.push(path);
|
|
index++;
|
|
}
|
|
|
|
return playlist;
|
|
}
|
|
|
|
getActiveFilename()
|
|
{
|
|
const row = this.getActiveRow();
|
|
if(!row) return null;
|
|
|
|
return row.filename;
|
|
}
|
|
|
|
changeActiveRow(rowId)
|
|
{
|
|
const row = this.get_row_at_index(rowId);
|
|
if(!row)
|
|
return false;
|
|
|
|
row.activate();
|
|
|
|
return true;
|
|
}
|
|
|
|
changeRepeatMode(mode)
|
|
{
|
|
const lastMode = Object.keys(RepeatMode).length - 1;
|
|
const row = this.getActiveRow();
|
|
if(!row) return null;
|
|
|
|
if(mode < 0 || mode > lastMode) {
|
|
warn(`ignored invalid repeat mode value: ${mode}`);
|
|
return;
|
|
}
|
|
|
|
if(mode >= 0)
|
|
this.repeatMode = mode;
|
|
else {
|
|
this.repeatMode++;
|
|
if(this.repeatMode > lastMode)
|
|
this.repeatMode = 0;
|
|
}
|
|
|
|
const repeatButton = row.child.get_first_child();
|
|
repeatButton.icon_name = repeatIcons[this.repeatMode];
|
|
|
|
debug(`set repeat mode: ${this.repeatMode}`);
|
|
}
|
|
|
|
_deactivateActiveItem(isRemoveChange)
|
|
{
|
|
if(this.activeRowId < 0)
|
|
return;
|
|
|
|
const row = this.getActiveRow();
|
|
if(!row) return null;
|
|
|
|
const repeatButton = row.child.get_first_child();
|
|
repeatButton.sensitive = false;
|
|
repeatButton.icon_name = 'open-menu-symbolic';
|
|
|
|
if(isRemoveChange) {
|
|
const removeButton = row.child.get_last_child();
|
|
removeButton.icon_name = 'list-remove-symbolic';
|
|
}
|
|
}
|
|
|
|
_switchTrack(isPrevious)
|
|
{
|
|
const rowId = (isPrevious)
|
|
? this.activeRowId - 1
|
|
: this.activeRowId + 1;
|
|
|
|
return this.changeActiveRow(rowId);
|
|
}
|
|
|
|
_onRowActivated(listBox, row)
|
|
{
|
|
const { player } = this.get_ancestor(Gtk.Grid);
|
|
const repeatButton = row.child.get_first_child();
|
|
const removeButton = row.child.get_last_child();
|
|
|
|
this._deactivateActiveItem(true);
|
|
repeatButton.sensitive = true;
|
|
repeatButton.icon_name = repeatIcons[this.repeatMode];
|
|
removeButton.icon_name = 'window-close-symbolic';
|
|
|
|
this.activeRowId = row.get_index();
|
|
player.set_uri(row.uri);
|
|
}
|
|
|
|
_handleStreamEnded(player)
|
|
{
|
|
/* Seek to beginning when repeating track
|
|
* or playlist with only one item */
|
|
if(
|
|
this.repeatMode === RepeatMode.TRACK
|
|
|| (this.repeatMode !== RepeatMode.NONE
|
|
&& this.activeRowId === 0
|
|
&& !this.get_row_at_index(1))
|
|
) {
|
|
debug('seeking to beginning');
|
|
|
|
player.seek(0);
|
|
return true;
|
|
}
|
|
|
|
if(this.repeatMode === RepeatMode.SHUFFLE) {
|
|
const playlistIds = [];
|
|
let index = 0;
|
|
|
|
debug('selecting random playlist item');
|
|
|
|
while(this.get_row_at_index(index)) {
|
|
/* We prefer to not repeat the same track */
|
|
if(index !== this.activeRowId)
|
|
playlistIds.push(index);
|
|
|
|
index++;
|
|
}
|
|
|
|
/* We always have non-empty array here,
|
|
* otherwise seek to beginning is performed */
|
|
const randomId = playlistIds[
|
|
Math.floor(Math.random() * playlistIds.length)
|
|
];
|
|
debug(`selected random playlist item: ${randomId}`);
|
|
|
|
return this.changeActiveRow(randomId);
|
|
}
|
|
|
|
if(this.nextTrack())
|
|
return true;
|
|
|
|
if(this.repeatMode === RepeatMode.PLAYLIST)
|
|
return this.changeActiveRow(0);
|
|
|
|
this._deactivateActiveItem(false);
|
|
|
|
return false;
|
|
}
|
|
});
|
|
|
|
let PlaylistItem = GObject.registerClass({
|
|
GTypeName: 'ClapperPlaylistItem',
|
|
},
|
|
class ClapperPlaylistItem extends Gtk.ListBoxRow
|
|
{
|
|
_init(uri)
|
|
{
|
|
super._init({
|
|
can_focus: false,
|
|
});
|
|
|
|
this.uri = uri;
|
|
this.isLocalFile = false;
|
|
|
|
let filename;
|
|
if(Misc.getUriProtocol(uri) === 'file') {
|
|
filename = GLib.path_get_basename(
|
|
GLib.filename_from_uri(uri)[0]
|
|
);
|
|
this.isLocalFile = true;
|
|
}
|
|
this.filename = filename || uri;
|
|
this.set_tooltip_text(this.filename);
|
|
|
|
const box = new Gtk.Box({
|
|
orientation: Gtk.Orientation.HORIZONTAL,
|
|
spacing: 6,
|
|
margin_start: 6,
|
|
margin_end: 6,
|
|
height_request: 22,
|
|
});
|
|
const repeatButton = new Gtk.Button({
|
|
icon_name: 'open-menu-symbolic',
|
|
sensitive: false,
|
|
});
|
|
repeatButton.add_css_class('flat');
|
|
repeatButton.add_css_class('circular');
|
|
repeatButton.connect('clicked', this._onRepeatClicked.bind(this));
|
|
const label = new Gtk.Label({
|
|
label: this.filename,
|
|
single_line_mode: true,
|
|
ellipsize: Pango.EllipsizeMode.END,
|
|
width_chars: 5,
|
|
hexpand: true,
|
|
halign: Gtk.Align.START,
|
|
});
|
|
const removeButton = new Gtk.Button({
|
|
icon_name: 'list-remove-symbolic',
|
|
});
|
|
removeButton.add_css_class('flat');
|
|
removeButton.add_css_class('circular');
|
|
removeButton.connect('clicked', this._onRemoveClicked.bind(this));
|
|
|
|
box.append(repeatButton);
|
|
box.append(label);
|
|
box.append(removeButton);
|
|
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);
|
|
*/
|
|
}
|
|
|
|
_onRepeatClicked(button)
|
|
{
|
|
const listBox = this.get_ancestor(Gtk.ListBox);
|
|
|
|
listBox.changeRepeatMode();
|
|
}
|
|
|
|
_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 = value.get_index();
|
|
|
|
if(destIndex === targetIndex)
|
|
return true;
|
|
|
|
const listBox = this.get_ancestor(Gtk.ListBox);
|
|
|
|
if(listBox && destIndex >= 0) {
|
|
listBox.remove(value);
|
|
listBox.insert(value, destIndex);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
});
|