Krolyxon
Commit 7f77734

rmptuisrcapp.rs

506 lines · 17.8 KB · Rust

1use std::time::Duration;
2
3use crate::{
4 browser::{BrowserEntry, FileBrowser},
5 config::Config,
6 connection::{Connection, MpdSnapshot, Song},
7 list::ContentList,
8 ui::InputMode,
9 utils::is_audio_file,
10 AppResult,
11};
12use ratatui::widgets::{ListState, TableState};
13
14#[derive(Debug)]
15pub struct App {
16 pub running: bool,
17 pub conn: Connection,
18 pub config: Config,
19 pub browser: FileBrowser,
20 pub queue_list: ContentList<Song>,
21 pub pl_list: ContentList<String>,
22 pub playlist_focus: PlaylistFocus,
23 pub playlist_song_index: usize,
24 pub selected_tab: SelectedTab,
25 pub inputmode: InputMode,
26 pub search_input: String,
27 pub search_cursor_pos: usize,
28 pub browser_search_results: Vec<usize>,
29 pub browser_search_index: usize,
30 pub pl_newname_input: String,
31 pub pl_cursor_pos: usize,
32 pub pl_new_pl_input: String,
33 pub pl_new_pl_cursor_pos: usize,
34 pub pl_new_pl_songs_buffer: Vec<Song>,
35 pub playlist_popup: bool,
36 pub append_list: ContentList<String>,
37 pub should_update_song_list: bool,
38 pub queue_state: TableState,
39 pub browser_state: TableState,
40 pub playlists_state: ListState,
41 pub playlist_songs_state: TableState,
42 pub playlist_contents: Vec<Song>,
43}
44
45#[derive(Debug, PartialEq, Clone, Copy)]
46pub enum SelectedTab {
47 DirectoryBrowser,
48 Queue,
49 Playlists,
50}
51
52#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
53pub enum PlaylistFocus {
54 #[default]
55 List,
56 Songs,
57}
58
59impl App {
60 pub async fn new(address: &str, password: Option<&str>, config: Config) -> AppResult<Self> {
61 let mut conn = Connection::connect(address, password).await?;
62 let mut queue_list = ContentList::new();
63 queue_list.list = conn.client.queue().await?;
64 let mut pl_list = ContentList::new();
65 pl_list.list = conn.client.playlists().await?;
66 pl_list.list.sort();
67 let append_list = Self::get_append_list(&mut conn).await?;
68 let mut browser = FileBrowser::new();
69 browser.update_directory(&mut conn).await?;
70
71 if let Some(position) = conn.current_song.place {
72 queue_list.index = position.pos as usize;
73 }
74
75 let mut app = Self {
76 running: true,
77 conn,
78 config,
79 browser,
80 queue_list,
81 pl_list,
82 playlist_focus: PlaylistFocus::List,
83 playlist_song_index: 0,
84 selected_tab: SelectedTab::Queue,
85 inputmode: InputMode::Normal,
86 search_input: String::new(),
87 search_cursor_pos: 0,
88 browser_search_results: Vec::new(),
89 browser_search_index: 0,
90 pl_newname_input: String::new(),
91 pl_cursor_pos: 0,
92 pl_new_pl_input: String::new(),
93 pl_new_pl_cursor_pos: 0,
94 pl_new_pl_songs_buffer: Vec::new(),
95 playlist_popup: false,
96 append_list,
97 should_update_song_list: false,
98 queue_state: TableState::default(),
99 browser_state: TableState::default(),
100 playlists_state: ListState::default(),
101 playlist_songs_state: TableState::default(),
102 playlist_contents: Vec::new(),
103 };
104 app.refresh_playlist_view().await?;
105 Ok(app)
106 }
107
108 pub async fn apply_snapshot(&mut self, snapshot: MpdSnapshot) -> AppResult<()> {
109 let MpdSnapshot {
110 status,
111 stats,
112 current_song,
113 queue,
114 } = snapshot;
115 self.conn.apply_snapshot(status, stats, current_song);
116 self.queue_list.list = queue;
117 if self.queue_list.list.is_empty() {
118 self.queue_list.index = 0;
119 } else {
120 self.queue_list.index = self.queue_list.index.min(self.queue_list.list.len() - 1);
121 }
122
123 if self.should_update_song_list && self.conn.status.updating_db.is_none() {
124 self.conn.refresh_song_list().await?;
125 self.browser.update_directory(&mut self.conn).await?;
126 self.should_update_song_list = false;
127 }
128 Ok(())
129 }
130
131 pub fn quit(&mut self) {
132 self.running = false;
133 }
134
135 pub async fn update_queue(&mut self) -> AppResult<()> {
136 self.queue_list.list = self.conn.client.queue().await?;
137 if self.queue_list.list.is_empty() {
138 self.queue_list.index = 0;
139 } else {
140 self.queue_list.index = self.queue_list.index.min(self.queue_list.list.len() - 1);
141 }
142 Ok(())
143 }
144
145 pub async fn refresh_playlists(&mut self) -> AppResult<()> {
146 self.pl_list.list = self.conn.client.playlists().await?;
147 self.pl_list.list.sort();
148 self.pl_list.index = self
149 .pl_list
150 .index
151 .min(self.pl_list.list.len().saturating_sub(1));
152 self.append_list = Self::get_append_list(&mut self.conn).await?;
153 self.refresh_playlist_view().await
154 }
155
156 pub async fn refresh_playlist_view(&mut self) -> AppResult<()> {
157 self.playlist_contents = match self.pl_list.list.get(self.pl_list.index) {
158 Some(name) => self.conn.client.playlist(name).await?,
159 None => Vec::new(),
160 };
161 self.playlist_song_index = self
162 .playlist_song_index
163 .min(self.playlist_contents.len().saturating_sub(1));
164 Ok(())
165 }
166
167 pub async fn delete_selected_playlist_song(&mut self) -> AppResult<()> {
168 let Some(playlist) = self.pl_list.list.get(self.pl_list.index) else {
169 return Ok(());
170 };
171 if self.playlist_song_index >= self.playlist_contents.len() {
172 return Ok(());
173 }
174 self.conn
175 .client
176 .delete_playlist_song(playlist, self.playlist_song_index)
177 .await?;
178 self.refresh_playlist_view().await
179 }
180
181 pub async fn move_selected_playlist_song(&mut self, down: bool) -> AppResult<()> {
182 let Some(playlist) = self.pl_list.list.get(self.pl_list.index) else {
183 return Ok(());
184 };
185 let target = if down {
186 self.playlist_song_index.checked_add(1)
187 } else {
188 self.playlist_song_index.checked_sub(1)
189 };
190 let Some(target) = target.filter(|target| *target < self.playlist_contents.len()) else {
191 return Ok(());
192 };
193 self.conn
194 .client
195 .move_playlist_song(playlist, self.playlist_song_index, target)
196 .await?;
197 self.playlist_song_index = target;
198 self.refresh_playlist_view().await
199 }
200
201 pub async fn delete_selected_playlist(&mut self) -> AppResult<()> {
202 let Some(playlist) = self.pl_list.list.get(self.pl_list.index) else {
203 return Ok(());
204 };
205 self.conn.client.delete_playlist(playlist).await?;
206 self.refresh_playlists().await?;
207 Ok(())
208 }
209
210 pub async fn play_selected_playlist_song(&mut self) -> AppResult<()> {
211 let Some(playlist) = self.pl_list.list.get(self.pl_list.index) else {
212 return Ok(());
213 };
214 if self.playlist_song_index >= self.playlist_contents.len() {
215 return Ok(());
216 }
217 let queue_position = self.conn.client.queue().await?.len();
218 self.conn.client.append_playlist(playlist).await?;
219 self.conn
220 .client
221 .play(Some((queue_position + self.playlist_song_index) as u32))
222 .await?;
223 self.update_queue().await?;
224 self.conn.update_status().await
225 }
226
227 pub async fn get_append_list(conn: &mut Connection) -> AppResult<ContentList<String>> {
228 let mut list = ContentList::new();
229 list.list.push("Current Playlist".to_string());
230 list.list.push("New Playlist".to_string());
231 list.list.extend(conn.client.playlists().await?);
232 Ok(list)
233 }
234
235 pub async fn toggle_selected_item(&mut self) -> AppResult<()> {
236 match self.selected_tab {
237 SelectedTab::DirectoryBrowser => {
238 let Some(entry) = self.browser.entries.get(self.browser.selected).cloned() else {
239 return Ok(());
240 };
241 match entry {
242 BrowserEntry::Directory(name) => {
243 let path = join_library_path(&self.browser.path, &name);
244 for (entry_type, file) in self.conn.client.list_files(&path).await? {
245 if entry_type == "file" && is_audio_file(&file) {
246 self.conn.client.add(&file).await?;
247 }
248 }
249 }
250 BrowserEntry::Song(song) => {
251 let positions: Vec<u32> = self
252 .queue_list
253 .list
254 .iter()
255 .enumerate()
256 .filter_map(|(index, queued)| {
257 (queued.file == song.file).then_some(index as u32)
258 })
259 .collect();
260 if positions.is_empty() {
261 self.conn.client.add(&song.file).await?;
262 } else {
263 for position in positions.into_iter().rev() {
264 self.conn.client.delete(position).await?;
265 }
266 }
267 }
268 }
269 if self.browser.selected < self.browser.entries.len().saturating_sub(1) {
270 self.browser.selected += 1;
271 }
272 }
273 SelectedTab::Queue => {
274 if !self.queue_list.list.is_empty() {
275 self.conn
276 .client
277 .delete(self.queue_list.index as u32)
278 .await?;
279 }
280 }
281 SelectedTab::Playlists => {}
282 }
283 self.update_queue().await?;
284 self.conn.update_status().await?;
285 Ok(())
286 }
287
288 pub fn cycle_tabs(&mut self) {
289 self.selected_tab = match self.selected_tab {
290 SelectedTab::Queue => SelectedTab::DirectoryBrowser,
291 SelectedTab::DirectoryBrowser => SelectedTab::Playlists,
292 SelectedTab::Playlists => SelectedTab::Queue,
293 };
294 }
295
296 pub async fn handle_enter(&mut self) -> AppResult<()> {
297 let Some(entry) = self.browser.entries.get(self.browser.selected).cloned() else {
298 return Ok(());
299 };
300 match entry {
301 BrowserEntry::Directory(name) => {
302 self.browser.prev_selected = self.browser.selected;
303 self.browser.path = join_library_path(&self.browser.path, &name);
304 self.browser.selected = 0;
305 self.browser.update_directory(&mut self.conn).await?;
306 }
307 BrowserEntry::Song(song) => {
308 if let Some(index) = self
309 .queue_list
310 .list
311 .iter()
312 .position(|queued| queued.file == song.file)
313 {
314 self.conn.client.play(Some(index as u32)).await?;
315 } else {
316 self.conn.push(&song).await?;
317 self.update_queue().await?;
318 }
319 self.conn.update_status().await?;
320 }
321 }
322 Ok(())
323 }
324
325 pub async fn append_selected_to(&mut self) -> AppResult<()> {
326 let Some(playlist) = self.append_list.list.get(self.append_list.index).cloned() else {
327 return Ok(());
328 };
329 let songs = match self.selected_tab {
330 SelectedTab::Queue => self
331 .queue_list
332 .list
333 .get(self.queue_list.index)
334 .cloned()
335 .into_iter()
336 .collect(),
337 SelectedTab::DirectoryBrowser => {
338 let Some(entry) = self.browser.entries.get(self.browser.selected).cloned() else {
339 return Ok(());
340 };
341 match entry {
342 BrowserEntry::Song(song) => vec![song],
343 BrowserEntry::Directory(name) => {
344 let path = join_library_path(&self.browser.path, &name);
345 self.conn
346 .client
347 .list_files(&path)
348 .await?
349 .into_iter()
350 .filter_map(|(kind, file)| {
351 (kind == "file" && is_audio_file(&file))
352 .then(|| Connection::song_from_file(&file))
353 })
354 .collect()
355 }
356 }
357 }
358 SelectedTab::Playlists => {
359 let Some(name) = self.pl_list.list.get(self.pl_list.index) else {
360 return Ok(());
361 };
362 self.conn.client.playlist(name).await?
363 }
364 };
365
366 if playlist == "Current Playlist" {
367 if self.selected_tab == SelectedTab::Playlists {
368 if let Some(name) = self.pl_list.list.get(self.pl_list.index) {
369 self.conn.load_playlist(name).await?;
370 self.update_queue().await?;
371 }
372 } else {
373 for song in songs {
374 self.conn.client.add(&song.file).await?;
375 }
376 self.update_queue().await?;
377 }
378 } else if playlist == "New Playlist" {
379 self.pl_new_pl_songs_buffer = songs;
380 self.inputmode = InputMode::NewPlaylist;
381 } else {
382 for song in songs {
383 self.conn.add_to_playlist(&playlist, &song).await?;
384 }
385 self.refresh_playlists().await?;
386 }
387 self.playlist_popup = false;
388 self.append_list.index = 0;
389 Ok(())
390 }
391
392 pub fn move_cursor_left(&mut self) {
393 let cursor = self.cursor_position().saturating_sub(1);
394 self.set_cursor_position(cursor);
395 }
396
397 pub fn move_cursor_right(&mut self) {
398 let cursor = self.cursor_position().saturating_add(1);
399 self.set_cursor_position(cursor);
400 }
401
402 fn cursor_position(&self) -> usize {
403 match self.inputmode {
404 InputMode::PlaylistRename => self.pl_cursor_pos,
405 InputMode::Editing => self.search_cursor_pos,
406 InputMode::NewPlaylist => self.pl_new_pl_cursor_pos,
407 InputMode::Normal => 0,
408 }
409 }
410
411 fn set_cursor_position(&mut self, position: usize) {
412 let length = match self.inputmode {
413 InputMode::PlaylistRename => self.pl_newname_input.chars().count(),
414 InputMode::Editing => self.search_input.chars().count(),
415 InputMode::NewPlaylist => self.pl_new_pl_input.chars().count(),
416 InputMode::Normal => 0,
417 };
418 let position = position.min(length);
419 match self.inputmode {
420 InputMode::PlaylistRename => self.pl_cursor_pos = position,
421 InputMode::Editing => self.search_cursor_pos = position,
422 InputMode::NewPlaylist => self.pl_new_pl_cursor_pos = position,
423 InputMode::Normal => {}
424 }
425 }
426
427 pub fn enter_char(&mut self, character: char) {
428 let (text, cursor) = match self.inputmode {
429 InputMode::PlaylistRename => (&mut self.pl_newname_input, self.pl_cursor_pos),
430 InputMode::Editing => (&mut self.search_input, self.search_cursor_pos),
431 InputMode::NewPlaylist => (&mut self.pl_new_pl_input, self.pl_new_pl_cursor_pos),
432 InputMode::Normal => return,
433 };
434 let byte_index = text
435 .char_indices()
436 .nth(cursor)
437 .map(|(index, _)| index)
438 .unwrap_or(text.len());
439 text.insert(byte_index, character);
440 self.move_cursor_right();
441 }
442
443 pub fn delete_char(&mut self) {
444 let cursor = self.cursor_position();
445 if cursor == 0 {
446 return;
447 }
448 let text = match self.inputmode {
449 InputMode::PlaylistRename => &mut self.pl_newname_input,
450 InputMode::Editing => &mut self.search_input,
451 InputMode::NewPlaylist => &mut self.pl_new_pl_input,
452 InputMode::Normal => return,
453 };
454 let start = text.char_indices().nth(cursor - 1).map(|(i, _)| i);
455 let end = text
456 .char_indices()
457 .nth(cursor)
458 .map(|(i, _)| i)
459 .unwrap_or(text.len());
460 if let Some(start) = start {
461 text.replace_range(start..end, "");
462 self.move_cursor_left();
463 }
464 }
465
466 pub fn reset_cursor(&mut self) {
467 self.set_cursor_position(0);
468 }
469
470 pub fn format_time(time: Duration) -> String {
471 let seconds = time.as_secs();
472 let hours = seconds / 3600;
473 let minutes = (seconds % 3600) / 60;
474 let seconds = seconds % 60;
475 if hours == 0 {
476 format!("{minutes:02}:{seconds:02}")
477 } else {
478 format!("{hours:02}:{minutes:02}:{seconds:02}")
479 }
480 }
481
482 pub fn handle_scroll_up(&mut self) {
483 match self.selected_tab {
484 SelectedTab::Queue => self.queue_list.prev(),
485 SelectedTab::DirectoryBrowser => self.browser.prev(),
486 SelectedTab::Playlists => self.pl_list.prev(),
487 }
488 }
489
490 pub fn handle_scroll_down(&mut self) {
491 match self.selected_tab {
492 SelectedTab::Queue => self.queue_list.next(),
493 SelectedTab::DirectoryBrowser => self.browser.next(),
494 SelectedTab::Playlists => self.pl_list.next(),
495 }
496 }
497}
498
499pub fn join_library_path(parent: &str, child: &str) -> String {
500 let parent = parent.trim_matches('/').trim_start_matches("./");
501 if parent.is_empty() || parent == "." {
502 child.trim_start_matches("./").to_string()
503 } else {
504 format!("{parent}/{}", child.trim_start_matches("./"))
505 }
506}