Krolyxon
Commit 9922b1c

rmptuisrcapp.rs

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