diff --git a/src/api.vala b/src/api.vala
index 27d8fa6..a8c0642 100644
--- a/src/api.vala
+++ b/src/api.vala
@@ -31,7 +31,6 @@ public class Wavelet.Artist : Object, Json.Serializable {
public string? cover_art;
public string? artist_image_url;
public int64 album_count;
- public DateTime? starred;
public Artist (string index, Json.Reader reader) {
this.index = index;
@@ -55,12 +54,6 @@ public class Wavelet.Artist : Object, Json.Serializable {
reader.read_member ("albumCount");
this.album_count = reader.get_int_value ();
reader.end_member ();
-
- reader.read_member ("starred");
- if (reader.is_value ()) {
- this.starred = new DateTime.from_iso8601 (reader.get_string_value (), null);
- }
- reader.end_member ();
}
}
@@ -86,6 +79,7 @@ public class Wavelet.Song : Object {
public string artist { get; private set; }
public int64 track { get; private set; }
public int64 year { get; private set; }
+ public DateTime? starred { get; private set; }
public Song (Json.Reader reader) {
reader.read_member ("id");
diff --git a/src/meson.build b/src/meson.build
index 4844168..4ee8d3c 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -2,6 +2,7 @@ wavelet_sources = [
'api.vala',
'application.vala',
'main.vala',
+ 'mpris.vala',
'play_queue.vala',
'playbin.vala',
'setup.vala',
diff --git a/src/mpris.vala b/src/mpris.vala
new file mode 100644
index 0000000..6c9e6a6
--- /dev/null
+++ b/src/mpris.vala
@@ -0,0 +1,85 @@
+/* play_queue.vala
+ *
+ * Copyright 2024 Erica Z
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+[DBus (name = "org.mpris.MediaPlayer2")]
+class Mpris : Object {
+ internal signal void on_raise ();
+ internal signal void on_quit ();
+
+ public bool can_raise { get { return true; } }
+ public void raise () throws Error {
+ this.on_raise ();
+ }
+
+ public bool can_quit { get { return true; } }
+ public void quit () throws Error {
+ this.on_quit ();
+ }
+
+ public bool can_set_fullscreen { get { return false; } }
+ public bool fullscreen { get { return false; } set { assert (false); } }
+ public bool has_track_list { get { return false; } }
+ public string identity { owned get { return "Wavelet"; } }
+ public string desktop_entry { owned get { return "wavelet"; } }
+ public string[] supported_uri_schemes { owned get { return {}; } }
+ public string[] supported_mime_types { owned get { return {}; } }
+}
+
+[DBus (name = "org.mpris.MediaPlayer2.Player")]
+class MprisPlayer : Object {
+ internal signal void on_next ();
+ internal signal void on_previous ();
+ internal signal void on_pause ();
+ internal signal void on_play_pause ();
+ internal signal void on_stop ();
+ internal signal void on_play ();
+ internal signal void on_seek (int64 offset);
+ internal signal void on_set_position (string track_id, int64 position);
+
+ public void next () throws Error { this.on_next (); }
+ public void previous () throws Error { this.on_previous (); }
+ public void pause () throws Error { print("pause\n");this.on_pause (); }
+ public void play_pause () throws Error { this.on_play_pause (); }
+ public void stop () throws Error { this.on_stop (); }
+ public void play () throws Error { this.on_play (); }
+ public void seek (int64 offset) throws Error { this.on_seek (offset); }
+ public void set_position (string track_id, int64 position) throws Error { this.on_set_position (track_id, position); }
+ public void open_uri (string uri) throws Error { assert (false); }
+
+ public signal void seeked (int64 position);
+
+ public string playback_status { owned get; internal set; default = "Stopped"; }
+ public string loop_status { owned get; set; default = "None"; }
+ public double rate { get; set; default = 1.0; }
+ public bool shuffle { get; set; default = false; }
+ public HashTable metadata_map { owned get; default = new HashTable(null, null); }
+ public double volume { get; set; default = 1.0; }
+ [CCode (notify = false)]
+ public int64 position { get; default = 0; }
+ public double minimum_rate { get { return 1.0; } }
+ public double maximum_rate { get { return 1.0; } }
+ public bool can_go_next { get; default = false; }
+ public bool can_go_previous { get; default = false; }
+ public bool can_play { get; default = false; }
+ public bool can_pause { get; default = false; }
+ public bool can_seek { get; default = false; }
+ [CCode (notify = false)]
+ public bool can_control { get { return false; } }
+}
diff --git a/src/play_queue.blp b/src/play_queue.blp
index 4f5d971..ef57322 100644
--- a/src/play_queue.blp
+++ b/src/play_queue.blp
@@ -6,28 +6,61 @@ template $WaveletPlayQueue: Adw.NavigationPage {
Adw.ToolbarView {
[top]
- Adw.HeaderBar {}
+ Adw.HeaderBar {
+ Button {
+ icon-name: "edit-clear-all";
+ clicked => $clear ();
+ sensitive: bind template.can_clear_all;
+ }
+ }
ScrolledWindow {
- ListView list_view {
- single-click-activate: true;
- show-separators: true;
+ ColumnView {
+ styles [ "data-table" ]
- activate => $on_song_activate ();
-
- model: Gtk.NoSelection {
+ model: SingleSelection selection {
model: bind template.songs;
+ selected: bind template.selected_index;
+ selection-changed => $on_song_selected ();
};
- factory: BuilderListItemFactory {
- template ListItem {
- child: Label {
- styles [ "bold" ]
- halign: start;
- label: bind template.item as <$WaveletSong>.title;
- };
- }
- };
+ ColumnViewColumn {
+ factory: SignalListItemFactory {
+ setup => $delete_cell_setup ();
+ };
+ }
+
+ ColumnViewColumn {
+ title: _("Title");
+ expand: true;
+
+ factory: BuilderListItemFactory {
+ template ColumnViewCell {
+ child: Label {
+ halign: start;
+ label: bind template.item as <$WaveletSong>.title;
+ tooltip-text: bind template.item as <$WaveletSong>.title;
+ ellipsize: end;
+ };
+ }
+ };
+ }
+
+ ColumnViewColumn {
+ title: _("Artist");
+ fixed-width: 200;
+
+ factory: BuilderListItemFactory {
+ template ColumnViewCell {
+ child: Label {
+ halign: start;
+ label: bind template.item as <$WaveletSong>.artist;
+ tooltip-text: bind template.item as <$WaveletSong>.artist;
+ ellipsize: end;
+ };
+ }
+ };
+ }
}
}
}
diff --git a/src/play_queue.vala b/src/play_queue.vala
index bdb4733..7c3129e 100644
--- a/src/play_queue.vala
+++ b/src/play_queue.vala
@@ -22,6 +22,7 @@
public class Wavelet.PlayQueue : Adw.NavigationPage {
public ListStore songs { get; private set; }
+ 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;
@@ -30,13 +31,20 @@ public class Wavelet.PlayQueue : Adw.NavigationPage {
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;
}
- public void clear () {
+ [GtkCallback] public void clear () {
this.songs.remove_all ();
+ this.can_clear_all = false;
}
public void queue (Song song) {
@@ -45,19 +53,36 @@ public class Wavelet.PlayQueue : Adw.NavigationPage {
if (new_index == next_stream_index) {
this.play_next (song);
}
+ this.can_clear_all = true;
}
- [GtkCallback] private void on_song_activate (uint position) {
- this.next_stream_index = position;
- Song song = (Song) this.songs.get_item (position);
- this.play_next (song);
- this.play_now (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) {
Song song = (Song) this.songs.get_item (this.next_stream_index);
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);
@@ -65,25 +90,27 @@ public class Wavelet.PlayQueue : Adw.NavigationPage {
}
internal void restart () {
- this.next_stream_index = 0;
- Song song = (Song) this.songs.get_item (0);
- this.play_next (song);
- this.play_now (song);
+ this.pick_song (0);
}
public void skip_forward () {
- Song song = (Song) this.songs.get_item (this.next_stream_index);
- this.play_now (song);
+ this.pick_song (this.selected_index + 1);
}
public void skip_backward () {
- if (this.next_stream_index <= 1) {
- this.next_stream_index = 0;
- } else {
- this.next_stream_index -= 2;
+ if (this.selected_index >= 1) {
+ this.pick_song (this.selected_index - 1);
}
- Song song = (Song) this.songs.get_item (this.next_stream_index);
- this.play_next (song);
- this.play_now (song);
+ }
+
+ [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;
+ });
}
}
diff --git a/src/playbin.vala b/src/playbin.vala
index f21d5fc..3c1dded 100644
--- a/src/playbin.vala
+++ b/src/playbin.vala
@@ -174,6 +174,7 @@ class Playbin : Object {
// pretend this track was locked in by about-to-finish before
this.next_uri_lock.lock ();
+ this.next_uri = uri;
this.next_uri_locked_in = uri;
this.next_uri_lock.unlock ();
diff --git a/src/window.blp b/src/window.blp
index 033c544..b8e0fb8 100644
--- a/src/window.blp
+++ b/src/window.blp
@@ -200,9 +200,11 @@ template $WaveletWindow: Adw.ApplicationWindow {
valign: center;
}
- Button mute {
- icon-name: "audio-volume-high";
+ Button {
+ icon-name: bind $mute_button_icon_name (template.mute) as ;
valign: center;
+
+ clicked => $on_mute_toggle ();
}
Scale {
diff --git a/src/window.vala b/src/window.vala
index 2ecf56c..cdc2786 100644
--- a/src/window.vala
+++ b/src/window.vala
@@ -19,7 +19,7 @@
*/
[GtkTemplate (ui = "/eu/callcc/Wavelet/window.ui")]
-public class Wavelet.Window : Adw.ApplicationWindow {
+class Wavelet.Window : Adw.ApplicationWindow {
[GtkChild] private unowned Gtk.ListBox sidebar;
[GtkChild] private unowned Gtk.ListBoxRow sidebar_setup;
[GtkChild] private unowned Gtk.ListBoxRow sidebar_play_queue;
@@ -29,14 +29,19 @@ public class Wavelet.Window : Adw.ApplicationWindow {
[GtkChild] public unowned Wavelet.PlayQueue play_queue;
[GtkChild] public unowned Adw.ButtonRow shuffle_all_tracks;
- [GtkChild] public unowned Gtk.Button mute;
-
public bool playing { get; private set; default = false; }
[GtkChild] private unowned Gtk.Scale play_position;
public int64 position { get; private set; }
- public double volume { get; set; default = 1.0; }
+ 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; }
+ }
public Song? song { get; set; default = null; }
@@ -49,9 +54,28 @@ public class Wavelet.Window : Adw.ApplicationWindow {
public Window (Gtk.Application app) {
Object (application: app);
+
+ this.close_request.connect (() => {
+ app.quit ();
+ return false;
+ });
}
construct {
+ Bus.own_name (
+ BusType.SESSION,
+ "org.mpris.MediaPlayer2.wavelet",
+ 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.setup.connected.connect ((api) => {
public_api = api;
@@ -129,14 +153,6 @@ public class Wavelet.Window : Adw.ApplicationWindow {
});
}
- public void show_mute () {
- this.mute.icon_name = "audio-volume-muted";
- }
-
- public void show_unmute () {
- this.mute.icon_name = "audio-volume-high";
- }
-
[GtkCallback] private void on_sidebar_row_activated (Gtk.ListBoxRow row) {
if (row == this.sidebar_setup) {
this.stack.set_visible_child_name("setup");
@@ -179,6 +195,14 @@ public class Wavelet.Window : Adw.ApplicationWindow {
return playing ? "media-playback-pause" : "media-playback-start";
}
+ [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 () {
this.play_queue.skip_forward ();
}