Krolyxon
Commit a459eec

rmptuisrcapp.rs

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