diff --git a/README.md b/README.md index e65e051..2b25aeb 100755 --- a/README.md +++ b/README.md @@ -39,5 +39,5 @@ A MPD client in Rust - [x] search for songs - [x] Human readable time format - [x] metadata based tree view -- [ ] view playlist -- [ ] change playlist name +- [x] view playlist +- [x] change playlist name diff --git a/src/app.rs b/src/app.rs index 35661e1..be55e7d 100755 --- a/src/app.rs +++ b/src/app.rs @@ -21,9 +21,11 @@ pub struct App { pub selected_tab: SelectedTab, // Used to switch between tabs // Search - pub inputmode: InputMode, // Defines input mode, Normal or Search - pub search_input: String, // Stores the userinput to be searched - pub cursor_position: usize, // Stores the cursor position + pub inputmode: InputMode, // Defines input mode, Normal or Search + pub search_input: String, // Stores the userinput to be searched + pub search_cursor_pos: usize, // Stores the cursor position for searching + pub pl_newname_input: String, // Stores the new name of the playlist + pub pl_cursor_pos: usize, // Stores the cursor position for renaming playlist // playlist variables // used to show playlist popup @@ -59,7 +61,9 @@ impl App { browser, inputmode: InputMode::Normal, search_input: String::new(), - cursor_position: 0, + pl_newname_input: String::new(), + search_cursor_pos: 0, + pl_cursor_pos: 0, playlist_popup: false, append_list, }) @@ -97,7 +101,7 @@ impl App { Self::get_queue(&mut self.conn, &mut self.queue_list.list); } - fn get_playlist(conn: &mut Client) -> AppResult> { + pub fn get_playlist(conn: &mut Client) -> AppResult> { let list: Vec = conn.playlists()?.iter().map(|p| p.clone().name).collect(); Ok(list) } @@ -220,49 +224,111 @@ impl App { // Cursor movements pub fn move_cursor_left(&mut self) { - let cursor_moved_left = self.cursor_position.saturating_sub(1); - self.cursor_position = self.clamp_cursor(cursor_moved_left); + match self.inputmode { + InputMode::PlaylistRename => { + let cursor_moved_left = self.pl_cursor_pos.saturating_sub(1); + self.pl_cursor_pos = self.clamp_cursor(cursor_moved_left); + } + InputMode::Editing => { + let cursor_moved_left = self.search_cursor_pos.saturating_sub(1); + self.search_cursor_pos = self.clamp_cursor(cursor_moved_left); + } + _ => {} + } } pub fn move_cursor_right(&mut self) { - let cursor_moved_right = self.cursor_position.saturating_add(1); - self.cursor_position = self.clamp_cursor(cursor_moved_right); + match self.inputmode { + InputMode::PlaylistRename => { + let cursor_moved_right = self.pl_cursor_pos.saturating_add(1); + self.pl_cursor_pos = self.clamp_cursor(cursor_moved_right); + } + InputMode::Editing => { + let cursor_moved_right = self.search_cursor_pos.saturating_add(1); + self.search_cursor_pos = self.clamp_cursor(cursor_moved_right); + } + _ => {} + } } pub fn enter_char(&mut self, new_char: char) { - self.search_input.insert(self.cursor_position, new_char); - - self.move_cursor_right(); + match self.inputmode { + InputMode::PlaylistRename => { + self.pl_newname_input.insert(self.pl_cursor_pos, new_char); + self.move_cursor_right(); + } + InputMode::Editing => { + self.search_input.insert(self.search_cursor_pos, new_char); + self.move_cursor_right(); + } + _ => {} + } } pub fn delete_char(&mut self) { - let is_not_cursor_leftmost = self.cursor_position != 0; + let is_not_cursor_leftmost = match self.inputmode { + InputMode::PlaylistRename => self.pl_cursor_pos != 0, + InputMode::Editing => self.search_cursor_pos != 0, + _ => false, + }; + if is_not_cursor_leftmost { // Method "remove" is not used on the saved text for deleting the selected char. // Reason: Using remove on String works on bytes instead of the chars. // Using remove would require special care because of char boundaries. - let current_index = self.cursor_position; + let current_index = match self.inputmode { + InputMode::Editing => self.search_cursor_pos, + InputMode::PlaylistRename => self.pl_cursor_pos, + _ => 0, + }; + let from_left_to_current_index = current_index - 1; - // Getting all characters before the selected character. - let before_char_to_delete = self.search_input.chars().take(from_left_to_current_index); - // Getting all characters after selected character. - let after_char_to_delete = self.search_input.chars().skip(current_index); - - // Put all characters together except the selected one. - // By leaving the selected one out, it is forgotten and therefore deleted. - self.search_input = before_char_to_delete.chain(after_char_to_delete).collect(); - self.move_cursor_left(); + if self.inputmode == InputMode::PlaylistRename { + // Getting all characters before the selected character. + let before_char_to_delete = self + .pl_newname_input + .chars() + .take(from_left_to_current_index); + // Getting all characters after selected character. + let after_char_to_delete = self.pl_newname_input.chars().skip(current_index); + // Put all characters together except the selected one. + // By leaving the selected one out, it is forgotten and therefore deleted. + self.pl_newname_input = before_char_to_delete.chain(after_char_to_delete).collect(); + self.move_cursor_left(); + } else if self.inputmode == InputMode::Editing { + // Getting all characters before the selected character. + let before_char_to_delete = + self.search_input.chars().take(from_left_to_current_index); + // Getting all characters after selected character. + let after_char_to_delete = self.search_input.chars().skip(current_index); + // Put all characters together except the selected one. + // By leaving the selected one out, it is forgotten and therefore deleted. + self.search_input = before_char_to_delete.chain(after_char_to_delete).collect(); + self.move_cursor_left(); + } } } pub fn clamp_cursor(&self, new_cursor_pos: usize) -> usize { - new_cursor_pos.clamp(0, self.search_input.len()) + match self.inputmode { + InputMode::PlaylistRename => new_cursor_pos.clamp(0, self.pl_newname_input.len()), + InputMode::Editing => new_cursor_pos.clamp(0, self.search_input.len()), + _ => 0, + } } pub fn reset_cursor(&mut self) { - self.cursor_position = 0; + match self.inputmode { + InputMode::Editing => { + self.search_cursor_pos = 0; + } + InputMode::PlaylistRename => { + self.pl_cursor_pos = 0; + } + _ => {} + } } /// Given time in seconds, convert it to hh:mm:ss @@ -277,4 +343,14 @@ impl App { format!("{:02}:{:02}:{:02}", h, m, s) } } + + pub fn change_playlist_name(&mut self) -> AppResult<()> { + match self.selected_tab { + SelectedTab::Playlists => { + self.inputmode = InputMode::PlaylistRename; + } + _ => {} + } + Ok(()) + } } diff --git a/src/handler.rs b/src/handler.rs index a5c4edb..86ab7cb 100755 --- a/src/handler.rs +++ b/src/handler.rs @@ -92,8 +92,8 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { } app.search_input.clear(); - app.inputmode = InputMode::Normal; app.reset_cursor(); + app.inputmode = InputMode::Normal; } KeyCode::Backspace => { @@ -110,10 +110,41 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { _ => {} } + } else if app.inputmode == InputMode::PlaylistRename { + match key_event.code { + KeyCode::Esc => { + app.pl_newname_input.clear(); + app.reset_cursor(); + app.inputmode = InputMode::Normal; + } + KeyCode::Char(to_insert) => { + app.enter_char(to_insert); + } + KeyCode::Enter => { + app.conn.conn.pl_rename(app.pl_list.list.get(app.pl_list.index).unwrap(), &app.pl_newname_input)?; + app.pl_list.list = App::get_playlist(&mut app.conn.conn)?; + app.pl_newname_input.clear(); + app.reset_cursor(); + app.inputmode = InputMode::Normal; + } - // Playlist popup keybinds - // - // Keybind for when the "append to playlist" popup is visible + KeyCode::Backspace => { + app.delete_char(); + } + + KeyCode::Left => { + app.move_cursor_left(); + } + + KeyCode::Right => { + app.move_cursor_right(); + } + + _ => {} + } + // Playlist popup keybinds + // + // Keybind for when the "append to playlist" popup is visible } else if app.playlist_popup { match key_event.code { KeyCode::Char('q') | KeyCode::Esc => { @@ -358,7 +389,11 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { // Search for songs KeyCode::Char('/') => { - app.inputmode = InputMode::toggle_editing_states(&app.inputmode); + if app.inputmode == InputMode::Normal { + app.inputmode = InputMode::Editing; + } else { + app.inputmode = InputMode::Normal; + } } // Remove from Current Playlsit @@ -366,9 +401,6 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { app.handle_add_or_remove_from_current_playlist()?; } - // Change playlist name - KeyCode::Char('e') => if app.selected_tab == SelectedTab::Playlists {}, - // go to top of list KeyCode::Char('g') => match app.selected_tab { SelectedTab::Queue => app.queue_list.index = 0, @@ -384,6 +416,9 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { } SelectedTab::Playlists => app.pl_list.index = app.pl_list.list.len() - 1, }, + + // Change playlist name + KeyCode::Char('e') => app.change_playlist_name()?, _ => {} } } diff --git a/src/ui.rs b/src/ui.rs index 9acf6a1..51a0fed 100755 --- a/src/ui.rs +++ b/src/ui.rs @@ -11,15 +11,7 @@ use ratatui::{ pub enum InputMode { Editing, Normal, -} - -impl InputMode { - pub fn toggle_editing_states(state: &InputMode) -> InputMode { - match state { - InputMode::Editing => return InputMode::Normal, - InputMode::Normal => return InputMode::Editing, - }; - } + PlaylistRename, } /// Renders the user interface widgets @@ -32,7 +24,6 @@ pub fn render(app: &mut App, frame: &mut Frame) { match app.selected_tab { SelectedTab::Queue => draw_queue(frame, app, layout[0]), - // SelectedTab::Playlists => draw_playlists(frame, app, layout[0]), SelectedTab::Playlists => draw_playlist_viewer(frame, app, layout[0]), SelectedTab::DirectoryBrowser => draw_directory_browser(frame, app, layout[0]), } @@ -44,6 +35,9 @@ pub fn render(app: &mut App, frame: &mut Frame) { InputMode::Editing => { draw_search_bar(frame, app, layout[1]); } + InputMode::PlaylistRename => { + draw_rename_playlist(frame, app, layout[1]); + } } if app.playlist_popup { @@ -254,27 +248,18 @@ fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) { frame.render_stateful_widget(table, size, &mut state); } - // Draw search bar fn draw_search_bar(frame: &mut Frame, app: &mut App, size: Rect) { - match app.inputmode { - InputMode::Normal => - // Hide the cursor. `Frame` does this by default, so we don't need to do anything here - {} - - InputMode::Editing => { - // Make the cursor visible and ask ratatui to put it at the specified coordinates after - // rendering - #[allow(clippy::cast_possible_truncation)] - frame.set_cursor( - // Draw the cursor at the current position in the input field. - // This position is can be controlled via the left and right arrow key - size.x + app.cursor_position as u16 + 2, - // Move one line down, from the border to the input line - size.y + 1, - ); - } - } + // Make the cursor visible and ask ratatui to put it at the specified coordinates after + // rendering + #[allow(clippy::cast_possible_truncation)] + frame.set_cursor( + // Draw the cursor at the current position in the input field. + // This position is can be controlled via the left and right arrow key + size.x + app.search_cursor_pos as u16 + 2, + // Move one line down, from the border to the input line + size.y + 1, + ); let input = Paragraph::new("/".to_string() + &app.search_input) .style(Style::default()) @@ -399,7 +384,7 @@ fn draw_playlist_viewer(frame: &mut Frame, app: &mut App, area: Rect) { let table = Table::new( rows, vec![ - Constraint::Percentage(40), + Constraint::Min(40), Constraint::Percentage(40), Constraint::Percentage(20), ], @@ -439,6 +424,26 @@ fn draw_add_to_playlist(frame: &mut Frame, app: &mut App, area: Rect) { frame.render_stateful_widget(list, area, &mut state); } +fn draw_rename_playlist(frame: &mut Frame, app: &mut App, area: Rect) { + #[allow(clippy::cast_possible_truncation)] + frame.set_cursor( + // Draw the cursor at the current position in the input field. + // This position is can be controlled via the left and right arrow key + area.x + app.pl_cursor_pos as u16 + 2, + // Move one line down, from the border to the input line + area.y + 1, + ); + + let input = Paragraph::new("/".to_string() + &app.pl_newname_input) + .style(Style::default()) + .block( + Block::default() + .borders(Borders::ALL) + .title("Enter New Name: ".bold().green()), + ); + frame.render_widget(input, area); +} + fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { let popup_layout = Layout::vertical([ Constraint::Percentage((100 - percent_y) / 2),