Compare commits

..

No commits in common. "7f7a84d74716dc06737e953b09014fca25bea4b1" and "fdbbfcd730f6a74a3e7a26ed114e534e048fadc6" have entirely different histories.

5 changed files with 133 additions and 183 deletions

View file

@ -4,7 +4,6 @@ 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',

View file

@ -1,136 +0,0 @@
// 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 ();
}
}

View file

@ -25,21 +25,18 @@ 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 position property // true if a timer should update the postion 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
// continues: whether the track is a gapless continuation public signal void now_playing (uint index, Song song, int64 duration);
public signal void now_playing (bool continues, uint index, Song song, int64 duration);
// the index of the track in the play queue that is currently playing // FIXME this should be synced with the selection model, right??
// must equal play queue len iff state is STOPPED public uint playing_index { get; private set; }
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) {
@ -48,7 +45,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
private GLib.AsyncQueue<string> next_uri = new GLib.AsyncQueue<string> (); 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;
@ -82,30 +79,27 @@ class Playbin : GLib.Object {
return; return;
} }
if (this.current_position >= position) { if (this.playing_index >= position) {
if (this.current_position < position+removed) { if (this.playing_index < 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
if (position == play_queue.get_n_items ()) { this.begin_playback (position);
this.stop ();
} else {
this.select_track (position);
}
} else { } else {
// unaffected // unaffected
// fix up playing index though // fix up playing index though
this.current_position = this.current_position + added - removed; this.playing_index += added;
this.playing_index -= removed;
} }
} else if (this.current_position+1 == position) { } else if (this.playing_index+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.current_position+1 < play_queue.get_n_items ()) { if (this.playing_index+1 < play_queue.get_n_items ()) {
Song song = (Song) play_queue.get_item (this.current_position+1); Song song = (Song) play_queue.get_item (this.playing_index+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 ("");
@ -168,20 +162,19 @@ 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.current_position += 1; this.playing_index += 1;
} else { } else {
this.next_gapless = true; this.next_gapless = true;
} }
var now_playing = (Song) play_queue.get_item (this.current_position); var now_playing = (Song) play_queue.get_item (this.playing_index);
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 (continues, this.current_position, now_playing, duration); this.now_playing (this.playing_index, now_playing, duration);
if (this.current_position+1 < play_queue.get_n_items ()) { if (this.playing_index+1 < play_queue.get_n_items ()) {
Song song = (Song) play_queue.get_item (this.current_position+1); Song song = (Song) play_queue.get_item (this.playing_index+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 ("");
@ -189,7 +182,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.select_track (this.current_position); this.begin_playback (this.playing_index);
} }
}); });
@ -213,13 +206,10 @@ class Playbin : GLib.Object {
} }
} }
// manually changes which track in the play queue to play public void begin_playback (uint position) {
public void select_track (uint position)
requires (position < this.play_queue.get_n_items ())
{
this.state = PlaybinState.PLAYING; this.state = PlaybinState.PLAYING;
this.current_position = position; this.playing_index = 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);
@ -243,10 +233,4 @@ 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 ();
}
} }

View file

@ -1,3 +1,108 @@
// 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;

View file

@ -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 Audrey.PlayQueue play_queue_model { get; private set; default = new Audrey.PlayQueue (); } public PlayQueueSelection play_queue_model { get; private set; default = new PlayQueueSelection (); }
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 ((continues, position, song, duration) => { this.playbin.now_playing.connect ((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.playbin_select (position); this.play_queue_model.selected_position = position;
}); });
this.play_queue_model.user_selected.connect ((position) => { this.play_queue_model.user_selected.connect ((position) => {
this.playbin.select_track (position); this.playbin.begin_playback (position);
}); });
public_api = api; public_api = api;
@ -189,14 +189,12 @@ class Ui.Window : Adw.ApplicationWindow {
} }
[GtkCallback] private void on_skip_forward_clicked () { [GtkCallback] private void on_skip_forward_clicked () {
if (this.playbin.current_position+1 < this.playbin.play_queue.get_n_items ()) { this.play_queue_model.select_item (this.playbin.playing_index+1, true);
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.current_position > 0) { if (this.playbin.playing_index > 0) {
this.play_queue_model.user_select (this.playbin.current_position-1); this.play_queue_model.select_item (this.playbin.playing_index-1, true);
} }
} }