Krolyxon
Commit dd39c7e

rmptuisrcapp.rs

464 lines · 16.5 KB · Rust

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