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