Compare commits
2 commits
fdbbfcd730
...
7f7a84d747
Author | SHA1 | Date | |
---|---|---|---|
7f7a84d747 | |||
de5c1179fa |
5 changed files with 183 additions and 133 deletions
|
@ -4,6 +4,7 @@ audrey_sources = [
|
||||||
'globalconf.vala',
|
'globalconf.vala',
|
||||||
'main.vala',
|
'main.vala',
|
||||||
'mpris.vala',
|
'mpris.vala',
|
||||||
|
'play_queue.vala',
|
||||||
'playbin.vala',
|
'playbin.vala',
|
||||||
'ui/play_queue.vala',
|
'ui/play_queue.vala',
|
||||||
'ui/setup.vala',
|
'ui/setup.vala',
|
||||||
|
|
136
src/play_queue.vala
Normal file
136
src/play_queue.vala
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
// this is a custom SelectionModel that lets us only signal the
|
||||||
|
// selection has changed on user interaction
|
||||||
|
internal class Audrey.PlayQueue : GLib.Object, GLib.ListModel, Gtk.SelectionModel {
|
||||||
|
public GLib.ListStore inner { get; private set; }
|
||||||
|
// if current_position == inner.get_n_items (), play queue is stopped
|
||||||
|
public uint current_position { get; private set; }
|
||||||
|
|
||||||
|
// emitted when a track is purposefully selected
|
||||||
|
public signal void user_selected (uint position);
|
||||||
|
|
||||||
|
public PlayQueue () {
|
||||||
|
this.inner = new GLib.ListStore (typeof (Song));
|
||||||
|
this.current_position = this.inner.get_n_items ();
|
||||||
|
|
||||||
|
this.inner.items_changed.connect (this.on_inner_items_changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// only called by playbin
|
||||||
|
// does not emit user_selected
|
||||||
|
internal void playbin_select (uint new_position) {
|
||||||
|
var previous_position = this.current_position;
|
||||||
|
this.current_position = new_position;
|
||||||
|
|
||||||
|
if (previous_position < this.inner.get_n_items ()) {
|
||||||
|
if (new_position < this.inner.get_n_items ()) {
|
||||||
|
if (previous_position < new_position) {
|
||||||
|
this.selection_changed (previous_position, new_position-previous_position+1);
|
||||||
|
} else {
|
||||||
|
this.selection_changed (new_position, previous_position-new_position+1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.selection_changed (previous_position, 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (new_position < this.inner.get_n_items ()) {
|
||||||
|
this.selection_changed (new_position, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// called by anything else that wishes to switch tracks
|
||||||
|
// emits user_selected
|
||||||
|
public void user_select (uint position)
|
||||||
|
requires (position < this.get_n_items ())
|
||||||
|
{
|
||||||
|
this.user_selected (position);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_inner_items_changed (GLib.ListModel inner, uint position, uint removed, uint added) {
|
||||||
|
// FIXME: potentially try to see if the current item was reordered
|
||||||
|
// see what Gtk.SingleSelection does
|
||||||
|
|
||||||
|
bool emit_signal = false;
|
||||||
|
|
||||||
|
if (this.current_position >= position) {
|
||||||
|
if (this.current_position < position+removed && added == 0) {
|
||||||
|
emit_signal = true;
|
||||||
|
this.current_position = position;
|
||||||
|
} else {
|
||||||
|
this.current_position += added;
|
||||||
|
this.current_position -= removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.items_changed (position, removed, added);
|
||||||
|
if (emit_signal) this.selection_changed (position, 1);
|
||||||
|
// user_select doesnt need to be signalled, since the playqueue
|
||||||
|
// handles this on its own
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gtk.SelectionModel methods
|
||||||
|
|
||||||
|
Gtk.Bitset get_selection_in_range (uint position, uint n_items)
|
||||||
|
requires (position+n_items <= this.inner.get_n_items ())
|
||||||
|
{
|
||||||
|
var bitset = new Gtk.Bitset.empty ();
|
||||||
|
bitset.add (this.current_position);
|
||||||
|
return bitset;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_selected (uint position)
|
||||||
|
requires (position < this.inner.get_n_items ())
|
||||||
|
{
|
||||||
|
return position == this.current_position;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool select_all () {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool select_item (uint position, bool unselect_rest)
|
||||||
|
requires (position < this.inner.get_n_items ())
|
||||||
|
{
|
||||||
|
if (!unselect_rest) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.user_select (position);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool select_range (uint position, uint n_items, bool unselect_rest) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool set_selection (Gtk.Bitset selected, Gtk.Bitset mask) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool unselect_all () {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool unselect_item (uint position) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool unselect_range (uint position, uint n_items) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GLib.ListView methods
|
||||||
|
|
||||||
|
Object? get_item (uint position) {
|
||||||
|
return this.inner.get_item (position);
|
||||||
|
}
|
||||||
|
|
||||||
|
Type get_item_type () {
|
||||||
|
return this.inner.get_item_type ();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint get_n_items () {
|
||||||
|
return this.inner.get_n_items ();
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,18 +25,21 @@ class Playbin : GLib.Object {
|
||||||
|
|
||||||
public PlaybinState state { get; private set; default = PlaybinState.STOPPED; }
|
public PlaybinState state { get; private set; default = PlaybinState.STOPPED; }
|
||||||
|
|
||||||
// true if a timer should update the postion property
|
// true if a timer should update the position property
|
||||||
private bool update_position = false;
|
private bool update_position = false;
|
||||||
public int64 position { get; private set; default = 0; }
|
public int64 position { get; private set; default = 0; }
|
||||||
|
|
||||||
public Subsonic api { get; set; default = null; }
|
public Subsonic api { get; set; default = null; }
|
||||||
|
|
||||||
// sent when a new song starts playing
|
// sent when a new song starts playing
|
||||||
public signal void now_playing (uint index, Song song, int64 duration);
|
// continues: whether the track is a gapless continuation
|
||||||
|
public signal void now_playing (bool continues, uint index, Song song, int64 duration);
|
||||||
|
|
||||||
// FIXME this should be synced with the selection model, right??
|
// the index of the track in the play queue that is currently playing
|
||||||
public uint playing_index { get; private set; }
|
// must equal play queue len iff state is STOPPED
|
||||||
|
public uint current_position { get; private set; }
|
||||||
|
|
||||||
|
// whether we are expecting a gapless continuation next
|
||||||
private bool next_gapless;
|
private bool next_gapless;
|
||||||
|
|
||||||
private void source_setup (Gst.Element playbin, dynamic Gst.Element source) {
|
private void source_setup (Gst.Element playbin, dynamic Gst.Element source) {
|
||||||
|
@ -45,7 +48,7 @@ class Playbin : GLib.Object {
|
||||||
|
|
||||||
// ASSUMPTION: about-to-finish will be signalled exactly once per track
|
// ASSUMPTION: about-to-finish will be signalled exactly once per track
|
||||||
// even if seeking backwards after
|
// even if seeking backwards after
|
||||||
GLib.AsyncQueue<string> next_uri = new GLib.AsyncQueue<string> ();
|
private GLib.AsyncQueue<string> next_uri = new GLib.AsyncQueue<string> ();
|
||||||
|
|
||||||
private ListModel _play_queue = null;
|
private ListModel _play_queue = null;
|
||||||
private ulong _play_queue_items_changed;
|
private ulong _play_queue_items_changed;
|
||||||
|
@ -79,27 +82,30 @@ class Playbin : GLib.Object {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.playing_index >= position) {
|
if (this.current_position >= position) {
|
||||||
if (this.playing_index < position+removed) {
|
if (this.current_position < position+removed) {
|
||||||
// current track was removed, start playing something else
|
// current track was removed, start playing something else
|
||||||
// TODO check if it was actually reordered
|
// TODO check if it was actually reordered
|
||||||
|
|
||||||
this.begin_playback (position);
|
if (position == play_queue.get_n_items ()) {
|
||||||
|
this.stop ();
|
||||||
|
} else {
|
||||||
|
this.select_track (position);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// unaffected
|
// unaffected
|
||||||
// fix up playing index though
|
// fix up playing index though
|
||||||
this.playing_index += added;
|
this.current_position = this.current_position + added - removed;
|
||||||
this.playing_index -= removed;
|
|
||||||
}
|
}
|
||||||
} else if (this.playing_index+1 == position) {
|
} else if (this.current_position+1 == position) {
|
||||||
// next track was changed
|
// next track was changed
|
||||||
// try to fix up gapless transition
|
// try to fix up gapless transition
|
||||||
string? next_uri = this.next_uri.try_pop ();
|
string? next_uri = this.next_uri.try_pop ();
|
||||||
if (next_uri != null) {
|
if (next_uri != null) {
|
||||||
// we're in luck, about-to-finish hasn't been triggered yet
|
// we're in luck, about-to-finish hasn't been triggered yet
|
||||||
// we can get away with replacing it
|
// we can get away with replacing it
|
||||||
if (this.playing_index+1 < play_queue.get_n_items ()) {
|
if (this.current_position+1 < play_queue.get_n_items ()) {
|
||||||
Song song = (Song) play_queue.get_item (this.playing_index+1);
|
Song song = (Song) play_queue.get_item (this.current_position+1);
|
||||||
this.next_uri.push (this.api.stream_uri (song.id));
|
this.next_uri.push (this.api.stream_uri (song.id));
|
||||||
} else {
|
} else {
|
||||||
this.next_uri.push ("");
|
this.next_uri.push ("");
|
||||||
|
@ -162,19 +168,20 @@ class Playbin : GLib.Object {
|
||||||
|
|
||||||
this.position = 0;
|
this.position = 0;
|
||||||
|
|
||||||
|
bool continues = this.next_gapless;
|
||||||
if (this.next_gapless) {
|
if (this.next_gapless) {
|
||||||
// advance position in play queue
|
// advance position in play queue
|
||||||
this.playing_index += 1;
|
this.current_position += 1;
|
||||||
} else {
|
} else {
|
||||||
this.next_gapless = true;
|
this.next_gapless = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var now_playing = (Song) play_queue.get_item (this.playing_index);
|
var now_playing = (Song) play_queue.get_item (this.current_position);
|
||||||
if (this.api.stream_uri (now_playing.id) == (string) this.playbin.current_uri) {
|
if (this.api.stream_uri (now_playing.id) == (string) this.playbin.current_uri) {
|
||||||
this.now_playing (this.playing_index, now_playing, duration);
|
this.now_playing (continues, this.current_position, now_playing, duration);
|
||||||
|
|
||||||
if (this.playing_index+1 < play_queue.get_n_items ()) {
|
if (this.current_position+1 < play_queue.get_n_items ()) {
|
||||||
Song song = (Song) play_queue.get_item (this.playing_index+1);
|
Song song = (Song) play_queue.get_item (this.current_position+1);
|
||||||
this.next_uri.push (this.api.stream_uri (song.id));
|
this.next_uri.push (this.api.stream_uri (song.id));
|
||||||
} else {
|
} else {
|
||||||
this.next_uri.push ("");
|
this.next_uri.push ("");
|
||||||
|
@ -182,7 +189,7 @@ class Playbin : GLib.Object {
|
||||||
} else {
|
} else {
|
||||||
// edge case
|
// edge case
|
||||||
// just flush everything and pray next stream-start is fine
|
// just flush everything and pray next stream-start is fine
|
||||||
this.begin_playback (this.playing_index);
|
this.select_track (this.current_position);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -206,10 +213,13 @@ class Playbin : GLib.Object {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void begin_playback (uint position) {
|
// manually changes which track in the play queue to play
|
||||||
|
public void select_track (uint position)
|
||||||
|
requires (position < this.play_queue.get_n_items ())
|
||||||
|
{
|
||||||
this.state = PlaybinState.PLAYING;
|
this.state = PlaybinState.PLAYING;
|
||||||
|
|
||||||
this.playing_index = position;
|
this.current_position = position;
|
||||||
this.playbin.set_state (Gst.State.READY);
|
this.playbin.set_state (Gst.State.READY);
|
||||||
this.playbin.uri = this.api.stream_uri (((Song) this.play_queue.get_item (position)).id);
|
this.playbin.uri = this.api.stream_uri (((Song) this.play_queue.get_item (position)).id);
|
||||||
this.playbin.set_state (Gst.State.PLAYING);
|
this.playbin.set_state (Gst.State.PLAYING);
|
||||||
|
@ -233,4 +243,10 @@ class Playbin : GLib.Object {
|
||||||
this.playbin.set_state (Gst.State.PLAYING);
|
this.playbin.set_state (Gst.State.PLAYING);
|
||||||
this.state = PlaybinState.PLAYING;
|
this.state = PlaybinState.PLAYING;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void stop () {
|
||||||
|
this.playbin.set_state (Gst.State.READY);
|
||||||
|
this.state = PlaybinState.STOPPED;
|
||||||
|
this.current_position = this.play_queue.get_n_items ();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,108 +1,3 @@
|
||||||
// this is a custom SelectionModel that lets us only signal the
|
|
||||||
// selection has changed on user interaction
|
|
||||||
class PlayQueueSelection : GLib.Object, GLib.ListModel, Gtk.SelectionModel {
|
|
||||||
public ListStore inner { get; private set; }
|
|
||||||
|
|
||||||
private uint _selected_position;
|
|
||||||
public uint selected_position {
|
|
||||||
get { return _selected_position; }
|
|
||||||
set {
|
|
||||||
var previous = _selected_position;
|
|
||||||
_selected_position = value;
|
|
||||||
|
|
||||||
if (previous < inner.get_n_items ()) {
|
|
||||||
this.selection_changed (previous, 1);
|
|
||||||
}
|
|
||||||
if (value < inner.get_n_items ()) {
|
|
||||||
this.selection_changed (value, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public signal void user_selected (uint position);
|
|
||||||
|
|
||||||
internal PlayQueueSelection () {
|
|
||||||
this.inner = new GLib.ListStore (typeof (Song));
|
|
||||||
this._selected_position = inner.get_n_items ();
|
|
||||||
|
|
||||||
this.inner.items_changed.connect ((position, removed, added) => {
|
|
||||||
bool emit_signal = false;
|
|
||||||
|
|
||||||
if (this.selected_position >= position) {
|
|
||||||
if (this.selected_position < position+removed && added == 0) {
|
|
||||||
emit_signal = true;
|
|
||||||
this._selected_position = position;
|
|
||||||
} else {
|
|
||||||
this._selected_position += added;
|
|
||||||
this._selected_position -= removed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.items_changed (position, removed, added);
|
|
||||||
if (emit_signal) this.selection_changed (position, 1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Bitset get_selection_in_range (uint position, uint n_items) {
|
|
||||||
var bitset = new Gtk.Bitset.empty ();
|
|
||||||
if (this.selected_position < this.inner.get_n_items ()) {
|
|
||||||
bitset.add (selected_position);
|
|
||||||
}
|
|
||||||
return bitset;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool is_selected (uint position) {
|
|
||||||
return position == this.selected_position;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool select_all () {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool select_item (uint position, bool unselect_rest) {
|
|
||||||
if (!unselect_rest) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selected_position = position;
|
|
||||||
this.user_selected (position);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool select_range (uint position, uint n_items, bool unselect_rest) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool set_selection (Gtk.Bitset selected, Gtk.Bitset mask) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool unselect_all () {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool unselect_item (uint position) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool unselect_range (uint position, uint n_items) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object? get_item (uint position) {
|
|
||||||
return this.inner.get_item (position);
|
|
||||||
}
|
|
||||||
|
|
||||||
Type get_item_type () {
|
|
||||||
return this.inner.get_item_type ();
|
|
||||||
}
|
|
||||||
|
|
||||||
uint get_n_items () {
|
|
||||||
return this.inner.get_n_items ();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[GtkTemplate (ui = "/eu/callcc/audrey/ui/play_queue.ui")]
|
[GtkTemplate (ui = "/eu/callcc/audrey/ui/play_queue.ui")]
|
||||||
public class Ui.PlayQueue : Adw.NavigationPage {
|
public class Ui.PlayQueue : Adw.NavigationPage {
|
||||||
[GtkChild] private unowned Gtk.ColumnView view;
|
[GtkChild] private unowned Gtk.ColumnView view;
|
||||||
|
|
|
@ -28,7 +28,7 @@ class Ui.Window : Adw.ApplicationWindow {
|
||||||
public Gdk.Paintable playing_cover_art { get; set; }
|
public Gdk.Paintable playing_cover_art { get; set; }
|
||||||
|
|
||||||
public Playbin playbin { get; private set; default = new Playbin (); }
|
public Playbin playbin { get; private set; default = new Playbin (); }
|
||||||
public PlayQueueSelection play_queue_model { get; private set; default = new PlayQueueSelection (); }
|
public Audrey.PlayQueue play_queue_model { get; private set; default = new Audrey.PlayQueue (); }
|
||||||
|
|
||||||
public Window (Gtk.Application app) {
|
public Window (Gtk.Application app) {
|
||||||
Object (application: app);
|
Object (application: app);
|
||||||
|
@ -61,15 +61,15 @@ class Ui.Window : Adw.ApplicationWindow {
|
||||||
this.setup.connected.connect ((api) => {
|
this.setup.connected.connect ((api) => {
|
||||||
this.playbin.api = api;
|
this.playbin.api = api;
|
||||||
|
|
||||||
this.playbin.now_playing.connect ((position, song, duration) => {
|
this.playbin.now_playing.connect ((continues, position, song, duration) => {
|
||||||
this.song = song;
|
this.song = song;
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
api.scrobble.begin (song.id);
|
api.scrobble.begin (song.id);
|
||||||
this.play_queue_model.selected_position = position;
|
this.play_queue_model.playbin_select (position);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.play_queue_model.user_selected.connect ((position) => {
|
this.play_queue_model.user_selected.connect ((position) => {
|
||||||
this.playbin.begin_playback (position);
|
this.playbin.select_track (position);
|
||||||
});
|
});
|
||||||
|
|
||||||
public_api = api;
|
public_api = api;
|
||||||
|
@ -189,12 +189,14 @@ class Ui.Window : Adw.ApplicationWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
[GtkCallback] private void on_skip_forward_clicked () {
|
[GtkCallback] private void on_skip_forward_clicked () {
|
||||||
this.play_queue_model.select_item (this.playbin.playing_index+1, true);
|
if (this.playbin.current_position+1 < this.playbin.play_queue.get_n_items ()) {
|
||||||
|
this.playbin.select_track (this.playbin.current_position+1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[GtkCallback] private void on_skip_backward_clicked () {
|
[GtkCallback] private void on_skip_backward_clicked () {
|
||||||
if (this.playbin.playing_index > 0) {
|
if (this.playbin.current_position > 0) {
|
||||||
this.play_queue_model.select_item (this.playbin.playing_index-1, true);
|
this.play_queue_model.user_select (this.playbin.current_position-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue