2024-11-02 11:24:25 +00:00
|
|
|
mod imp {
|
2024-11-07 20:54:01 +00:00
|
|
|
use crate::model::{Album, Song};
|
2024-11-06 12:10:39 +00:00
|
|
|
use crate::{mpris, mpv};
|
2024-11-02 11:24:25 +00:00
|
|
|
use adw::prelude::*;
|
|
|
|
use adw::subclass::prelude::*;
|
|
|
|
use glib::subclass::InitializingObject;
|
2024-11-05 10:02:02 +00:00
|
|
|
use glib::JoinHandle;
|
2024-11-04 11:31:43 +00:00
|
|
|
use gtk::{gdk, gio, glib};
|
2024-11-02 11:24:25 +00:00
|
|
|
use std::cell::{Cell, RefCell};
|
2024-11-03 09:37:37 +00:00
|
|
|
use std::rc::Rc;
|
2024-11-04 21:00:17 +00:00
|
|
|
use tracing::{event, span, Level};
|
2024-11-06 12:10:39 +00:00
|
|
|
use zbus::object_server::InterfaceRef;
|
2024-10-29 13:02:29 +00:00
|
|
|
|
2024-11-04 09:40:51 +00:00
|
|
|
#[derive(gtk::CompositeTemplate, glib::Properties)]
|
2024-11-02 11:24:25 +00:00
|
|
|
#[template(resource = "/eu/callcc/audrey/window.ui")]
|
|
|
|
#[properties(wrapper_type = super::Window)]
|
|
|
|
pub struct Window {
|
|
|
|
#[template_child]
|
|
|
|
pub(super) playbar: TemplateChild<crate::ui::Playbar>,
|
|
|
|
|
2024-11-03 17:45:52 +00:00
|
|
|
#[template_child]
|
|
|
|
pub(super) play_queue: TemplateChild<crate::ui::PlayQueue>,
|
|
|
|
|
2024-11-02 11:24:25 +00:00
|
|
|
#[property(get, set, default = false)]
|
|
|
|
can_click_shuffle_all: Cell<bool>,
|
|
|
|
|
|
|
|
#[property(get, set, nullable)]
|
|
|
|
playing_cover_art: RefCell<Option<gdk::Paintable>>,
|
|
|
|
|
2024-11-05 20:44:20 +00:00
|
|
|
#[property(type = Option<Song>, get = Self::song, nullable)]
|
2024-11-04 12:29:10 +00:00
|
|
|
_song: (),
|
2024-11-02 11:24:25 +00:00
|
|
|
|
|
|
|
pub(super) setup: crate::ui::Setup,
|
2024-11-03 12:41:02 +00:00
|
|
|
|
2024-11-04 07:03:59 +00:00
|
|
|
pub(super) api: RefCell<Option<Rc<crate::subsonic::Client>>>,
|
2024-11-04 09:40:51 +00:00
|
|
|
|
2024-11-04 11:31:43 +00:00
|
|
|
pub(super) mpv: mpv::Handle,
|
|
|
|
#[property(get)]
|
|
|
|
playlist_model: gio::ListStore,
|
|
|
|
|
2024-11-07 20:54:01 +00:00
|
|
|
#[property(get)]
|
|
|
|
albums_model: gio::ListStore,
|
|
|
|
|
2024-11-07 09:05:26 +00:00
|
|
|
#[property(type = i64, get = Self::volume, set = Self::set_volume, minimum = 0, maximum = 100)]
|
2024-11-04 11:31:43 +00:00
|
|
|
_volume: (),
|
|
|
|
#[property(type = bool, get = Self::mute, set = Self::set_mute)]
|
|
|
|
_mute: (),
|
|
|
|
#[property(type = bool, get = Self::pause, set = Self::set_pause)]
|
|
|
|
_pause: (),
|
2024-11-07 09:05:26 +00:00
|
|
|
#[property(type = i64, get = Self::playlist_pos)]
|
2024-11-04 11:31:43 +00:00
|
|
|
_playlist_pos: (),
|
2024-11-04 12:22:25 +00:00
|
|
|
#[property(type = f64, get = Self::time_pos)]
|
|
|
|
_time_pos: (),
|
2024-11-05 09:01:07 +00:00
|
|
|
#[property(type = f64, get = Self::duration)]
|
|
|
|
_duration: (), // as reported by mpv, compare with song.duration
|
2024-11-04 12:43:30 +00:00
|
|
|
#[property(type = bool, get = Self::idle_active)]
|
|
|
|
_idle_active: (),
|
2024-11-07 09:05:26 +00:00
|
|
|
#[property(type = i64, get = Self::playlist_count)]
|
2024-11-05 09:01:07 +00:00
|
|
|
_playlist_count: (),
|
2024-11-05 09:48:50 +00:00
|
|
|
|
|
|
|
pub(super) queued_seek: Cell<Option<f64>>,
|
2024-11-05 10:02:02 +00:00
|
|
|
|
2024-11-05 10:12:03 +00:00
|
|
|
pub(crate) initial_setup_handle: RefCell<Option<JoinHandle<()>>>,
|
2024-11-05 10:02:02 +00:00
|
|
|
mpv_event_loop_handle: RefCell<Option<JoinHandle<()>>>, // really !, not ()
|
|
|
|
zbus_executor_loop_handle: RefCell<Option<JoinHandle<()>>>, // same
|
2024-11-05 11:07:30 +00:00
|
|
|
|
2024-11-05 11:19:33 +00:00
|
|
|
loading_cover_handle: RefCell<Option<JoinHandle<()>>>,
|
2024-11-05 20:59:59 +00:00
|
|
|
|
|
|
|
buffering_timeout: Cell<Option<glib::SourceId>>,
|
|
|
|
time_pos_notify_timeout: Cell<Option<glib::SourceId>>,
|
2024-11-06 12:10:39 +00:00
|
|
|
|
|
|
|
mpris_player: RefCell<Option<InterfaceRef<mpris::Player>>>,
|
2024-11-04 09:40:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for Window {
|
|
|
|
fn default() -> Self {
|
2024-11-04 11:31:43 +00:00
|
|
|
let mpv = mpv::Handle::new();
|
|
|
|
mpv.set_property("audio-client-name", "audrey").unwrap();
|
|
|
|
mpv.set_property("user-agent", crate::USER_AGENT).unwrap();
|
2024-11-05 10:07:53 +00:00
|
|
|
mpv.set_property("vid", false).unwrap();
|
2024-11-04 11:31:43 +00:00
|
|
|
mpv.set_property("prefetch-playlist", true).unwrap();
|
|
|
|
|
|
|
|
mpv.observe_property(0, "volume").unwrap();
|
|
|
|
mpv.observe_property(1, "mute").unwrap();
|
|
|
|
mpv.observe_property(2, "pause").unwrap();
|
|
|
|
mpv.observe_property(3, "playlist-pos").unwrap();
|
2024-11-04 12:43:30 +00:00
|
|
|
mpv.observe_property(4, "idle-active").unwrap();
|
2024-11-05 09:01:07 +00:00
|
|
|
mpv.observe_property(6, "playlist-count").unwrap();
|
|
|
|
mpv.observe_property(7, "duration").unwrap();
|
2024-11-05 09:22:50 +00:00
|
|
|
mpv.observe_property(8, "path").unwrap();
|
2024-11-04 11:31:43 +00:00
|
|
|
|
|
|
|
// "Useful to drain property changes before a new file is loaded."
|
|
|
|
mpv.add_hook(0, "on_before_start_file", 0).unwrap();
|
2024-11-04 09:40:51 +00:00
|
|
|
|
|
|
|
Self {
|
|
|
|
playbar: Default::default(),
|
|
|
|
play_queue: Default::default(),
|
|
|
|
can_click_shuffle_all: Cell::new(false),
|
|
|
|
playing_cover_art: Default::default(),
|
2024-11-04 12:29:10 +00:00
|
|
|
_song: (),
|
2024-11-04 09:40:51 +00:00
|
|
|
setup: Default::default(),
|
|
|
|
api: Default::default(),
|
2024-11-04 11:31:43 +00:00
|
|
|
mpv,
|
2024-11-07 20:54:01 +00:00
|
|
|
|
2024-11-05 20:44:20 +00:00
|
|
|
playlist_model: gio::ListStore::new::<Song>(),
|
2024-11-07 20:54:01 +00:00
|
|
|
albums_model: gio::ListStore::new::<Album>(),
|
2024-11-04 11:31:43 +00:00
|
|
|
|
|
|
|
_volume: (),
|
|
|
|
_mute: (),
|
|
|
|
_pause: (),
|
|
|
|
_playlist_pos: (),
|
2024-11-04 12:22:25 +00:00
|
|
|
_time_pos: (),
|
2024-11-05 09:01:07 +00:00
|
|
|
_duration: (),
|
2024-11-04 12:43:30 +00:00
|
|
|
_idle_active: (),
|
2024-11-05 09:01:07 +00:00
|
|
|
_playlist_count: (),
|
2024-11-05 09:48:50 +00:00
|
|
|
|
2024-11-05 10:33:59 +00:00
|
|
|
queued_seek: Default::default(),
|
2024-11-05 10:02:02 +00:00
|
|
|
|
2024-11-05 10:12:03 +00:00
|
|
|
initial_setup_handle: Default::default(),
|
2024-11-05 10:02:02 +00:00
|
|
|
mpv_event_loop_handle: Default::default(),
|
|
|
|
zbus_executor_loop_handle: Default::default(),
|
2024-11-05 11:07:30 +00:00
|
|
|
|
|
|
|
loading_cover_handle: Default::default(),
|
2024-11-05 20:59:59 +00:00
|
|
|
|
|
|
|
buffering_timeout: Default::default(),
|
|
|
|
time_pos_notify_timeout: Default::default(),
|
2024-11-06 12:10:39 +00:00
|
|
|
|
|
|
|
mpris_player: Default::default(),
|
2024-11-04 09:40:51 +00:00
|
|
|
}
|
|
|
|
}
|
2024-10-29 13:02:29 +00:00
|
|
|
}
|
|
|
|
|
2024-11-02 11:24:25 +00:00
|
|
|
#[glib::object_subclass]
|
|
|
|
impl ObjectSubclass for Window {
|
|
|
|
const NAME: &'static str = "AudreyUiWindow";
|
|
|
|
type Type = super::Window;
|
|
|
|
type ParentType = adw::ApplicationWindow;
|
|
|
|
|
|
|
|
fn class_init(klass: &mut Self::Class) {
|
|
|
|
klass.bind_template();
|
|
|
|
klass.bind_template_callbacks();
|
|
|
|
}
|
|
|
|
|
|
|
|
fn instance_init(obj: &InitializingObject<Self>) {
|
|
|
|
obj.init_template();
|
|
|
|
}
|
2024-10-29 13:02:29 +00:00
|
|
|
}
|
|
|
|
|
2024-11-02 11:24:25 +00:00
|
|
|
#[glib::derived_properties]
|
2024-11-03 09:37:37 +00:00
|
|
|
impl ObjectImpl for Window {
|
|
|
|
fn constructed(&self) {
|
|
|
|
self.parent_constructed();
|
|
|
|
|
2024-11-05 20:59:59 +00:00
|
|
|
// update time-pos every 100 ms
|
|
|
|
let window = self.obj().downgrade();
|
|
|
|
self.time_pos_notify_timeout
|
|
|
|
.replace(Some(glib::timeout_add_local(
|
|
|
|
std::time::Duration::from_millis(100),
|
|
|
|
move || match window.upgrade() {
|
|
|
|
None => glib::ControlFlow::Break,
|
|
|
|
Some(window) => {
|
|
|
|
if !window.idle_active() && !window.pause() {
|
|
|
|
window.notify("time-pos");
|
|
|
|
}
|
|
|
|
glib::ControlFlow::Continue
|
|
|
|
}
|
|
|
|
},
|
|
|
|
)));
|
|
|
|
|
2024-11-04 11:31:43 +00:00
|
|
|
let window = self.obj().downgrade();
|
2024-11-05 10:02:02 +00:00
|
|
|
let mpv_event_loop_handle = glib::spawn_future_local(async move {
|
|
|
|
loop {
|
|
|
|
let window = window.upgrade().unwrap();
|
2024-11-04 11:31:43 +00:00
|
|
|
let listener = window.imp().mpv.wakeup_listener();
|
2024-11-06 11:30:02 +00:00
|
|
|
|
|
|
|
// only send property change notifications after the event queue is drained
|
|
|
|
let freeze_notify = window.freeze_notify();
|
|
|
|
|
2024-11-04 11:31:43 +00:00
|
|
|
while let Some(event) = window.imp().mpv.wait_event(0.0) {
|
|
|
|
use crate::mpv::Event;
|
|
|
|
|
|
|
|
match event {
|
2024-11-06 11:21:45 +00:00
|
|
|
Event::PropertyChange(event) => window.imp().on_property_change(event),
|
|
|
|
Event::StartFile(_) => window.imp().on_start_file(),
|
|
|
|
Event::Hook(event) => window.imp().on_hook(event),
|
|
|
|
Event::LogMessage(event) => window.imp().on_log_message(event),
|
|
|
|
Event::Seek => window.imp().on_seek(),
|
|
|
|
Event::PlaybackRestart => window.imp().on_playback_restart(),
|
|
|
|
Event::EndFile(event) => window.imp().on_end_file(event),
|
2024-11-04 12:52:33 +00:00
|
|
|
|
2024-11-05 05:28:48 +00:00
|
|
|
Event::Unknown(_) => {
|
|
|
|
// either deprecated or future, ignore
|
|
|
|
}
|
|
|
|
|
2024-11-05 09:35:20 +00:00
|
|
|
Event::AudioReconfig => {
|
|
|
|
// "This is relatively uninteresting, because there is no such thing as audio output embedding."
|
|
|
|
// ^ ignore
|
|
|
|
}
|
|
|
|
|
2024-11-04 11:31:43 +00:00
|
|
|
_ => event!(Level::DEBUG, "unhandled {event:?}"),
|
2024-11-03 09:37:37 +00:00
|
|
|
}
|
|
|
|
}
|
2024-11-06 11:30:02 +00:00
|
|
|
|
|
|
|
// send property change notifications now
|
|
|
|
drop(freeze_notify);
|
2024-11-04 11:31:43 +00:00
|
|
|
drop(window);
|
|
|
|
listener.await;
|
2024-11-03 09:37:37 +00:00
|
|
|
}
|
2024-11-04 11:31:43 +00:00
|
|
|
});
|
2024-11-05 10:12:03 +00:00
|
|
|
self.mpv_event_loop_handle
|
|
|
|
.replace(Some(mpv_event_loop_handle));
|
2024-11-03 17:45:52 +00:00
|
|
|
|
2024-11-03 09:37:37 +00:00
|
|
|
// set up mpris
|
|
|
|
let window = self.obj().clone();
|
2024-11-05 10:02:02 +00:00
|
|
|
let zbus_executor_loop_handle = glib::spawn_future_local(async move {
|
2024-11-03 09:37:37 +00:00
|
|
|
let conn = zbus::connection::Builder::session()
|
|
|
|
.expect("could not connect to the session bus")
|
|
|
|
.internal_executor(false)
|
|
|
|
.build()
|
|
|
|
.await
|
|
|
|
.expect("could not build connection to the session bus");
|
|
|
|
|
2024-11-05 10:12:03 +00:00
|
|
|
futures::join!(
|
|
|
|
glib::clone!(
|
|
|
|
#[strong]
|
|
|
|
conn,
|
|
|
|
async move {
|
|
|
|
crate::Mpris::setup(conn.object_server(), &window)
|
|
|
|
.await
|
|
|
|
.expect("could not serve mpris");
|
2024-11-06 12:10:39 +00:00
|
|
|
|
|
|
|
window.imp().mpris_player.replace(Some(
|
|
|
|
mpris::Player::new(conn.object_server(), &window)
|
|
|
|
.await
|
|
|
|
.expect("could not serve mpris player"),
|
|
|
|
));
|
2024-11-03 09:37:37 +00:00
|
|
|
|
2024-11-05 10:12:03 +00:00
|
|
|
// always set up handlers before requesting service name
|
|
|
|
conn.request_name("org.mpris.MediaPlayer2.audrey")
|
|
|
|
.await
|
|
|
|
.expect("could not register name in session bus");
|
|
|
|
}
|
|
|
|
),
|
|
|
|
async {
|
|
|
|
loop {
|
|
|
|
conn.executor().tick().await;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
2024-11-03 09:37:37 +00:00
|
|
|
});
|
2024-11-05 10:02:02 +00:00
|
|
|
self.zbus_executor_loop_handle
|
|
|
|
.replace(Some(zbus_executor_loop_handle));
|
2024-11-03 09:37:37 +00:00
|
|
|
}
|
|
|
|
}
|
2024-11-02 11:24:25 +00:00
|
|
|
|
|
|
|
impl WidgetImpl for Window {}
|
|
|
|
|
|
|
|
impl WindowImpl for Window {}
|
|
|
|
|
|
|
|
impl ApplicationWindowImpl for Window {}
|
|
|
|
|
|
|
|
impl AdwApplicationWindowImpl for Window {}
|
|
|
|
|
|
|
|
#[gtk::template_callbacks]
|
|
|
|
impl Window {
|
|
|
|
#[template_callback]
|
|
|
|
fn show_playbar_cover_art(&self, stack_child: Option<&str>) -> bool {
|
|
|
|
stack_child != Some("play-queue")
|
|
|
|
}
|
|
|
|
|
|
|
|
#[template_callback]
|
|
|
|
async fn shuffle_all(&self) {
|
2024-11-03 15:11:25 +00:00
|
|
|
self.obj().set_can_click_shuffle_all(false);
|
2024-11-04 11:31:43 +00:00
|
|
|
|
|
|
|
self.mpv.command(["stop"]).unwrap();
|
|
|
|
self.playlist_model.remove_all();
|
2024-11-04 12:43:30 +00:00
|
|
|
self.set_pause(false);
|
2024-11-04 11:31:43 +00:00
|
|
|
|
2024-11-04 12:22:25 +00:00
|
|
|
let api = {
|
|
|
|
let api = self.api.borrow();
|
|
|
|
Rc::clone(api.as_ref().unwrap())
|
|
|
|
};
|
2024-11-07 18:53:57 +00:00
|
|
|
for song in api.random_songs(10).await.unwrap().into_iter() {
|
2024-11-06 11:46:49 +00:00
|
|
|
let song = Song::from_child(&api, &song, true);
|
2024-11-04 11:31:43 +00:00
|
|
|
self.mpv
|
|
|
|
.command(["loadfile", &song.stream_url(), "append-play"])
|
|
|
|
.unwrap();
|
|
|
|
self.playlist_model.append(&song);
|
2024-11-03 15:11:25 +00:00
|
|
|
}
|
|
|
|
self.obj().set_can_click_shuffle_all(true);
|
2024-11-02 11:24:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[template_callback]
|
|
|
|
fn show_setup_dialog(&self) {
|
|
|
|
self.setup.present(Some(self.obj().as_ref()));
|
|
|
|
}
|
|
|
|
|
2024-11-07 09:05:26 +00:00
|
|
|
fn volume(&self) -> i64 {
|
2024-11-07 19:42:45 +00:00
|
|
|
self.mpv.get_property("volume").unwrap()
|
2024-11-04 11:31:43 +00:00
|
|
|
}
|
|
|
|
|
2024-11-07 09:05:26 +00:00
|
|
|
fn set_volume(&self, volume: i64) {
|
2024-11-07 19:42:45 +00:00
|
|
|
self.mpv.set_property("volume", volume).unwrap();
|
2024-11-04 11:31:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn mute(&self) -> bool {
|
|
|
|
self.mpv.get_property("mute").unwrap()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_mute(&self, mute: bool) {
|
|
|
|
self.mpv.set_property("mute", mute).unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
fn pause(&self) -> bool {
|
|
|
|
self.mpv.get_property("pause").unwrap()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_pause(&self, pause: bool) {
|
|
|
|
self.mpv.set_property("pause", pause).unwrap();
|
|
|
|
}
|
|
|
|
|
2024-11-07 09:05:26 +00:00
|
|
|
fn playlist_pos(&self) -> i64 {
|
2024-11-07 18:53:57 +00:00
|
|
|
self.mpv.get_property("playlist-pos").unwrap()
|
2024-11-04 11:31:43 +00:00
|
|
|
}
|
2024-11-04 12:22:25 +00:00
|
|
|
|
|
|
|
fn time_pos(&self) -> f64 {
|
2024-11-05 09:48:50 +00:00
|
|
|
if let Some(queued_seek) = self.queued_seek.get() {
|
|
|
|
// counterfeit time-pos while the seek is ongoing
|
|
|
|
queued_seek
|
|
|
|
} else {
|
|
|
|
match self.mpv.get_property("time-pos") {
|
|
|
|
Ok(time_pos) => Ok(time_pos),
|
|
|
|
Err(err) if err.is_property_unavailable() => Ok(0.0), //placeholder
|
|
|
|
Err(err) => Err(err),
|
|
|
|
}
|
|
|
|
.unwrap()
|
2024-11-04 12:22:25 +00:00
|
|
|
}
|
|
|
|
}
|
2024-11-04 12:29:10 +00:00
|
|
|
|
2024-11-05 09:01:07 +00:00
|
|
|
fn duration(&self) -> f64 {
|
|
|
|
let duration = match self.mpv.get_property::<f64>("duration") {
|
|
|
|
Ok(duration) => Ok(Some(duration)),
|
|
|
|
Err(err) if err.is_property_unavailable() => {
|
|
|
|
Ok(self.song().as_ref().map(|song| song.duration() as f64))
|
|
|
|
}
|
|
|
|
Err(err) => Err(err),
|
|
|
|
}
|
|
|
|
.unwrap();
|
|
|
|
|
2024-11-05 09:24:59 +00:00
|
|
|
{
|
|
|
|
let left = duration.map(|f| f as i64);
|
2024-11-05 20:44:20 +00:00
|
|
|
let right = self.song().as_ref().map(crate::model::Song::duration);
|
2024-11-05 09:24:59 +00:00
|
|
|
if left != right {
|
|
|
|
event!(
|
|
|
|
Level::WARN,
|
|
|
|
"mpv duration {left:?} doesn not match subsonic duration {right:?}"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2024-11-05 09:01:07 +00:00
|
|
|
|
|
|
|
duration.unwrap_or(0.0) // placeholder
|
|
|
|
}
|
|
|
|
|
2024-11-04 12:43:30 +00:00
|
|
|
fn idle_active(&self) -> bool {
|
|
|
|
self.mpv.get_property("idle-active").unwrap()
|
|
|
|
}
|
|
|
|
|
2024-11-07 09:05:26 +00:00
|
|
|
fn playlist_count(&self) -> i64 {
|
2024-11-07 19:42:45 +00:00
|
|
|
self.mpv.get_property::<i64>("playlist-count").unwrap()
|
2024-11-05 09:01:07 +00:00
|
|
|
}
|
|
|
|
|
2024-11-05 20:44:20 +00:00
|
|
|
fn song(&self) -> Option<Song> {
|
2024-11-07 09:05:26 +00:00
|
|
|
match self.obj().playlist_pos().try_into() {
|
|
|
|
Ok(playlist_pos) => Some(
|
2024-11-06 11:11:14 +00:00
|
|
|
self.obj()
|
|
|
|
.playlist_model()
|
2024-11-07 09:05:26 +00:00
|
|
|
.item(playlist_pos)
|
2024-11-06 11:11:14 +00:00
|
|
|
.unwrap()
|
|
|
|
.dynamic_cast()
|
|
|
|
.unwrap(),
|
|
|
|
),
|
2024-11-07 09:05:26 +00:00
|
|
|
Err(_) => None,
|
2024-11-04 12:29:10 +00:00
|
|
|
}
|
|
|
|
}
|
2024-11-05 10:33:59 +00:00
|
|
|
|
|
|
|
fn buffering_start(&self) {
|
|
|
|
let started_buffering = std::time::Instant::now();
|
2024-11-05 20:59:59 +00:00
|
|
|
let window = self.obj().downgrade();
|
2024-11-07 19:42:45 +00:00
|
|
|
if let Some(source) = self.buffering_timeout.replace(Some(glib::timeout_add_local(
|
|
|
|
std::time::Duration::from_millis(100),
|
|
|
|
move || {
|
|
|
|
match window.upgrade() {
|
|
|
|
None => glib::ControlFlow::Break,
|
|
|
|
Some(window) => {
|
|
|
|
// 3 second period from gnome hig
|
|
|
|
if started_buffering.elapsed() > std::time::Duration::from_secs(3) {
|
|
|
|
window.imp().playbar.set_show_pulse_bar(true);
|
|
|
|
window.imp().playbar.pulse_bar().pulse();
|
2024-11-05 20:59:59 +00:00
|
|
|
}
|
2024-11-07 19:42:45 +00:00
|
|
|
glib::ControlFlow::Continue
|
2024-11-05 20:59:59 +00:00
|
|
|
}
|
2024-11-07 19:42:45 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
))) {
|
|
|
|
source.remove()
|
|
|
|
}
|
2024-11-05 10:33:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn buffering_end(&self) {
|
2024-11-07 19:42:45 +00:00
|
|
|
if let Some(source) = self.buffering_timeout.take() {
|
|
|
|
source.remove();
|
|
|
|
}
|
2024-11-05 10:33:59 +00:00
|
|
|
self.playbar.set_show_pulse_bar(false);
|
|
|
|
}
|
2024-11-06 11:21:45 +00:00
|
|
|
|
|
|
|
fn on_property_change(&self, event: crate::mpv::event::PropertyEvent) {
|
|
|
|
match event.reply_userdata {
|
|
|
|
0 => {
|
|
|
|
assert_eq!(event.name, "volume");
|
|
|
|
self.obj().notify("volume");
|
|
|
|
}
|
|
|
|
|
|
|
|
1 => {
|
|
|
|
assert_eq!(event.name, "mute");
|
|
|
|
self.obj().notify("mute");
|
|
|
|
}
|
|
|
|
|
|
|
|
2 => {
|
|
|
|
assert_eq!(event.name, "pause");
|
|
|
|
self.obj().notify("pause");
|
|
|
|
}
|
|
|
|
|
|
|
|
3 => {
|
|
|
|
assert_eq!(event.name, "playlist-pos");
|
|
|
|
self.obj().notify("playlist-pos");
|
|
|
|
}
|
|
|
|
|
|
|
|
4 => {
|
|
|
|
assert_eq!(event.name, "idle-active");
|
|
|
|
self.obj().notify("idle-active");
|
|
|
|
}
|
|
|
|
|
|
|
|
6 => {
|
|
|
|
assert_eq!(event.name, "playlist-count");
|
|
|
|
self.obj().notify("playlist-count");
|
|
|
|
}
|
|
|
|
|
|
|
|
7 => {
|
|
|
|
assert_eq!(event.name, "duration");
|
|
|
|
self.obj().notify("duration");
|
|
|
|
}
|
|
|
|
|
|
|
|
8 => {
|
|
|
|
assert_eq!(event.name, "path");
|
|
|
|
// sanity check
|
|
|
|
match self.mpv.get_property::<String>("path") {
|
|
|
|
Ok(path) => {
|
|
|
|
assert_eq!(path, self.obj().song().unwrap().stream_url())
|
|
|
|
}
|
|
|
|
Err(err) if err.is_property_unavailable() => {}
|
2024-11-07 19:42:45 +00:00
|
|
|
Err(err) => panic!("{err:?}"),
|
2024-11-06 11:21:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_ => unreachable!(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn on_start_file(&self) {
|
|
|
|
event!(Level::INFO, "start file event");
|
|
|
|
self.obj().notify("song");
|
|
|
|
self.buffering_start();
|
|
|
|
|
|
|
|
let window = self.obj().clone();
|
|
|
|
let song_id = window.song().unwrap().id();
|
2024-11-07 19:42:45 +00:00
|
|
|
if let Some(handle) = self
|
|
|
|
.loading_cover_handle
|
2024-11-06 11:21:45 +00:00
|
|
|
.replace(Some(glib::spawn_future_local(async move {
|
|
|
|
let api = window.imp().api.borrow().as_ref().unwrap().clone();
|
|
|
|
let bytes = api
|
2024-11-07 09:01:28 +00:00
|
|
|
.cover_art(&song_id, None) // full size
|
2024-11-06 11:21:45 +00:00
|
|
|
.await
|
|
|
|
.expect("could not load cover art for song {song_id}");
|
|
|
|
|
|
|
|
match window.song() {
|
|
|
|
Some(song) if song.id() == song_id => {
|
|
|
|
let texture = gdk::Texture::from_bytes(&glib::Bytes::from_owned(bytes))
|
|
|
|
.expect("could not create texture from cover art for {song_id}");
|
|
|
|
window.set_playing_cover_art(Some(texture));
|
|
|
|
}
|
|
|
|
_ => {
|
|
|
|
event!(
|
|
|
|
Level::WARN,
|
|
|
|
"was too late to fetch cover for song {song_id}"
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})))
|
2024-11-07 19:42:45 +00:00
|
|
|
{
|
|
|
|
handle.abort();
|
|
|
|
}
|
2024-11-06 11:21:45 +00:00
|
|
|
|
|
|
|
// make sure this is reported as 0
|
|
|
|
self.obj().notify("time-pos");
|
|
|
|
}
|
|
|
|
|
|
|
|
fn on_hook(&self, event: crate::mpv::event::HookEvent) {
|
|
|
|
match event.reply_userdata {
|
|
|
|
0 => {
|
|
|
|
assert_eq!(&event.name, "on_before_start_file");
|
|
|
|
event!(Level::DEBUG, "on_before_start_file triggered");
|
|
|
|
// just use this as a barrier
|
|
|
|
self.mpv.continue_hook(event.id).unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
_ => unreachable!(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn on_log_message(&self, event: crate::mpv::event::LogMessageEvent) {
|
|
|
|
let span = span!(Level::DEBUG, "mpv_log", prefix = event.prefix);
|
|
|
|
let _guard = span.enter();
|
|
|
|
match event.log_level {
|
|
|
|
// level has to be 'static so this sux
|
|
|
|
l if l <= 20 => {
|
2024-11-07 10:41:41 +00:00
|
|
|
event!(target: "mpv_event", Level::ERROR, "{}", event.text.trim())
|
2024-11-06 11:21:45 +00:00
|
|
|
}
|
|
|
|
l if l <= 30 => {
|
2024-11-07 10:41:41 +00:00
|
|
|
event!(target: "mpv_event", Level::WARN, "{}", event.text.trim())
|
2024-11-06 11:21:45 +00:00
|
|
|
}
|
|
|
|
l if l <= 40 => {
|
2024-11-07 10:41:41 +00:00
|
|
|
event!(target: "mpv_event", Level::INFO, "{}", event.text.trim())
|
2024-11-06 11:21:45 +00:00
|
|
|
}
|
|
|
|
l if l <= 60 => {
|
2024-11-07 10:41:41 +00:00
|
|
|
event!(target: "mpv_event", Level::DEBUG, "{}", event.text.trim())
|
2024-11-06 11:21:45 +00:00
|
|
|
}
|
|
|
|
l if l <= 70 => {
|
2024-11-07 10:41:41 +00:00
|
|
|
event!(target: "mpv_event", Level::TRACE, "{}", event.text.trim())
|
2024-11-06 11:21:45 +00:00
|
|
|
}
|
|
|
|
// should be unused
|
|
|
|
_ => event!(
|
|
|
|
target: "mpv_erroneus_event",
|
|
|
|
Level::DEBUG,
|
|
|
|
log_level = event.log_level,
|
|
|
|
"{}",
|
2024-11-07 10:41:41 +00:00
|
|
|
event.text.trim(),
|
2024-11-06 11:21:45 +00:00
|
|
|
),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
fn on_seek(&self) {
|
|
|
|
event!(Level::INFO, "seek event");
|
|
|
|
self.buffering_start();
|
|
|
|
}
|
|
|
|
|
|
|
|
fn on_playback_restart(&self) {
|
|
|
|
event!(Level::INFO, "playback restart event");
|
|
|
|
self.buffering_end();
|
|
|
|
self.obj().notify("time-pos");
|
|
|
|
|
|
|
|
if let Some(queued_seek) = self.queued_seek.take() {
|
|
|
|
// a seek was tried before and failed, try again now
|
|
|
|
event!(Level::INFO, "performing queued seek to {queued_seek}");
|
|
|
|
self.obj().seek(queued_seek);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn on_end_file(&self, event: crate::mpv::event::EndFileEvent) {
|
|
|
|
event!(Level::INFO, "end file event: {event:?}");
|
|
|
|
self.obj().notify("song");
|
|
|
|
self.buffering_end();
|
|
|
|
|
|
|
|
if let Err(err) = event.reason {
|
|
|
|
event!(Level::ERROR, "end file error: {err}");
|
|
|
|
}
|
|
|
|
|
|
|
|
// cancel queued seek always
|
|
|
|
self.queued_seek.set(None);
|
|
|
|
|
|
|
|
self.obj().set_playing_cover_art(None::<gdk::Texture>);
|
|
|
|
|
|
|
|
// make sure the seekbar looks full
|
|
|
|
self.obj().notify("time-pos");
|
|
|
|
}
|
2024-11-03 09:37:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Drop for Window {
|
|
|
|
fn drop(&mut self) {
|
2024-11-04 09:12:13 +00:00
|
|
|
event!(Level::DEBUG, "dropping AudreyUiWindow");
|
2024-11-05 10:02:02 +00:00
|
|
|
|
|
|
|
self.mpv_event_loop_handle.take().unwrap().abort();
|
|
|
|
self.zbus_executor_loop_handle.take().unwrap().abort();
|
2024-11-05 20:59:59 +00:00
|
|
|
|
2024-11-07 19:42:45 +00:00
|
|
|
if let Some(source) = self.time_pos_notify_timeout.take() {
|
|
|
|
source.remove();
|
|
|
|
}
|
|
|
|
if let Some(source) = self.buffering_timeout.take() {
|
|
|
|
source.remove();
|
|
|
|
}
|
2024-11-03 09:37:37 +00:00
|
|
|
}
|
2024-10-29 13:02:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
use adw::prelude::*;
|
2024-11-02 11:24:25 +00:00
|
|
|
use adw::subclass::prelude::*;
|
2024-11-03 19:18:12 +00:00
|
|
|
use gtk::{gio, glib};
|
2024-11-04 12:34:30 +00:00
|
|
|
use std::rc::Rc;
|
2024-11-05 09:27:22 +00:00
|
|
|
use tracing::{event, Level};
|
2024-10-29 13:02:29 +00:00
|
|
|
|
|
|
|
glib::wrapper! {
|
2024-11-02 11:24:25 +00:00
|
|
|
pub struct Window(ObjectSubclass<imp::Window>)
|
2024-10-29 13:02:29 +00:00
|
|
|
@extends adw::ApplicationWindow, gtk::ApplicationWindow, gtk::Window, gtk::Widget,
|
|
|
|
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Window {
|
|
|
|
pub fn new(app: &impl IsA<gtk::Application>) -> Self {
|
2024-11-02 11:24:25 +00:00
|
|
|
let window: Self = glib::Object::builder().property("application", app).build();
|
|
|
|
|
2024-11-04 12:34:30 +00:00
|
|
|
window.imp().setup.set_window(&window);
|
2024-11-05 10:12:03 +00:00
|
|
|
let initial_setup_handle = {
|
|
|
|
let setup = window.imp().setup.clone();
|
|
|
|
glib::spawn_future_local(async move { setup.load().await })
|
|
|
|
};
|
|
|
|
window
|
|
|
|
.imp()
|
|
|
|
.initial_setup_handle
|
|
|
|
.replace(Some(initial_setup_handle));
|
2024-11-02 11:24:25 +00:00
|
|
|
|
|
|
|
window
|
2024-11-01 12:50:29 +00:00
|
|
|
}
|
2024-11-04 09:40:51 +00:00
|
|
|
|
2024-11-04 11:31:43 +00:00
|
|
|
pub fn playbin(&self) -> ! {
|
|
|
|
todo!()
|
2024-11-04 09:40:51 +00:00
|
|
|
}
|
|
|
|
|
2024-11-05 14:38:46 +00:00
|
|
|
pub fn playlist_play_index(&self, index: Option<i64>) {
|
|
|
|
match index.as_ref() {
|
|
|
|
Some(index) => self
|
|
|
|
.imp()
|
|
|
|
.mpv
|
|
|
|
.command(["playlist-play-index", &index.to_string()])
|
|
|
|
.unwrap(),
|
|
|
|
None => self
|
|
|
|
.imp()
|
|
|
|
.mpv
|
|
|
|
.command(["playlist-play-index", "none"])
|
|
|
|
.unwrap(),
|
|
|
|
};
|
2024-11-04 09:40:51 +00:00
|
|
|
}
|
2024-11-04 11:37:46 +00:00
|
|
|
|
|
|
|
pub fn playlist_remove(&self, index: i64) {
|
|
|
|
self.imp()
|
|
|
|
.mpv
|
|
|
|
.command(["playlist-remove", &index.to_string()])
|
|
|
|
.unwrap();
|
|
|
|
self.playlist_model().remove(index as u32);
|
|
|
|
}
|
2024-11-04 12:34:30 +00:00
|
|
|
|
|
|
|
pub fn setup_connected(&self, api: crate::subsonic::Client) {
|
2024-11-07 20:54:01 +00:00
|
|
|
if self.imp().api.replace(Some(Rc::new(api))).is_some() {
|
|
|
|
unimplemented!("changing the api object");
|
|
|
|
}
|
|
|
|
|
2024-11-04 12:34:30 +00:00
|
|
|
self.set_can_click_shuffle_all(true);
|
2024-11-07 18:53:57 +00:00
|
|
|
|
|
|
|
let api = Rc::clone(self.imp().api.borrow().as_ref().unwrap());
|
2024-11-07 20:54:01 +00:00
|
|
|
let albums_model = self.albums_model().clone();
|
2024-11-07 18:53:57 +00:00
|
|
|
glib::spawn_future_local(async move {
|
|
|
|
use futures::TryStreamExt;
|
|
|
|
|
|
|
|
let mut albums =
|
|
|
|
std::pin::pin!(api.album_list_full(crate::subsonic::AlbumListType::Newest));
|
2024-11-07 20:54:01 +00:00
|
|
|
while let Some(album) = albums.try_next().await.unwrap() {
|
|
|
|
albums_model.append(
|
|
|
|
&glib::Object::builder::<crate::model::Album>()
|
|
|
|
.property("id", &album.id)
|
|
|
|
.property("name", &album.name)
|
|
|
|
.property("artist", &album.artist)
|
|
|
|
.build(),
|
|
|
|
);
|
2024-11-07 18:53:57 +00:00
|
|
|
}
|
|
|
|
});
|
2024-11-04 12:34:30 +00:00
|
|
|
}
|
2024-11-05 09:16:22 +00:00
|
|
|
|
|
|
|
pub fn playlist_next(&self) {
|
|
|
|
self.imp().mpv.command(["playlist-next"]).unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn playlist_prev(&self) {
|
|
|
|
self.imp().mpv.command(["playlist-prev"]).unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn seek(&self, new_position: f64) {
|
2024-11-05 09:27:22 +00:00
|
|
|
match self
|
|
|
|
.imp()
|
2024-11-05 09:16:22 +00:00
|
|
|
.mpv
|
|
|
|
.command(["seek", &new_position.to_string(), "absolute", "exact"])
|
2024-11-05 09:27:22 +00:00
|
|
|
{
|
|
|
|
Ok(()) => {}
|
2024-11-05 09:48:50 +00:00
|
|
|
Err(err) => {
|
|
|
|
event!(Level::INFO, "queuing seek to {new_position}: {err}");
|
|
|
|
self.imp().queued_seek.set(Some(new_position));
|
2024-11-06 10:44:28 +00:00
|
|
|
self.notify("time-pos");
|
2024-11-05 09:48:50 +00:00
|
|
|
}
|
2024-11-05 09:27:22 +00:00
|
|
|
}
|
2024-11-05 09:16:22 +00:00
|
|
|
}
|
2024-11-05 15:25:17 +00:00
|
|
|
|
|
|
|
pub fn playlist_move(&self, from: u32, to: u32) {
|
|
|
|
// NOTE: for mpv, to refers to the "gap" right before i
|
|
|
|
// so playlist-move i 0 makes a track the first
|
|
|
|
// playlist-move i 1 makes a track the second
|
|
|
|
// and playlist-move i playlist-count makes a track the last
|
|
|
|
// (so if to is the position of another track, from is left behind to)
|
|
|
|
self.imp()
|
|
|
|
.mpv
|
|
|
|
.command(["playlist-move", &from.to_string(), &to.to_string()])
|
|
|
|
.unwrap();
|
|
|
|
|
2024-11-07 19:42:45 +00:00
|
|
|
use std::cmp::Ordering;
|
|
|
|
match from.cmp(&to) {
|
|
|
|
Ordering::Less => {
|
|
|
|
// F1234T -> 1234FT
|
|
|
|
let mut spliced = Vec::with_capacity((to - from) as usize);
|
|
|
|
for i in from + 1..to {
|
|
|
|
spliced.push(self.playlist_model().item(i).unwrap());
|
|
|
|
}
|
|
|
|
spliced.push(self.playlist_model().item(from).unwrap());
|
|
|
|
|
|
|
|
self.playlist_model()
|
|
|
|
.splice(from, spliced.len() as u32, &spliced);
|
2024-11-05 15:25:17 +00:00
|
|
|
}
|
2024-11-07 19:42:45 +00:00
|
|
|
Ordering::Greater => {
|
|
|
|
// T1234F -> FT1234
|
|
|
|
let mut spliced = Vec::with_capacity((from - to + 1) as usize);
|
|
|
|
spliced.push(self.playlist_model().item(from).unwrap());
|
|
|
|
spliced.push(self.playlist_model().item(to).unwrap());
|
|
|
|
for i in to + 1..from {
|
|
|
|
spliced.push(self.playlist_model().item(i).unwrap());
|
|
|
|
}
|
2024-11-05 15:25:17 +00:00
|
|
|
|
2024-11-07 19:42:45 +00:00
|
|
|
self.playlist_model()
|
|
|
|
.splice(to, spliced.len() as u32, &spliced);
|
|
|
|
}
|
|
|
|
Ordering::Equal => {}
|
2024-11-05 15:25:17 +00:00
|
|
|
}
|
|
|
|
}
|
2024-11-07 20:54:01 +00:00
|
|
|
|
|
|
|
pub fn stop(&self) {
|
|
|
|
self.imp().mpv.command(["stop"]).unwrap();
|
|
|
|
self.playlist_model().remove_all();
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn api(&self) -> Rc<crate::subsonic::Client> {
|
|
|
|
Rc::clone(self.imp().api.borrow().as_ref().unwrap())
|
|
|
|
}
|
2024-10-29 13:02:29 +00:00
|
|
|
}
|