use cascade::cascade; use futures::StreamExt; use gtk4::{ gdk_pixbuf, gio, glib::{self, clone}, pango, prelude::*, subclass::prelude::*, }; use std::{cell::RefCell, collections::HashMap}; use zbus::dbus_proxy; use zvariant::OwnedValue; use crate::deref_cell::DerefCell; #[derive(Default)] pub struct MprisPlayerInner { box_: DerefCell, backward_button: DerefCell, play_pause_button: DerefCell, forward_button: DerefCell, player: DerefCell>, image: DerefCell, image_uri: RefCell>, title_label: DerefCell, artist_label: DerefCell, } #[glib::object_subclass] impl ObjectSubclass for MprisPlayerInner { const NAME: &'static str = "S76MprisPlayer"; type ParentType = gtk4::Widget; type Type = MprisPlayer; fn class_init(klass: &mut Self::Class) { klass.set_layout_manager_type::(); } } impl ObjectImpl for MprisPlayerInner { fn constructed(&self, obj: &MprisPlayer) { let image = cascade! { gtk4::Image::new(); ..set_pixel_size(64); }; let title_label = cascade! { gtk4::Label::new(None); ..set_halign(gtk4::Align::Start); ..set_ellipsize(pango::EllipsizeMode::End); ..set_max_width_chars(20); ..set_attributes(Some(&cascade! { pango::AttrList::new(); ..insert(pango::AttrInt::new_weight(pango::Weight::Bold)); })); }; let artist_label = cascade! { gtk4::Label::new(None); ..set_halign(gtk4::Align::Start); ..set_ellipsize(pango::EllipsizeMode::End); ..set_max_width_chars(20); }; let backward_button = cascade! { gtk4::Button::from_icon_name("media-skip-backward-symbolic"); ..connect_clicked(clone!(@strong obj => move |_| obj.call("Previous"))); }; let play_pause_button = cascade! { gtk4::Button::from_icon_name("media-playback-start-symbolic"); ..connect_clicked(clone!(@strong obj => move |_| obj.call("PlayPause"))); }; let forward_button = cascade! { gtk4::Button::from_icon_name("media-skip-forward-symbolic"); ..connect_clicked(clone!(@strong obj => move |_| obj.call("Next"))); }; let box_ = cascade! { gtk4::Box::new(gtk4::Orientation::Horizontal, 6); ..set_parent(obj); ..append(&image); ..append(&cascade! { gtk4::Box::new(gtk4::Orientation::Vertical, 0); ..append(&title_label); ..append(&artist_label); ..append(&cascade! { gtk4::Box::new(gtk4::Orientation::Horizontal, 0); ..set_valign(gtk4::Align::Start); ..append(&backward_button); ..append(&play_pause_button); ..append(&forward_button); }); }); }; self.box_.set(box_); self.backward_button.set(backward_button); self.play_pause_button.set(play_pause_button); self.forward_button.set(forward_button); self.image.set(image); self.title_label.set(title_label); self.artist_label.set(artist_label); } fn dispose(&self, _obj: &MprisPlayer) { self.box_.unparent(); } } impl WidgetImpl for MprisPlayerInner {} glib::wrapper! { pub struct MprisPlayer(ObjectSubclass) @extends gtk4::Widget; } impl MprisPlayer { pub async fn new(name: &str) -> zbus::Result { let obj = glib::Object::new::(&[]).unwrap(); let connection = zbus::Connection::session().await?; let player = PlayerProxy::builder(&connection) .destination(name.to_string())? .build() .await?; let mut status_stream = player.receive_playback_status_changed().await; let mut metadata_stream = player.receive_metadata_changed().await; obj.inner().player.set(player); glib::MainContext::default().spawn_local(clone!(@strong obj => async move { while status_stream.next().await.is_some() { obj.update_status(); } })); glib::MainContext::default().spawn_local(clone!(@strong obj => async move { while metadata_stream.next().await.is_some() { obj.update_metadata(); } })); Ok(obj) } fn inner(&self) -> &MprisPlayerInner { MprisPlayerInner::from_instance(self) } fn call(&self, method: &'static str) { glib::MainContext::default().spawn_local(clone!(@strong self as self_ => async move { if let Err(err) = self_.inner().player.call::<_, _, ()>(method, &()).await { eprintln!("Failed to call '{}': {}", method, err); } })); } async fn update_arturl(&self, arturl: Option<&str>) { let mut image_uri = self.inner().image_uri.borrow_mut(); if image_uri.as_deref() == arturl { return; } *image_uri = arturl.map(String::from); drop(image_uri); let pixbuf = async { // TODO: Security? let file = gio::File::for_uri(&arturl?); let stream = file.read_future(glib::PRIORITY_DEFAULT).await.ok()?; gdk_pixbuf::Pixbuf::from_stream_future(&stream).await.ok() } .await; if let Some(pixbuf) = pixbuf { self.inner().image.set_from_pixbuf(Some(&pixbuf)); } } fn update_status(&self) { let status = match self.inner().player.cached_playback_status() { Ok(Some(status)) => status, _ => return, }; let play_pause_icon = if status == "Playing" { "media-playback-pause-symbolic" } else { "media-playback-start-symbolic" }; self.inner() .play_pause_button .set_icon_name(play_pause_icon); } fn update_metadata(&self) { let metadata = match self.inner().player.cached_metadata() { Ok(Some(metadata)) => metadata, _ => return, }; let title = metadata.title().unwrap_or_else(|| String::new()); // XXX correct way to handle multiple? let artist = metadata .artist() .and_then(|x| x.get(0).cloned()) .unwrap_or_default(); let _album = metadata.album(); // TODO let arturl = metadata.arturl(); glib::MainContext::default().spawn_local(clone!(@strong self as self_ => async move { self_.update_arturl(arturl.as_deref()).await; })); self.inner().title_label.set_label(&title); self.inner().artist_label.set_label(&artist); } } pub struct Metadata(HashMap); impl TryFrom for Metadata { type Error = zbus::Error; fn try_from(value: OwnedValue) -> zbus::Result { Ok(Self(value.try_into()?)) } } impl Metadata { fn lookup<'a, T: TryFrom>(&self, key: &str) -> Option { T::try_from(self.0.get(key)?.clone()).ok() } fn title(&self) -> Option { self.lookup("xesam:title") } fn album(&self) -> Option { self.lookup("xesam:album") } fn artist(&self) -> Option> { self.lookup("xesam:artist") } fn arturl(&self) -> Option { self.lookup("mpris:artUrl") } } #[dbus_proxy( interface = "org.mpris.MediaPlayer2.Player", default_path = "/org/mpris/MediaPlayer2" )] trait Player { #[dbus_proxy(property)] fn metadata(&self) -> zbus::Result; #[dbus_proxy(property)] fn playback_status(&self) -> zbus::Result; }