azalea_shell/window/taskbar/widget/media/
mod.rs1use std::collections::HashMap;
2
3use crate::{
4 factory::{self, media::player::PlayerName},
5 service::{
6 self,
7 dbus::mpris::{
8 self,
9 proxy::{PlaybackRate, PlaybackStatus},
10 },
11 },
12};
13use azalea_service::{LocalListenerHandle, StaticServiceManager};
14use gtk::{
15 glib::object::Cast,
16 prelude::{BoxExt, ButtonExt, OrientableExt, PopoverExt, WidgetExt},
17};
18use relm4::{
19 Component, ComponentController, ComponentParts, ComponentSender, RelmWidgetExt, component,
20 prelude::FactoryVecDeque,
21};
22
23use crate::{component::image, icon};
24
25struct Player {
26 status: PlaybackStatus,
27 rate: PlaybackRate,
28 title: Option<String>,
29 artist: Option<String>,
30 length: Option<i64>,
31 art_url: Option<String>,
32}
33
34impl Default for Player {
35 fn default() -> Self {
36 Self {
37 status: Default::default(),
38 rate: 1.,
39 title: Default::default(),
40 artist: Default::default(),
41 length: Default::default(),
42 art_url: Default::default(),
43 }
44 }
45}
46
47crate::init! {
48 Model {
49 position: f64,
50 selected: Option<PlayerName>,
51 players: HashMap<PlayerName, Player>,
52 art_cover: relm4::Controller<image::Model>,
53 menu: FactoryVecDeque<factory::media::player::Model>,
54 _event_listener_handle: LocalListenerHandle,
55 }
56
57 Config {}
58}
59
60#[derive(Debug)]
61pub enum Action {
62 Previous,
63 Next,
64 PlayPause,
65}
66
67#[derive(Debug)]
68pub enum Input {
69 Select(PlayerName),
70 Event(mpris::Output),
71 Action(Action),
72}
73
74#[derive(Debug)]
75pub enum CommandOutput {
76 PositionDelta(f64),
77}
78
79#[component(pub)]
80impl Component for Model {
81 type Init = Init;
82 type Input = Input;
83 type Output = ();
84 type CommandOutput = CommandOutput;
85
86 view! {
87 gtk::Revealer {
88 #[watch]
89 set_reveal_child: !model.players.is_empty(),
90 set_transition_type: gtk::RevealerTransitionType::Crossfade,
91 set_transition_duration: 300,
92
93
94 gtk::Box{
95 set_spacing: 12,
96
97 gtk::MenuButton {
98 set_hexpand: false,
99 set_vexpand: false,
100 set_valign: gtk::Align::Center,
101
102 set_direction: gtk::ArrowType::Up,
103
104 #[wrap(Some)]
105 set_popover = >k::Popover {
106 set_position: gtk::PositionType::Right,
107
108 #[local_ref]
109 menu_widget -> gtk::Box {
110 set_orientation: gtk::Orientation::Vertical,
111 set_spacing: 5,
112 },
113 },
114 },
115
116 #[local_ref]
117 art_cover_widget -> gtk::Widget {},
118
119 gtk::Box {
120 set_orientation: gtk::Orientation::Vertical,
121 set_valign: gtk::Align::Center,
122 set_vexpand: true,
123 inline_css: "font-size: 11px;",
124
125 gtk::Label {
126 set_halign: gtk::Align::Start,
127
128 #[watch]
129 set_label: &model.title(),
130 },
131
132 gtk::Label {
133 set_halign: gtk::Align::Start,
134
135 #[watch]
136 set_label: &model.artist(),
137 },
138 },
139
140 gtk::Button {
141 set_vexpand: false,
142 set_valign: gtk::Align::Center,
143
144 set_icon_name: icon::PREVIOUS,
145 connect_clicked => Input::Action(Action::Previous)
146 },
147
148 gtk::Button {
149 set_vexpand: false,
150 set_valign: gtk::Align::Center,
151
152 #[watch]
153 set_icon_name: if model.is_playing() { icon::PAUSE } else { icon::PLAY },
154 connect_clicked => Input::Action(Action::PlayPause)
155 },
156
157 gtk::Button {
158 set_vexpand: false,
159 set_valign: gtk::Align::Center,
160
161 set_icon_name: icon::NEXT,
162 connect_clicked => Input::Action(Action::Next)
163 },
164
165 gtk::Label {
166 inline_css: "font-size: 13px;",
167
168 #[watch]
169 set_label:
170 &format!(
171 "{}/{}",
172 Self::format_time(model.position as i64),
173 model.length(),
174 )
175 },
176 },
177 }
178 }
179
180 fn init(
181 _init: Self::Init,
182 _root: Self::Root,
183 sender: ComponentSender<Self>,
184 ) -> ComponentParts<Self> {
185 let model = Model {
186 selected: None,
187 players: Default::default(),
188 position: 0.,
189
190 art_cover: image::Model::builder()
191 .launch(image::Init {
192 fallback: None,
193 width: None,
194 height: Some(30),
195 })
196 .detach(),
197
198 menu: FactoryVecDeque::builder()
199 .launch(gtk::Box::default())
200 .forward(sender.input_sender(), |output| match output {
201 factory::media::player::Output::Select(name) => Input::Select(name),
202 }),
203
204 _event_listener_handle: mpris::Service::forward_local(
205 sender.input_sender().clone(),
206 Input::Event,
207 ),
208 };
209
210 let cmd_sender = sender.command_sender().clone();
211 sender.oneshot_command(async move {
212 loop {
213 tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
214 drop(cmd_sender.send(CommandOutput::PositionDelta(1e6)));
215 }
216 });
217
218 let menu_widget = model.menu.widget();
219 let art_cover_widget: >k::Widget = model.art_cover.widget().upcast_ref();
220 let widgets = view_output!();
221
222 ComponentParts { model, widgets }
223 }
224
225 fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>, _root: &Self::Root) {
226 match message {
227 Input::Select(name) => {
228 self.selected = Some(name.clone());
229 mpris::Service::send(mpris::Input::UpdateMetadata(name));
230 }
231 Input::Event(output) => {
232 if !self.players.contains_key(&output.name) {
233 azalea_log::debug!(Self, "Player added with name {}", output.name);
234 self.menu.guard().push_back(output.name.clone());
236 self.players.insert(output.name.clone(), Default::default());
237 if self.selected.is_none() || output.name.to_lowercase().contains("music") {
239 self.selected = Some(output.name.clone());
240 self.reset();
241 }
242 }
243 let Some(player) = self.players.get_mut(&output.name) else {
244 return;
245 };
246 let is_selected = if let Some(selected) = &self.selected {
247 *selected == output.name
248 } else {
249 false
250 };
251 use service::dbus::mpris::Event;
252 match output.event {
253 Event::Volume(_) => {}
254 Event::Position(position) => {
255 if is_selected {
256 self.position = position as f64
257 }
258 }
259 Event::Metadata(metadata) => {
260 player.artist = metadata
261 .artist
262 .map(|v| v.first().unwrap_or(&format!("no artist")).to_owned());
263 player.title = metadata.title;
264 player.length = metadata.length;
265 player.art_url = metadata.art_url;
266 self.reset();
267 }
268 Event::PlaybackStatus(playback_status) => player.status = playback_status,
269 Event::PlaybackRate(playback_rate) => player.rate = playback_rate,
270 };
271 }
272 Input::Action(action) => {
273 let Some(name) = self.selected.clone() else {
274 return;
275 };
276 mpris::Service::send(mpris::Input::Action(match action {
277 Action::Previous => mpris::Action::Previous(name),
278 Action::Next => mpris::Action::Next(name),
279 Action::PlayPause => mpris::Action::PlayPause(name),
280 }));
281 }
282 }
283 }
284
285 fn update_cmd(
286 &mut self,
287 message: Self::CommandOutput,
288 _sender: ComponentSender<Self>,
289 _root: &Self::Root,
290 ) {
291 let Some(player) = self.player_mut() else {
292 return;
293 };
294 match message {
295 CommandOutput::PositionDelta(delta) => {
296 if let PlaybackStatus::Playing = player.status {
297 self.position += delta * player.rate
298 }
299 }
300 }
301 }
302}
303
304impl Model {
305 fn player(&self) -> Option<&Player> {
306 self.players.get(self.selected.as_ref()?)
307 }
308
309 fn player_mut(&mut self) -> Option<&mut Player> {
310 self.players.get_mut(self.selected.as_ref()?)
311 }
312
313 fn is_playing(&self) -> bool {
314 self.player()
315 .map(|p| matches!(p.status, PlaybackStatus::Playing))
316 .unwrap_or(false)
317 }
318
319 fn title(&self) -> String {
320 self.player()
321 .and_then(|p| p.title.to_owned())
322 .unwrap_or(format!("no title"))
323 }
324
325 fn artist(&self) -> String {
326 self.player()
327 .and_then(|p| p.artist.to_owned())
328 .unwrap_or(format!("no artist"))
329 }
330
331 fn length(&self) -> String {
332 self.player()
333 .and_then(|p| p.length.map(Self::format_time))
334 .unwrap_or(format!("00:00"))
335 }
336
337 fn reset(&mut self) {
338 drop(match self.player().and_then(|p| p.art_url.as_ref()) {
339 Some(url) => self
340 .art_cover
341 .sender()
342 .send(image::Input::LoadImage(url.to_string())),
343 None => self.art_cover.sender().send(image::Input::Unload),
344 });
345
346 if let Some(name) = &self.selected {
347 mpris::Service::send(mpris::Input::UpdatePositionAndRate(name.clone()));
348 }
349 }
350
351 fn format_time(us: i64) -> String {
352 let time = us / 1000000;
353 let hours = time / 3600;
354 let minutes = time / 60 - hours * 60;
355 let seconds = time - minutes * 60 - hours * 3600;
356
357 if hours == 0 {
358 format!("{:0>2}:{:0>2}", minutes, seconds)
359 } else {
360 format!("{:0>2}:{:0>2}:{:0>2}", hours, minutes, seconds)
361 }
362 }
363}