Files
clapper/src/playlist.js
Rafał Dzięgiel dc25ec976a Name our classes
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
2021-09-02 20:12:39 +02:00

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;
}
});