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