Compare commits
10 commits
907f7b9ea5
...
45b3444f98
Author | SHA1 | Date | |
---|---|---|---|
|
45b3444f98 | ||
|
cce049c814 | ||
|
4ba47c2614 | ||
|
47873d85d5 | ||
|
ebcf0dca68 | ||
|
353596d65d | ||
|
1584ebc161 | ||
|
7b66f3b18c | ||
|
666c312521 | ||
|
aa20cd5667 |
12 changed files with 386 additions and 319 deletions
|
@ -239,12 +239,12 @@ public class Subsonic : Object {
|
||||||
return @"$(this.url)/rest/stream?id=$(Uri.escape_string(id))&$(this.parameters)";
|
return @"$(this.url)/rest/stream?id=$(Uri.escape_string(id))&$(this.parameters)";
|
||||||
}
|
}
|
||||||
|
|
||||||
public string cover_art_uri (string id, int size) {
|
public string cover_art_uri (string id) {
|
||||||
return @"$(this.url)/rest/getCoverArt?id=$(Uri.escape_string(id))&$(size)&$(this.parameters)";
|
return @"$(this.url)/rest/getCoverArt?id=$(Uri.escape_string(id))&$(this.parameters)";
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Gdk.Pixbuf cover_art (string id, int size, Cancellable cancellable) throws Error {
|
public async Gdk.Pixbuf cover_art (string id, Cancellable cancellable) throws Error {
|
||||||
var msg = new Soup.Message("GET", this.cover_art_uri (id, size));
|
var msg = new Soup.Message("GET", this.cover_art_uri (id));
|
||||||
assert (msg != null);
|
assert (msg != null);
|
||||||
|
|
||||||
var stream = yield this.session.send_async (msg, Priority.DEFAULT, cancellable);
|
var stream = yield this.session.send_async (msg, Priority.DEFAULT, cancellable);
|
||||||
|
|
|
@ -21,7 +21,7 @@ public class Application : Adw.Application {
|
||||||
public override void activate () {
|
public override void activate () {
|
||||||
base.activate ();
|
base.activate ();
|
||||||
// if this.active_window not null, this isnt the primary instance
|
// if this.active_window not null, this isnt the primary instance
|
||||||
var win = this.active_window ?? new Window (this);
|
var win = this.active_window ?? new Ui.Window (this);
|
||||||
win.present ();
|
win.present ();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
<gresource prefix="/eu/callcc/audrey">
|
<gresource prefix="/eu/callcc/audrey">
|
||||||
<file>audrey.css</file>
|
<file>audrey.css</file>
|
||||||
<file preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
|
<file preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
|
||||||
<file preprocess="xml-stripblanks">play_queue.ui</file>
|
<file preprocess="xml-stripblanks">ui/play_queue.ui</file>
|
||||||
<file preprocess="xml-stripblanks">setup.ui</file>
|
<file preprocess="xml-stripblanks">ui/setup.ui</file>
|
||||||
<file preprocess="xml-stripblanks">window.ui</file>
|
<file preprocess="xml-stripblanks">ui/window.ui</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
</gresources>
|
</gresources>
|
||||||
|
|
|
@ -3,10 +3,10 @@ audrey_sources = [
|
||||||
'application.vala',
|
'application.vala',
|
||||||
'main.vala',
|
'main.vala',
|
||||||
'mpris.vala',
|
'mpris.vala',
|
||||||
'play_queue.vala',
|
|
||||||
'playbin.vala',
|
'playbin.vala',
|
||||||
'setup.vala',
|
'ui/play_queue.vala',
|
||||||
'window.vala',
|
'ui/setup.vala',
|
||||||
|
'ui/window.vala',
|
||||||
]
|
]
|
||||||
|
|
||||||
audrey_deps = [
|
audrey_deps = [
|
||||||
|
@ -22,9 +22,9 @@ audrey_deps = [
|
||||||
|
|
||||||
blueprints = custom_target('blueprints',
|
blueprints = custom_target('blueprints',
|
||||||
input: files(
|
input: files(
|
||||||
'play_queue.blp',
|
'ui/play_queue.blp',
|
||||||
'setup.blp',
|
'ui/setup.blp',
|
||||||
'window.blp',
|
'ui/window.blp',
|
||||||
),
|
),
|
||||||
output: [
|
output: [
|
||||||
'play_queue.ui',
|
'play_queue.ui',
|
||||||
|
|
|
@ -1,96 +0,0 @@
|
||||||
[GtkTemplate (ui = "/eu/callcc/audrey/play_queue.ui")]
|
|
||||||
public class 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;
|
|
||||||
|
|
||||||
public signal void play_next (Song? song);
|
|
||||||
public signal void play_now (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 {
|
|
||||||
this.songs = new ListStore (typeof (Song));
|
|
||||||
this.next_stream_index = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[GtkCallback] public void clear () {
|
|
||||||
this.songs.remove_all ();
|
|
||||||
this.can_clear_all = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void queue (Song song) {
|
|
||||||
uint new_index = this.songs.get_n_items ();
|
|
||||||
this.songs.append (song);
|
|
||||||
if (new_index == next_stream_index) {
|
|
||||||
this.play_next (song);
|
|
||||||
}
|
|
||||||
this.can_clear_all = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
[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);
|
|
||||||
this.play_next (next_song);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void restart () {
|
|
||||||
this.pick_song (0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void skip_forward () {
|
|
||||||
this.pick_song (this.selected_index + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void skip_backward () {
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
220
src/playbin.vala
220
src/playbin.vala
|
@ -1,9 +1,13 @@
|
||||||
|
enum PlaybinState {
|
||||||
|
STOPPED,
|
||||||
|
PAUSED,
|
||||||
|
PLAYING,
|
||||||
|
}
|
||||||
|
|
||||||
class Playbin : Object {
|
class Playbin : Object {
|
||||||
// dynamic: undocumented vala feature
|
// dynamic: undocumented vala feature
|
||||||
// lets us access the about-to-finish signal
|
// lets us access the about-to-finish signal
|
||||||
private dynamic Gst.Element playbin;
|
private dynamic Gst.Element playbin = Gst.ElementFactory.make ("playbin3", null);
|
||||||
|
|
||||||
private SourceFunc? async_done_callback;
|
|
||||||
|
|
||||||
// cubic: recommended for media player volume sliders?
|
// cubic: recommended for media player volume sliders?
|
||||||
public double volume {
|
public double volume {
|
||||||
|
@ -16,173 +20,116 @@ class Playbin : Object {
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool mute {
|
public bool mute {
|
||||||
get { return playbin.mute; }
|
get { return this.playbin.mute; }
|
||||||
set { playbin.mute = value; }
|
set { this.playbin.mute = value; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public signal void set_position (int64 position);
|
public PlaybinState state { get; private set; default = PlaybinState.STOPPED; }
|
||||||
|
|
||||||
|
private bool update_position = false;
|
||||||
|
|
||||||
|
public int64 position { get; private set; }
|
||||||
public int64 duration { get; private set; }
|
public int64 duration { get; private set; }
|
||||||
|
|
||||||
public signal void stream_started ();
|
private bool notify_next_transition;
|
||||||
public signal void stream_over ();
|
|
||||||
|
|
||||||
private void on_source_setup (Gst.Element playbin, dynamic Gst.Element source) {
|
// public void begin_playback (string uri);
|
||||||
|
// public void prepare_next (string? next_uri);
|
||||||
|
public signal void song_transition (string uri);
|
||||||
|
public signal void playback_finished ();
|
||||||
|
// public void stop_playback ();
|
||||||
|
|
||||||
|
private void source_setup (Gst.Element playbin, dynamic Gst.Element source) {
|
||||||
source.user_agent = "audrey/linux";
|
source.user_agent = "audrey/linux";
|
||||||
}
|
}
|
||||||
|
|
||||||
construct {
|
construct {
|
||||||
this.playbin = Gst.ElementFactory.make ("playbin3", null);
|
this.playbin.source_setup.connect (this.source_setup);
|
||||||
assert (this.playbin != null);
|
this.playbin.about_to_finish.connect (this.about_to_finish);
|
||||||
|
|
||||||
this.playbin.source_setup.connect (this.on_source_setup);
|
// regularly update position/duration
|
||||||
|
|
||||||
// regularly update position
|
|
||||||
Timeout.add (500, () => {
|
Timeout.add (500, () => {
|
||||||
|
if (this.update_position) {
|
||||||
int64 new_position;
|
int64 new_position;
|
||||||
if (this.playbin.query_position (Gst.Format.TIME, out new_position)) {
|
if (this.playbin.query_position (Gst.Format.TIME, out new_position)) {
|
||||||
this.set_position (new_position < this.duration ? new_position : this.duration);
|
this.position = new_position < this.duration ? new_position : this.duration;
|
||||||
|
} else {
|
||||||
|
this.position = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep rerunning
|
// keep rerunning
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.playbin.get_bus ().add_watch (Priority.DEFAULT, (bus, message) => {
|
var bus = this.playbin.get_bus ();
|
||||||
// message.type actually seems to be flags
|
bus.add_signal_watch ();
|
||||||
if (Gst.MessageType.ERROR in message.type) {
|
|
||||||
|
bus.message["error"].connect ((message) => {
|
||||||
Error err;
|
Error err;
|
||||||
string? debug;
|
string? debug;
|
||||||
message.parse_error (out err, out debug);
|
message.parse_error (out err, out debug);
|
||||||
warning ("gst playbin bus error: %s", err.message);
|
error ("gst playbin bus error: %s", err.message);
|
||||||
}
|
|
||||||
|
|
||||||
if (Gst.MessageType.ASYNC_DONE in message.type) {
|
|
||||||
assert (this.async_done_callback != null);
|
|
||||||
var cb = (owned) this.async_done_callback;
|
|
||||||
assert (this.async_done_callback == null); // sanity check
|
|
||||||
cb ();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Gst.MessageType.STREAM_START in message.type) {
|
|
||||||
int64 new_duration;
|
|
||||||
assert (this.playbin.query_duration (Gst.Format.TIME, out new_duration));
|
|
||||||
this.duration = new_duration;
|
|
||||||
|
|
||||||
string? next_uri = null;
|
|
||||||
|
|
||||||
this.next_uri_lock.lock ();
|
|
||||||
next_uri = this.next_uri;
|
|
||||||
|
|
||||||
if (next_uri != (string) this.playbin.current_uri) {
|
|
||||||
this.next_uri_lock.unlock ();
|
|
||||||
// WHOOPS! didn't actually switch to the track the play queue wanted
|
|
||||||
// we can still fix this though
|
|
||||||
assert (next_uri != null);
|
|
||||||
this.playbin.set_state (Gst.State.READY);
|
|
||||||
this.playbin.uri = next_uri;
|
|
||||||
this.playbin.set_state (Gst.State.PLAYING);
|
|
||||||
// no one will ever know
|
|
||||||
} else {
|
|
||||||
this.next_uri = null;
|
|
||||||
this.next_uri_lock.unlock ();
|
|
||||||
|
|
||||||
this.stream_started ();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Gst.MessageType.EOS in message.type) {
|
|
||||||
string next_uri;
|
|
||||||
|
|
||||||
this.next_uri_lock.lock ();
|
|
||||||
next_uri = this.next_uri;
|
|
||||||
this.next_uri_lock.unlock ();
|
|
||||||
|
|
||||||
if (next_uri == null) {
|
|
||||||
// no next track was arranged, we're done
|
|
||||||
this.stream_over ();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.playbin.about_to_finish.connect (this.on_about_to_finish);
|
bus.message["warning"].connect ((message) => {
|
||||||
|
Error err;
|
||||||
|
string? debug;
|
||||||
|
message.parse_error (out err, out debug);
|
||||||
|
warning ("gst playbin bus warning: %s", err.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
bus.message["stream-start"].connect ((message) => {
|
||||||
|
int64 new_duration;
|
||||||
|
if (this.playbin.query_duration (Gst.Format.TIME, out new_duration)) {
|
||||||
|
this.duration = new_duration;
|
||||||
|
} else {
|
||||||
|
warning ("could not obtain new stream duration");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void set_state (Gst.State state) {
|
this.position = 0;
|
||||||
assert (this.async_done_callback == null);
|
|
||||||
|
|
||||||
switch (this.playbin.set_state (state)) {
|
if (notify_next_transition) {
|
||||||
case Gst.StateChangeReturn.SUCCESS:
|
this.song_transition ((string) this.playbin.current_uri);
|
||||||
break;
|
} else {
|
||||||
|
notify_next_transition = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
case Gst.StateChangeReturn.ASYNC:
|
bus.message["state-changed"].connect ((message) => {
|
||||||
this.async_done_callback = this.set_state.callback;
|
if (message.src != this.playbin) return;
|
||||||
yield;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
Gst.State new_state;
|
||||||
assert (false);
|
message.parse_state_changed (null, out new_state, null);
|
||||||
break;
|
|
||||||
|
this.update_position = new_state == Gst.State.PLAYING;
|
||||||
|
});
|
||||||
|
|
||||||
|
bus.message["eos"].connect ((message) => {
|
||||||
|
assert (notify_next_transition);
|
||||||
|
this.playback_finished ();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void seek (int64 position) {
|
||||||
|
if (!this.playbin.seek_simple (Gst.Format.TIME, Gst.SeekFlags.FLUSH, position)) {
|
||||||
|
warning ("could not seek");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async bool seek (int64 position) {
|
public void begin_playback (string uri) {
|
||||||
// don't actually seek if an operation is pending
|
this.playbin.set_state (Gst.State.READY);
|
||||||
if (this.async_done_callback != null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ASSUMPTION: this can only work asynchronously
|
|
||||||
// (will wait for an ASYNC_DONE in the bus)
|
|
||||||
assert (this.playbin.seek_simple (Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, position));
|
|
||||||
this.async_done_callback = this.seek.callback;
|
|
||||||
yield;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string? play_now_queued;
|
|
||||||
// returns true if the "play now" request wasn't overriden by a later call
|
|
||||||
public async bool play_now (string uri) {
|
|
||||||
if (this.async_done_callback != null) {
|
|
||||||
// an operation was already pending
|
|
||||||
// last writer wins here
|
|
||||||
this.play_now_queued = uri;
|
|
||||||
// idle spinning is probably fine
|
|
||||||
while (this.async_done_callback != null) {
|
|
||||||
Idle.add (this.play_now.callback);
|
|
||||||
yield;
|
|
||||||
// only keep spinning if we can still win
|
|
||||||
if (this.play_now_queued != uri) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.play_now_queued = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// pretend this track was locked in by about-to-finish before
|
|
||||||
this.next_uri_lock.lock ();
|
|
||||||
this.next_uri = uri;
|
|
||||||
this.next_uri_lock.unlock ();
|
|
||||||
|
|
||||||
yield this.set_state (Gst.State.READY);
|
|
||||||
this.playbin.uri = uri;
|
this.playbin.uri = uri;
|
||||||
yield this.set_state (Gst.State.PLAYING);
|
this.playbin.set_state (Gst.State.PLAYING);
|
||||||
|
|
||||||
if (this.play_now_queued != null) {
|
this.state = PlaybinState.PLAYING;
|
||||||
// another "play now" was queued while we were busy
|
this.notify_next_transition = false;
|
||||||
// defer to it
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Mutex next_uri_lock;
|
Mutex next_uri_lock;
|
||||||
string? next_uri;
|
string? next_uri;
|
||||||
|
|
||||||
public void set_next_uri (string? next_uri) {
|
public void prepare_next (string? next_uri) {
|
||||||
this.next_uri_lock.lock ();
|
this.next_uri_lock.lock ();
|
||||||
this.next_uri = next_uri;
|
this.next_uri = next_uri;
|
||||||
this.next_uri_lock.unlock ();
|
this.next_uri_lock.unlock ();
|
||||||
|
@ -190,7 +137,7 @@ class Playbin : Object {
|
||||||
|
|
||||||
// called when uri can be switched for gapless playback
|
// called when uri can be switched for gapless playback
|
||||||
// need async queue because this might be called from a gstreamer thread
|
// need async queue because this might be called from a gstreamer thread
|
||||||
private void on_about_to_finish (dynamic Gst.Element playbin) {
|
private void about_to_finish (dynamic Gst.Element playbin) {
|
||||||
this.next_uri_lock.lock ();
|
this.next_uri_lock.lock ();
|
||||||
string? next_uri = this.next_uri;
|
string? next_uri = this.next_uri;
|
||||||
this.next_uri_lock.unlock ();
|
this.next_uri_lock.unlock ();
|
||||||
|
@ -201,10 +148,19 @@ class Playbin : Object {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void pause () {
|
public void pause () {
|
||||||
|
assert (this.state != PlaybinState.STOPPED);
|
||||||
this.playbin.set_state (Gst.State.PAUSED);
|
this.playbin.set_state (Gst.State.PAUSED);
|
||||||
|
this.state = PlaybinState.PAUSED;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void play () {
|
public void play () {
|
||||||
|
assert (this.state != PlaybinState.STOPPED);
|
||||||
this.playbin.set_state (Gst.State.PLAYING);
|
this.playbin.set_state (Gst.State.PLAYING);
|
||||||
|
this.state = PlaybinState.PLAYING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop_playback() {
|
||||||
|
this.playbin.set_state (Gst.State.READY);
|
||||||
|
this.state = PlaybinState.STOPPED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
using Gtk 4.0;
|
using Gtk 4.0;
|
||||||
using Adw 1;
|
using Adw 1;
|
||||||
|
|
||||||
template $PlayQueue: Adw.NavigationPage {
|
template $UiPlayQueue: Adw.NavigationPage {
|
||||||
title: _("Play queue");
|
title: _("Play queue");
|
||||||
|
|
||||||
Adw.ToolbarView {
|
Adw.ToolbarView {
|
||||||
|
@ -15,20 +15,14 @@ template $PlayQueue: Adw.NavigationPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrolledWindow {
|
ScrolledWindow {
|
||||||
ColumnView {
|
ColumnView view {
|
||||||
styles [ "data-table" ]
|
styles [ "data-table" ]
|
||||||
|
|
||||||
model: SingleSelection selection {
|
//ColumnViewColumn {
|
||||||
model: bind template.songs;
|
// factory: SignalListItemFactory {
|
||||||
selected: bind template.selected_index;
|
// setup => $on_delete_cell_setup ();
|
||||||
selection-changed => $on_song_selected ();
|
// };
|
||||||
};
|
//}
|
||||||
|
|
||||||
ColumnViewColumn {
|
|
||||||
factory: SignalListItemFactory {
|
|
||||||
setup => $delete_cell_setup ();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnViewColumn {
|
ColumnViewColumn {
|
||||||
title: _("Title");
|
title: _("Title");
|
||||||
|
@ -65,4 +59,3 @@ template $PlayQueue: Adw.NavigationPage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
186
src/ui/play_queue.vala
Normal file
186
src/ui/play_queue.vala
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
class Ui.PlayQueueStore : Object, ListModel, Gtk.SelectionModel {
|
||||||
|
public ListStore inner = new ListStore (typeof (Song));
|
||||||
|
public uint playing_index { get; private set; default = 0; }
|
||||||
|
|
||||||
|
public signal void begin_playback (Song song);
|
||||||
|
public signal void prepare_next (Song? song);
|
||||||
|
public signal void playback_continues (Song song);
|
||||||
|
public signal void stop_playback ();
|
||||||
|
|
||||||
|
construct {
|
||||||
|
this.inner.items_changed.connect ((position, removed, added) => {
|
||||||
|
bool emit_signal = false;
|
||||||
|
|
||||||
|
if (this.playing_index >= position) {
|
||||||
|
if (this.playing_index < position+removed) {
|
||||||
|
this.playing_index = position;
|
||||||
|
if (this.playing_index < this.inner.get_n_items ()) {
|
||||||
|
emit_signal = true;
|
||||||
|
this.begin_playback ((Song) this.inner.get_item (this.playing_index));
|
||||||
|
this.prepare_next ((Song?) this.inner.get_item (this.playing_index+1));
|
||||||
|
} else {
|
||||||
|
this.stop_playback ();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.playing_index += added;
|
||||||
|
this.playing_index -= removed;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.prepare_next ((Song?) this.inner.get_item (this.playing_index+1));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.items_changed (position, removed, added);
|
||||||
|
if (emit_signal) {
|
||||||
|
this.selection_changed (this.playing_index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void song_transition () {
|
||||||
|
this.playing_index += 1;
|
||||||
|
this.selection_changed (this.playing_index-1, 2);
|
||||||
|
this.playback_continues ((Song) this.inner.get_item (this.playing_index));
|
||||||
|
this.prepare_next ((Song?) this.inner.get_item (this.playing_index+1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playback_finished () {
|
||||||
|
this.playing_index += 1;
|
||||||
|
assert (this.playing_index == this.inner.get_n_items ());
|
||||||
|
this.selection_changed (this.playing_index-1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Gtk.Bitset get_selection_in_range (uint position, uint n_items) {
|
||||||
|
var bitset = new Gtk.Bitset.empty ();
|
||||||
|
if (this.playing_index < this.inner.get_n_items ()) {
|
||||||
|
bitset.add (playing_index);
|
||||||
|
}
|
||||||
|
return bitset;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_selected (uint position) {
|
||||||
|
return position == this.playing_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool select_all () {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool select_item (uint position, bool unselect_rest) {
|
||||||
|
if (!unselect_rest) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var previous = this.playing_index;
|
||||||
|
this.playing_index = position;
|
||||||
|
|
||||||
|
if (previous < this.inner.get_n_items ()) {
|
||||||
|
if (previous < position) {
|
||||||
|
this.selection_changed (previous, position-previous+1);
|
||||||
|
} else if (previous > position) {
|
||||||
|
this.selection_changed (position, previous-position+1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.selection_changed (position, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.begin_playback ((Song) this.inner.get_item (this.playing_index));
|
||||||
|
this.prepare_next ((Song) this.inner.get_item (this.playing_index+1));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool select_range (uint position, uint n_items, bool unselect_rest) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool set_selection (Gtk.Bitset selected, Gtk.Bitset mask) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool unselect_all () {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool unselect_item (uint position) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool unselect_range (uint position, uint n_items) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object? get_item (uint position) {
|
||||||
|
return this.inner.get_item (position);
|
||||||
|
}
|
||||||
|
|
||||||
|
Type get_item_type () {
|
||||||
|
return this.inner.get_item_type ();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint get_n_items () {
|
||||||
|
return this.inner.get_n_items ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[GtkTemplate (ui = "/eu/callcc/audrey/ui/play_queue.ui")]
|
||||||
|
public class Ui.PlayQueue : Adw.NavigationPage {
|
||||||
|
[GtkChild] private unowned Gtk.ColumnView view;
|
||||||
|
PlayQueueStore store = new PlayQueueStore ();
|
||||||
|
|
||||||
|
public signal void begin_playback (Song song);
|
||||||
|
public signal void prepare_next (Song? next_song);
|
||||||
|
public signal void playback_continues (Song song);
|
||||||
|
public signal void stop_playback ();
|
||||||
|
|
||||||
|
public bool can_clear_all { get; private set; default = false; }
|
||||||
|
|
||||||
|
construct {
|
||||||
|
this.view.model = this.store;
|
||||||
|
|
||||||
|
this.store.begin_playback.connect ((song) => this.begin_playback (song));
|
||||||
|
this.store.prepare_next.connect ((next_song) => this.prepare_next (next_song));
|
||||||
|
this.store.playback_continues.connect ((song) => this.playback_continues (song));
|
||||||
|
this.store.stop_playback.connect (() => this.stop_playback ());
|
||||||
|
}
|
||||||
|
|
||||||
|
[GtkCallback] public void clear () {
|
||||||
|
this.store.inner.remove_all ();
|
||||||
|
this.can_clear_all = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void queue (Song song) {
|
||||||
|
this.store.inner.append (song);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void song_transition () {
|
||||||
|
this.store.song_transition ();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void playback_finished () {
|
||||||
|
this.store.playback_finished ();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void restart () {
|
||||||
|
this.store.select_item (0, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void skip_forward () {
|
||||||
|
this.store.select_item (this.store.playing_index+1, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void skip_backward () {
|
||||||
|
if (this.store.playing_index >= 1) {
|
||||||
|
this.store.select_item (this.store.playing_index-1, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
[GtkCallback] private void on_delete_cell_setup (Object object) {
|
||||||
|
var cell = (Gtk.ColumnViewCell) object;
|
||||||
|
var button = new Gtk.Button.from_icon_name ("edit-delete");
|
||||||
|
button.add_css_class ("flat");
|
||||||
|
button.clicked.connect (() => {
|
||||||
|
//this.store.inner.remove (cell.position);
|
||||||
|
});
|
||||||
|
cell.child = button;
|
||||||
|
}*/
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
using Gtk 4.0;
|
using Gtk 4.0;
|
||||||
using Adw 1;
|
using Adw 1;
|
||||||
|
|
||||||
template $Setup: Adw.PreferencesDialog {
|
template $UiSetup: Adw.PreferencesDialog {
|
||||||
title: _("Setup");
|
title: _("Setup");
|
||||||
|
|
||||||
Adw.ToolbarView {
|
Adw.ToolbarView {
|
|
@ -1,8 +1,8 @@
|
||||||
[CCode (cname = "sqlite3_randomness")]
|
[CCode (cname = "sqlite3_randomness")]
|
||||||
public extern void randomness (int N, void *P);
|
public extern void randomness (int N, void *P);
|
||||||
|
|
||||||
[GtkTemplate (ui = "/eu/callcc/audrey/setup.ui")]
|
[GtkTemplate (ui = "/eu/callcc/audrey/ui/setup.ui")]
|
||||||
public class Setup : Adw.PreferencesDialog {
|
public class Ui.Setup : Adw.PreferencesDialog {
|
||||||
public string status { get; private set; default = _("Not connected"); }
|
public string status { get; private set; default = _("Not connected"); }
|
||||||
|
|
||||||
public bool authn_can_edit { get; private set; default = true; }
|
public bool authn_can_edit { get; private set; default = true; }
|
|
@ -1,7 +1,7 @@
|
||||||
using Gtk 4.0;
|
using Gtk 4.0;
|
||||||
using Adw 1;
|
using Adw 1;
|
||||||
|
|
||||||
template $Window: Adw.ApplicationWindow {
|
template $UiWindow: Adw.ApplicationWindow {
|
||||||
title: _("audrey");
|
title: _("audrey");
|
||||||
default-width: 800;
|
default-width: 800;
|
||||||
default-height: 600;
|
default-height: 600;
|
||||||
|
@ -69,7 +69,7 @@ template $Window: Adw.ApplicationWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
Picture {
|
Picture {
|
||||||
paintable: bind template.playing_cover_art;
|
paintable: bind template.playing_cover_art;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,7 @@ paintable: bind template.playing_cover_art;
|
||||||
name: "play_queue";
|
name: "play_queue";
|
||||||
title: _("Play queue");
|
title: _("Play queue");
|
||||||
|
|
||||||
child: $PlayQueue play_queue {};
|
child: $UiPlayQueue play_queue {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,18 +99,24 @@ paintable: bind template.playing_cover_art;
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
styles [ "heading" ]
|
styles [ "heading" ]
|
||||||
|
xalign: 0;
|
||||||
halign: start;
|
halign: start;
|
||||||
label: bind template.song as <$Song>.title;
|
label: bind template.song as <$Song>.title;
|
||||||
ellipsize: end;
|
ellipsize: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
Box {
|
|
||||||
halign: start;
|
|
||||||
Label {
|
Label {
|
||||||
styles [ "caption" ]
|
styles [ "caption" ]
|
||||||
label: bind $format_song_below_title (template.song) as <string>;
|
xalign: 0;
|
||||||
|
label: bind template.song as <$Song>.artist;
|
||||||
ellipsize: end;
|
ellipsize: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
styles [ "caption" ]
|
||||||
|
xalign: 0;
|
||||||
|
label: bind template.song as <$Song>.album;
|
||||||
|
ellipsize: end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,6 +170,7 @@ paintable: bind template.playing_cover_art;
|
||||||
Button {
|
Button {
|
||||||
icon-name: "media-skip-backward";
|
icon-name: "media-skip-backward";
|
||||||
valign: center;
|
valign: center;
|
||||||
|
sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as <bool>;
|
||||||
|
|
||||||
clicked => $on_skip_backward_clicked ();
|
clicked => $on_skip_backward_clicked ();
|
||||||
}
|
}
|
||||||
|
@ -171,13 +178,15 @@ paintable: bind template.playing_cover_art;
|
||||||
Button {
|
Button {
|
||||||
icon-name: "media-seek-backward";
|
icon-name: "media-seek-backward";
|
||||||
valign: center;
|
valign: center;
|
||||||
|
sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as <bool>;
|
||||||
|
|
||||||
clicked => $seek_backward ();
|
clicked => $seek_backward ();
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
icon-name: bind $play_button_icon_name (template.playing) as <string>;
|
icon-name: bind $play_pause_icon_name (template.playbin as <$Playbin>.state as <$PlaybinState>) as <string>;
|
||||||
valign: center;
|
valign: center;
|
||||||
|
sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as <bool>;
|
||||||
|
|
||||||
clicked => $on_play_pause_clicked ();
|
clicked => $on_play_pause_clicked ();
|
||||||
}
|
}
|
||||||
|
@ -185,6 +194,7 @@ paintable: bind template.playing_cover_art;
|
||||||
Button {
|
Button {
|
||||||
icon-name: "media-seek-forward";
|
icon-name: "media-seek-forward";
|
||||||
valign: center;
|
valign: center;
|
||||||
|
sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as <bool>;
|
||||||
|
|
||||||
clicked => $seek_forward ();
|
clicked => $seek_forward ();
|
||||||
}
|
}
|
||||||
|
@ -192,6 +202,7 @@ paintable: bind template.playing_cover_art;
|
||||||
Button {
|
Button {
|
||||||
icon-name: "media-skip-forward";
|
icon-name: "media-skip-forward";
|
||||||
valign: center;
|
valign: center;
|
||||||
|
sensitive: bind $playbin_active (template.playbin as <$Playbin>.state as <$PlaybinState>) as <bool>;
|
||||||
|
|
||||||
clicked => $on_skip_forward_clicked ();
|
clicked => $on_skip_forward_clicked ();
|
||||||
}
|
}
|
|
@ -1,16 +1,14 @@
|
||||||
[GtkTemplate (ui = "/eu/callcc/audrey/window.ui")]
|
[GtkTemplate (ui = "/eu/callcc/audrey/ui/window.ui")]
|
||||||
class Window : Adw.ApplicationWindow {
|
class Ui.Window : Adw.ApplicationWindow {
|
||||||
[GtkChild] private unowned Gtk.ListBox sidebar;
|
[GtkChild] private unowned Gtk.ListBox sidebar;
|
||||||
[GtkChild] private unowned Gtk.ListBoxRow sidebar_play_queue;
|
[GtkChild] private unowned Gtk.ListBoxRow sidebar_play_queue;
|
||||||
[GtkChild] private unowned Gtk.Stack stack;
|
[GtkChild] private unowned Gtk.Stack stack;
|
||||||
|
|
||||||
[GtkChild] public unowned PlayQueue play_queue;
|
[GtkChild] public unowned Ui.PlayQueue play_queue;
|
||||||
[GtkChild] public unowned Adw.ButtonRow shuffle_all_tracks;
|
[GtkChild] public unowned Adw.ButtonRow shuffle_all_tracks;
|
||||||
|
|
||||||
private Setup setup;
|
private Setup setup;
|
||||||
|
|
||||||
public bool playing { get; private set; default = false; }
|
|
||||||
|
|
||||||
public int64 position { get; private set; }
|
public int64 position { get; private set; }
|
||||||
|
|
||||||
public double volume {
|
public double volume {
|
||||||
|
@ -28,6 +26,8 @@ class Window : Adw.ApplicationWindow {
|
||||||
public bool cover_art_loading { get; set; default = false; }
|
public bool cover_art_loading { get; set; default = false; }
|
||||||
public Gdk.Paintable playing_cover_art { get; set; }
|
public Gdk.Paintable playing_cover_art { get; set; }
|
||||||
|
|
||||||
|
private Gdk.Paintable next_cover_art = null;
|
||||||
|
|
||||||
internal Playbin playbin { get; default = new Playbin (); }
|
internal Playbin playbin { get; default = new Playbin (); }
|
||||||
|
|
||||||
public Window (Gtk.Application app) {
|
public Window (Gtk.Application app) {
|
||||||
|
@ -82,23 +82,36 @@ class Window : Adw.ApplicationWindow {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
playbin.stream_started.connect (this.play_queue.on_stream_start);
|
this.play_queue.begin_playback.connect ((song) => {
|
||||||
|
var uri = api.stream_uri (song.id);
|
||||||
|
this.playbin.begin_playback (uri);
|
||||||
|
|
||||||
this.play_queue.now_playing.connect ((song) => {
|
|
||||||
this.playing = true;
|
|
||||||
this.song = song;
|
this.song = song;
|
||||||
api.scrobble.begin (song.id);
|
api.scrobble.begin (song.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.play_queue.play_now.connect ((song) => {
|
this.play_queue.prepare_next.connect ((next_song) => {
|
||||||
playbin.play_now.begin (api.stream_uri (song.id));
|
var next_uri = next_song == null ? null : api.stream_uri (next_song.id);
|
||||||
|
this.playbin.prepare_next (next_uri);
|
||||||
});
|
});
|
||||||
this.play_queue.play_next.connect ((song) => {
|
|
||||||
if (song == null) {
|
this.playbin.song_transition.connect ((uri) => {
|
||||||
playbin.set_next_uri (null);
|
this.play_queue.song_transition ();
|
||||||
} else {
|
});
|
||||||
playbin.set_next_uri (api.stream_uri (song.id));
|
|
||||||
}
|
this.play_queue.playback_continues.connect ((song) => {
|
||||||
|
this.song = song;
|
||||||
|
api.scrobble.begin (song.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.playbin.playback_finished.connect (() => {
|
||||||
|
this.play_queue.playback_finished ();
|
||||||
|
this.song = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.play_queue.stop_playback.connect (() => {
|
||||||
|
this.playbin.stop_playback ();
|
||||||
|
this.song = null;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.setup.load ();
|
this.setup.load ();
|
||||||
|
@ -111,12 +124,12 @@ class Window : Adw.ApplicationWindow {
|
||||||
}
|
}
|
||||||
this.cancel_loading_art = new Cancellable ();
|
this.cancel_loading_art = new Cancellable ();
|
||||||
|
|
||||||
this.playing_cover_art = Gdk.Paintable.empty (100, 100);
|
this.playing_cover_art = Gdk.Paintable.empty (1, 1);
|
||||||
if (this.song != null) {
|
if (this.song != null) {
|
||||||
this.cover_art_loading = true;
|
this.cover_art_loading = true;
|
||||||
|
|
||||||
string song_id = this.song.id;
|
string song_id = this.song.id;
|
||||||
public_api.cover_art.begin (song_id, 100, this.cancel_loading_art, (obj, res) => {
|
public_api.cover_art.begin (song_id, this.cancel_loading_art, (obj, res) => {
|
||||||
try {
|
try {
|
||||||
this.playing_cover_art = Gdk.Texture.for_pixbuf (public_api.cover_art.end (res));
|
this.playing_cover_art = Gdk.Texture.for_pixbuf (public_api.cover_art.end (res));
|
||||||
this.cover_art_loading = false;
|
this.cover_art_loading = false;
|
||||||
|
@ -131,10 +144,10 @@ class Window : Adw.ApplicationWindow {
|
||||||
});
|
});
|
||||||
this.song = null;
|
this.song = null;
|
||||||
|
|
||||||
this.playbin.set_position.connect ((sender, new_position) => {
|
this.playbin.notify["position"].connect (() => {
|
||||||
// only set if we aren't seeking
|
// only set if we aren't seeking
|
||||||
if (this.seek_timeout_id == 0) {
|
if (this.seek_timeout_id == 0) {
|
||||||
this.position = new_position;
|
this.position = this.playbin.position;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -151,32 +164,42 @@ class Window : Adw.ApplicationWindow {
|
||||||
return "%02d:%02d".printf (s/60, s%60);
|
return "%02d:%02d".printf (s/60, s%60);
|
||||||
}
|
}
|
||||||
|
|
||||||
// same timeout logic as https://code.videolan.org/videolan/npapi-vlc/blob/6eae0ffb9cbaf8f6e04423de2ff38daabdf7cae3/npapi/vlcplugin_gtk.cpp#L312
|
private void seek_impl (int64 position) {
|
||||||
private uint seek_timeout_id = 0;
|
this.position = position;
|
||||||
[GtkCallback] private bool on_play_position_seek (Gtk.Range range, Gtk.ScrollType scroll_type, double value) {
|
|
||||||
if (this.seek_timeout_id == 0) {
|
if (this.seek_timeout_id == 0) {
|
||||||
this.seek_timeout_id = Timeout.add (500, () => {
|
this.seek_timeout_id = Timeout.add (500, () => {
|
||||||
playbin.seek.begin ((int64) range.adjustment.value, (obj, res) => {
|
playbin.seek(this.position);
|
||||||
this.seek_timeout_id = 0;
|
this.seek_timeout_id = 0;
|
||||||
});
|
|
||||||
return false;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
[GtkCallback] private void on_play_pause_clicked () {
|
[GtkCallback] private void on_play_pause_clicked () {
|
||||||
if (this.playing) {
|
if (this.playbin.state == PlaybinState.PLAYING) {
|
||||||
this.playbin.pause();
|
this.playbin.pause();
|
||||||
this.playing = false;
|
|
||||||
} else {
|
} else {
|
||||||
this.playbin.play();
|
this.playbin.play();
|
||||||
this.playing = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[GtkCallback] private string play_button_icon_name (bool playing) {
|
[GtkCallback] private string play_pause_icon_name (PlaybinState state) {
|
||||||
return playing ? "media-playback-pause" : "media-playback-start";
|
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) {
|
[GtkCallback] private string mute_button_icon_name (bool mute) {
|
||||||
|
@ -199,23 +222,17 @@ class Window : Adw.ApplicationWindow {
|
||||||
this.setup.present (this);
|
this.setup.present (this);
|
||||||
}
|
}
|
||||||
|
|
||||||
[GtkCallback] private string format_song_below_title (Song? song) {
|
|
||||||
return song == null ? "" : @"$(song.artist) - $(song.album) - $(song.year)";
|
|
||||||
}
|
|
||||||
|
|
||||||
[GtkCallback] private void seek_backward () {
|
[GtkCallback] private void seek_backward () {
|
||||||
// 10 seconds
|
// 10 seconds
|
||||||
int64 new_position = position - (int64)10 * 1000 * 1000000;
|
int64 new_position = position - (int64)10 * 1000 * 1000000;
|
||||||
if (new_position < 0) new_position = 0;
|
if (new_position < 0) new_position = 0;
|
||||||
this.position = new_position;
|
this.seek_impl (new_position);
|
||||||
this.playbin.seek.begin (new_position);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[GtkCallback] private void seek_forward () {
|
[GtkCallback] private void seek_forward () {
|
||||||
// 10 seconds
|
// 10 seconds
|
||||||
int64 new_position = position + (int64)10 * 1000 * 1000000;
|
int64 new_position = position + (int64)10 * 1000 * 1000000;
|
||||||
if (new_position > this.playbin.duration) new_position = this.playbin.duration;
|
if (new_position > this.playbin.duration) new_position = this.playbin.duration;
|
||||||
this.position = new_position;
|
this.seek_impl (new_position);
|
||||||
this.playbin.seek.begin (new_position);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue