stuff
This commit is contained in:
parent
8a22d255c6
commit
24d8c5cdd9
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? 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");
|
||||
|
|
|
@ -2,6 +2,7 @@ wavelet_sources = [
|
|||
'api.vala',
|
||||
'application.vala',
|
||||
'main.vala',
|
||||
'mpris.vala',
|
||||
'play_queue.vala',
|
||||
'playbin.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 {
|
||||
[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 ();
|
||||
};
|
||||
|
||||
ColumnViewColumn {
|
||||
factory: SignalListItemFactory {
|
||||
setup => $delete_cell_setup ();
|
||||
};
|
||||
}
|
||||
|
||||
ColumnViewColumn {
|
||||
title: _("Title");
|
||||
expand: true;
|
||||
|
||||
factory: BuilderListItemFactory {
|
||||
template ListItem {
|
||||
template ColumnViewCell {
|
||||
child: Label {
|
||||
styles [ "bold" ]
|
||||
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;
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
[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;
|
||||
}
|
||||
Song song = (Song) this.songs.get_item (this.next_stream_index);
|
||||
this.play_next (song);
|
||||
this.play_now (song);
|
||||
if (this.selected_index >= 1) {
|
||||
this.pick_song (this.selected_index - 1);
|
||||
}
|
||||
}
|
||||
|
||||
[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
|
||||
this.next_uri_lock.lock ();
|
||||
this.next_uri = uri;
|
||||
this.next_uri_locked_in = uri;
|
||||
this.next_uri_lock.unlock ();
|
||||
|
||||
|
|
|
@ -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 <string>;
|
||||
valign: center;
|
||||
|
||||
clicked => $on_mute_toggle ();
|
||||
}
|
||||
|
||||
Scale {
|
||||
|
|
|
@ -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 ();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue