From 407551313b9954de692545a8d879487d117aef6e Mon Sep 17 00:00:00 2001 From: Erica Z Date: Sun, 13 Oct 2024 14:24:25 +0000 Subject: [PATCH] brand new play queue selection logic --- src/ui/play_queue.blp | 9 +- src/ui/play_queue.vala | 219 +++++++++++++++++++++++++---------------- 2 files changed, 137 insertions(+), 91 deletions(-) diff --git a/src/ui/play_queue.blp b/src/ui/play_queue.blp index 07eec39..5934841 100644 --- a/src/ui/play_queue.blp +++ b/src/ui/play_queue.blp @@ -15,18 +15,11 @@ template $UiPlayQueue: Adw.NavigationPage { } ScrolledWindow { - ColumnView { + ColumnView view { styles [ "data-table" ] - model: SingleSelection selection { - model: bind template.songs; - selected: bind template.selected_index; - selection-changed => $on_song_selected (); - }; - ColumnViewColumn { factory: SignalListItemFactory { - setup => $delete_cell_setup (); }; } diff --git a/src/ui/play_queue.vala b/src/ui/play_queue.vala index 72980ed..66eeae9 100644 --- a/src/ui/play_queue.vala +++ b/src/ui/play_queue.vala @@ -1,113 +1,166 @@ +class Ui.PlayQueueStore : Object, ListModel, Gtk.SelectionModel { + public ListStore inner = new ListStore (typeof (Song)); + public uint playing_index { get; private set; default = 0; } + + private bool starting_stream = false; + + public signal void stop_stream (); + public signal void start_stream (Song song); + // public void on_song_start (); + public signal void now_playing (Song song); + public signal void prepare_next_song (Song? song); + // public void on_stream_end (); + + construct { + this.inner.items_changed.connect ((position, removed, added) => { + if (this.playing_index >= position) { + if (this.playing_index < position+removed) { + this.playing_index = position; + if (this.playing_index < this.inner.get_n_items ()) { + this.starting_stream = true; + this.now_playing ((Song) this.inner.get_item (this.playing_index)); + this.start_stream((Song) this.inner.get_item (this.playing_index)); + } else { + this.stop_stream (); + } + } else { + this.playing_index += added; + this.playing_index -= removed; + } + } else { + this.prepare_next_song ((Song?) this.inner.get_item (this.playing_index+1)); + } + + this.items_changed (position, removed, added); + if (this.playing_index < this.inner.get_n_items ()) { + this.selection_changed (this.playing_index, 1); + } + }); + } + + public void on_stream_start () { + if (!starting_stream) { + this.playing_index += 1; + this.selection_changed (this.playing_index-1, 2); + this.now_playing ((Song) this.inner.get_item (this.playing_index)); + } + + this.prepare_next_song ((Song?) this.inner.get_item (this.playing_index+1)); + starting_stream = false; + } + + Gtk.Bitset get_selection_in_range (uint position, uint n_items) { + var bitset = new Gtk.Bitset.empty (); + if (this.playing_index < this.inner.get_n_items ()) { + bitset.add (playing_index); + } + return bitset; + } + + bool is_selected (uint position) { + return position == this.playing_index; + } + + bool select_all () { + return false; + } + + bool select_item (uint position, bool unselect_rest) { + if (!unselect_rest) { + return false; + } + + var previous = this.playing_index; + this.playing_index = position; + + if (previous < this.inner.get_n_items ()) { + this.selection_changed (previous, 1); + } + this.selection_changed (position, 1); + + this.starting_stream = true; + this.now_playing ((Song) this.inner.get_item (this.playing_index)); + this.start_stream ((Song) this.inner.get_item (this.playing_index)); + + 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")] public class Ui.PlayQueue : Adw.NavigationPage { - public ListStore songs { get; private set; } + [GtkChild] private unowned Gtk.ColumnView view; + PlayQueueStore store = new PlayQueueStore (); - public uint selected_index { get; set; } - // this is the index of the song that will play on next on_stream_start - private uint next_stream_index; - - public signal void play_next (Song? song); public signal void play_now (Song song); - + public signal void play_next (Song? song); public signal void now_playing (Song song); - private bool ignore_selection = false; - - [GtkChild] private unowned Gtk.SingleSelection selection; - public bool can_clear_all { get; private set; default = false; } construct { - this.songs = new ListStore (typeof (Song)); - this.next_stream_index = 0; + this.view.model = this.store; } [GtkCallback] public void clear () { - this.songs.remove_all (); + this.store.inner.remove_all (); this.can_clear_all = false; + + this.store.start_stream.connect ((song) => this.play_now(song)); + this.store.now_playing.connect ((song) => this.now_playing(song)); + this.store.prepare_next_song.connect ((song) => this.play_next(song)); } public void queue (Song song) { - uint new_index = this.songs.get_n_items (); - this.songs.append (song); - if (new_index == next_stream_index) { - this.play_next (song); - } - this.can_clear_all = true; + this.store.inner.append (song); } - [GtkCallback] private void on_song_selected () { - this.selected_index = this.selection.selected; // manual bidi binding - if (this.ignore_selection) return; - this.pick_song (this.selected_index); - } - - private void pick_song (uint index) { - Song? song = (Song?) this.songs.get_item (index); - if (song != null) { - this.ignore_selection = true; - this.selected_index = index; - this.ignore_selection = false; - - this.next_stream_index = index; - this.play_next (song); - this.play_now (song); - } - } - - internal void on_stream_start (Playbin playbin, string uri) { - Song song = (Song) this.songs.get_item (this.next_stream_index); - if (public_api.stream_uri (song.id) != uri) { - // prerolled track wasnt actually next one! - // this can happen if it was deleted from the play queue after about-to-finish was signalled - // gapless playback is ruined anyway, go wild - this.pick_song (this.next_stream_index); - return; - } - this.now_playing (song); - - this.ignore_selection = true; - this.selected_index = this.next_stream_index; - this.ignore_selection = false; - - // prepare for next song ahead of time (gapless) - this.next_stream_index += 1; - Song? next_song = (Song?) this.songs.get_item (this.next_stream_index); - this.play_next (next_song); + internal void on_stream_start () { + this.store.on_stream_start (); } internal void restart () { - this.pick_song (0); + this.store.select_item (0, true); } public void skip_forward () { - this.pick_song (this.selected_index + 1); + this.store.select_item (this.store.playing_index+1, true); } public void skip_backward () { - if (this.selected_index >= 1) { - this.pick_song (this.selected_index - 1); + if (this.store.playing_index >= 1) { + this.store.select_item (this.store.playing_index-1, true); } } - - [GtkCallback] private void delete_cell_setup (Object object) { - Gtk.ColumnViewCell cell = (Gtk.ColumnViewCell) object; - Gtk.Button button = new Gtk.Button.from_icon_name ("edit-delete"); - button.add_css_class ("flat"); - cell.child = button; - button.clicked.connect (() => { - this.songs.remove (cell.position); - this.can_clear_all = this.songs.get_n_items() > 0; - - if (cell.position == this.next_stream_index) { - // we just deleted the track that was to be prerolled next - // replace it - this.play_next ((Song?) this.songs.get_item (cell.position)); - } else if (cell.position+1 == this.next_stream_index) { - // conversely, we just deleted the currently playing track - // redo - this.pick_song (cell.position); - } - }); - } }