mxl_player_components/ui/playlist/
widget.rs

1use crate::icon_names;
2use anyhow::Result;
3use gst_pbutils::DiscovererInfo;
4use log::*;
5use mxl_relm4_components::relm4::{
6    self, actions::*, adw::prelude::*, css as adw_css, factory::FactoryVecDeque, gtk::glib, prelude::*,
7};
8
9use glib::clone;
10
11use crate::localization::helper::fl;
12use crate::ui::playlist::{
13    messages::{
14        PlaylistChange, PlaylistCommandOutput, PlaylistComponentInput, PlaylistComponentOutput, PlaylistState,
15        RepeatMode, SortOrder,
16    },
17    model::{InsertMode, PlaylistComponentInit, PlaylistComponentModel},
18};
19
20use super::factory::{PlaylistEntryInput, PlaylistEntryOutput};
21
22relm4::new_action_group!(SortActionGroup, "sort_action_group");
23relm4::new_stateless_action!(SortByStartTime, SortActionGroup, "sort_by_start_time");
24relm4::new_stateless_action!(SortByShortUri, SortActionGroup, "sort_by_short_uri");
25
26#[relm4::component(pub)]
27impl Component for PlaylistComponentModel {
28    type Init = PlaylistComponentInit;
29    type Input = PlaylistComponentInput;
30    type Output = PlaylistComponentOutput;
31    type CommandOutput = PlaylistCommandOutput;
32
33    view! {
34        gtk::Box {
35            set_orientation: gtk::Orientation::Vertical,
36            set_width_request: 650,
37            set_css_classes: &[adw_css::BACKGROUND],
38
39            adw::HeaderBar {
40                set_css_classes: &[adw_css::FLAT],
41                set_show_end_title_buttons: false,
42                set_title_widget: Some(&gtk::Label::new(Some(&fl!("playlist")))),
43                pack_start = &gtk::Button {
44                    #[watch]
45                    set_visible: model.is_user_mutable,
46                    set_has_tooltip: true,
47                    set_tooltip_text: Some(&fl!("add-file")),
48                    set_icon_name: icon_names::PLUS,
49                    set_css_classes: &[adw_css::FLAT, "image-button"],
50                    set_valign: gtk::Align::Center,
51                    connect_clicked => PlaylistComponentInput::FileChooserRequest,
52                },
53                pack_end = &gtk::Button {
54                    #[watch]
55                    set_sensitive: model.is_user_mutable,
56                    set_has_tooltip: true,
57                    #[watch]
58                    set_tooltip_text: Some(match model.repeat {
59                            RepeatMode::Off => fl!("repeat", "none"),
60                            RepeatMode::All => fl!("repeat", "all"),
61                        }.as_ref()),
62                    #[watch]
63                    set_icon_name: match model.repeat {
64                            RepeatMode::Off => icon_names::ARROW_REPEAT_ALL_OFF_FILLED,
65                            RepeatMode::All => icon_names::ARROW_REPEAT_ALL_FILLED,
66                        },
67                    connect_clicked[sender] => move |_| {
68                        sender.input(PlaylistComponentInput::ToggleRepeat);
69                    }
70                },
71                 pack_end = &gtk::MenuButton {
72                    set_label: &fl!("sort-by"),
73                    #[watch]
74                    set_visible: model.is_user_mutable,
75
76
77                    set_menu_model: Some(&{
78                        let menu_model = gtk::gio::Menu::new();
79                        menu_model.append(
80                            Some(&fl!("sort-by", "start-time")),
81                            Some(&SortByStartTime::action_name()),
82                        );
83                        menu_model.append(
84                            Some(&fl!("sort-by", "file-name")),
85                            Some(&SortByShortUri::action_name()),
86                        );
87                        menu_model
88                    }),
89                }
90            },
91
92            #[name="drop_box"]
93            gtk::Box {
94                set_orientation: gtk::Orientation::Vertical,
95                set_vexpand: true,
96
97                gtk::ScrolledWindow {
98                    #[watch]
99                    set_visible: !model.show_placeholder,
100                    set_hscrollbar_policy: gtk::PolicyType::Never,
101                    set_vexpand: true,
102
103                    #[local_ref]
104                    file_list_box -> gtk::ListBox {
105                        add_css_class: adw_css::BOXED_LIST,
106                        set_activate_on_single_click: false,
107                        connect_row_activated[sender] => move |_, row| {
108                            sender.input(PlaylistComponentInput::Activate(row.index() as usize))
109                        }
110                    }
111                },
112                adw::StatusPage {
113                    #[watch]
114                    set_visible: model.show_placeholder,
115                    set_vexpand: true,
116                    set_icon_name: Some(icon_names::VIDEO_CLIP_MULTIPLE_REGULAR),
117                    set_title: &fl!("playlist-empty"),
118                    set_description: Some(&fl!("playlist-empty", "desc")),
119                }
120            }
121        }
122    }
123
124    fn shutdown(&mut self, _widgets: &mut Self::Widgets, _output: relm4::Sender<Self::Output>) {
125        if let Some(pool) = self.thread_pool.take() {
126            debug!("Shutting down thread pool...");
127            pool.shutdown_join();
128        }
129    }
130
131    // Initialize the component.
132    fn init(init: Self::Init, root: Self::Root, sender: ComponentSender<Self>) -> ComponentParts<Self> {
133        let mut group = RelmActionGroup::<SortActionGroup>::new();
134        group.add_action(RelmAction::<SortByStartTime>::new_stateless(clone!(
135            #[strong]
136            sender,
137            move |_| {
138                sender.input(PlaylistComponentInput::Sort(SortOrder::StartTime));
139            }
140        )));
141        group.add_action({
142            RelmAction::<SortByShortUri>::new_stateless(clone!(
143                #[strong]
144                sender,
145                move |_| {
146                    sender.input(PlaylistComponentInput::Sort(SortOrder::ShortUri));
147                }
148            ))
149        });
150        group.register_for_widget(&root);
151
152        let uris =
153            FactoryVecDeque::builder()
154                .launch(gtk::ListBox::default())
155                .forward(sender.input_sender(), |output| match output {
156                    PlaylistEntryOutput::RemoveItem(index) => Self::Input::Remove(index),
157                    PlaylistEntryOutput::Updated(index) => Self::Input::Updated(index),
158                    PlaylistEntryOutput::Move(from, to) => Self::Input::Move(from, to),
159                    PlaylistEntryOutput::AddBefore(index, files) => Self::Input::AddBefore(index, files),
160                    PlaylistEntryOutput::AddAfter(index, files) => Self::Input::AddAfter(index, files),
161                    PlaylistEntryOutput::FetchMetadata(uri, sender) => Self::Input::FetchMetadataForUri(uri, sender),
162                });
163
164        let mut model = PlaylistComponentModel {
165            uris,
166            index: None,
167            state: PlaylistState::Stopped,
168            show_file_index: init.show_file_index,
169            show_placeholder: init.uris.is_empty(),
170            repeat: init.repeat,
171            thread_pool: Some(PlaylistComponentModel::init_thread_pool()),
172            is_user_mutable: init.is_user_mutable,
173        };
174
175        // Add URIs to model:
176        model.add_uris(&sender, InsertMode::Back, &init.uris);
177
178        // Mark as playing:
179        if let Some(index) = init.mark_index_as_playing {
180            let mut guard = model.uris.guard();
181            if let Some(e) = guard.get_mut(index) {
182                model.index = Some(e.index.clone());
183            }
184            if let Some(i) = &model.index {
185                guard.send(i.current_index(), PlaylistEntryInput::Activate);
186            }
187        }
188
189        let file_list_box = model.uris.widget();
190        let widgets: PlaylistComponentModelWidgets = view_output!();
191        if model.is_user_mutable {
192            widgets
193                .drop_box
194                .add_controller(PlaylistComponentModel::new_drop_target(sender.input_sender().clone()));
195        }
196
197        ComponentParts { model, widgets }
198    }
199
200    fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>, _: &Self::Root) {
201        match msg {
202            PlaylistComponentInput::Start => {
203                debug!("Playlist start");
204                if let Some(entry) = self.uris.guard().get(0) {
205                    sender.input(PlaylistComponentInput::Switch(entry.index.clone()));
206                }
207            }
208            PlaylistComponentInput::Stop => {
209                self.state = PlaylistState::Stopping;
210                sender
211                    .output(PlaylistComponentOutput::StateChanged(PlaylistState::Stopping))
212                    .unwrap_or_default();
213            }
214            PlaylistComponentInput::PlayerStopped => match self.state {
215                PlaylistState::Stopping => {
216                    self.uris.broadcast(PlaylistEntryInput::Deactivate);
217                    self.index = None;
218                    sender
219                        .output(PlaylistComponentOutput::StateChanged(PlaylistState::Stopped))
220                        .unwrap_or_default();
221                }
222                PlaylistState::Playing => (),
223                PlaylistState::Stopped => (),
224            },
225            PlaylistComponentInput::PlayerPlaying => {
226                self.state = PlaylistState::Playing;
227            }
228            PlaylistComponentInput::Previous => {
229                self.previous(&sender);
230            }
231            PlaylistComponentInput::Next => {
232                self.next(&sender);
233            }
234            PlaylistComponentInput::Activate(index) => {
235                if let Some(entry) = self.uris.get(index) {
236                    sender.input(PlaylistComponentInput::Switch(entry.index.clone()))
237                }
238            }
239            PlaylistComponentInput::Switch(index) => {
240                self.uris.broadcast(PlaylistEntryInput::Deactivate);
241                self.uris.send(index.current_index(), PlaylistEntryInput::Activate);
242                self.index = Some(index.clone());
243                if let Some(entry) = self.uris.guard().get_mut(index.current_index()) {
244                    sender
245                        .output(PlaylistComponentOutput::SwitchUri(entry.uri.clone()))
246                        .unwrap_or_default();
247                }
248            }
249            PlaylistComponentInput::EndOfPlaylist(_index) => {
250                self.uris.broadcast(PlaylistEntryInput::Deactivate);
251                self.index = None;
252                sender
253                    .output(PlaylistComponentOutput::EndOfPlaylist)
254                    .unwrap_or_default();
255            }
256            PlaylistComponentInput::Add(files) => {
257                self.add_uris(
258                    &sender,
259                    InsertMode::Back,
260                    &files.into_iter().map(|x| x.into()).collect(),
261                );
262            }
263            PlaylistComponentInput::AddBefore(index, files) => {
264                self.add_uris(
265                    &sender,
266                    InsertMode::AtIndex(index),
267                    &files.into_iter().map(|x| x.into()).collect(),
268                );
269            }
270            PlaylistComponentInput::AddAfter(index, files) => {
271                let edit = self.uris.guard();
272                if let Some(index) = index.current_index().checked_add(1) {
273                    if let Some(index) = edit.get(index) {
274                        let index = index.index.clone();
275                        drop(edit);
276                        self.add_uris(
277                            &sender,
278                            InsertMode::AtIndex(index),
279                            &files.into_iter().map(|x| x.into()).collect(),
280                        );
281                    } else {
282                        drop(edit);
283                        self.add_uris(
284                            &sender,
285                            InsertMode::Back,
286                            &files.into_iter().map(|x| x.into()).collect(),
287                        );
288                    }
289                }
290            }
291            PlaylistComponentInput::Remove(index) => {
292                debug!("Remove item {index:?}");
293                if let Some(current_index) = self.index.clone()
294                    && index == current_index
295                {
296                    self.next(&sender);
297                }
298                self.uris.guard().remove(index.current_index());
299                sender
300                    .command_sender()
301                    .emit(PlaylistCommandOutput::ShowPlaceholder(self.uris.guard().is_empty()));
302                sender
303                    .output_sender()
304                    .emit(PlaylistComponentOutput::PlaylistChanged(PlaylistChange::Removed));
305            }
306            PlaylistComponentInput::Updated(index) => {
307                sender
308                    .output_sender()
309                    .emit(PlaylistComponentOutput::PlaylistChanged(PlaylistChange::Updated));
310                trace!("Updated item {}", index.current_index());
311            }
312            PlaylistComponentInput::Move(from, to) => {
313                let mut edit = self.uris.guard();
314                if let Some(to) = edit.get(to) {
315                    let from = from.current_index();
316                    let to = to.index.current_index();
317                    trace!("Move playlist entry from index {from} to {to}");
318                    edit.move_to(from, to);
319                    sender
320                        .output_sender()
321                        .emit(PlaylistComponentOutput::PlaylistChanged(PlaylistChange::Reordered));
322                }
323            }
324            PlaylistComponentInput::FetchMetadata => {
325                self.uris.broadcast(PlaylistEntryInput::FetchMetadata);
326            }
327            PlaylistComponentInput::FileChooserRequest => {
328                sender
329                    .output(PlaylistComponentOutput::FileChooserRequest)
330                    .unwrap_or_default();
331            }
332            PlaylistComponentInput::Sort(order) => {
333                debug!("Sort playlist by {order:?}");
334                self.sort_factory(&order);
335                sender
336                    .output_sender()
337                    .emit(PlaylistComponentOutput::PlaylistChanged(PlaylistChange::Reordered));
338            }
339            PlaylistComponentInput::ToggleRepeat => {
340                self.repeat = match self.repeat {
341                    RepeatMode::Off => RepeatMode::All,
342                    RepeatMode::All => RepeatMode::Off,
343                };
344                debug!("Change repeat to {:?}", self.repeat);
345            }
346            PlaylistComponentInput::FetchMetadataForUri(uri, sender) => {
347                if let Some(pool) = &self.thread_pool {
348                    pool.execute({
349                        move || {
350                            let result = get_media_info(&uri);
351                            match result {
352                                Ok(info) => sender.emit(PlaylistEntryInput::UpdateMetadata(info)),
353                                Err(err) => sender.emit(PlaylistEntryInput::UpdateMetadataError(err.to_string())),
354                            }
355                        }
356                    });
357                }
358            }
359        }
360    }
361
362    fn update_cmd(&mut self, msg: Self::CommandOutput, _sender: ComponentSender<Self>, _root: &Self::Root) {
363        match msg {
364            PlaylistCommandOutput::ShowPlaceholder(val) => {
365                self.show_placeholder = val;
366            }
367        }
368    }
369}
370
371fn get_media_info(uri: &str) -> Result<DiscovererInfo> {
372    let timeout: gst::ClockTime = gst::ClockTime::from_seconds(10);
373    let discoverer = gst_pbutils::Discoverer::new(timeout)?;
374    let info = discoverer.discover_uri(uri)?;
375
376    Ok(info)
377}