Krolyxon
Commit b9bbabd

rmptuisrcapp.rs

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