Krolyxon
Commit 0c8cfe3

rmptuisrcapp.rs

359 lines · 12.6 KB · Rust

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