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(get, set, nullable)] song: RefCell>, 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: (), } 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(); // "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: Default::default(), setup: Default::default(), api: Default::default(), mpv, playlist_model: gio::ListStore::new::(), _volume: (), _mute: (), _pause: (), _playlist_pos: (), } } } #[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"); } _ => unreachable!(), }, mpv::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!(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"); */ 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"); }); } } 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(); let api = self.api.borrow(); let api = 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())); } pub(super) fn now_playing(&self, _song: &PlaybinSong) { /* this.song = song; // api.scrobble.begin (this.song.id); TODO if (this.cancel_loading_art != null) { this.cancel_loading_art.cancel (); } this.cancel_loading_art = new GLib.Cancellable (); this.playing_cover_art = null; // TODO: preload next art somehow this.cover_art_loading = true; string song_id = this.song.id; this.api.cover_art.begin (song_id, -1, Priority.DEFAULT, this.cancel_loading_art, (obj, res) => { try { this.playing_cover_art = Gdk.Texture.for_pixbuf (this.api.cover_art.end (res)); this.cover_art_loading = false; } catch (Error e) { if (!(e is IOError.CANCELLED)) { warning ("could not load cover for %s: %s", song_id, e.message); this.cover_art_loading = false; } } }); */ todo!() } 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() } } impl Drop for Window { fn drop(&mut self) { event!(Level::DEBUG, "dropping AudreyUiWindow"); } } } use adw::prelude::*; use adw::subclass::prelude::*; use gtk::{gio, glib}; 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(); // manual bidirectional sync /* window .imp() .playbar .set_volume(window.imp().mpv.get_property::("volume") as i32); */ /* window.imp().playbar.set_mute(window.imp().playbin.muted()); window.imp().playbar.connect_notify_local( Some("mute"), glib::clone!( #[weak(rename_to = playbin)] window.imp().playbin, move |playbar, _| playbin.set_muted(playbar.mute()) ), ); window.imp().playbin.file_started().connect_object( &*window.imp().playbar, |playbin, playbar, ()| { let entry = &playbin.entries()[playbin.current_entry().unwrap()]; playbar.set_duration(entry.duration() as f64); true }, );*/ // update position every 100 ms /* glib::source::timeout_add_local(std::time::Duration::from_millis(100), { let playbar = window.imp().playbar.downgrade(); let playbin = Rc::downgrade(&window.imp().playbin); move || { let playbar = match playbar.upgrade() { None => return glib::ControlFlow::Break, Some(playbar) => playbar, }; let playbin = match playbin.upgrade() { None => return glib::ControlFlow::Break, Some(playbin) => playbin, }; playbar.set_position(playbin.position().unwrap_or(0.0)); glib::ControlFlow::Continue } });*/ window .imp() .setup .connected() .connect_object(&window, |_setup, window, api| { window.imp().api.replace(Some(api)); //window.imp().playbin.stop(); window.set_can_click_shuffle_all(true); true }); window.imp().setup.load(); /* window .imp() .playbin .file_started() .connect_object(&window, |playbin, window, ()| { window .imp() .now_playing(&playbin.entries()[playbin.current_entry().unwrap()]); true });*/ 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); } }