rmptui6da9e0c
new feature: add to new playlist
+136 −18
8 files changed
- M README.md +2 −0
- M src/app.rs +42 −13
- M src/event/handler.rs +7 −3
- M src/event/mod.rs +1 −0
- A src/event/new_pl_keys.rs +45 −0
- M src/event/pl_append_keys.rs +15 −0
- M src/ui.rs +24 −0
- D todo.md +0 −2
| @@ -38,6 +38,7 @@ rmptui is a minimal tui mpd client made with rust. | |||
| 38 | 38 | | `r` | Toggle repeat | | |
| 39 | 39 | | `z` | Toggle random | | |
| 40 | 40 | | `/` | Search | | |
| 41 | + | | `R` | Rename Playlist | | |
| 41 | 42 | | `g` | Go to top of list | | |
| 42 | 43 | | `G` | Go to bottom of list | | |
| 43 | 44 | ||
| @@ -54,4 +55,5 @@ rmptui is a minimal tui mpd client made with rust. | |||
| 54 | 55 | - [x] metadata based tree view | |
| 55 | 56 | - [x] view playlist | |
| 56 | 57 | - [x] change playlist name | |
| 58 | + | - [x] add to new playlist | |
| 57 | 59 | - [ ] add lyrics fetcher | |
| @@ -25,9 +25,14 @@ pub struct App { | |||
| 25 | 25 | pub inputmode: InputMode, // Defines input mode, Normal or Search | |
| 26 | 26 | pub search_input: String, // Stores the userinput to be searched | |
| 27 | 27 | pub search_cursor_pos: usize, // Stores the cursor position for searching | |
| 28 | + | ||
| 28 | 29 | pub pl_newname_input: String, // Stores the new name of the playlist | |
| 29 | 30 | pub pl_cursor_pos: usize, // Stores the cursor position for renaming playlist | |
| 30 | 31 | ||
| 32 | + | pub pl_new_pl_input: String, // Stores the name of new playlist to be created | |
| 33 | + | pub pl_new_pl_cursor_pos: usize, // Stores the cursor position of new playlist to be created | |
| 34 | + | pub pl_new_pl_songs_buffer: Vec<Song>, // Buffer for songs that need to be added to the newly created playlist | |
| 35 | + | ||
| 31 | 36 | // playlist variables | |
| 32 | 37 | // used to show playlist popup | |
| 33 | 38 | pub playlist_popup: bool, | |
| @@ -80,6 +85,9 @@ impl App { | |||
| 80 | 85 | search_cursor_pos: 0, | |
| 81 | 86 | pl_cursor_pos: 0, | |
| 82 | 87 | playlist_popup: false, | |
| 88 | + | pl_new_pl_input: String::new(), | |
| 89 | + | pl_new_pl_cursor_pos: 0, | |
| 90 | + | pl_new_pl_songs_buffer: Vec::new(), | |
| 83 | 91 | append_list, | |
| 84 | 92 | should_update_song_list: false, | |
| 85 | 93 | queue_state, | |
| @@ -136,6 +144,7 @@ impl App { | |||
| 136 | 144 | ||
| 137 | 145 | pub fn get_append_list(conn: &mut Client) -> AppResult<ContentList<String>> { | |
| 138 | 146 | let mut list = ContentList::new(); | |
| 147 | + | list.list.push("New Playlist".to_string()); | |
| 139 | 148 | list.list.push("Current Playlist".to_string()); | |
| 140 | 149 | for item in Self::get_playlist(conn)? { | |
| 141 | 150 | list.list.push(item.to_string()); | |
| @@ -286,6 +295,10 @@ impl App { | |||
| 286 | 295 | let cursor_moved_left = self.search_cursor_pos.saturating_sub(1); | |
| 287 | 296 | self.search_cursor_pos = self.clamp_cursor(cursor_moved_left); | |
| 288 | 297 | } | |
| 298 | + | InputMode::NewPlaylist => { | |
| 299 | + | let cursor_moved_left = self.pl_new_pl_cursor_pos.saturating_sub(1); | |
| 300 | + | self.pl_new_pl_cursor_pos = self.clamp_cursor(cursor_moved_left); | |
| 301 | + | } | |
| 289 | 302 | _ => {} | |
| 290 | 303 | } | |
| 291 | 304 | } | |
| @@ -300,6 +313,12 @@ impl App { | |||
| 300 | 313 | let cursor_moved_right = self.search_cursor_pos.saturating_add(1); | |
| 301 | 314 | self.search_cursor_pos = self.clamp_cursor(cursor_moved_right); | |
| 302 | 315 | } | |
| 316 | + | ||
| 317 | + | InputMode::NewPlaylist => { | |
| 318 | + | let cursor_moved_right = self.pl_new_pl_cursor_pos.saturating_add(1); | |
| 319 | + | self.pl_new_pl_cursor_pos = self.clamp_cursor(cursor_moved_right); | |
| 320 | + | } | |
| 321 | + | ||
| 303 | 322 | _ => {} | |
| 304 | 323 | } | |
| 305 | 324 | } | |
| @@ -308,19 +327,24 @@ impl App { | |||
| 308 | 327 | match self.inputmode { | |
| 309 | 328 | InputMode::PlaylistRename => { | |
| 310 | 329 | self.pl_newname_input.insert(self.pl_cursor_pos, new_char); | |
| 311 | − | self.move_cursor_right(); | |
| 330 | + | } | |
| 331 | + | InputMode::NewPlaylist => { | |
| 332 | + | self.pl_new_pl_input | |
| 333 | + | .insert(self.pl_new_pl_cursor_pos, new_char); | |
| 312 | 334 | } | |
| 313 | 335 | InputMode::Editing => { | |
| 314 | 336 | self.search_input.insert(self.search_cursor_pos, new_char); | |
| 315 | − | self.move_cursor_right(); | |
| 316 | 337 | } | |
| 317 | 338 | _ => {} | |
| 318 | 339 | } | |
| 340 | + | ||
| 341 | + | self.move_cursor_right(); | |
| 319 | 342 | } | |
| 320 | 343 | ||
| 321 | 344 | pub fn delete_char(&mut self) { | |
| 322 | 345 | let is_not_cursor_leftmost = match self.inputmode { | |
| 323 | 346 | InputMode::PlaylistRename => self.pl_cursor_pos != 0, | |
| 347 | + | InputMode::NewPlaylist => self.pl_new_pl_cursor_pos != 0, | |
| 324 | 348 | InputMode::Editing => self.search_cursor_pos != 0, | |
| 325 | 349 | _ => false, | |
| 326 | 350 | }; | |
| @@ -333,27 +357,35 @@ impl App { | |||
| 333 | 357 | let current_index = match self.inputmode { | |
| 334 | 358 | InputMode::Editing => self.search_cursor_pos, | |
| 335 | 359 | InputMode::PlaylistRename => self.pl_cursor_pos, | |
| 360 | + | InputMode::NewPlaylist => self.pl_new_pl_cursor_pos, | |
| 336 | 361 | _ => 0, | |
| 337 | 362 | }; | |
| 338 | 363 | ||
| 339 | 364 | let from_left_to_current_index = current_index - 1; | |
| 340 | 365 | ||
| 341 | 366 | if self.inputmode == InputMode::PlaylistRename { | |
| 342 | − | // Getting all characters before the selected character. | |
| 343 | 367 | let before_char_to_delete = self | |
| 344 | 368 | .pl_newname_input | |
| 345 | 369 | .chars() | |
| 346 | 370 | .take(from_left_to_current_index); | |
| 347 | − | // Getting all characters after selected character. | |
| 348 | 371 | let after_char_to_delete = self.pl_newname_input.chars().skip(current_index); | |
| 349 | − | // Put all characters together except the selected one. | |
| 350 | − | // By leaving the selected one out, it is forgotten and therefore deleted. | |
| 372 | + | ||
| 351 | 373 | self.pl_newname_input = before_char_to_delete.chain(after_char_to_delete).collect(); | |
| 352 | 374 | self.move_cursor_left(); | |
| 375 | + | } else if self.inputmode == InputMode::NewPlaylist { | |
| 376 | + | let before_char_to_delete = self | |
| 377 | + | .pl_new_pl_input | |
| 378 | + | .chars() | |
| 379 | + | .take(from_left_to_current_index); | |
| 380 | + | let after_char_to_delete = self.pl_new_pl_input.chars().skip(current_index); | |
| 381 | + | ||
| 382 | + | self.pl_new_pl_input = before_char_to_delete.chain(after_char_to_delete).collect(); | |
| 383 | + | self.move_cursor_left(); | |
| 353 | 384 | } else if self.inputmode == InputMode::Editing { | |
| 354 | 385 | let before_char_to_delete = | |
| 355 | 386 | self.search_input.chars().take(from_left_to_current_index); | |
| 356 | 387 | let after_char_to_delete = self.search_input.chars().skip(current_index); | |
| 388 | + | ||
| 357 | 389 | self.search_input = before_char_to_delete.chain(after_char_to_delete).collect(); | |
| 358 | 390 | self.move_cursor_left(); | |
| 359 | 391 | } | |
| @@ -363,6 +395,7 @@ impl App { | |||
| 363 | 395 | pub fn clamp_cursor(&self, new_cursor_pos: usize) -> usize { | |
| 364 | 396 | match self.inputmode { | |
| 365 | 397 | InputMode::PlaylistRename => new_cursor_pos.clamp(0, self.pl_newname_input.len()), | |
| 398 | + | InputMode::NewPlaylist => new_cursor_pos.clamp(0, self.pl_new_pl_input.len()), | |
| 366 | 399 | InputMode::Editing => new_cursor_pos.clamp(0, self.search_input.len()), | |
| 367 | 400 | _ => 0, | |
| 368 | 401 | } | |
| @@ -376,6 +409,9 @@ impl App { | |||
| 376 | 409 | InputMode::PlaylistRename => { | |
| 377 | 410 | self.pl_cursor_pos = 0; | |
| 378 | 411 | } | |
| 412 | + | InputMode::NewPlaylist => { | |
| 413 | + | self.pl_new_pl_cursor_pos = 0; | |
| 414 | + | } | |
| 379 | 415 | _ => {} | |
| 380 | 416 | } | |
| 381 | 417 | } | |
| @@ -393,13 +429,6 @@ impl App { | |||
| 393 | 429 | } | |
| 394 | 430 | } | |
| 395 | 431 | ||
| 396 | − | pub fn change_playlist_name(&mut self) -> AppResult<()> { | |
| 397 | − | if self.selected_tab == SelectedTab::Playlists { | |
| 398 | − | self.inputmode = InputMode::PlaylistRename; | |
| 399 | − | } | |
| 400 | − | Ok(()) | |
| 401 | − | } | |
| 402 | − | ||
| 403 | 432 | // Mouse event handlers | |
| 404 | 433 | pub fn handle_scroll_up(&mut self) { | |
| 405 | 434 | match self.selected_tab { | |
| @@ -6,7 +6,7 @@ use crate::{ | |||
| 6 | 6 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}; | |
| 7 | 7 | use std::time::Duration; | |
| 8 | 8 | ||
| 9 | − | use super::{pl_append_keys, pl_rename_keys, search_keys}; | |
| 9 | + | use super::{pl_append_keys, pl_rename_keys, new_pl_keys, search_keys}; | |
| 10 | 10 | ||
| 11 | 11 | pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |
| 12 | 12 | // searching, playlist renaming, playlist appending | |
| @@ -14,6 +14,8 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |||
| 14 | 14 | search_keys::handle_search_keys(key_event, app)?; | |
| 15 | 15 | } else if app.inputmode == InputMode::PlaylistRename { | |
| 16 | 16 | pl_rename_keys::handle_pl_rename_keys(key_event, app)?; | |
| 17 | + | } else if app.inputmode == InputMode::NewPlaylist { | |
| 18 | + | new_pl_keys::handle_new_pl_keys(key_event, app)?; | |
| 17 | 19 | } else if app.playlist_popup { | |
| 18 | 20 | pl_append_keys::hande_pl_append_keys(key_event, app)?; | |
| 19 | 21 | } else { | |
| @@ -295,8 +297,10 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |||
| 295 | 297 | // go to bottom of list | |
| 296 | 298 | KeyCode::Char('G') => app.pl_list.index = app.pl_list.list.len() - 1, | |
| 297 | 299 | ||
| 298 | − | // Change playlist name | |
| 299 | − | KeyCode::Char('e') => app.change_playlist_name()?, | |
| 300 | + | // Playlist Rename | |
| 301 | + | KeyCode::Char('R') => { | |
| 302 | + | app.inputmode = InputMode::PlaylistRename; | |
| 303 | + | } | |
| 300 | 304 | ||
| 301 | 305 | // add to current playlist | |
| 302 | 306 | KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right | KeyCode::Char(' ') => { | |
| @@ -3,3 +3,4 @@ pub mod handler; | |||
| 3 | 3 | pub mod search_keys; | |
| 4 | 4 | pub mod pl_rename_keys; | |
| 5 | 5 | pub mod pl_append_keys; | |
| 6 | + | pub mod new_pl_keys; | |
| @@ -0,0 +1,45 @@ | |||
| 1 | + | use crate::{ | |
| 2 | + | app::{App, AppResult}, | |
| 3 | + | ui::InputMode, | |
| 4 | + | }; | |
| 5 | + | use crossterm::event::{KeyCode, KeyEvent}; | |
| 6 | + | ||
| 7 | + | pub fn handle_new_pl_keys(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |
| 8 | + | match key_event.code { | |
| 9 | + | KeyCode::Esc => { | |
| 10 | + | app.pl_new_pl_input.clear(); | |
| 11 | + | app.reset_cursor(); | |
| 12 | + | app.inputmode = InputMode::Normal; | |
| 13 | + | } | |
| 14 | + | KeyCode::Char(to_insert) => { | |
| 15 | + | app.enter_char(to_insert); | |
| 16 | + | } | |
| 17 | + | KeyCode::Enter => { | |
| 18 | + | let pl_name = &app.pl_new_pl_input; | |
| 19 | + | ||
| 20 | + | for song in app.pl_new_pl_songs_buffer.iter() { | |
| 21 | + | app.conn.conn.pl_push(pl_name, song)?; | |
| 22 | + | } | |
| 23 | + | app.pl_new_pl_input.clear(); | |
| 24 | + | ||
| 25 | + | app.pl_list.list = App::get_playlist(&mut app.conn.conn)?; | |
| 26 | + | app.reset_cursor(); | |
| 27 | + | app.inputmode = InputMode::Normal; | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | KeyCode::Backspace => { | |
| 31 | + | app.delete_char(); | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | KeyCode::Left => { | |
| 35 | + | app.move_cursor_left(); | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | KeyCode::Right => { | |
| 39 | + | app.move_cursor_right(); | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | _ => {} | |
| 43 | + | } | |
| 44 | + | Ok(()) | |
| 45 | + | } | |
| @@ -1,4 +1,5 @@ | |||
| 1 | 1 | use crate::app::{App, AppResult, SelectedTab}; | |
| 2 | + | use crate::ui::InputMode; | |
| 2 | 3 | use crate::utils::FileExtension; | |
| 3 | 4 | use crossterm::event::{KeyCode, KeyEvent}; | |
| 4 | 5 | use std::path::Path; | |
| @@ -30,6 +31,10 @@ pub fn hande_pl_append_keys(key_event: KeyEvent, app: &mut App) -> AppResult<()> | |||
| 30 | 31 | if *pl_name == "Current Playlist" { | |
| 31 | 32 | app.conn.conn.push(&song)?; | |
| 32 | 33 | app.update_queue(); | |
| 34 | + | } else if *pl_name == "New Playlist" { | |
| 35 | + | app.pl_new_pl_songs_buffer.clear(); | |
| 36 | + | app.pl_new_pl_songs_buffer.push(song.clone()); | |
| 37 | + | app.inputmode = InputMode::NewPlaylist; | |
| 33 | 38 | } else { | |
| 34 | 39 | app.conn.add_to_playlist(pl_name, &song)?; | |
| 35 | 40 | } | |
| @@ -47,12 +52,17 @@ pub fn hande_pl_append_keys(key_event: KeyEvent, app: &mut App) -> AppResult<()> | |||
| 47 | 52 | if *pl_name == "Current Playlist" { | |
| 48 | 53 | app.conn.conn.push(&song)?; | |
| 49 | 54 | app.update_queue(); | |
| 55 | + | } else if *pl_name == "New Playlist" { | |
| 56 | + | app.pl_new_pl_songs_buffer.clear(); | |
| 57 | + | app.pl_new_pl_songs_buffer.push(song.clone()); | |
| 58 | + | app.inputmode = InputMode::NewPlaylist; | |
| 50 | 59 | } else { | |
| 51 | 60 | app.conn.add_to_playlist(pl_name, &song)?; | |
| 52 | 61 | } | |
| 53 | 62 | } | |
| 54 | 63 | } else if t == "directory" { | |
| 55 | 64 | let file = format!("{}/{}", app.browser.path, f); | |
| 65 | + | app.pl_new_pl_songs_buffer.clear(); | |
| 56 | 66 | for (t, f) in app.conn.conn.listfiles(&file)?.iter() { | |
| 57 | 67 | // dir_vec.push((t, f)); | |
| 58 | 68 | if t == "file" | |
| @@ -65,6 +75,9 @@ pub fn hande_pl_append_keys(key_event: KeyEvent, app: &mut App) -> AppResult<()> | |||
| 65 | 75 | let song = app.conn.get_song_with_only_filename(&full_path); | |
| 66 | 76 | if *pl_name == "Current Playlist" { | |
| 67 | 77 | app.conn.conn.push(&song)?; | |
| 78 | + | } else if *pl_name == "New Playlist" { | |
| 79 | + | app.pl_new_pl_songs_buffer.push(song.clone()); | |
| 80 | + | app.inputmode = InputMode::NewPlaylist; | |
| 68 | 81 | } else { | |
| 69 | 82 | app.conn.add_to_playlist(pl_name, &song)?; | |
| 70 | 83 | } | |
| @@ -78,6 +91,8 @@ pub fn hande_pl_append_keys(key_event: KeyEvent, app: &mut App) -> AppResult<()> | |||
| 78 | 91 | if *pl_name == "Current Playlist" { | |
| 79 | 92 | app.conn.load_playlist(playlist_name)?; | |
| 80 | 93 | app.update_queue(); | |
| 94 | + | } else if *pl_name == "New Playlist" { | |
| 95 | + | app.inputmode = InputMode::NewPlaylist; | |
| 81 | 96 | } else { | |
| 82 | 97 | let songs = app.conn.conn.playlist(playlist_name)?; | |
| 83 | 98 | for song in songs { | |
| @@ -14,6 +14,7 @@ pub enum InputMode { | |||
| 14 | 14 | Editing, | |
| 15 | 15 | Normal, | |
| 16 | 16 | PlaylistRename, | |
| 17 | + | NewPlaylist, | |
| 17 | 18 | } | |
| 18 | 19 | ||
| 19 | 20 | /// Renders the user interface widgets | |
| @@ -40,6 +41,9 @@ pub fn render(app: &mut App, frame: &mut Frame) { | |||
| 40 | 41 | InputMode::PlaylistRename => { | |
| 41 | 42 | draw_rename_playlist(frame, app, layout[1]); | |
| 42 | 43 | } | |
| 44 | + | InputMode::NewPlaylist => { | |
| 45 | + | draw_new_playlist(frame, app, layout[1]); | |
| 46 | + | } | |
| 43 | 47 | } | |
| 44 | 48 | ||
| 45 | 49 | if app.playlist_popup { | |
| @@ -453,6 +457,26 @@ fn draw_rename_playlist(frame: &mut Frame, app: &mut App, area: Rect) { | |||
| 453 | 457 | frame.render_widget(input, area); | |
| 454 | 458 | } | |
| 455 | 459 | ||
| 460 | + | fn draw_new_playlist(frame: &mut Frame, app: &mut App, area: Rect) { | |
| 461 | + | #[allow(clippy::cast_possible_truncation)] | |
| 462 | + | frame.set_cursor( | |
| 463 | + | // Draw the cursor at the current position in the input field. | |
| 464 | + | // This position is can be controlled via the left and right arrow key | |
| 465 | + | area.x + app.pl_new_pl_cursor_pos as u16 + 2, | |
| 466 | + | // Move one line down, from the border to the input line | |
| 467 | + | area.y + 1, | |
| 468 | + | ); | |
| 469 | + | ||
| 470 | + | let input = Paragraph::new("/".to_string() + &app.pl_new_pl_input) | |
| 471 | + | .style(Style::default()) | |
| 472 | + | .block( | |
| 473 | + | Block::default() | |
| 474 | + | .borders(Borders::ALL) | |
| 475 | + | .title("Enter New Playlist's Name: ".bold().green()), | |
| 476 | + | ); | |
| 477 | + | frame.render_widget(input, area); | |
| 478 | + | } | |
| 479 | + | ||
| 456 | 480 | fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { | |
| 457 | 481 | let popup_layout = Layout::vertical([ | |
| 458 | 482 | Constraint::Percentage((100 - percent_y) / 2), | |
| @@ -1,2 +0,0 @@ | |||
| 1 | − | - [ ] create new playlist | |
| 2 | − | - [ ] add to new playlist | |