Krolyxon
Commit 9ff7ca4

rmptuisrcapp.rs

501 lines · 17.5 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.queue().await?;
64 let mut pl_list = ContentList::new();
65 pl_list.list = conn.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.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.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.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 .delete_playlist_song(playlist, self.playlist_song_index)
176 .await?;
177 self.refresh_playlist_view().await
178 }
179
180 pub async fn move_selected_playlist_song(&mut self, down: bool) -> AppResult<()> {
181 let Some(playlist) = self.pl_list.list.get(self.pl_list.index) else {
182 return Ok(());
183 };
184 let target = if down {
185 self.playlist_song_index.checked_add(1)
186 } else {
187 self.playlist_song_index.checked_sub(1)
188 };
189 let Some(target) = target.filter(|target| *target < self.playlist_contents.len()) else {
190 return Ok(());
191 };
192 self.conn
193 .move_playlist_song(playlist, self.playlist_song_index, target)
194 .await?;
195 self.playlist_song_index = target;
196 self.refresh_playlist_view().await
197 }
198
199 pub async fn delete_selected_playlist(&mut self) -> AppResult<()> {
200 let Some(playlist) = self.pl_list.list.get(self.pl_list.index) else {
201 return Ok(());
202 };
203 self.conn.delete_playlist(playlist).await?;
204 self.refresh_playlists().await?;
205 Ok(())
206 }
207
208 pub async fn play_selected_playlist_song(&mut self) -> AppResult<()> {
209 let Some(playlist) = self.pl_list.list.get(self.pl_list.index) else {
210 return Ok(());
211 };
212 if self.playlist_song_index >= self.playlist_contents.len() {
213 return Ok(());
214 }
215 let queue_position = self.conn.queue().await?.len();
216 self.conn.append_playlist(playlist).await?;
217 self.conn
218 .play(Some((queue_position + self.playlist_song_index) as u32))
219 .await?;
220 self.update_queue().await?;
221 self.conn.update_status().await
222 }
223
224 pub async fn get_append_list(conn: &mut Connection) -> AppResult<ContentList<String>> {
225 let mut list = ContentList::new();
226 list.list.push("Current Playlist".to_string());
227 list.list.push("New Playlist".to_string());
228 list.list.extend(conn.playlists().await?);
229 Ok(list)
230 }
231
232 pub async fn toggle_selected_item(&mut self) -> AppResult<()> {
233 match self.selected_tab {
234 SelectedTab::DirectoryBrowser => {
235 let Some(entry) = self.browser.entries.get(self.browser.selected).cloned() else {
236 return Ok(());
237 };
238 match entry {
239 BrowserEntry::Directory(name) => {
240 self.conn.add(&name).await?;
241 }
242 BrowserEntry::Song(song) => {
243 let positions: Vec<u32> = self
244 .queue_list
245 .list
246 .iter()
247 .enumerate()
248 .filter_map(|(index, queued)| {
249 (queued.file == song.file).then_some(index as u32)
250 })
251 .collect();
252 if positions.is_empty() {
253 self.conn.add(&song.file).await?;
254 } else {
255 for position in positions.into_iter().rev() {
256 self.conn.delete(position).await?;
257 }
258 }
259 }
260 }
261 if self.browser.selected < self.browser.entries.len().saturating_sub(1) {
262 self.browser.selected += 1;
263 }
264 }
265 SelectedTab::Queue => {
266 if !self.queue_list.list.is_empty() {
267 self.conn.delete(self.queue_list.index as u32).await?;
268 }
269 }
270 SelectedTab::Playlists => {}
271 }
272 self.update_queue().await?;
273 self.conn.update_status().await?;
274 Ok(())
275 }
276
277 pub fn cycle_tabs(&mut self) {
278 self.selected_tab = match self.selected_tab {
279 SelectedTab::Queue => SelectedTab::DirectoryBrowser,
280 SelectedTab::DirectoryBrowser => SelectedTab::Playlists,
281 SelectedTab::Playlists => SelectedTab::Queue,
282 };
283 }
284
285 pub async fn handle_enter(&mut self) -> AppResult<()> {
286 let Some(entry) = self.browser.entries.get(self.browser.selected).cloned() else {
287 return Ok(());
288 };
289 match entry {
290 BrowserEntry::Directory(name) => {
291 self.browser.prev_selected = self.browser.selected;
292 self.browser.path = name;
293 self.browser.selected = 0;
294 self.browser.update_directory(&mut self.conn).await?;
295 }
296 BrowserEntry::Song(song) => {
297 if let Some(index) = self
298 .queue_list
299 .list
300 .iter()
301 .position(|queued| queued.file == song.file)
302 {
303 self.conn.play(Some(index as u32)).await?;
304 } else {
305 self.conn.push(&song).await?;
306 self.update_queue().await?;
307 }
308 self.conn.update_status().await?;
309 }
310 }
311 Ok(())
312 }
313
314 pub async fn append_selected_to(&mut self) -> AppResult<()> {
315 let Some(playlist) = self.append_list.list.get(self.append_list.index).cloned() else {
316 return Ok(());
317 };
318 let songs = match self.selected_tab {
319 SelectedTab::Queue => self
320 .queue_list
321 .list
322 .get(self.queue_list.index)
323 .cloned()
324 .into_iter()
325 .collect(),
326 SelectedTab::DirectoryBrowser => {
327 let Some(entry) = self.browser.entries.get(self.browser.selected).cloned() else {
328 return Ok(());
329 };
330 match entry {
331 BrowserEntry::Song(song) => vec![song],
332 BrowserEntry::Directory(name) => self
333 .conn
334 .list_files(&name)
335 .await?
336 .into_iter()
337 .filter_map(|(kind, file)| {
338 (kind == "file" && is_audio_file(&file))
339 .then(|| Connection::song_from_file(&file))
340 })
341 .collect(),
342 }
343 }
344 SelectedTab::Playlists => {
345 let Some(name) = self.pl_list.list.get(self.pl_list.index) else {
346 return Ok(());
347 };
348 self.conn.playlist(name).await?
349 }
350 };
351
352 if playlist == "Current Playlist" {
353 if self.selected_tab == SelectedTab::Playlists {
354 if let Some(name) = self.pl_list.list.get(self.pl_list.index) {
355 self.conn.load_playlist(name).await?;
356 self.update_queue().await?;
357 }
358 } else {
359 let selected_directory = if self.selected_tab == SelectedTab::DirectoryBrowser {
360 self.browser
361 .entries
362 .get(self.browser.selected)
363 .and_then(|entry| {
364 if let BrowserEntry::Directory(path) = entry {
365 Some(path.as_str())
366 } else {
367 None
368 }
369 })
370 } else {
371 None
372 };
373 if let Some(path) = selected_directory {
374 self.conn.add(path).await?;
375 } else {
376 for song in songs {
377 self.conn.add(&song.file).await?;
378 }
379 }
380 self.update_queue().await?;
381 }
382 } else if playlist == "New Playlist" {
383 self.pl_new_pl_songs_buffer = songs;
384 self.inputmode = InputMode::NewPlaylist;
385 } else {
386 for song in songs {
387 self.conn.add_to_playlist(&playlist, &song).await?;
388 }
389 self.refresh_playlists().await?;
390 }
391 self.playlist_popup = false;
392 self.append_list.index = 0;
393 Ok(())
394 }
395
396 pub fn move_cursor_left(&mut self) {
397 let cursor = self.cursor_position().saturating_sub(1);
398 self.set_cursor_position(cursor);
399 }
400
401 pub fn move_cursor_right(&mut self) {
402 let cursor = self.cursor_position().saturating_add(1);
403 self.set_cursor_position(cursor);
404 }
405
406 fn cursor_position(&self) -> usize {
407 match self.inputmode {
408 InputMode::PlaylistRename => self.pl_cursor_pos,
409 InputMode::Editing => self.search_cursor_pos,
410 InputMode::NewPlaylist => self.pl_new_pl_cursor_pos,
411 InputMode::Normal => 0,
412 }
413 }
414
415 fn set_cursor_position(&mut self, position: usize) {
416 let length = match self.inputmode {
417 InputMode::PlaylistRename => self.pl_newname_input.chars().count(),
418 InputMode::Editing => self.search_input.chars().count(),
419 InputMode::NewPlaylist => self.pl_new_pl_input.chars().count(),
420 InputMode::Normal => 0,
421 };
422 let position = position.min(length);
423 match self.inputmode {
424 InputMode::PlaylistRename => self.pl_cursor_pos = position,
425 InputMode::Editing => self.search_cursor_pos = position,
426 InputMode::NewPlaylist => self.pl_new_pl_cursor_pos = position,
427 InputMode::Normal => {}
428 }
429 }
430
431 pub fn enter_char(&mut self, character: char) {
432 let (text, cursor) = match self.inputmode {
433 InputMode::PlaylistRename => (&mut self.pl_newname_input, self.pl_cursor_pos),
434 InputMode::Editing => (&mut self.search_input, self.search_cursor_pos),
435 InputMode::NewPlaylist => (&mut self.pl_new_pl_input, self.pl_new_pl_cursor_pos),
436 InputMode::Normal => return,
437 };
438 let byte_index = text
439 .char_indices()
440 .nth(cursor)
441 .map(|(index, _)| index)
442 .unwrap_or(text.len());
443 text.insert(byte_index, character);
444 self.move_cursor_right();
445 }
446
447 pub fn delete_char(&mut self) {
448 let cursor = self.cursor_position();
449 if cursor == 0 {
450 return;
451 }
452 let text = match self.inputmode {
453 InputMode::PlaylistRename => &mut self.pl_newname_input,
454 InputMode::Editing => &mut self.search_input,
455 InputMode::NewPlaylist => &mut self.pl_new_pl_input,
456 InputMode::Normal => return,
457 };
458 let start = text.char_indices().nth(cursor - 1).map(|(i, _)| i);
459 let end = text
460 .char_indices()
461 .nth(cursor)
462 .map(|(i, _)| i)
463 .unwrap_or(text.len());
464 if let Some(start) = start {
465 text.replace_range(start..end, "");
466 self.move_cursor_left();
467 }
468 }
469
470 pub fn reset_cursor(&mut self) {
471 self.set_cursor_position(0);
472 }
473
474 pub fn format_time(time: Duration) -> String {
475 let seconds = time.as_secs();
476 let hours = seconds / 3600;
477 let minutes = (seconds % 3600) / 60;
478 let seconds = seconds % 60;
479 if hours == 0 {
480 format!("{minutes:02}:{seconds:02}")
481 } else {
482 format!("{hours:02}:{minutes:02}:{seconds:02}")
483 }
484 }
485
486 pub fn handle_scroll_up(&mut self) {
487 match self.selected_tab {
488 SelectedTab::Queue => self.queue_list.prev(),
489 SelectedTab::DirectoryBrowser => self.browser.prev(),
490 SelectedTab::Playlists => self.pl_list.prev(),
491 }
492 }
493
494 pub fn handle_scroll_down(&mut self) {
495 match self.selected_tab {
496 SelectedTab::Queue => self.queue_list.next(),
497 SelectedTab::DirectoryBrowser => self.browser.next(),
498 SelectedTab::Playlists => self.pl_list.next(),
499 }
500 }
501}