mod imp { use crate::mpv; use crate::PlaybinSong; use adw::prelude::*; use adw::subclass::prelude::*; use glib::subclass::InitializingObject; use gtk::{gdk, gio, glib}; use std::cell::{Cell, RefCell}; use std::rc::Rc; use tracing::{event, Level}; #[derive(gtk::CompositeTemplate, glib::Properties)] #[template(resource = "/eu/callcc/audrey/window.ui")] #[properties(wrapper_type = super::Window)] pub struct Window { #[template_child] pub(super) playbar: TemplateChild, #[template_child] pub(super) play_queue: TemplateChild, #[property(get, set, default = false)] can_click_shuffle_all: Cell, #[property(get, set, nullable)] playing_cover_art: RefCell>, #[property(type = Option, get = Self::song, nullable)] _song: (), pub(super) setup: crate::ui::Setup, pub(super) api: RefCell>>, pub(super) mpv: mpv::Handle, #[property(get)] playlist_model: gio::ListStore, #[property(type = i64, get = Self::volume, set = Self::set_volume)] _volume: (), #[property(type = bool, get = Self::mute, set = Self::set_mute)] _mute: (), #[property(type = bool, get = Self::pause, set = Self::set_pause)] _pause: (), #[property(type = i64, get = Self::playlist_pos)] _playlist_pos: (), #[property(type = f64, get = Self::time_pos)] _time_pos: (), #[property(type = bool, get = Self::idle_active)] _idle_active: (), } impl Default for Window { fn default() -> Self { let mpv = mpv::Handle::new(); mpv.set_property("audio-client-name", "audrey").unwrap(); mpv.set_property("user-agent", crate::USER_AGENT).unwrap(); mpv.set_property("video", false).unwrap(); mpv.set_property("prefetch-playlist", true).unwrap(); mpv.set_property("gapless-audio", 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(); mpv.observe_property(4, "idle-active").unwrap(); // "Useful to drain property changes before a new file is loaded." mpv.add_hook(0, "on_before_start_file", 0).unwrap(); Self { playbar: Default::default(), play_queue: Default::default(), can_click_shuffle_all: Cell::new(false), playing_cover_art: Default::default(), _song: (), setup: Default::default(), api: Default::default(), mpv, playlist_model: gio::ListStore::new::(), _volume: (), _mute: (), _pause: (), _playlist_pos: (), _time_pos: (), _idle_active: (), } } } #[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) { obj.init_template(); } } #[glib::derived_properties] impl ObjectImpl for Window { fn constructed(&self) { self.parent_constructed(); let window = self.obj().downgrade(); glib::spawn_future_local(async move { while let Some(window) = window.upgrade() { let listener = window.imp().mpv.wakeup_listener(); while let Some(event) = window.imp().mpv.wait_event(0.0) { use crate::mpv::Event; match event { Event::PropertyChange(event) => match event.reply_userdata { 0 => { assert_eq!(event.name, "volume"); window.notify("volume"); } 1 => { assert_eq!(event.name, "mute"); window.notify("mute"); } 2 => { assert_eq!(event.name, "pause"); window.notify("pause"); } 3 => { assert_eq!(event.name, "playlist-pos"); window.notify("playlist-pos"); } 4 => { assert_eq!(event.name, "idle-active"); window.notify("idle-active"); } _ => unreachable!(), }, Event::StartFile(_) => { window.notify("song"); // TODO: load cover art } Event::Hook(event) => 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 window.imp().mpv.continue_hook(event.id).unwrap(); } _ => unreachable!(), }, Event::LogMessage(event) => { // TODO: levels etc if event.log_level < 30 { event!(target: "mpv", Level::ERROR, "[{}] {}", event.prefix, event.text.trim()); } else if event.log_level < 40 { event!(target: "mpv", Level::WARN, "[{}] {}", event.prefix, event.text.trim()); } else if event.log_level < 50 { event!(target: "mpv", Level::INFO, "[{}] {}", event.prefix, event.text.trim()); } else if event.log_level < 70 { event!(target: "mpv", Level::DEBUG, "[{}] {}", event.prefix, event.text.trim()); } else { event!(target: "mpv", Level::TRACE, "[{}] {}", event.prefix, event.text.trim()); }; } _ => event!(Level::DEBUG, "unhandled {event:?}"), } } drop(window); listener.await; } }); // set up mpris let window = self.obj().clone(); glib::spawn_future_local(async move { 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"); // run this in glib's main loop glib::spawn_future_local(glib::clone!( #[strong] conn, async move { loop { conn.executor().tick().await; } } )); crate::Mpris::setup(conn.object_server(), &window) .await .expect("could not serve mpris"); /* crate::mpris::Player::setup(conn.object_server(), &window.imp().playbin) .await .expect("could not serve mpris player"); FIXME */ drop(window); // don't keep this alive // always set up handlers before requesting service name conn.request_name("org.mpris.MediaPlayer2.audrey") .await .expect("could not register name in session bus"); }); // notify of new time-pos every 100 ms glib::source::timeout_add_local(std::time::Duration::from_millis(100), { let window = self.obj().downgrade(); move || { let window = match window.upgrade() { None => return glib::ControlFlow::Break, Some(window) => window, }; window.notify("time-pos"); glib::ControlFlow::Continue } }); } } 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) { self.obj().set_can_click_shuffle_all(false); self.mpv.command(["stop"]).unwrap(); self.playlist_model.remove_all(); self.set_pause(false); let api = { let api = self.api.borrow(); Rc::clone(api.as_ref().unwrap()) }; for song in api.get_random_songs(10).await.unwrap().into_iter() { let song = PlaybinSong::from_child(&api, &song); self.mpv .command(["loadfile", &song.stream_url(), "append-play"]) .unwrap(); self.playlist_model.append(&song); } self.obj().set_can_click_shuffle_all(true); } #[template_callback] fn show_setup_dialog(&self) { self.setup.present(Some(self.obj().as_ref())); } fn volume(&self) -> i64 { self.mpv.get_property("volume").unwrap() } fn set_volume(&self, volume: i64) { self.mpv.set_property("volume", volume).unwrap(); } 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(); } fn playlist_pos(&self) -> i64 { self.mpv.get_property("playlist-pos").unwrap() } fn time_pos(&self) -> f64 { 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() } fn idle_active(&self) -> bool { self.mpv.get_property("idle-active").unwrap() } fn song(&self) -> Option { if self.obj().playlist_pos() < 0 { None } else { let song: PlaybinSong = self .obj() .playlist_model() .item(self.obj().playlist_pos() as u32) .unwrap() .dynamic_cast() .unwrap(); // sanity check assert_eq!( song.stream_url(), self.mpv.get_property::("path").unwrap() ); Some(song) } } } impl Drop for Window { fn drop(&mut self) { event!(Level::DEBUG, "dropping AudreyUiWindow"); } } } use adw::prelude::*; use adw::subclass::prelude::*; use gtk::{gio, glib}; use std::rc::Rc; glib::wrapper! { pub struct Window(ObjectSubclass) @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) -> Self { let window: Self = glib::Object::builder().property("application", app).build(); window.imp().setup.set_window(&window); window.imp().setup.load(); window } pub fn playbin(&self) -> ! { todo!() } pub fn playlist_play_index(&self, index: i64) { self.imp() .mpv .command(["playlist-play-index", &index.to_string()]) .unwrap(); } pub fn playlist_remove(&self, index: i64) { self.imp() .mpv .command(["playlist-remove", &index.to_string()]) .unwrap(); self.playlist_model().remove(index as u32); } pub fn setup_connected(&self, api: crate::subsonic::Client) { self.imp().api.replace(Some(Rc::new(api))); self.set_can_click_shuffle_all(true); } }