mxl_player_components/ui/playlist/
model.rs

1use gst_pbutils::DiscovererInfo;
2use log::*;
3use mxl_relm4_components::relm4::{
4    Sender, adw::prelude::*, factory::FactoryVecDeque, gtk::gdk::DragAction, prelude::*,
5};
6use std::path::PathBuf;
7
8use crate::ui::playlist::messages::{
9    PlaylistChange, PlaylistCommandOutput, PlaylistComponentInput, PlaylistComponentOutput, PlaylistState, RepeatMode,
10    SortOrder,
11};
12use crate::uri_helpers::uri_from_pathbuf;
13
14use super::factory::PlaylistEntryInit;
15pub use super::factory::PlaylistEntryModel;
16
17#[derive(Debug, Clone)]
18pub struct PlaylistEntry {
19    pub uri: PathBuf,
20    pub media_info: Option<DiscovererInfo>,
21}
22
23impl From<PathBuf> for PlaylistEntry {
24    fn from(uri: PathBuf) -> Self {
25        PlaylistEntry {
26            uri: uri.clone(),
27            media_info: None,
28        }
29    }
30}
31
32#[derive(Debug)]
33pub struct PlaylistComponentInit {
34    pub uris: Vec<PlaylistEntry>,
35    pub mark_index_as_playing: Option<usize>,
36    pub repeat: RepeatMode,
37    pub is_user_mutable: bool,
38    pub show_file_index: bool,
39}
40
41impl Default for PlaylistComponentInit {
42    fn default() -> Self {
43        Self {
44            uris: vec![],
45            mark_index_as_playing: None,
46            repeat: RepeatMode::default(),
47            is_user_mutable: true,
48            show_file_index: false,
49        }
50    }
51}
52
53pub struct PlaylistComponentModel {
54    pub uris: FactoryVecDeque<PlaylistEntryModel>,
55    pub index: Option<DynamicIndex>,
56    pub state: PlaylistState,
57    pub show_file_index: bool,
58    pub show_placeholder: bool,
59    pub repeat: RepeatMode,
60    pub thread_pool: Option<rusty_pool::ThreadPool>,
61    pub is_user_mutable: bool,
62}
63
64#[allow(dead_code)]
65pub(super) enum InsertMode {
66    Front,
67    AtIndex(DynamicIndex),
68    Back,
69}
70
71impl PlaylistComponentModel {
72    pub(super) fn init_thread_pool() -> rusty_pool::ThreadPool {
73        const DEFAULT_NUMBER_THREADS: usize = 3;
74        let thread_count = std::thread::available_parallelism()
75            .map_or(DEFAULT_NUMBER_THREADS, |v| v.get().min(DEFAULT_NUMBER_THREADS));
76        debug!("Create thread pool with {thread_count} number of threads");
77        rusty_pool::Builder::new()
78            .name("mxl_playlist_pool".to_owned())
79            .max_size(thread_count)
80            .build()
81    }
82
83    pub fn dynamic_index(&self) -> Option<&DynamicIndex> {
84        self.index.as_ref()
85    }
86
87    pub fn new_drop_target(sender: Sender<PlaylistComponentInput>) -> gtk::DropTarget {
88        let formats = gtk::gdk::ContentFormatsBuilder::new()
89            .add_type(gtk::gdk::FileList::static_type())
90            .add_type(gtk::gio::File::static_type())
91            .build();
92        let drop_target = gtk::DropTarget::builder()
93            .actions(DragAction::COPY)
94            .formats(&formats)
95            .build();
96
97        drop_target.set_types(&[gtk::gdk::FileList::static_type(), gtk::gio::File::static_type()]);
98
99        drop_target.connect_drop(move |_, value, _, _| {
100            if let Ok(files) = value.get::<gtk::gdk::FileList>() {
101                let files: Vec<_> = files.files().iter().filter_map(|file| file.path()).collect();
102
103                sender.emit(PlaylistComponentInput::Add(files));
104                return true;
105            } else if let Ok(file) = value.get::<gtk::gio::File>()
106                && let Some(file) = file.path()
107            {
108                sender.emit(PlaylistComponentInput::Add(vec![file]));
109                return true;
110            }
111            false
112        });
113
114        drop_target
115    }
116
117    pub(super) fn add_uris(
118        &mut self,
119        sender: &ComponentSender<Self>,
120        insert_mode: InsertMode,
121        uris: &Vec<PlaylistEntry>,
122    ) {
123        macro_rules! insert {
124            ($edit:expr, $insert_mode:expr, $entry:expr) => {
125                match &$insert_mode {
126                    InsertMode::Front => {
127                        $edit.push_front($entry);
128                    }
129                    InsertMode::AtIndex(index) => {
130                        $edit.insert(index.current_index(), $entry);
131                    }
132                    InsertMode::Back => {
133                        $edit.push_back($entry);
134                    }
135                }
136            };
137        }
138
139        let mut edit = self.uris.guard();
140        for entry in uris {
141            match uri_from_pathbuf(&entry.uri) {
142                Ok(file) => {
143                    let file_name = entry
144                        .uri
145                        .file_name()
146                        .map(|x| x.to_str().unwrap_or_default().to_string());
147
148                    insert!(
149                        edit,
150                        insert_mode,
151                        PlaylistEntryInit {
152                            uri: file,
153                            short_uri: file_name,
154                            media_info: entry.media_info.clone(),
155                            error: None,
156                            show_index: self.show_file_index,
157                            movable: self.is_user_mutable,
158                            removable: self.is_user_mutable,
159                            drop_files_to_add: self.is_user_mutable,
160                        }
161                    );
162                }
163                Err(error) => {
164                    let file_name = entry
165                        .uri
166                        .file_name()
167                        .map(|x| x.to_str().unwrap_or_default().to_string());
168                    let file = entry.uri.to_str().unwrap_or_default().to_string();
169
170                    insert!(
171                        edit,
172                        insert_mode,
173                        PlaylistEntryInit {
174                            uri: file,
175                            short_uri: file_name,
176                            media_info: entry.media_info.clone(),
177                            error: Some(error),
178                            show_index: self.show_file_index,
179                            movable: self.is_user_mutable,
180                            removable: self.is_user_mutable,
181                            drop_files_to_add: self.is_user_mutable,
182                        }
183                    );
184                }
185            }
186        }
187        sender
188            .command_sender()
189            .emit(PlaylistCommandOutput::ShowPlaceholder(edit.is_empty()));
190        sender
191            .output_sender()
192            .emit(PlaylistComponentOutput::PlaylistChanged(PlaylistChange::Added));
193        drop(edit);
194    }
195
196    pub(super) fn sort_factory(&mut self, order: &SortOrder) {
197        macro_rules! sort_factory {
198            ($guard:expr, $key:ident) => {{
199                let length = $guard.len();
200                for from_pos in 1..length {
201                    let mut j = from_pos;
202                    while j > 0 && $guard.get(j).unwrap().$key < $guard.get(j - 1).unwrap().$key {
203                        trace!(
204                            "Swap item {}[{:?}] with item {}[{:?}]",
205                            j,
206                            $guard.get(j).unwrap().$key,
207                            j - 1,
208                            $guard.get(j - 1).unwrap().$key
209                        );
210                        $guard.swap(j, j - 1);
211                        j -= 1;
212                    }
213                }
214            }};
215        }
216
217        let mut guard = self.uris.guard();
218        if !guard.is_empty() {
219            match order {
220                SortOrder::StartTime => {
221                    sort_factory!(guard, date_time);
222                }
223                SortOrder::ShortUri => {
224                    sort_factory!(guard, short_uri);
225                }
226            }
227        }
228    }
229
230    pub(super) fn previous(&mut self, sender: &ComponentSender<Self>) {
231        if let Some(index) = self.index.as_ref() {
232            if let Some(i) = index.current_index().checked_sub(1)
233                && let Some(entry) = self.uris.guard().get(i)
234            {
235                // Switch to previous file:
236                debug!("Playlist previous -> switch to index {i}");
237                sender.input(PlaylistComponentInput::Switch(entry.index.clone()));
238                return;
239            }
240            if let Some(entry) = self.uris.guard().get(index.current_index()) {
241                // Restart playback of current first file in playlist:
242                debug!("Playlist previous -> switch to index {}", index.current_index());
243                sender.input(PlaylistComponentInput::Switch(entry.index.clone()));
244            }
245        }
246    }
247
248    pub(super) fn next(&mut self, sender: &ComponentSender<Self>) {
249        if let Some(current_index) = self.index.as_ref() {
250            if let Some(i) = current_index.current_index().checked_add(1)
251                && let Some(entry) = self.uris.guard().get(i)
252            {
253                // Switch to next file:
254                debug!("Playlist next -> switch to index {i}");
255                sender.input(PlaylistComponentInput::Switch(entry.index.clone()));
256                return;
257            }
258            match self.repeat {
259                RepeatMode::Off => (),
260                RepeatMode::All => {
261                    if let Some(entry) = self.uris.guard().get(0) {
262                        debug!("Playlist repeat all -> switch to index 0");
263                        sender.input(PlaylistComponentInput::Switch(entry.index.clone()));
264                        return;
265                    }
266                }
267            }
268            sender.input(PlaylistComponentInput::EndOfPlaylist(current_index.clone()));
269        }
270    }
271}