[GtkTemplate (ui = "/eu/callcc/audrey/ui/window.ui")] class Ui.Window : Adw.ApplicationWindow { [GtkChild] private unowned Gtk.ListBox sidebar; [GtkChild] private unowned Gtk.ListBoxRow sidebar_play_queue; [GtkChild] private unowned Gtk.Stack stack; [GtkChild] public unowned Ui.PlayQueue play_queue; [GtkChild] public unowned Adw.ButtonRow shuffle_all_tracks; private Setup setup; private Subsonic.Client api; public int64 position { get; private set; } public double volume { get { return this.playbin.volume; } set { this.playbin.volume = value; } } public bool mute { get { return this.playbin.mute; } set { this.playbin.mute = value; } } private Cancellable cancel_loading_art; public bool cover_art_loading { get; set; default = false; } public Gdk.Paintable playing_cover_art { get; set; } public Playbin playbin { get; private set; default = new Playbin (); } public ListStore play_queue_store { get; private set; default = new ListStore (typeof (Subsonic.Song)); } public Window (Gtk.Application app) { Object (application: app); var provider = new Gtk.CssProvider (); provider.load_from_resource("/eu/callcc/audrey/audrey.css"); Gtk.StyleContext.add_provider_for_display (Gdk.Display.get_default (), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); } construct { Bus.own_name ( BusType.SESSION, "org.mpris.MediaPlayer2.audrey", BusNameOwnerFlags.NONE, (conn) => { try { // TODO: mpris } catch (IOError e) { error ("could not register dbus service: %s", e.message); } }, () => {}, () => { error ("could not acquire dbus name"); }); this.playbin.play_queue = this.play_queue_store; this.setup = new Setup (); this.setup.connected.connect ((api) => { this.api = api; this.playbin.api = api; this.playbin.now_playing.connect ((continues) => { api.scrobble.begin (playbin.current_song.id); this.play_queue.selection.playbin_select (playbin.current_position); }); this.playbin.stopped.connect (() => { this.play_queue.selection.playbin_select (this.play_queue_store.get_n_items ()); }); this.play_queue.selection.user_selected.connect ((position) => { this.playbin.select_track (position); }); this.shuffle_all_tracks.sensitive = true; this.shuffle_all_tracks.activated.connect (() => { this.shuffle_all_tracks.sensitive = false; this.play_queue_store.remove_all (); api.get_random_songs.begin (null, (song) => { this.play_queue_store.append (song); }, (obj, res) => { try { api.get_random_songs.end (res); } catch (Error e) { error ("could not get random songs: %s", e.message); } this.shuffle_all_tracks.sensitive = true; this.playbin.select_track (0); }); }); }); this.setup.load (); this.sidebar.select_row (this.sidebar.get_row_at_index (0)); this.playbin.notify["current-song"].connect (() => { if (this.cancel_loading_art != null) { this.cancel_loading_art.cancel (); } this.cancel_loading_art = new GLib.Cancellable (); this.playing_cover_art = Gdk.Paintable.empty (1, 1); if (playbin.current_song != null) { this.cover_art_loading = true; string song_id = playbin.current_song.id; this.api.cover_art.begin (song_id, this.cancel_loading_art, (obj, res) => { try { this.playing_cover_art = Gdk.Texture.for_pixbuf (this.api.cover_art.end (res)); this.cover_art_loading = false; } catch (Error e) { if (!(e is IOError.CANCELLED)) { warning ("could not load cover for %s: %s", song_id, e.message); this.cover_art_loading = false; } } }); } }); this.playbin.notify["position"].connect (() => { // only set if we aren't seeking if (this.seek_timeout_id == 0) { this.position = this.playbin.position; } }); } [GtkCallback] private void on_sidebar_row_activated (Gtk.ListBoxRow row) { if (row == this.sidebar_play_queue) { this.stack.set_visible_child_name("play_queue"); } } [GtkCallback] private string format_timestamp (int64 ns) { int64 ms = ns / 1000000; int s = (int) (ms / 1000); return "%02d:%02d".printf (s/60, s%60); } private void seek_impl (int64 position) { this.position = position; if (this.seek_timeout_id == 0) { this.seek_timeout_id = Timeout.add (500, () => { playbin.seek(this.position); this.seek_timeout_id = 0; return false; }); } } // same timeout logic as https://code.videolan.org/videolan/npapi-vlc/blob/6eae0ffb9cbaf8f6e04423de2ff38daabdf7cae3/npapi/vlcplugin_gtk.cpp#L312 private uint seek_timeout_id = 0; [GtkCallback] private bool on_play_position_seek (Gtk.Range range, Gtk.ScrollType scroll_type, double value) { this.seek_impl((int64) value); return false; } [GtkCallback] private void on_play_pause_clicked () { if (this.playbin.state == PlaybinState.PLAYING) { this.playbin.pause(); } else { this.playbin.play(); } } [GtkCallback] private string play_pause_icon_name (PlaybinState state) { if (state == PlaybinState.PLAYING) { return "media-playback-pause"; } else { return "media-playback-start"; } } [GtkCallback] private bool playbin_active (PlaybinState state) { return state != PlaybinState.STOPPED; } [GtkCallback] private string mute_button_icon_name (bool mute) { return mute ? "audio-volume-muted" : "audio-volume-high"; } [GtkCallback] private void on_mute_toggle () { this.mute = !this.mute; } [GtkCallback] private void on_skip_forward_clicked () { 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 () { if (this.playbin.current_position > 0) { this.playbin.select_track (this.playbin.current_position-1); } } [GtkCallback] private void show_setup_dialog () { this.setup.present (this); } [GtkCallback] private void seek_backward () { // 10 seconds int64 new_position = position - (int64)10 * 1000 * 1000000; if (new_position < 0) new_position = 0; this.seek_impl (new_position); } [GtkCallback] private void seek_forward () { // 10 seconds int64 new_position = position + (int64)10 * 1000 * 1000000; if (new_position > this.playbin.duration) new_position = this.playbin.duration; this.seek_impl (new_position); } [GtkCallback] private string song_title (Subsonic.Song? song) { return song == null ? "" : song.title; } [GtkCallback] private string song_artist (Subsonic.Song? song) { return song == null ? "" : song.artist; } [GtkCallback] private string song_album (Subsonic.Song? song) { return song == null ? "" : song.album; } }