Krolyxon
Commit 971b336

rmptuisrcapp.rs

388 lines · 13.9 KB · Rust

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