stuff
This commit is contained in:
parent
cc1da96fef
commit
2c4e493833
8 changed files with 223 additions and 56 deletions
|
@ -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");
|
||||||
|
|
|
@ -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
85
src/mpris.vala
Normal 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; } }
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ();
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 ();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue