Krolyxon
Commit 87f4837

rmptuisrcapp.rs

389 lines · 14.0 KB · Rust

1use std::{path::Path, time::Duration};
2
3use crate::browser::FileBrowser;
4use crate::utils::FileExtension;
5use crate::connection::Connection;
6use crate::list::ContentList;
7use crate::ui::InputMode;
8use mpd::{Client, Song};
9
10// Application result type
11pub type AppResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
12
13/// Application
14#[derive(Debug)]
15pub struct App {
16 pub running: bool, // Check if app is running
17 pub conn: Connection, // Connection
18 pub browser: FileBrowser, // Directory browser
19 pub queue_list: ContentList<Song>, // Stores the current playing queue
20 pub pl_list: ContentList<String>, // Stores list of playlists
21 pub selected_tab: SelectedTab, // Used to switch between tabs
22
23 // Search
24 pub inputmode: InputMode, // Defines input mode, Normal or Search
25 pub search_input: String, // Stores the userinput to be searched
26 pub search_cursor_pos: usize, // Stores the cursor position for searching
27 pub pl_newname_input: String, // Stores the new name of the playlist
28 pub pl_cursor_pos: usize, // Stores the cursor position for renaming playlist
29
30 // playlist variables
31 // used to show playlist popup
32 pub playlist_popup: bool,
33 pub append_list: ContentList<String>,
34
35 // Determines if the database should be updated or not
36 pub should_update_song_list: bool,
37}
38
39#[derive(Debug, PartialEq, Clone)]
40pub enum SelectedTab {
41 DirectoryBrowser,
42 Queue,
43 Playlists,
44}
45
46impl App {
47 pub fn builder(addrs: &str) -> AppResult<Self> {
48 let mut conn = Connection::builder(addrs)?;
49 let mut queue_list = ContentList::new();
50 let mut pl_list = ContentList::new();
51
52 pl_list.list = Self::get_playlist(&mut conn.conn)?;
53 pl_list.list.sort();
54
55 let append_list = Self::get_append_list(&mut conn.conn)?;
56 Self::get_queue(&mut conn, &mut queue_list.list);
57
58 let browser = FileBrowser::new();
59
60 Ok(Self {
61 running: true,
62 conn,
63 queue_list,
64 pl_list,
65 selected_tab: SelectedTab::Queue,
66 browser,
67 inputmode: InputMode::Normal,
68 search_input: String::new(),
69 pl_newname_input: String::new(),
70 search_cursor_pos: 0,
71 pl_cursor_pos: 0,
72 playlist_popup: false,
73 append_list,
74 should_update_song_list: false,
75 })
76 }
77
78 pub fn tick(&mut self) -> AppResult<()> {
79 self.conn.update_status();
80 self.update_queue();
81
82 // Deals with database update
83 if self.should_update_song_list {
84 if let None = self.conn.status.updating_db {
85 // Update the songs list
86 self.conn.songs_filenames = self
87 .conn
88 .conn
89 .listall()?
90 .into_iter()
91 .map(|x| x.file)
92 .collect();
93
94 self.browser.update_directory(&mut self.conn)?;
95
96 self.should_update_song_list = false;
97 }
98 }
99
100 Ok(())
101 }
102
103 pub fn quit(&mut self) {
104 self.running = false;
105 }
106
107 pub fn get_queue(conn: &mut Connection, vec: &mut Vec<Song>) {
108 conn.conn.queue().unwrap().into_iter().for_each(|x| {
109 vec.push(x);
110 });
111 }
112
113 // Rescan the queue into queue_list
114 pub fn update_queue(&mut self) {
115 self.queue_list.list.clear();
116 Self::get_queue(&mut self.conn, &mut self.queue_list.list);
117 }
118
119 pub fn get_playlist(conn: &mut Client) -> AppResult<Vec<String>> {
120 let list: Vec<String> = conn.playlists()?.iter().map(|p| p.clone().name).collect();
121 Ok(list)
122 }
123
124 pub fn get_append_list(conn: &mut Client) -> AppResult<ContentList<String>> {
125 let mut list = ContentList::new();
126 list.list.push("Current Playlist".to_string());
127 for item in Self::get_playlist(conn)? {
128 list.list.push(item.to_string());
129 }
130
131 Ok(list)
132 }
133
134 /// Handles the <Space> event key
135 pub fn handle_add_or_remove_from_current_playlist(&mut self) -> AppResult<()> {
136 match self.selected_tab {
137 SelectedTab::DirectoryBrowser => {
138 let (content_type, content) =
139 self.browser.filetree.get(self.browser.selected).unwrap();
140 if content_type == "directory" {
141 let file = format!("{}/{}", self.browser.path, content);
142 let songs = self.conn.conn.listfiles(&file).unwrap_or_default();
143 for (t, f) in songs.iter() {
144 if t == "file" {
145 if Path::new(&f).has_extension(&[
146 "mp3", "ogg", "flac", "m4a", "wav", "aac", "opus", "ape", "wma",
147 "mpc", "aiff", "dff", "mp2", "mka",
148 ]) {
149 let path = file.clone() + "/" + f;
150 let full_path = path.strip_prefix("./").unwrap_or_else(|| "");
151 let song = self.conn.get_song_with_only_filename(&full_path);
152 self.conn.conn.push(&song)?;
153 }
154 }
155 }
156 } else if content_type == "file" {
157 let mut status = false;
158 for (i, song) in self.queue_list.list.clone().iter().enumerate() {
159 let song_path = song.file.split("/").last().unwrap_or_default();
160 if song_path.eq(content) {
161 self.conn.conn.delete(i as u32).unwrap();
162 status = true;
163 }
164 }
165
166 if !status {
167 let mut filename = format!("{}/{}", self.browser.path, content);
168
169 // Remove "./" from the beginning of filename
170 filename.remove(0);
171 filename.remove(0);
172
173 let song = self.conn.get_song_with_only_filename(&filename);
174 self.conn.conn.push(&song)?;
175
176 // updating queue, to avoid multiple pushes of the same songs if we enter multiple times before the queue gets updated
177 self.update_queue();
178 }
179 }
180
181 // Highlight next row if possible
182 if self.browser.selected != self.browser.filetree.len() - 1 {
183 self.browser.selected += 1;
184 }
185 }
186
187 SelectedTab::Queue => {
188 if self.queue_list.list.is_empty() {
189 return Ok(());
190 }
191 let file = self
192 .queue_list
193 .list
194 .get(self.queue_list.index)
195 .unwrap()
196 .file
197 .to_string();
198
199 for (i, song) in self.queue_list.list.clone().iter().enumerate() {
200 if song.file.eq(&file) {
201 self.conn.conn.delete(i as u32).unwrap();
202 if self.queue_list.index == self.queue_list.list.len() - 1
203 && self.queue_list.index != 0
204 {
205 self.queue_list.index -= 1;
206 }
207 }
208 }
209 }
210
211 _ => {}
212 }
213
214 self.update_queue();
215 self.conn.update_status();
216 Ok(())
217 }
218
219 /// Cycle through tabs
220 pub fn cycle_tabls(&mut self) {
221 self.selected_tab = match self.selected_tab {
222 SelectedTab::Queue => SelectedTab::DirectoryBrowser,
223 SelectedTab::DirectoryBrowser => SelectedTab::Playlists,
224 SelectedTab::Playlists => SelectedTab::DirectoryBrowser,
225 };
226 }
227
228 /// handles the Enter event on the directory browser
229 pub fn handle_enter(&mut self) -> AppResult<()> {
230 let browser = &mut self.browser;
231 let (t, path) = browser.filetree.get(browser.selected).unwrap();
232 if t == "directory" {
233 if path != "." {
234 browser.prev_path = browser.path.clone();
235 browser.path = browser.prev_path.clone() + "/" + path;
236 browser.update_directory(&mut self.conn)?;
237 browser.prev_selected = browser.selected;
238 browser.selected = 0;
239 }
240 } else {
241 let index = self.queue_list.list.iter().position(|x| {
242 let file = x.file.split("/").last().unwrap_or_default();
243 file.eq(path)
244 });
245
246 if index.is_some() {
247 self.conn.conn.switch(index.unwrap() as u32)?;
248 } else {
249 let mut filename = format!("{}/{}", browser.path, path);
250
251 // Remove "./" from the beginning of filename
252 filename.remove(0);
253 filename.remove(0);
254
255 let song = self.conn.get_song_with_only_filename(&filename);
256 self.conn.push(&song)?;
257
258 // updating queue, to avoid multiple pushes of the same songs if we enter multiple times before the queue gets updated
259 self.update_queue();
260 }
261 }
262 Ok(())
263 }
264
265 // Cursor movements
266 pub fn move_cursor_left(&mut self) {
267 match self.inputmode {
268 InputMode::PlaylistRename => {
269 let cursor_moved_left = self.pl_cursor_pos.saturating_sub(1);
270 self.pl_cursor_pos = self.clamp_cursor(cursor_moved_left);
271 }
272 InputMode::Editing => {
273 let cursor_moved_left = self.search_cursor_pos.saturating_sub(1);
274 self.search_cursor_pos = self.clamp_cursor(cursor_moved_left);
275 }
276 _ => {}
277 }
278 }
279
280 pub fn move_cursor_right(&mut self) {
281 match self.inputmode {
282 InputMode::PlaylistRename => {
283 let cursor_moved_right = self.pl_cursor_pos.saturating_add(1);
284 self.pl_cursor_pos = self.clamp_cursor(cursor_moved_right);
285 }
286 InputMode::Editing => {
287 let cursor_moved_right = self.search_cursor_pos.saturating_add(1);
288 self.search_cursor_pos = self.clamp_cursor(cursor_moved_right);
289 }
290 _ => {}
291 }
292 }
293
294 pub fn enter_char(&mut self, new_char: char) {
295 match self.inputmode {
296 InputMode::PlaylistRename => {
297 self.pl_newname_input.insert(self.pl_cursor_pos, new_char);
298 self.move_cursor_right();
299 }
300 InputMode::Editing => {
301 self.search_input.insert(self.search_cursor_pos, new_char);
302 self.move_cursor_right();
303 }
304 _ => {}
305 }
306 }
307
308 pub fn delete_char(&mut self) {
309 let is_not_cursor_leftmost = match self.inputmode {
310 InputMode::PlaylistRename => self.pl_cursor_pos != 0,
311 InputMode::Editing => self.search_cursor_pos != 0,
312 _ => false,
313 };
314
315 if is_not_cursor_leftmost {
316 // Method "remove" is not used on the saved text for deleting the selected char.
317 // Reason: Using remove on String works on bytes instead of the chars.
318 // Using remove would require special care because of char boundaries.
319
320 let current_index = match self.inputmode {
321 InputMode::Editing => self.search_cursor_pos,
322 InputMode::PlaylistRename => self.pl_cursor_pos,
323 _ => 0,
324 };
325
326 let from_left_to_current_index = current_index - 1;
327
328 if self.inputmode == InputMode::PlaylistRename {
329 // Getting all characters before the selected character.
330 let before_char_to_delete = self
331 .pl_newname_input
332 .chars()
333 .take(from_left_to_current_index);
334 // Getting all characters after selected character.
335 let after_char_to_delete = self.pl_newname_input.chars().skip(current_index);
336 // Put all characters together except the selected one.
337 // By leaving the selected one out, it is forgotten and therefore deleted.
338 self.pl_newname_input = before_char_to_delete.chain(after_char_to_delete).collect();
339 self.move_cursor_left();
340 } else if self.inputmode == InputMode::Editing {
341 let before_char_to_delete =
342 self.search_input.chars().take(from_left_to_current_index);
343 let after_char_to_delete = self.search_input.chars().skip(current_index);
344 self.search_input = before_char_to_delete.chain(after_char_to_delete).collect();
345 self.move_cursor_left();
346 }
347 }
348 }
349
350 pub fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
351 match self.inputmode {
352 InputMode::PlaylistRename => new_cursor_pos.clamp(0, self.pl_newname_input.len()),
353 InputMode::Editing => new_cursor_pos.clamp(0, self.search_input.len()),
354 _ => 0,
355 }
356 }
357
358 pub fn reset_cursor(&mut self) {
359 match self.inputmode {
360 InputMode::Editing => {
361 self.search_cursor_pos = 0;
362 }
363 InputMode::PlaylistRename => {
364 self.pl_cursor_pos = 0;
365 }
366 _ => {}
367 }
368 }
369
370 /// Given time in seconds, convert it to hh:mm:ss
371 pub fn format_time(time: Duration) -> String {
372 let time = time.as_secs();
373 let h = time / 3600;
374 let m = (time % 3600) / 60;
375 let s = (time % 3600) % 60;
376 if h == 0 {
377 format!("{:02}:{:02}", m, s)
378 } else {
379 format!("{:02}:{:02}:{:02}", h, m, s)
380 }
381 }
382
383 pub fn change_playlist_name(&mut self) -> AppResult<()> {
384 if self.selected_tab == SelectedTab::Playlists {
385 self.inputmode = InputMode::PlaylistRename;
386 }
387 Ok(())
388 }
389}