mxl_player_components/ui/playlist/
widget.rs1use 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(>k::Label::new(Some(&fl!("playlist")))),
43 pack_start = >k::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 = >k::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 = >k::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 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 model.add_uris(&sender, InsertMode::Back, &init.uris);
177
178 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}