From 9caec7663f731a596410884e934efab7b183889e Mon Sep 17 00:00:00 2001 From: Azraei Yusof Date: Sun, 2 Feb 2025 20:00:42 +0800 Subject: [PATCH 01/14] Changed media player module to direct DBus The media player panel now also lists all media players. Styling is still to be done. --- src/modules/media_player.rs | 301 ++++++++++++++++++------------------ src/modules/window_title.rs | 13 +- src/services/mod.rs | 1 + src/services/mpris/dbus.rs | 32 ++++ src/services/mpris/mod.rs | 293 +++++++++++++++++++++++++++++++++++ src/utils/mod.rs | 13 ++ 6 files changed, 488 insertions(+), 165 deletions(-) create mode 100644 src/services/mpris/dbus.rs create mode 100644 src/services/mpris/mod.rs diff --git a/src/modules/media_player.rs b/src/modules/media_player.rs index f6413e8..5d74342 100644 --- a/src/modules/media_player.rs +++ b/src/modules/media_player.rs @@ -1,4 +1,4 @@ -use std::{any::TypeId, ops::Not, process::Stdio, time::Duration}; +use std::ops::Deref; use super::{Module, OnModulePress}; use crate::{ @@ -6,89 +6,38 @@ use crate::{ components::icons::{icon, Icons}, config::MediaPlayerModuleConfig, menu::MenuType, + services::{ + mpris::{MprisPlayerCommand, MprisPlayerService, PlayerCommand}, + ReadOnlyService, Service, ServiceEvent, + }, style::SettingsButtonStyle, - utils::launcher::execute_command, + utils::truncate_text, }; use iced::{ - stream::channel, - widget::{button, column, row, slider, text}, - Alignment::Center, - Element, Subscription, Task, + widget::{button, column, container, row, slider, text}, + Alignment::{self, Center}, + Element, Subscription, Task, Theme, }; -use log::error; -use tokio::{process, time::sleep}; - -async fn get_current_song() -> Option { - let get_current_song_cmd = process::Command::new("bash") - .arg("-c") - .arg("playerctl metadata --format \"{{ artist }} - {{ title }}\"") - .stdout(Stdio::piped()) - .output() - .await; - - match get_current_song_cmd { - Ok(get_current_song_cmd) => { - if !get_current_song_cmd.status.success() { - return None; - } - let s = String::from_utf8_lossy(&get_current_song_cmd.stdout); - let trimmed = s.trim(); - trimmed.is_empty().not().then(|| trimmed.into()) - } - Err(e) => { - error!("Error: {:?}", e); - None - } - } -} - -async fn get_volume() -> Option { - let get_volume_cmd = process::Command::new("bash") - .arg("-c") - .arg("playerctl volume") - .stdout(Stdio::piped()) - .output() - .await; - - match get_volume_cmd { - Ok(get_volume_cmd) => { - if !get_volume_cmd.status.success() { - return None; - } - let v = String::from_utf8_lossy(&get_volume_cmd.stdout); - let trimmed = v.trim(); - if trimmed.is_empty() { - return None; - } - match trimmed.parse::() { - Ok(v) => Some(v * 100.0), - Err(e) => { - error!("Error: {:?}", e); - None - } - } - } - Err(e) => { - error!("Error: {:?}", e); - None - } - } -} #[derive(Default)] pub struct MediaPlayer { + data: Vec, + service: Option, +} + +struct PlayerData { + name: String, song: Option, volume: Option, } #[derive(Debug, Clone)] pub enum Message { - SetSong(Option), - Prev, - Play, - Next, - SetVolume(Option), - SyncVolume(Option), + Prev(String), + PlayPause(String), + Next(String), + SetVolume(String, f64), + Event(ServiceEvent), } impl MediaPlayer { @@ -98,81 +47,134 @@ impl MediaPlayer { config: &MediaPlayerModuleConfig, ) -> Task { match message { - Message::SetSong(song) => { - if let Some(song) = song { - let length = song.len(); - - self.song = Some(if length > config.max_title_length as usize { - let split = config.max_title_length as usize / 2; - let first_part = song.chars().take(split).collect::(); - let last_part = song.chars().skip(length - split).collect::(); - format!("{}...{}", first_part, last_part) - } else { - song - }); + Message::Prev(n) => { + if let Some(s) = self.service.as_mut() { + s.command(MprisPlayerCommand { + service: n, + command: PlayerCommand::Prev, + }) + .map(|event| crate::app::Message::MediaPlayer(Message::Event(event))) } else { - self.song = None; + Task::none() } - - Task::none() - } - Message::Prev => { - execute_command("playerctl previous".to_string()); - Task::perform(async move { get_current_song().await }, move |song| { - app::Message::MediaPlayer(Message::SetSong(song)) - }) } - Message::Play => { - execute_command("playerctl play-pause".to_string()); - Task::perform(async move { get_current_song().await }, move |song| { - app::Message::MediaPlayer(Message::SetSong(song)) - }) - } - Message::Next => { - execute_command("playerctl next".to_string()); - Task::perform(async move { get_current_song().await }, move |song| { - app::Message::MediaPlayer(Message::SetSong(song)) - }) + Message::PlayPause(n) => { + if let Some(s) = self.service.as_mut() { + s.command(MprisPlayerCommand { + service: n, + command: PlayerCommand::PlayPause, + }) + .map(|event| crate::app::Message::MediaPlayer(Message::Event(event))) + } else { + Task::none() + } } - Message::SetVolume(v) => { - if let Some(v) = v { - execute_command(format!("playerctl volume {}", v / 100.0)); + Message::Next(n) => { + if let Some(s) = self.service.as_mut() { + s.command(MprisPlayerCommand { + service: n, + command: PlayerCommand::Next, + }) + .map(|event| crate::app::Message::MediaPlayer(Message::Event(event))) + } else { + Task::none() } - self.volume = v; - Task::none() } - Message::SyncVolume(v) => { - self.volume = v; - Task::none() + Message::SetVolume(n, v) => { + if let Some(s) = self.service.as_mut() { + s.command(MprisPlayerCommand { + service: n, + command: PlayerCommand::Volume(v), + }) + .map(|event| crate::app::Message::MediaPlayer(Message::Event(event))) + } else { + Task::none() + } } + Message::Event(d) => match d { + ServiceEvent::Init(s) => { + let data = s.deref(); + self.data = data + .iter() + .map(|d| PlayerData { + name: d.service.clone(), + song: d.metadata.clone().and_then(|d| match (d.artists, d.title) { + (None, None) => None, + (None, Some(t)) => Some(truncate_text(&t, config.max_title_length)), + (Some(a), None) => { + Some(truncate_text(&a.join(", "), config.max_title_length)) + } + (Some(a), Some(t)) => Some(truncate_text( + &format!("{} - {}", a.join(", "), t), + config.max_title_length, + )), + }), + volume: d.volume, + }) + .collect(); + self.service = Some(s); + Task::none() + } + ServiceEvent::Update(d) => { + self.data = d + .iter() + .map(|d| PlayerData { + name: d.service.clone(), + song: d.metadata.clone().and_then(|d| match (d.artists, d.title) { + (None, None) => None, + (None, Some(t)) => Some(t), + (Some(a), None) => Some(a.join(", ")), + (Some(a), Some(t)) => Some(format!("{} - {}", a.join(", "), t)), + }), + volume: d.volume, + }) + .collect(); + Task::none() + } + ServiceEvent::Error(_) => Task::none(), + }, } } pub fn menu_view(&self) -> Element { - column![] - .push_maybe( - self.volume - .map(|v| slider(0.0..=100.0, v, |new_v| Message::SetVolume(Some(new_v)))), + column(self.data.iter().map(|d| { + container( + column![] + .push_maybe(d.song.clone().map(|s| text(s))) + .push_maybe(d.volume.map(|v| { + slider(0.0..=100.0, v, |new_v| { + Message::SetVolume(d.name.clone(), new_v) + }) + })) + .push( + row![ + button(icon(Icons::SkipPrevious)) + .on_press(Message::Prev(d.name.clone())) + .padding([5, 12]) + .style(SettingsButtonStyle.into_style()), + button(icon(Icons::PlayPause)) + .on_press(Message::PlayPause(d.name.clone())) + .style(SettingsButtonStyle.into_style()), + button(icon(Icons::SkipNext)) + .on_press(Message::Next(d.name.clone())) + .padding([5, 12]) + .style(SettingsButtonStyle.into_style()) + ] + .spacing(8), + ) + .spacing(8) + .align_x(Center), ) - .push( - row![ - button(icon(Icons::SkipPrevious)) - .on_press(Message::Prev) - .padding([5, 12]) - .style(SettingsButtonStyle.into_style()), - button(icon(Icons::PlayPause)) - .on_press(Message::Play) - .style(SettingsButtonStyle.into_style()), - button(icon(Icons::SkipNext)) - .on_press(Message::Next) - .padding([5, 12]) - .style(SettingsButtonStyle.into_style()) - ] - .spacing(8), - ) - .spacing(8) - .align_x(Center) + .padding(4) + .style(|theme: &Theme| container::Style { + background: Some(iced::Background::Color(theme.palette().primary)), + ..container::Style::default() + }) .into() + })) + .spacing(8) + .align_x(Alignment::Center) + .into() } } @@ -184,31 +186,22 @@ impl Module for MediaPlayer { &self, (): Self::ViewData<'_>, ) -> Option<(Element, Option)> { - self.song.clone().map(|s| { - ( - text(s).size(12).into(), - Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), - ) - }) + Some(( + text("media").into(), + Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), + )) + // self.data[0].song.clone().map(|s| { + // ( + // text(s).size(12).into(), + // Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), + // ) + // }) } fn subscription(&self, (): Self::SubscriptionData<'_>) -> Option> { - let id = TypeId::of::(); - Some( - Subscription::run_with_id( - id, - channel(10, |mut output| async move { - loop { - let song = get_current_song().await; - let _ = output.try_send(Message::SetSong(song)); - let volume = get_volume().await; - let _ = output.try_send(Message::SyncVolume(volume)); - sleep(Duration::from_secs(1)).await; - } - }), - ) - .map(app::Message::MediaPlayer), + MprisPlayerService::subscribe() + .map(|event| app::Message::MediaPlayer(Message::Event(event))), ) } } diff --git a/src/modules/window_title.rs b/src/modules/window_title.rs index aa71c24..d097988 100644 --- a/src/modules/window_title.rs +++ b/src/modules/window_title.rs @@ -1,4 +1,4 @@ -use crate::app; +use crate::{app, utils::truncate_text}; use hyprland::{data::Client, event_listener::AsyncEventListener, shared::HyprDataActiveOptional}; use iced::{stream::channel, widget::text, Element, Subscription}; use log::{debug, error}; @@ -31,16 +31,7 @@ impl WindowTitle { match message { Message::TitleChanged(value) => { if let Some(value) = value { - let length = value.len(); - - self.value = Some(if length > truncate_title_after_length as usize { - let split = truncate_title_after_length as usize / 2; - let first_part = value.chars().take(split).collect::(); - let last_part = value.chars().skip(length - split).collect::(); - format!("{}...{}", first_part, last_part) - } else { - value - }); + self.value = Some(truncate_text(&value, truncate_title_after_length)); } else { self.value = None; } diff --git a/src/services/mod.rs b/src/services/mod.rs index 40b3ba2..fed7532 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -4,6 +4,7 @@ pub mod audio; pub mod bluetooth; pub mod brightness; pub mod idle_inhibitor; +pub mod mpris; pub mod network; pub mod privacy; pub mod tray; diff --git a/src/services/mpris/dbus.rs b/src/services/mpris/dbus.rs new file mode 100644 index 0000000..7b9364a --- /dev/null +++ b/src/services/mpris/dbus.rs @@ -0,0 +1,32 @@ +use std::collections::HashMap; +use std::ops::Deref; +use zbus::{proxy, zvariant::OwnedValue, Result}; + +pub struct MprisPlayerDbus<'a>(MprisPlayerProxy<'a>); + +impl<'a> Deref for MprisPlayerDbus<'a> { + type Target = MprisPlayerProxy<'a>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[proxy( + interface = "org.mpris.MediaPlayer2.Player", + default_path = "/org/mpris/MediaPlayer2" +)] +pub trait MprisPlayer { + fn next(&self) -> Result<()>; + fn play_pause(&self) -> Result<()>; + fn previous(&self) -> Result<()>; + + #[zbus(property)] + fn metadata(&self) -> Result>; + #[zbus(property)] + fn set_volume(&self, v: f64) -> Result<()>; + #[zbus(property)] + fn volume(&self) -> Result; + #[zbus(property)] + fn can_control(&self) -> Result; +} diff --git a/src/services/mpris/mod.rs b/src/services/mpris/mod.rs new file mode 100644 index 0000000..4d9738d --- /dev/null +++ b/src/services/mpris/mod.rs @@ -0,0 +1,293 @@ +use super::{ReadOnlyService, Service, ServiceEvent}; +use dbus::MprisPlayerProxy; +use iced::{ + futures::{ + channel::mpsc::Sender, + future::join_all, + stream::{pending, SelectAll}, + SinkExt, Stream, StreamExt, + }, + stream::channel, + Subscription, +}; +use log::{error, info}; +use std::{any::TypeId, collections::HashMap, ops::Deref}; +use zbus::{fdo::DBusProxy, zvariant::OwnedValue}; + +mod dbus; + +#[derive(Debug, Clone)] +pub struct MprisPlayerData { + pub service: String, + pub metadata: Option, + pub volume: Option, + proxy: MprisPlayerProxy<'static>, +} + +#[derive(Debug, Clone)] +pub struct MprisPlayerMetadata { + pub artists: Option>, + pub title: Option, +} + +impl From> for MprisPlayerMetadata { + fn from(value: HashMap) -> Self { + let artists = match value.get("xesam:artist") { + Some(v) => match v.clone().try_into() { + Ok(v) => Some(v), + Err(_) => None, + }, + None => None, + }; + let title = match value.get("xesam:title") { + Some(v) => match v.clone().try_into() { + Ok(v) => Some(v), + Err(_) => None, + }, + None => None, + }; + Self { artists, title } + } +} + +#[derive(Debug, Clone)] +pub struct MprisPlayerService { + data: Vec, + conn: zbus::Connection, +} + +impl Deref for MprisPlayerService { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +enum State { + Init, + Active(zbus::Connection), + Error, +} + +impl ReadOnlyService for MprisPlayerService { + type UpdateEvent = Vec; + type Error = (); + + fn update(&mut self, event: Self::UpdateEvent) { + self.data = event; + } + + fn subscribe() -> Subscription> { + let id = TypeId::of::(); + + Subscription::run_with_id( + id, + channel(10, |mut output| async move { + let mut state = State::Init; + + loop { + state = MprisPlayerService::start_listening(state, &mut output).await; + } + }), + ) + } +} + +const MPRIS_PLAYER_SERVICE_PREFIX: &str = "org.mpris.MediaPlayer2."; + +impl MprisPlayerService { + async fn initialize_data(conn: &zbus::Connection) -> anyhow::Result> { + let dbus = DBusProxy::new(&conn).await?; + let names = dbus.list_names().await?; + Ok(join_all( + names + .iter() + .filter(|a| a.starts_with(MPRIS_PLAYER_SERVICE_PREFIX)) + .map(|s| async { + let service = s.to_string(); + match MprisPlayerProxy::new(conn, s.to_string()).await { + Ok(player) => { + let m = player + .metadata() + .await + .map_or(None, |m| Some(MprisPlayerMetadata::from(m))); + let v = player.volume().await.map(|v| v * 100.0).ok(); + Some(MprisPlayerData { + service, + metadata: m, + volume: v, + proxy: player, + }) + } + Err(_) => None, + } + }), + ) + .await + .iter() + .filter_map(|d| d.clone()) + .collect()) + } + + async fn events(conn: &zbus::Connection) -> anyhow::Result> { + let dbus = DBusProxy::new(conn).await?; + let services = join_all( + dbus.list_names() + .await? + .iter() + .map(|n| async move { MprisPlayerProxy::new(conn, n.clone()).await.unwrap() }), + ) + .await; + let mut combined = SelectAll::new(); + combined.push( + dbus.receive_name_owner_changed() + .await? + .filter_map(|s| { + iced::futures::future::ready(match s.args() { + Ok(a) => a.name.starts_with("org.mpris.MediaPlayer2.").then_some(()), + Err(_) => None, + }) + }) + .boxed(), + ); + for s in services.iter() { + combined.push(s.receive_metadata_changed().await.map(|_| ()).boxed()); + } + for s in services.iter() { + combined.push(s.receive_volume_changed().await.map(|_| ()).boxed()); + } + Ok(combined) + } + + async fn start_listening(state: State, output: &mut Sender>) -> State { + match state { + State::Init => match zbus::Connection::session().await { + Ok(conn) => { + let data = MprisPlayerService::initialize_data(&conn).await; + match data { + Ok(data) => { + info!("MPRIS player service initialized"); + + let _ = output + .send(ServiceEvent::Init(MprisPlayerService { + data, + conn: conn.clone(), + })) + .await; + + State::Active(conn) + } + Err(err) => { + error!("Failed to initialize MPRIS player service: {}", err); + + State::Error + } + } + } + Err(err) => { + error!("Failed to connect to system bus for MPRIS player: {}", err); + State::Error + } + }, + State::Active(conn) => match MprisPlayerService::events(&conn).await { + Ok(mut events) => { + while let Some(_) = events.next().await { + let data = MprisPlayerService::initialize_data(&conn).await; + match data { + Ok(data) => { + info!("MPRIS player service new data"); + + let _ = output.send(ServiceEvent::Update(data)).await; + } + Err(err) => { + error!("Failed to fetch MPRIS player data: {}", err); + } + } + } + + State::Active(conn) + } + Err(err) => { + error!("Failed to listen for MPRIS player events: {}", err); + + State::Error + } + }, + State::Error => { + let _ = pending::().next().await; + + State::Error + } + } + } +} + +#[derive(Debug)] +pub struct MprisPlayerCommand { + pub service: String, + pub command: PlayerCommand, +} + +#[derive(Debug)] +pub enum PlayerCommand { + Prev, + PlayPause, + Next, + Volume(f64), +} + +impl Service for MprisPlayerService { + type Command = MprisPlayerCommand; + + fn command(&mut self, command: Self::Command) -> iced::Task> { + { + let s = self.data.iter().find(|d| d.service == command.service); + if let Some(s) = s { + let mpris_player_proxy = s.proxy.clone(); + let mpris_player_datas = self.data.clone(); + let conn = self.conn.clone(); + iced::Task::perform( + async move { + match command.command { + PlayerCommand::Prev => { + let _ = mpris_player_proxy + .previous() + .await + .inspect_err(|e| error!("Previous command error: {}", e)); + } + PlayerCommand::PlayPause => { + let _ = mpris_player_proxy + .play_pause() + .await + .inspect_err(|e| error!("Play/pause command error: {}", e)); + } + PlayerCommand::Next => { + let _ = mpris_player_proxy + .next() + .await + .inspect_err(|e| error!("Next command error: {}", e)); + } + PlayerCommand::Volume(v) => { + let _ = mpris_player_proxy + .set_volume(v / 100.0) + .await + .inspect_err(|e| error!("Set volume command error: {}", e)); + } + } + match MprisPlayerService::initialize_data(&conn).await { + Ok(d) => d, + Err(e) => { + error!("initialize data failed: {}", e); + mpris_player_datas + } + } + }, + |data| ServiceEvent::Update(data), + ) + } else { + iced::Task::none() + } + } + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 66a3968..f92af32 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -18,3 +18,16 @@ pub fn format_duration(duration: &Duration) -> String { format!("{:>2}m", m) } } + +pub fn truncate_text(value: &str, max_length: u32) -> String { + let length = value.len(); + + if length > max_length as usize { + let split = max_length as usize / 2; + let first_part = value.chars().take(split).collect::(); + let last_part = value.chars().skip(length - split).collect::(); + format!("{}...{}", first_part, last_part) + } else { + value.to_string() + } +} From 4d66a9d9746280583f1df3d8ca87a7ed01f9af20 Mon Sep 17 00:00:00 2001 From: Azraei Yusof Date: Mon, 3 Feb 2025 00:27:31 +0800 Subject: [PATCH 02/14] Changed media player module styling --- src/app.rs | 2 +- src/components/icons.rs | 2 + src/modules/media_player.rs | 87 ++++++++++++++++++------------------- 3 files changed, 46 insertions(+), 45 deletions(-) diff --git a/src/app.rs b/src/app.rs index 2cd4231..5d8debf 100644 --- a/src/app.rs +++ b/src/app.rs @@ -273,7 +273,7 @@ impl App { Some((MenuType::MediaPlayer, button_ui_ref)) => menu_wrapper( id, self.media_player.menu_view().map(Message::MediaPlayer), - MenuSize::Normal, + MenuSize::Large, *button_ui_ref, self.config.position, ), diff --git a/src/components/icons.rs b/src/components/icons.rs index 3de38f8..0960b27 100644 --- a/src/components/icons.rs +++ b/src/components/icons.rs @@ -68,6 +68,7 @@ pub enum Icons { SkipPrevious, PlayPause, SkipNext, + MusicNote, } impl From for &'static str { @@ -135,6 +136,7 @@ impl From for &'static str { Icons::SkipPrevious => "󰒮", Icons::PlayPause => "󰐎", Icons::SkipNext => "󰒭", + Icons::MusicNote => "󰎇", } } } diff --git a/src/modules/media_player.rs b/src/modules/media_player.rs index 5d74342..0ea8a0b 100644 --- a/src/modules/media_player.rs +++ b/src/modules/media_player.rs @@ -16,7 +16,7 @@ use crate::{ use iced::{ widget::{button, column, container, row, slider, text}, Alignment::{self, Center}, - Element, Subscription, Task, Theme, + Element, Subscription, Task, }; #[derive(Default)] @@ -137,42 +137,47 @@ impl MediaPlayer { } pub fn menu_view(&self) -> Element { - column(self.data.iter().map(|d| { - container( - column![] - .push_maybe(d.song.clone().map(|s| text(s))) - .push_maybe(d.volume.map(|v| { - slider(0.0..=100.0, v, |new_v| { - Message::SetVolume(d.name.clone(), new_v) - }) - })) - .push( - row![ - button(icon(Icons::SkipPrevious)) - .on_press(Message::Prev(d.name.clone())) - .padding([5, 12]) - .style(SettingsButtonStyle.into_style()), - button(icon(Icons::PlayPause)) - .on_press(Message::PlayPause(d.name.clone())) - .style(SettingsButtonStyle.into_style()), - button(icon(Icons::SkipNext)) - .on_press(Message::Next(d.name.clone())) - .padding([5, 12]) - .style(SettingsButtonStyle.into_style()) - ] - .spacing(8), - ) - .spacing(8) - .align_x(Center), - ) - .padding(4) - .style(|theme: &Theme| container::Style { - background: Some(iced::Background::Color(theme.palette().primary)), - ..container::Style::default() - }) - .into() - })) - .spacing(8) + column( + self.data + .iter() + .flat_map(|d| { + [ + iced::widget::horizontal_rule(2).into(), + container( + column![] + .push_maybe(d.song.clone().map(|s| text(s))) + .push_maybe(d.volume.map(|v| { + slider(0.0..=100.0, v, |v| { + Message::SetVolume(d.name.clone(), v) + }) + })) + .push( + row![ + button(icon(Icons::SkipPrevious)) + .on_press(Message::Prev(d.name.clone())) + .padding([5, 12]) + .style(SettingsButtonStyle.into_style()), + button(icon(Icons::PlayPause)) + .on_press(Message::PlayPause(d.name.clone())) + .style(SettingsButtonStyle.into_style()), + button(icon(Icons::SkipNext)) + .on_press(Message::Next(d.name.clone())) + .padding([5, 12]) + .style(SettingsButtonStyle.into_style()) + ] + .spacing(8), + ) + .width(iced::Length::Fill) + .spacing(8) + .align_x(Center), + ) + .padding(16) + .into(), + ] + }) + .skip(1), + ) + .spacing(16) .align_x(Alignment::Center) .into() } @@ -187,15 +192,9 @@ impl Module for MediaPlayer { (): Self::ViewData<'_>, ) -> Option<(Element, Option)> { Some(( - text("media").into(), + icon(Icons::MusicNote).into(), Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), )) - // self.data[0].song.clone().map(|s| { - // ( - // text(s).size(12).into(), - // Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), - // ) - // }) } fn subscription(&self, (): Self::SubscriptionData<'_>) -> Option> { From 76cfb9e7b3364098189c9b924d805c7c72657e72 Mon Sep 17 00:00:00 2001 From: Azraei Yusof Date: Mon, 3 Feb 2025 01:10:59 +0800 Subject: [PATCH 03/14] Reduced DBus calls in MPRIS service It will only fetch MPRIS player data without refetching the list of MPRIS players if there was no DBus signal that the list of players changed. --- src/modules/media_player.rs | 4 ++ src/services/mpris/mod.rs | 130 +++++++++++++++++++++++------------- 2 files changed, 86 insertions(+), 48 deletions(-) diff --git a/src/modules/media_player.rs b/src/modules/media_player.rs index 0ea8a0b..5c08e07 100644 --- a/src/modules/media_player.rs +++ b/src/modules/media_player.rs @@ -129,6 +129,10 @@ impl MediaPlayer { volume: d.volume, }) .collect(); + + if let Some(service) = self.service.as_mut() { + service.update(d); + } Task::none() } ServiceEvent::Error(_) => Task::none(), diff --git a/src/services/mpris/mod.rs b/src/services/mpris/mod.rs index 4d9738d..6b868b3 100644 --- a/src/services/mpris/mod.rs +++ b/src/services/mpris/mod.rs @@ -66,7 +66,7 @@ impl Deref for MprisPlayerService { enum State { Init, - Active(zbus::Connection), + Active(zbus::Connection, Vec), Error, } @@ -96,41 +96,61 @@ impl ReadOnlyService for MprisPlayerService { const MPRIS_PLAYER_SERVICE_PREFIX: &str = "org.mpris.MediaPlayer2."; +enum Event { + NameOwner, + Metadata, + Volume, +} + impl MprisPlayerService { - async fn initialize_data(conn: &zbus::Connection) -> anyhow::Result> { + async fn initialize_data( + conn: &zbus::Connection, + ) -> anyhow::Result<(Vec, Vec)> { let dbus = DBusProxy::new(&conn).await?; - let names = dbus.list_names().await?; - Ok(join_all( - names - .iter() - .filter(|a| a.starts_with(MPRIS_PLAYER_SERVICE_PREFIX)) - .map(|s| async { - let service = s.to_string(); - match MprisPlayerProxy::new(conn, s.to_string()).await { - Ok(player) => { - let m = player - .metadata() - .await - .map_or(None, |m| Some(MprisPlayerMetadata::from(m))); - let v = player.volume().await.map(|v| v * 100.0).ok(); - Some(MprisPlayerData { - service, - metadata: m, - volume: v, - proxy: player, - }) - } - Err(_) => None, - } - }), - ) + let names: Vec = dbus + .list_names() + .await? + .iter() + .filter_map(|a| { + a.starts_with(MPRIS_PLAYER_SERVICE_PREFIX) + .then(|| a.to_string()) + }) + .collect(); + Ok(( + names.clone(), + MprisPlayerService::get_mpris_player_data(conn, &names).await, + )) + } + + async fn get_mpris_player_data( + conn: &zbus::Connection, + names: &[String], + ) -> Vec { + join_all(names.iter().map(|s| async { + match MprisPlayerProxy::new(conn, s.to_string()).await { + Ok(proxy) => { + let metadata = proxy + .metadata() + .await + .map_or(None, |m| Some(MprisPlayerMetadata::from(m))); + let volume = proxy.volume().await.map(|v| v * 100.0).ok(); + Some(MprisPlayerData { + service: s.to_string(), + metadata, + volume, + proxy, + }) + } + Err(_) => None, + } + })) .await .iter() .filter_map(|d| d.clone()) - .collect()) + .collect() } - async fn events(conn: &zbus::Connection) -> anyhow::Result> { + async fn events(conn: &zbus::Connection) -> anyhow::Result> { let dbus = DBusProxy::new(conn).await?; let services = join_all( dbus.list_names() @@ -145,17 +165,30 @@ impl MprisPlayerService { .await? .filter_map(|s| { iced::futures::future::ready(match s.args() { - Ok(a) => a.name.starts_with("org.mpris.MediaPlayer2.").then_some(()), + Ok(a) => a + .name + .starts_with(MPRIS_PLAYER_SERVICE_PREFIX) + .then_some(Event::NameOwner), Err(_) => None, }) }) .boxed(), ); for s in services.iter() { - combined.push(s.receive_metadata_changed().await.map(|_| ()).boxed()); + combined.push( + s.receive_metadata_changed() + .await + .map(|_| Event::Metadata) + .boxed(), + ); } for s in services.iter() { - combined.push(s.receive_volume_changed().await.map(|_| ()).boxed()); + combined.push( + s.receive_volume_changed() + .await + .map(|_| Event::Volume) + .boxed(), + ); } Ok(combined) } @@ -166,7 +199,7 @@ impl MprisPlayerService { Ok(conn) => { let data = MprisPlayerService::initialize_data(&conn).await; match data { - Ok(data) => { + Ok((names, data)) => { info!("MPRIS player service initialized"); let _ = output @@ -176,7 +209,7 @@ impl MprisPlayerService { })) .await; - State::Active(conn) + State::Active(conn, names) } Err(err) => { error!("Failed to initialize MPRIS player service: {}", err); @@ -190,15 +223,22 @@ impl MprisPlayerService { State::Error } }, - State::Active(conn) => match MprisPlayerService::events(&conn).await { + State::Active(conn, names) => match MprisPlayerService::events(&conn).await { Ok(mut events) => { - while let Some(_) = events.next().await { - let data = MprisPlayerService::initialize_data(&conn).await; + let mut names = names; + while let Some(e) = events.next().await { + let data = match e { + Event::NameOwner => MprisPlayerService::initialize_data(&conn).await, + _ => Ok(( + names.clone(), + MprisPlayerService::get_mpris_player_data(&conn, &names).await, + )), + }; match data { Ok(data) => { info!("MPRIS player service new data"); - - let _ = output.send(ServiceEvent::Update(data)).await; + names = data.0; + let _ = output.send(ServiceEvent::Update(data.1)).await; } Err(err) => { error!("Failed to fetch MPRIS player data: {}", err); @@ -206,7 +246,7 @@ impl MprisPlayerService { } } - State::Active(conn) + State::Active(conn, names) } Err(err) => { error!("Failed to listen for MPRIS player events: {}", err); @@ -242,10 +282,10 @@ impl Service for MprisPlayerService { fn command(&mut self, command: Self::Command) -> iced::Task> { { + let names: Vec = self.data.iter().map(|d| d.service.clone()).collect(); let s = self.data.iter().find(|d| d.service == command.service); if let Some(s) = s { let mpris_player_proxy = s.proxy.clone(); - let mpris_player_datas = self.data.clone(); let conn = self.conn.clone(); iced::Task::perform( async move { @@ -275,13 +315,7 @@ impl Service for MprisPlayerService { .inspect_err(|e| error!("Set volume command error: {}", e)); } } - match MprisPlayerService::initialize_data(&conn).await { - Ok(d) => d, - Err(e) => { - error!("initialize data failed: {}", e); - mpris_player_datas - } - } + MprisPlayerService::get_mpris_player_data(&conn, &names).await }, |data| ServiceEvent::Update(data), ) From a616539e11af45bdb29ccc98b63bc5b6efdb9c4b Mon Sep 17 00:00:00 2001 From: Azraei Yusof Date: Mon, 3 Feb 2025 02:38:12 +0800 Subject: [PATCH 04/14] Refactored Media Player and MPRIS integration Media Player module will now show the title if there's only 1 player, mimicking the previous behaviour. It will show the note icon if there is more than 1 player. --- src/modules/media_player.rs | 174 +++++++++++++++--------------------- src/services/mpris/mod.rs | 89 ++++++++++-------- src/utils/mod.rs | 2 +- 3 files changed, 124 insertions(+), 141 deletions(-) diff --git a/src/modules/media_player.rs b/src/modules/media_player.rs index 5c08e07..4c6be20 100644 --- a/src/modules/media_player.rs +++ b/src/modules/media_player.rs @@ -7,7 +7,7 @@ use crate::{ config::MediaPlayerModuleConfig, menu::MenuType, services::{ - mpris::{MprisPlayerCommand, MprisPlayerService, PlayerCommand}, + mpris::{MprisPlayerCommand, MprisPlayerData, MprisPlayerService, PlayerCommand}, ReadOnlyService, Service, ServiceEvent, }, style::SettingsButtonStyle, @@ -15,7 +15,7 @@ use crate::{ }; use iced::{ widget::{button, column, container, row, slider, text}, - Alignment::{self, Center}, + Alignment::Center, Element, Subscription, Task, }; @@ -47,88 +47,18 @@ impl MediaPlayer { config: &MediaPlayerModuleConfig, ) -> Task { match message { - Message::Prev(n) => { - if let Some(s) = self.service.as_mut() { - s.command(MprisPlayerCommand { - service: n, - command: PlayerCommand::Prev, - }) - .map(|event| crate::app::Message::MediaPlayer(Message::Event(event))) - } else { - Task::none() - } - } - Message::PlayPause(n) => { - if let Some(s) = self.service.as_mut() { - s.command(MprisPlayerCommand { - service: n, - command: PlayerCommand::PlayPause, - }) - .map(|event| crate::app::Message::MediaPlayer(Message::Event(event))) - } else { - Task::none() - } - } - Message::Next(n) => { - if let Some(s) = self.service.as_mut() { - s.command(MprisPlayerCommand { - service: n, - command: PlayerCommand::Next, - }) - .map(|event| crate::app::Message::MediaPlayer(Message::Event(event))) - } else { - Task::none() - } - } - Message::SetVolume(n, v) => { - if let Some(s) = self.service.as_mut() { - s.command(MprisPlayerCommand { - service: n, - command: PlayerCommand::Volume(v), - }) - .map(|event| crate::app::Message::MediaPlayer(Message::Event(event))) - } else { - Task::none() - } - } - Message::Event(d) => match d { + Message::Prev(s) => self.handle_command(s, PlayerCommand::Prev), + Message::PlayPause(s) => self.handle_command(s, PlayerCommand::PlayPause), + Message::Next(s) => self.handle_command(s, PlayerCommand::Next), + Message::SetVolume(s, v) => self.handle_command(s, PlayerCommand::Volume(v)), + Message::Event(event) => match event { ServiceEvent::Init(s) => { - let data = s.deref(); - self.data = data - .iter() - .map(|d| PlayerData { - name: d.service.clone(), - song: d.metadata.clone().and_then(|d| match (d.artists, d.title) { - (None, None) => None, - (None, Some(t)) => Some(truncate_text(&t, config.max_title_length)), - (Some(a), None) => { - Some(truncate_text(&a.join(", "), config.max_title_length)) - } - (Some(a), Some(t)) => Some(truncate_text( - &format!("{} - {}", a.join(", "), t), - config.max_title_length, - )), - }), - volume: d.volume, - }) - .collect(); + self.data = Self::map_service_to_module_data(s.deref(), config); self.service = Some(s); Task::none() } ServiceEvent::Update(d) => { - self.data = d - .iter() - .map(|d| PlayerData { - name: d.service.clone(), - song: d.metadata.clone().and_then(|d| match (d.artists, d.title) { - (None, None) => None, - (None, Some(t)) => Some(t), - (Some(a), None) => Some(a.join(", ")), - (Some(a), Some(t)) => Some(format!("{} - {}", a.join(", "), t)), - }), - volume: d.volume, - }) - .collect(); + self.data = Self::map_service_to_module_data(&d, config); if let Some(service) = self.service.as_mut() { service.update(d); @@ -145,34 +75,34 @@ impl MediaPlayer { self.data .iter() .flat_map(|d| { + let buttons = row![ + button(icon(Icons::SkipPrevious)) + .on_press(Message::Prev(d.name.clone())) + .padding([5, 12]) + .style(SettingsButtonStyle.into_style()), + button(icon(Icons::PlayPause)) + .on_press(Message::PlayPause(d.name.clone())) + .style(SettingsButtonStyle.into_style()), + button(icon(Icons::SkipNext)) + .on_press(Message::Next(d.name.clone())) + .padding([5, 12]) + .style(SettingsButtonStyle.into_style()) + ] + .spacing(8); + [ iced::widget::horizontal_rule(2).into(), container( column![] - .push_maybe(d.song.clone().map(|s| text(s))) + .push_maybe(d.song.clone().map(text)) .push_maybe(d.volume.map(|v| { slider(0.0..=100.0, v, |v| { Message::SetVolume(d.name.clone(), v) }) })) - .push( - row![ - button(icon(Icons::SkipPrevious)) - .on_press(Message::Prev(d.name.clone())) - .padding([5, 12]) - .style(SettingsButtonStyle.into_style()), - button(icon(Icons::PlayPause)) - .on_press(Message::PlayPause(d.name.clone())) - .style(SettingsButtonStyle.into_style()), - button(icon(Icons::SkipNext)) - .on_press(Message::Next(d.name.clone())) - .padding([5, 12]) - .style(SettingsButtonStyle.into_style()) - ] - .spacing(8), - ) + .push(buttons) .width(iced::Length::Fill) - .spacing(8) + .spacing(12) .align_x(Center), ) .padding(16) @@ -182,9 +112,40 @@ impl MediaPlayer { .skip(1), ) .spacing(16) - .align_x(Alignment::Center) .into() } + + fn handle_command( + &mut self, + service_name: String, + command: PlayerCommand, + ) -> Task { + if let Some(s) = self.service.as_mut() { + s.command(MprisPlayerCommand { + service_name, + command, + }) + .map(|event| crate::app::Message::MediaPlayer(Message::Event(event))) + } else { + Task::none() + } + } + + fn map_service_to_module_data( + data: &[MprisPlayerData], + config: &MediaPlayerModuleConfig, + ) -> Vec { + data.iter() + .map(|d| PlayerData { + name: d.service.clone(), + song: d + .metadata + .clone() + .map(|d| truncate_text(&d.to_string(), config.max_title_length)), + volume: d.volume, + }) + .collect() + } } impl Module for MediaPlayer { @@ -195,10 +156,19 @@ impl Module for MediaPlayer { &self, (): Self::ViewData<'_>, ) -> Option<(Element, Option)> { - Some(( - icon(Icons::MusicNote).into(), - Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), - )) + match self.data.len() { + 0 => None, + 1 => self.data[0].song.clone().map(|s| { + ( + text(s).into(), + Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), + ) + }), + _ => Some(( + icon(Icons::MusicNote).into(), + Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), + )), + } } fn subscription(&self, (): Self::SubscriptionData<'_>) -> Option> { diff --git a/src/services/mpris/mod.rs b/src/services/mpris/mod.rs index 6b868b3..0e8c3e2 100644 --- a/src/services/mpris/mod.rs +++ b/src/services/mpris/mod.rs @@ -11,7 +11,7 @@ use iced::{ Subscription, }; use log::{error, info}; -use std::{any::TypeId, collections::HashMap, ops::Deref}; +use std::{any::TypeId, collections::HashMap, fmt::Display, ops::Deref}; use zbus::{fdo::DBusProxy, zvariant::OwnedValue}; mod dbus; @@ -30,6 +30,18 @@ pub struct MprisPlayerMetadata { pub title: Option, } +impl Display for MprisPlayerMetadata { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let t = match (self.artists.clone(), self.title.clone()) { + (None, None) => String::new(), + (None, Some(t)) => t, + (Some(a), None) => a.join(", "), + (Some(a), Some(t)) => format!("{} - {}", a.join(", "), t), + }; + write!(f, "{}", t) + } +} + impl From> for MprisPlayerMetadata { fn from(value: HashMap) -> Self { let artists = match value.get("xesam:artist") { @@ -87,7 +99,7 @@ impl ReadOnlyService for MprisPlayerService { let mut state = State::Init; loop { - state = MprisPlayerService::start_listening(state, &mut output).await; + state = Self::start_listening(state, &mut output).await; } }), ) @@ -106,19 +118,17 @@ impl MprisPlayerService { async fn initialize_data( conn: &zbus::Connection, ) -> anyhow::Result<(Vec, Vec)> { - let dbus = DBusProxy::new(&conn).await?; + let dbus = DBusProxy::new(conn).await?; let names: Vec = dbus .list_names() .await? .iter() - .filter_map(|a| { - a.starts_with(MPRIS_PLAYER_SERVICE_PREFIX) - .then(|| a.to_string()) - }) + .filter(|&a| a.starts_with(MPRIS_PLAYER_SERVICE_PREFIX)) + .map(|a| a.to_string()) .collect(); Ok(( names.clone(), - MprisPlayerService::get_mpris_player_data(conn, &names).await, + Self::get_mpris_player_data(conn, &names).await, )) } @@ -152,13 +162,6 @@ impl MprisPlayerService { async fn events(conn: &zbus::Connection) -> anyhow::Result> { let dbus = DBusProxy::new(conn).await?; - let services = join_all( - dbus.list_names() - .await? - .iter() - .map(|n| async move { MprisPlayerProxy::new(conn, n.clone()).await.unwrap() }), - ) - .await; let mut combined = SelectAll::new(); combined.push( dbus.receive_name_owner_changed() @@ -174,6 +177,17 @@ impl MprisPlayerService { }) .boxed(), ); + + let services: Vec> = + join_all(dbus.list_names().await?.iter().map(|n| async move { + MprisPlayerProxy::new(conn, n.clone()) + .await + .inspect_err(|e| error!("Failed to connect MPRIS player proxy: {e}")) + })) + .await + .iter() + .filter_map(|r| r.clone().ok()) + .collect(); for s in services.iter() { combined.push( s.receive_metadata_changed() @@ -197,7 +211,7 @@ impl MprisPlayerService { match state { State::Init => match zbus::Connection::session().await { Ok(conn) => { - let data = MprisPlayerService::initialize_data(&conn).await; + let data = Self::initialize_data(&conn).await; match data { Ok((names, data)) => { info!("MPRIS player service initialized"); @@ -223,27 +237,26 @@ impl MprisPlayerService { State::Error } }, - State::Active(conn, names) => match MprisPlayerService::events(&conn).await { + State::Active(conn, names) => match Self::events(&conn).await { Ok(mut events) => { let mut names = names; - while let Some(e) = events.next().await { - let data = match e { - Event::NameOwner => MprisPlayerService::initialize_data(&conn).await, - _ => Ok(( - names.clone(), - MprisPlayerService::get_mpris_player_data(&conn, &names).await, - )), - }; - match data { - Ok(data) => { - info!("MPRIS player service new data"); - names = data.0; - let _ = output.send(ServiceEvent::Update(data.1)).await; + while let Some(event) = events.next().await { + match event { + Event::NameOwner => match Self::initialize_data(&conn).await { + Ok(data) => { + info!("MPRIS player service new data"); + names = data.0; + let _ = output.send(ServiceEvent::Update(data.1)).await; + } + Err(err) => { + error!("Failed to fetch MPRIS player data: {}", err); + } + }, + _ => { + let data = Self::get_mpris_player_data(&conn, &names).await; + let _ = output.send(ServiceEvent::Update(data)).await; } - Err(err) => { - error!("Failed to fetch MPRIS player data: {}", err); - } - } + }; } State::Active(conn, names) @@ -265,7 +278,7 @@ impl MprisPlayerService { #[derive(Debug)] pub struct MprisPlayerCommand { - pub service: String, + pub service_name: String, pub command: PlayerCommand, } @@ -283,7 +296,7 @@ impl Service for MprisPlayerService { fn command(&mut self, command: Self::Command) -> iced::Task> { { let names: Vec = self.data.iter().map(|d| d.service.clone()).collect(); - let s = self.data.iter().find(|d| d.service == command.service); + let s = self.data.iter().find(|d| d.service == command.service_name); if let Some(s) = s { let mpris_player_proxy = s.proxy.clone(); let conn = self.conn.clone(); @@ -315,9 +328,9 @@ impl Service for MprisPlayerService { .inspect_err(|e| error!("Set volume command error: {}", e)); } } - MprisPlayerService::get_mpris_player_data(&conn, &names).await + Self::get_mpris_player_data(&conn, &names).await }, - |data| ServiceEvent::Update(data), + ServiceEvent::Update, ) } else { iced::Task::none() diff --git a/src/utils/mod.rs b/src/utils/mod.rs index f92af32..89b0bb1 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -26,7 +26,7 @@ pub fn truncate_text(value: &str, max_length: u32) -> String { let split = max_length as usize / 2; let first_part = value.chars().take(split).collect::(); let last_part = value.chars().skip(length - split).collect::(); - format!("{}...{}", first_part, last_part) + format!("{first_part}...{last_part}") } else { value.to_string() } From 97623343f2741d164fa502ac5b63fc50b386f472 Mon Sep 17 00:00:00 2001 From: Azraei Yusof Date: Wed, 5 Feb 2025 21:22:43 +0800 Subject: [PATCH 05/14] Changed Mpris service Simplified a filter + map into a filter_map --- src/services/mpris/mod.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/services/mpris/mod.rs b/src/services/mpris/mod.rs index 0e8c3e2..3d5aa23 100644 --- a/src/services/mpris/mod.rs +++ b/src/services/mpris/mod.rs @@ -123,8 +123,13 @@ impl MprisPlayerService { .list_names() .await? .iter() - .filter(|&a| a.starts_with(MPRIS_PLAYER_SERVICE_PREFIX)) - .map(|a| a.to_string()) + .filter_map(|a| { + if a.starts_with(MPRIS_PLAYER_SERVICE_PREFIX) { + Some(a.to_string()) + } else { + None + } + }) .collect(); Ok(( names.clone(), From 772a0781b524c351a6e86dedbb6d965e04d4a59e Mon Sep 17 00:00:00 2001 From: Azraei Yusof Date: Wed, 5 Feb 2025 21:24:17 +0800 Subject: [PATCH 06/14] Changed Mpris service Simplified a future::ready with async move --- src/services/mpris/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/mpris/mod.rs b/src/services/mpris/mod.rs index 3d5aa23..4e8d417 100644 --- a/src/services/mpris/mod.rs +++ b/src/services/mpris/mod.rs @@ -171,14 +171,14 @@ impl MprisPlayerService { combined.push( dbus.receive_name_owner_changed() .await? - .filter_map(|s| { - iced::futures::future::ready(match s.args() { + .filter_map(|s| async move { + match s.args() { Ok(a) => a .name .starts_with(MPRIS_PLAYER_SERVICE_PREFIX) .then_some(Event::NameOwner), Err(_) => None, - }) + } }) .boxed(), ); From f835c9c2323fc4eae772aeeac5362592f665fdbf Mon Sep 17 00:00:00 2001 From: Azraei Yusof Date: Wed, 5 Feb 2025 21:25:15 +0800 Subject: [PATCH 07/14] Changed a log from info to debug in MPRIS service --- src/services/mpris/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/mpris/mod.rs b/src/services/mpris/mod.rs index 4e8d417..c195c96 100644 --- a/src/services/mpris/mod.rs +++ b/src/services/mpris/mod.rs @@ -10,7 +10,7 @@ use iced::{ stream::channel, Subscription, }; -use log::{error, info}; +use log::{debug, error, info}; use std::{any::TypeId, collections::HashMap, fmt::Display, ops::Deref}; use zbus::{fdo::DBusProxy, zvariant::OwnedValue}; @@ -249,7 +249,7 @@ impl MprisPlayerService { match event { Event::NameOwner => match Self::initialize_data(&conn).await { Ok(data) => { - info!("MPRIS player service new data"); + debug!("MPRIS player service new data"); names = data.0; let _ = output.send(ServiceEvent::Update(data.1)).await; } From 0817ed8ee4e2906aa223a9ad8a89497821d0d0a3 Mon Sep 17 00:00:00 2001 From: Azraei Yusof Date: Wed, 5 Feb 2025 21:26:09 +0800 Subject: [PATCH 08/14] Changed to explicit match cases in MPRIS service --- src/services/mpris/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/mpris/mod.rs b/src/services/mpris/mod.rs index c195c96..7541537 100644 --- a/src/services/mpris/mod.rs +++ b/src/services/mpris/mod.rs @@ -257,7 +257,7 @@ impl MprisPlayerService { error!("Failed to fetch MPRIS player data: {}", err); } }, - _ => { + Event::Metadata | Event::Volume => { let data = Self::get_mpris_player_data(&conn, &names).await; let _ = output.send(ServiceEvent::Update(data)).await; } From 0db915f929844f9a981d883415467a230df1423e Mon Sep 17 00:00:00 2001 From: Azraei Yusof Date: Wed, 5 Feb 2025 22:07:26 +0800 Subject: [PATCH 09/14] Changed MediaPlayer module and MPRIS service MediaPlayer module no longer keeps its own state of the connected players. It instead relies on the data in the MPRIS service itself. --- src/app.rs | 6 +- src/modules/media_player.rs | 165 +++++++++++++++++------------------- src/modules/mod.rs | 2 +- 3 files changed, 82 insertions(+), 91 deletions(-) diff --git a/src/app.rs b/src/app.rs index 5d8debf..e29f1a8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -227,7 +227,7 @@ impl App { }, _ => Task::none(), }, - Message::MediaPlayer(msg) => self.media_player.update(msg, &self.config.media_player), + Message::MediaPlayer(msg) => self.media_player.update(msg), } } @@ -272,7 +272,9 @@ impl App { ), Some((MenuType::MediaPlayer, button_ui_ref)) => menu_wrapper( id, - self.media_player.menu_view().map(Message::MediaPlayer), + self.media_player + .menu_view(&self.config.media_player) + .map(Message::MediaPlayer), MenuSize::Large, *button_ui_ref, self.config.position, diff --git a/src/modules/media_player.rs b/src/modules/media_player.rs index 4c6be20..3d13cf3 100644 --- a/src/modules/media_player.rs +++ b/src/modules/media_player.rs @@ -7,7 +7,7 @@ use crate::{ config::MediaPlayerModuleConfig, menu::MenuType, services::{ - mpris::{MprisPlayerCommand, MprisPlayerData, MprisPlayerService, PlayerCommand}, + mpris::{MprisPlayerCommand, MprisPlayerService, PlayerCommand}, ReadOnlyService, Service, ServiceEvent, }, style::SettingsButtonStyle, @@ -21,16 +21,9 @@ use iced::{ #[derive(Default)] pub struct MediaPlayer { - data: Vec, service: Option, } -struct PlayerData { - name: String, - song: Option, - volume: Option, -} - #[derive(Debug, Clone)] pub enum Message { Prev(String), @@ -41,11 +34,7 @@ pub enum Message { } impl MediaPlayer { - pub fn update( - &mut self, - message: Message, - config: &MediaPlayerModuleConfig, - ) -> Task { + pub fn update(&mut self, message: Message) -> Task { match message { Message::Prev(s) => self.handle_command(s, PlayerCommand::Prev), Message::PlayPause(s) => self.handle_command(s, PlayerCommand::PlayPause), @@ -53,13 +42,10 @@ impl MediaPlayer { Message::SetVolume(s, v) => self.handle_command(s, PlayerCommand::Volume(v)), Message::Event(event) => match event { ServiceEvent::Init(s) => { - self.data = Self::map_service_to_module_data(s.deref(), config); self.service = Some(s); Task::none() } ServiceEvent::Update(d) => { - self.data = Self::map_service_to_module_data(&d, config); - if let Some(service) = self.service.as_mut() { service.update(d); } @@ -70,49 +56,58 @@ impl MediaPlayer { } } - pub fn menu_view(&self) -> Element { - column( - self.data - .iter() - .flat_map(|d| { - let buttons = row![ - button(icon(Icons::SkipPrevious)) - .on_press(Message::Prev(d.name.clone())) - .padding([5, 12]) - .style(SettingsButtonStyle.into_style()), - button(icon(Icons::PlayPause)) - .on_press(Message::PlayPause(d.name.clone())) - .style(SettingsButtonStyle.into_style()), - button(icon(Icons::SkipNext)) - .on_press(Message::Next(d.name.clone())) - .padding([5, 12]) - .style(SettingsButtonStyle.into_style()) - ] - .spacing(8); + pub fn menu_view(&self, config: &MediaPlayerModuleConfig) -> Element { + match &self.service { + Some(s) => column( + s.deref() + .iter() + .flat_map(|d| { + let d = d.clone(); + let buttons = row![ + button(icon(Icons::SkipPrevious)) + .on_press(Message::Prev(d.service.clone())) + .padding([5, 12]) + .style(SettingsButtonStyle.into_style()), + button(icon(Icons::PlayPause)) + .on_press(Message::PlayPause(d.service.clone())) + .style(SettingsButtonStyle.into_style()), + button(icon(Icons::SkipNext)) + .on_press(Message::Next(d.service.clone())) + .padding([5, 12]) + .style(SettingsButtonStyle.into_style()) + ] + .spacing(8); - [ - iced::widget::horizontal_rule(2).into(), - container( - column![] - .push_maybe(d.song.clone().map(text)) - .push_maybe(d.volume.map(|v| { - slider(0.0..=100.0, v, |v| { - Message::SetVolume(d.name.clone(), v) - }) - })) - .push(buttons) - .width(iced::Length::Fill) - .spacing(12) - .align_x(Center), - ) - .padding(16) - .into(), - ] - }) - .skip(1), - ) - .spacing(16) - .into() + [ + iced::widget::horizontal_rule(2).into(), + container( + column![] + .push(text(match d.metadata { + Some(m) => { + truncate_text(&m.to_string(), config.max_title_length) + } + None => "No Title".to_string(), + })) + .push_maybe(d.volume.map(|v| { + slider(0.0..=100.0, v, move |v| { + Message::SetVolume(d.service.clone(), v) + }) + })) + .push(buttons) + .width(iced::Length::Fill) + .spacing(12) + .align_x(Center), + ) + .padding(16) + .into(), + ] + }) + .skip(1), + ) + .spacing(16) + .into(), + None => text("Not connected to MPRIS service").into(), + } } fn handle_command( @@ -130,44 +125,38 @@ impl MediaPlayer { Task::none() } } - - fn map_service_to_module_data( - data: &[MprisPlayerData], - config: &MediaPlayerModuleConfig, - ) -> Vec { - data.iter() - .map(|d| PlayerData { - name: d.service.clone(), - song: d - .metadata - .clone() - .map(|d| truncate_text(&d.to_string(), config.max_title_length)), - volume: d.volume, - }) - .collect() - } } impl Module for MediaPlayer { - type ViewData<'a> = (); + type ViewData<'a> = &'a MediaPlayerModuleConfig; type SubscriptionData<'a> = (); fn view( &self, - (): Self::ViewData<'_>, + config: Self::ViewData<'_>, ) -> Option<(Element, Option)> { - match self.data.len() { - 0 => None, - 1 => self.data[0].song.clone().map(|s| { - ( - text(s).into(), - Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), - ) - }), - _ => Some(( - icon(Icons::MusicNote).into(), - Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), - )), + match &self.service { + Some(s) => { + let deref = s.deref(); + match deref.len() { + 0 => None, + 1 => match &deref[0].metadata { + Some(m) => Some(( + text(truncate_text(&m.to_string(), config.max_title_length)).into(), + Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), + )), + None => Some(( + icon(Icons::MusicNote).into(), + Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), + )), + }, + _ => Some(( + icon(Icons::MusicNote).into(), + Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), + )), + } + } + None => None, } } diff --git a/src/modules/mod.rs b/src/modules/mod.rs index d34f50e..26b5fdb 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -224,7 +224,7 @@ impl App { ModuleName::Clock => self.clock.view(&self.config.clock.format), ModuleName::Privacy => self.privacy.view(()), ModuleName::Settings => self.settings.view(()), - ModuleName::MediaPlayer => self.media_player.view(()), + ModuleName::MediaPlayer => self.media_player.view(&self.config.media_player), } } From e0a6fc1313a3b72858c3bd2d705a3d5f5b2bf9de Mon Sep 17 00:00:00 2001 From: Azraei Yusof Date: Wed, 5 Feb 2025 23:15:10 +0800 Subject: [PATCH 10/14] Refactored MediaPlayer module --- src/modules/media_player.rs | 71 ++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/src/modules/media_player.rs b/src/modules/media_player.rs index 3d13cf3..4e59833 100644 --- a/src/modules/media_player.rs +++ b/src/modules/media_player.rs @@ -7,7 +7,7 @@ use crate::{ config::MediaPlayerModuleConfig, menu::MenuType, services::{ - mpris::{MprisPlayerCommand, MprisPlayerService, PlayerCommand}, + mpris::{MprisPlayerCommand, MprisPlayerData, MprisPlayerService, PlayerCommand}, ReadOnlyService, Service, ServiceEvent, }, style::SettingsButtonStyle, @@ -58,11 +58,13 @@ impl MediaPlayer { pub fn menu_view(&self, config: &MediaPlayerModuleConfig) -> Element { match &self.service { + None => text("Not connected to MPRIS service").into(), Some(s) => column( s.deref() .iter() .flat_map(|d| { let d = d.clone(); + let title = text(Self::get_title(&d, config)); let buttons = row![ button(icon(Icons::SkipPrevious)) .on_press(Message::Prev(d.service.clone())) @@ -77,22 +79,17 @@ impl MediaPlayer { .style(SettingsButtonStyle.into_style()) ] .spacing(8); + let volume_slider = d.volume.map(|v| { + slider(0.0..=100.0, v, move |v| { + Message::SetVolume(d.service.clone(), v) + }) + }); [ iced::widget::horizontal_rule(2).into(), container( - column![] - .push(text(match d.metadata { - Some(m) => { - truncate_text(&m.to_string(), config.max_title_length) - } - None => "No Title".to_string(), - })) - .push_maybe(d.volume.map(|v| { - slider(0.0..=100.0, v, move |v| { - Message::SetVolume(d.service.clone(), v) - }) - })) + column![title] + .push_maybe(volume_slider) .push(buttons) .width(iced::Length::Fill) .spacing(12) @@ -106,7 +103,6 @@ impl MediaPlayer { ) .spacing(16) .into(), - None => text("Not connected to MPRIS service").into(), } } @@ -125,6 +121,13 @@ impl MediaPlayer { Task::none() } } + + fn get_title(d: &MprisPlayerData, config: &MediaPlayerModuleConfig) -> String { + match &d.metadata { + Some(m) => truncate_text(&m.to_string(), config.max_title_length), + None => "No Title".to_string(), + } + } } impl Module for MediaPlayer { @@ -135,29 +138,25 @@ impl Module for MediaPlayer { &self, config: Self::ViewData<'_>, ) -> Option<(Element, Option)> { - match &self.service { - Some(s) => { - let deref = s.deref(); - match deref.len() { - 0 => None, - 1 => match &deref[0].metadata { - Some(m) => Some(( - text(truncate_text(&m.to_string(), config.max_title_length)).into(), - Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), - )), - None => Some(( - icon(Icons::MusicNote).into(), - Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), - )), - }, - _ => Some(( - icon(Icons::MusicNote).into(), - Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), - )), - } + self.service.as_ref().and_then(|s| { + let data = s.deref(); + match data.len() { + 0 => None, + 1 => Some(( + row![ + icon(Icons::MusicNote), + text(Self::get_title(&data[0], config)) + ] + .spacing(8) + .into(), + Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), + )), + _ => Some(( + icon(Icons::MusicNote).into(), + Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), + )), } - None => None, - } + }) } fn subscription(&self, (): Self::SubscriptionData<'_>) -> Option> { From 6da1d1bae5656412a6f9095f337527c1ad939d63 Mon Sep 17 00:00:00 2001 From: Azraei Yusof Date: Wed, 5 Feb 2025 23:34:36 +0800 Subject: [PATCH 11/14] MediaPlayer now always shows first players title --- src/modules/media_player.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/modules/media_player.rs b/src/modules/media_player.rs index 4e59833..d95230e 100644 --- a/src/modules/media_player.rs +++ b/src/modules/media_player.rs @@ -142,7 +142,7 @@ impl Module for MediaPlayer { let data = s.deref(); match data.len() { 0 => None, - 1 => Some(( + _ => Some(( row![ icon(Icons::MusicNote), text(Self::get_title(&data[0], config)) @@ -151,10 +151,6 @@ impl Module for MediaPlayer { .into(), Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), )), - _ => Some(( - icon(Icons::MusicNote).into(), - Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), - )), } }) } From 34a9919de3c7489b7455d3a1beda6f76c40fa173 Mon Sep 17 00:00:00 2001 From: Azraei Yusof Date: Mon, 10 Feb 2025 21:23:49 +0800 Subject: [PATCH 12/14] Removed unnecessary deref calls --- src/modules/media_player.rs | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/modules/media_player.rs b/src/modules/media_player.rs index d95230e..b1fea28 100644 --- a/src/modules/media_player.rs +++ b/src/modules/media_player.rs @@ -60,8 +60,7 @@ impl MediaPlayer { match &self.service { None => text("Not connected to MPRIS service").into(), Some(s) => column( - s.deref() - .iter() + s.iter() .flat_map(|d| { let d = d.clone(); let title = text(Self::get_title(&d, config)); @@ -138,20 +137,14 @@ impl Module for MediaPlayer { &self, config: Self::ViewData<'_>, ) -> Option<(Element, Option)> { - self.service.as_ref().and_then(|s| { - let data = s.deref(); - match data.len() { - 0 => None, - _ => Some(( - row![ - icon(Icons::MusicNote), - text(Self::get_title(&data[0], config)) - ] + self.service.as_ref().and_then(|s| match s.len() { + 0 => None, + _ => Some(( + row![icon(Icons::MusicNote), text(Self::get_title(&s[0], config))] .spacing(8) .into(), - Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), - )), - } + Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)), + )), }) } From b638448271bc01ac7d2da92ac1b877456acd1ae4 Mon Sep 17 00:00:00 2001 From: Azraei Yusof Date: Tue, 11 Feb 2025 21:13:49 +0800 Subject: [PATCH 13/14] Removed unused use declaration --- src/modules/media_player.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/media_player.rs b/src/modules/media_player.rs index b1fea28..4e34510 100644 --- a/src/modules/media_player.rs +++ b/src/modules/media_player.rs @@ -1,5 +1,3 @@ -use std::ops::Deref; - use super::{Module, OnModulePress}; use crate::{ app, From 09d5510bf60ef4e8c8f66cf4926c748f207ea2f9 Mon Sep 17 00:00:00 2001 From: Azraei Yusof Date: Tue, 11 Feb 2025 21:15:47 +0800 Subject: [PATCH 14/14] Updated README with media player documentation --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 201f562..f27330b 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,7 @@ position: Top # optional, default Top # - Tray # - Clock # - Privacy +# - MediaPlayer # - Settings # optional, the following is the default configuration modules: