azalea_shell/window/taskbar/widget/media/
mod.rs

1use 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 = &gtk::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: &gtk::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                    // TODO: Implement removal from menu
235                    self.menu.guard().push_back(output.name.clone());
236                    self.players.insert(output.name.clone(), Default::default());
237                    // TODO: custom default filters
238                    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}