diff --git a/src/main.rs b/src/main.rs index 866f47db..8e67564a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use gtk4::{gdk, glib, prelude::*}; mod deref_cell; mod mpris; +mod mpris_player; mod status_area; mod status_menu; mod time_button; diff --git a/src/mpris.rs b/src/mpris.rs index e086e136..a65d6eea 100644 --- a/src/mpris.rs +++ b/src/mpris.rs @@ -1,8 +1,7 @@ use cascade::cascade; use gtk4::{ - gdk, gdk_pixbuf, gio, + gio, glib::{self, clone}, - pango, prelude::*, subclass::prelude::*, }; @@ -10,19 +9,13 @@ use once_cell::unsync::OnceCell; use std::{cell::RefCell, collections::HashMap}; use crate::deref_cell::DerefCell; +use crate::mpris_player::MprisPlayer; #[derive(Default)] pub struct MprisControlsInner { box_: DerefCell, - backward_button: DerefCell, - play_pause_button: DerefCell, - forward_button: DerefCell, dbus: OnceCell, - players: RefCell>, - picture: DerefCell, - picture_uri: RefCell>, - title_label: DerefCell, - artist_label: DerefCell, + players: RefCell>, } #[glib::object_subclass] @@ -38,58 +31,9 @@ impl ObjectSubclass for MprisControlsInner { impl ObjectImpl for MprisControlsInner { fn constructed(&self, obj: &MprisControls) { - let picture = cascade! { - gtk4::Picture::new(); - ..set_halign(gtk4::Align::Center); - ..set_valign(gtk4::Align::Center); - ..set_can_shrink(true); - ..set_size_request(32, 32); - }; - - let title_label = cascade! { - gtk4::Label::new(None); - ..set_ellipsize(pango::EllipsizeMode::End); - ..set_max_width_chars(20); - ..set_attributes(Some(&cascade! { - pango::AttrList::new(); - ..insert(pango::Attribute::new_weight(pango::Weight::Bold)); - })); - }; - - let artist_label = cascade! { - gtk4::Label::new(None); - ..set_ellipsize(pango::EllipsizeMode::End); - ..set_max_width_chars(20); - }; - - let backward_button = cascade! { - gtk4::Button::from_icon_name(Some("media-skip-backward-symbolic")); - ..connect_clicked(clone!(@strong obj => move |_| obj.call("Previous"))); - }; - - let play_pause_button = cascade! { - gtk4::Button::from_icon_name(Some("media-playback-start-symbolic")); - ..connect_clicked(clone!(@strong obj => move |_| obj.call("PlayPause"))); - }; - - let forward_button = cascade! { - gtk4::Button::from_icon_name(Some("media-skip-forward-symbolic")); - ..connect_clicked(clone!(@strong obj => move |_| obj.call("Next"))); - }; - let box_ = cascade! { gtk4::Box::new(gtk4::Orientation::Vertical, 0); ..set_parent(obj); - ..append(&picture); - ..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); - }); }; glib::MainContext::default().spawn_local(clone!(@strong obj => async move { @@ -129,12 +73,6 @@ impl ObjectImpl for MprisControlsInner { })); 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.picture.set(picture); - self.title_label.set(title_label); - self.artist_label.set(artist_label); } fn dispose(&self, _obj: &MprisControls) { @@ -158,13 +96,8 @@ impl MprisControls { MprisControlsInner::from_instance(self) } - fn player(&self) -> Option { - // XXX correctly choose which player to show - self.inner().players.borrow().values().next().cloned() - } - async fn player_added(&self, name: &str) { - let player = match Player::new(&name).await { + let player = match MprisPlayer::new(&name).await { Ok(player) => player, Err(err) => { eprintln!("Failed to connect to '{}': {}", name, err); @@ -172,167 +105,17 @@ impl MprisControls { } }; + self.inner().box_.append(&player); // XXX + self.inner() .players .borrow_mut() .insert(name.to_owned(), player.clone()); - - player.connect_properties_changed(clone!(@weak self as self_ => move |_player| { - // TODO - self_.update(); - })); - self.update(); } fn player_removed(&self, name: &str) { self.inner().players.borrow_mut().remove(name); } - - fn call(&self, method: &'static str) { - glib::MainContext::default().spawn_local(clone!(@strong self as self_ => async move { - if let Some(player) = self_.player() { - if let Err(err) = player.call(method).await { - eprintln!("Failed to call '{}': {}", method, err); - } - } - })); - } - - async fn update_arturl(&self, arturl: Option<&str>) { - let mut picture_uri = self.inner().picture_uri.borrow_mut(); - if picture_uri.as_deref() == arturl { - return; - } - *picture_uri = arturl.map(String::from); - drop(picture_uri); - - let pixbuf = async { - let file = gio::File::for_uri(&arturl?); - let stream = file.read_async_future(glib::PRIORITY_DEFAULT).await.ok()?; - gdk_pixbuf::Pixbuf::from_stream_at_scale_async_future(&stream, 256, 256, false) - .await - .ok() - } - .await; - if let Some(pixbuf) = pixbuf { - let texture = gdk::Texture::for_pixbuf(&pixbuf); - self.inner().picture.set_paintable(Some(&texture)); - } - } - - fn update(&self) { - let player = match self.player() { - Some(player) => player, - None => return, - }; - - // XXX status - let (status, metadata) = match (player.playback_status(), player.metadata()) { - (Some(status), Some(metadata)) => (status, metadata), - _ => return, - }; - - let play_pause_icon = if status == "Playing" { - "media-playback-pause-symbolic" - } else { - "media-playback-start-symbolic" - }; - - 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() - .play_pause_button - .set_icon_name(play_pause_icon); - self.inner().title_label.set_label(&title); - self.inner().artist_label.set_label(&artist); - } -} - -struct Metadata(glib::VariantDict); - -impl Metadata { - fn lookup(&self, key: &str) -> Option { - self.0.lookup_value(key, None)?.get() - } - - 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") - } -} - -#[derive(Clone)] -struct Player(gio::DBusProxy); - -impl Player { - async fn new(name: &str) -> Result { - let proxy = gio::DBusProxy::for_bus_future( - gio::BusType::Session, - gio::DBusProxyFlags::NONE, - None, - name, - "/org/mpris/MediaPlayer2", - "org.mpris.MediaPlayer2.Player", - ) - .await?; - Ok(Self(proxy)) - } - - async fn call(&self, method: &str) -> Result<(), glib::Error> { - self.0 - .call_future(method, None, gio::DBusCallFlags::NONE, 1000) - .await?; - Ok(()) - } - - fn property(&self, prop: &str) -> Option { - self.0.cached_property(prop)?.get() - } - - fn connect_properties_changed(&self, f: F) -> glib::SignalHandlerId { - let proxy = &self.0; - self.0 - .connect_local( - "g-properties-changed", - false, - clone!(@weak proxy => @default-panic, move |_| { - f(Self(proxy)); - None - }), - ) - .unwrap() - } - - fn playback_status(&self) -> Option { - self.property("PlaybackStatus") - } - - fn metadata(&self) -> Option { - Some(Metadata(self.property("Metadata")?)) - } } struct DBus(gio::DBusProxy); diff --git a/src/mpris_player.rs b/src/mpris_player.rs new file mode 100644 index 00000000..3d85c56b --- /dev/null +++ b/src/mpris_player.rs @@ -0,0 +1,272 @@ +use cascade::cascade; +use gtk4::{ + gdk, gdk_pixbuf, gio, + glib::{self, clone}, + pango, + prelude::*, + subclass::prelude::*, +}; +use std::cell::RefCell; + +use crate::deref_cell::DerefCell; + +#[derive(Default)] +pub struct MprisPlayerInner { + box_: DerefCell, + backward_button: DerefCell, + play_pause_button: DerefCell, + forward_button: DerefCell, + player: DerefCell, + picture: DerefCell, + picture_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 picture = cascade! { + gtk4::Picture::new(); + ..set_halign(gtk4::Align::Center); + ..set_valign(gtk4::Align::Center); + ..set_can_shrink(true); + ..set_size_request(32, 32); + }; + + let title_label = cascade! { + gtk4::Label::new(None); + ..set_ellipsize(pango::EllipsizeMode::End); + ..set_max_width_chars(20); + ..set_attributes(Some(&cascade! { + pango::AttrList::new(); + ..insert(pango::Attribute::new_weight(pango::Weight::Bold)); + })); + }; + + let artist_label = cascade! { + gtk4::Label::new(None); + ..set_ellipsize(pango::EllipsizeMode::End); + ..set_max_width_chars(20); + }; + + let backward_button = cascade! { + gtk4::Button::from_icon_name(Some("media-skip-backward-symbolic")); + ..connect_clicked(clone!(@strong obj => move |_| obj.call("Previous"))); + }; + + let play_pause_button = cascade! { + gtk4::Button::from_icon_name(Some("media-playback-start-symbolic")); + ..connect_clicked(clone!(@strong obj => move |_| obj.call("PlayPause"))); + }; + + let forward_button = cascade! { + gtk4::Button::from_icon_name(Some("media-skip-forward-symbolic")); + ..connect_clicked(clone!(@strong obj => move |_| obj.call("Next"))); + }; + + let box_ = cascade! { + gtk4::Box::new(gtk4::Orientation::Vertical, 0); + ..set_parent(obj); + ..append(&picture); + ..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.picture.set(picture); + 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) -> Result { + let obj = glib::Object::new::(&[]).unwrap(); + + let player = Player::new(name).await?; + player.connect_properties_changed(clone!(@weak obj => move |_player| { + obj.update(); + })); + obj.inner().player.set(player); + obj.update(); + + 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 picture_uri = self.inner().picture_uri.borrow_mut(); + if picture_uri.as_deref() == arturl { + return; + } + *picture_uri = arturl.map(String::from); + drop(picture_uri); + + let pixbuf = async { + let file = gio::File::for_uri(&arturl?); + let stream = file.read_async_future(glib::PRIORITY_DEFAULT).await.ok()?; + gdk_pixbuf::Pixbuf::from_stream_at_scale_async_future(&stream, 256, 256, false) + .await + .ok() + } + .await; + if let Some(pixbuf) = pixbuf { + let texture = gdk::Texture::for_pixbuf(&pixbuf); + self.inner().picture.set_paintable(Some(&texture)); + } + } + + fn update(&self) { + let player = &self.inner().player; + + // XXX status + let (status, metadata) = match (player.playback_status(), player.metadata()) { + (Some(status), Some(metadata)) => (status, metadata), + _ => return, + }; + + let play_pause_icon = if status == "Playing" { + "media-playback-pause-symbolic" + } else { + "media-playback-start-symbolic" + }; + + 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() + .play_pause_button + .set_icon_name(play_pause_icon); + self.inner().title_label.set_label(&title); + self.inner().artist_label.set_label(&artist); + } +} + +struct Metadata(glib::VariantDict); + +impl Metadata { + fn lookup(&self, key: &str) -> Option { + self.0.lookup_value(key, None)?.get() + } + + 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") + } +} + +#[derive(Clone)] +struct Player(gio::DBusProxy); + +impl Player { + async fn new(name: &str) -> Result { + let proxy = gio::DBusProxy::for_bus_future( + gio::BusType::Session, + gio::DBusProxyFlags::NONE, + None, + name, + "/org/mpris/MediaPlayer2", + "org.mpris.MediaPlayer2.Player", + ) + .await?; + Ok(Self(proxy)) + } + + async fn call(&self, method: &str) -> Result<(), glib::Error> { + self.0 + .call_future(method, None, gio::DBusCallFlags::NONE, 1000) + .await?; + Ok(()) + } + + fn property(&self, prop: &str) -> Option { + self.0.cached_property(prop)?.get() + } + + fn connect_properties_changed(&self, f: F) -> glib::SignalHandlerId { + let proxy = &self.0; + self.0 + .connect_local( + "g-properties-changed", + false, + clone!(@weak proxy => @default-panic, move |_| { + f(Self(proxy)); + None + }), + ) + .unwrap() + } + + fn playback_status(&self) -> Option { + self.property("PlaybackStatus") + } + + fn metadata(&self) -> Option { + Some(Metadata(self.property("Metadata")?)) + } +}