This commit is contained in:
Erica Z 2024-10-12 16:35:42 +00:00
parent cc1da96fef
commit 2c4e493833
8 changed files with 223 additions and 56 deletions

View file

@ -31,7 +31,6 @@ public class Wavelet.Artist : Object, Json.Serializable {
public string? cover_art; public string? cover_art;
public string? artist_image_url; public string? artist_image_url;
public int64 album_count; public int64 album_count;
public DateTime? starred;
public Artist (string index, Json.Reader reader) { public Artist (string index, Json.Reader reader) {
this.index = index; this.index = index;
@ -55,12 +54,6 @@ public class Wavelet.Artist : Object, Json.Serializable {
reader.read_member ("albumCount"); reader.read_member ("albumCount");
this.album_count = reader.get_int_value (); this.album_count = reader.get_int_value ();
reader.end_member (); 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 string artist { get; private set; }
public int64 track { get; private set; } public int64 track { get; private set; }
public int64 year { get; private set; } public int64 year { get; private set; }
public DateTime? starred { get; private set; }
public Song (Json.Reader reader) { public Song (Json.Reader reader) {
reader.read_member ("id"); reader.read_member ("id");

View file

@ -2,6 +2,7 @@ wavelet_sources = [
'api.vala', 'api.vala',
'application.vala', 'application.vala',
'main.vala', 'main.vala',
'mpris.vala',
'play_queue.vala', 'play_queue.vala',
'playbin.vala', 'playbin.vala',
'setup.vala', 'setup.vala',

85
src/mpris.vala Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*
* 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<string, Variant> metadata_map { owned get; default = new HashTable<string,Variant>(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; } }
}

View file

@ -6,29 +6,62 @@ template $WaveletPlayQueue: Adw.NavigationPage {
Adw.ToolbarView { Adw.ToolbarView {
[top] [top]
Adw.HeaderBar {} Adw.HeaderBar {
Button {
icon-name: "edit-clear-all";
clicked => $clear ();
sensitive: bind template.can_clear_all;
}
}
ScrolledWindow { ScrolledWindow {
ListView list_view { ColumnView {
single-click-activate: true; styles [ "data-table" ]
show-separators: true;
activate => $on_song_activate (); model: SingleSelection selection {
model: Gtk.NoSelection {
model: bind template.songs; model: bind template.songs;
selected: bind template.selected_index;
selection-changed => $on_song_selected ();
}; };
ColumnViewColumn {
factory: SignalListItemFactory {
setup => $delete_cell_setup ();
};
}
ColumnViewColumn {
title: _("Title");
expand: true;
factory: BuilderListItemFactory { factory: BuilderListItemFactory {
template ListItem { template ColumnViewCell {
child: Label { child: Label {
styles [ "bold" ]
halign: start; halign: start;
label: bind template.item as <$WaveletSong>.title; 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;
};
}
};
}
}
} }
} }
} }

View file

@ -22,6 +22,7 @@
public class Wavelet.PlayQueue : Adw.NavigationPage { public class Wavelet.PlayQueue : Adw.NavigationPage {
public ListStore songs { get; private set; } 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 // this is the index of the song that will play on next on_stream_start
private uint next_stream_index; private uint next_stream_index;
@ -30,13 +31,20 @@ public class Wavelet.PlayQueue : Adw.NavigationPage {
public signal void now_playing (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 { construct {
this.songs = new ListStore (typeof (Song)); this.songs = new ListStore (typeof (Song));
this.next_stream_index = 0; this.next_stream_index = 0;
} }
public void clear () { [GtkCallback] public void clear () {
this.songs.remove_all (); this.songs.remove_all ();
this.can_clear_all = false;
} }
public void queue (Song song) { public void queue (Song song) {
@ -45,19 +53,36 @@ public class Wavelet.PlayQueue : Adw.NavigationPage {
if (new_index == next_stream_index) { if (new_index == next_stream_index) {
this.play_next (song); this.play_next (song);
} }
this.can_clear_all = true;
} }
[GtkCallback] private void on_song_activate (uint position) { [GtkCallback] private void on_song_selected () {
this.next_stream_index = position; this.selected_index = this.selection.selected; // manual bidi binding
Song song = (Song) this.songs.get_item (position); 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_next (song);
this.play_now (song); this.play_now (song);
} }
}
internal void on_stream_start (Playbin playbin) { internal void on_stream_start (Playbin playbin) {
Song song = (Song) this.songs.get_item (this.next_stream_index); Song song = (Song) this.songs.get_item (this.next_stream_index);
this.now_playing (song); 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) // prepare for next song ahead of time (gapless)
this.next_stream_index += 1; this.next_stream_index += 1;
Song? next_song = (Song?) this.songs.get_item (this.next_stream_index); 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 () { internal void restart () {
this.next_stream_index = 0; this.pick_song (0);
Song song = (Song) this.songs.get_item (0);
this.play_next (song);
this.play_now (song);
} }
public void skip_forward () { public void skip_forward () {
Song song = (Song) this.songs.get_item (this.next_stream_index); this.pick_song (this.selected_index + 1);
this.play_now (song);
} }
public void skip_backward () { public void skip_backward () {
if (this.next_stream_index <= 1) { if (this.selected_index >= 1) {
this.next_stream_index = 0; this.pick_song (this.selected_index - 1);
} else {
this.next_stream_index -= 2;
} }
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;
});
} }
} }

View file

@ -174,6 +174,7 @@ class Playbin : Object {
// pretend this track was locked in by about-to-finish before // pretend this track was locked in by about-to-finish before
this.next_uri_lock.lock (); this.next_uri_lock.lock ();
this.next_uri = uri;
this.next_uri_locked_in = uri; this.next_uri_locked_in = uri;
this.next_uri_lock.unlock (); this.next_uri_lock.unlock ();

View file

@ -200,9 +200,11 @@ template $WaveletWindow: Adw.ApplicationWindow {
valign: center; valign: center;
} }
Button mute { Button {
icon-name: "audio-volume-high"; icon-name: bind $mute_button_icon_name (template.mute) as <string>;
valign: center; valign: center;
clicked => $on_mute_toggle ();
} }
Scale { Scale {

View file

@ -19,7 +19,7 @@
*/ */
[GtkTemplate (ui = "/eu/callcc/Wavelet/window.ui")] [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.ListBox sidebar;
[GtkChild] private unowned Gtk.ListBoxRow sidebar_setup; [GtkChild] private unowned Gtk.ListBoxRow sidebar_setup;
[GtkChild] private unowned Gtk.ListBoxRow sidebar_play_queue; [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 Wavelet.PlayQueue play_queue;
[GtkChild] public unowned Adw.ButtonRow shuffle_all_tracks; [GtkChild] public unowned Adw.ButtonRow shuffle_all_tracks;
[GtkChild] public unowned Gtk.Button mute;
public bool playing { get; private set; default = false; } public bool playing { get; private set; default = false; }
[GtkChild] private unowned Gtk.Scale play_position; [GtkChild] private unowned Gtk.Scale play_position;
public int64 position { get; private set; } 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; } public Song? song { get; set; default = null; }
@ -49,9 +54,28 @@ public class Wavelet.Window : Adw.ApplicationWindow {
public Window (Gtk.Application app) { public Window (Gtk.Application app) {
Object (application: app); Object (application: app);
this.close_request.connect (() => {
app.quit ();
return false;
});
} }
construct { 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) => { this.setup.connected.connect ((api) => {
public_api = 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) { [GtkCallback] private void on_sidebar_row_activated (Gtk.ListBoxRow row) {
if (row == this.sidebar_setup) { if (row == this.sidebar_setup) {
this.stack.set_visible_child_name("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"; 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 () { [GtkCallback] private void on_skip_forward_clicked () {
this.play_queue.skip_forward (); this.play_queue.skip_forward ();
} }