add rename playlists feature

This commit is contained in:
krolxon 2024-04-30 22:40:56 +05:30
parent e95e1bbb36
commit 32a3c60471
4 changed files with 181 additions and 65 deletions

View File

@ -39,5 +39,5 @@ A MPD client in Rust
- [x] search for songs - [x] search for songs
- [x] Human readable time format - [x] Human readable time format
- [x] metadata based tree view - [x] metadata based tree view
- [ ] view playlist - [x] view playlist
- [ ] change playlist name - [x] change playlist name

View File

@ -21,9 +21,11 @@ pub struct App {
pub selected_tab: SelectedTab, // Used to switch between tabs pub selected_tab: SelectedTab, // Used to switch between tabs
// Search // Search
pub inputmode: InputMode, // Defines input mode, Normal or Search pub inputmode: InputMode, // Defines input mode, Normal or Search
pub search_input: String, // Stores the userinput to be searched pub search_input: String, // Stores the userinput to be searched
pub cursor_position: usize, // Stores the cursor position 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 // playlist variables
// used to show playlist popup // used to show playlist popup
@ -59,7 +61,9 @@ impl App {
browser, browser,
inputmode: InputMode::Normal, inputmode: InputMode::Normal,
search_input: String::new(), search_input: String::new(),
cursor_position: 0, pl_newname_input: String::new(),
search_cursor_pos: 0,
pl_cursor_pos: 0,
playlist_popup: false, playlist_popup: false,
append_list, append_list,
}) })
@ -97,7 +101,7 @@ impl App {
Self::get_queue(&mut self.conn, &mut self.queue_list.list); Self::get_queue(&mut self.conn, &mut self.queue_list.list);
} }
fn get_playlist(conn: &mut Client) -> AppResult<Vec<String>> { pub fn get_playlist(conn: &mut Client) -> AppResult<Vec<String>> {
let list: Vec<String> = conn.playlists()?.iter().map(|p| p.clone().name).collect(); let list: Vec<String> = conn.playlists()?.iter().map(|p| p.clone().name).collect();
Ok(list) Ok(list)
} }
@ -220,49 +224,111 @@ impl App {
// Cursor movements // Cursor movements
pub fn move_cursor_left(&mut self) { pub fn move_cursor_left(&mut self) {
let cursor_moved_left = self.cursor_position.saturating_sub(1); match self.inputmode {
self.cursor_position = self.clamp_cursor(cursor_moved_left); 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) { pub fn move_cursor_right(&mut self) {
let cursor_moved_right = self.cursor_position.saturating_add(1); match self.inputmode {
self.cursor_position = self.clamp_cursor(cursor_moved_right); 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) { pub fn enter_char(&mut self, new_char: char) {
self.search_input.insert(self.cursor_position, new_char); match self.inputmode {
InputMode::PlaylistRename => {
self.move_cursor_right(); 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) { 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 { if is_not_cursor_leftmost {
// Method "remove" is not used on the saved text for deleting the selected char. // 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. // Reason: Using remove on String works on bytes instead of the chars.
// Using remove would require special care because of char boundaries. // 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; let from_left_to_current_index = current_index - 1;
// Getting all characters before the selected character. if self.inputmode == InputMode::PlaylistRename {
let before_char_to_delete = self.search_input.chars().take(from_left_to_current_index); // Getting all characters before the selected character.
// Getting all characters after selected character. let before_char_to_delete = self
let after_char_to_delete = self.search_input.chars().skip(current_index); .pl_newname_input
.chars()
// Put all characters together except the selected one. .take(from_left_to_current_index);
// By leaving the selected one out, it is forgotten and therefore deleted. // Getting all characters after selected character.
self.search_input = before_char_to_delete.chain(after_char_to_delete).collect(); let after_char_to_delete = self.pl_newname_input.chars().skip(current_index);
self.move_cursor_left(); // 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 { 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) { 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 /// Given time in seconds, convert it to hh:mm:ss
@ -277,4 +343,14 @@ impl App {
format!("{:02}:{:02}:{:02}", h, m, s) 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(())
}
} }

View File

@ -92,8 +92,8 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
} }
app.search_input.clear(); app.search_input.clear();
app.inputmode = InputMode::Normal;
app.reset_cursor(); app.reset_cursor();
app.inputmode = InputMode::Normal;
} }
KeyCode::Backspace => { 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 KeyCode::Backspace => {
// app.delete_char();
// Keybind for when the "append to playlist" popup is visible }
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 { } else if app.playlist_popup {
match key_event.code { match key_event.code {
KeyCode::Char('q') | KeyCode::Esc => { KeyCode::Char('q') | KeyCode::Esc => {
@ -358,7 +389,11 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
// Search for songs // Search for songs
KeyCode::Char('/') => { 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 // 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()?; 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 // go to top of list
KeyCode::Char('g') => match app.selected_tab { KeyCode::Char('g') => match app.selected_tab {
SelectedTab::Queue => app.queue_list.index = 0, 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, SelectedTab::Playlists => app.pl_list.index = app.pl_list.list.len() - 1,
}, },
// Change playlist name
KeyCode::Char('e') => app.change_playlist_name()?,
_ => {} _ => {}
} }
} }

View File

@ -11,15 +11,7 @@ use ratatui::{
pub enum InputMode { pub enum InputMode {
Editing, Editing,
Normal, Normal,
} PlaylistRename,
impl InputMode {
pub fn toggle_editing_states(state: &InputMode) -> InputMode {
match state {
InputMode::Editing => return InputMode::Normal,
InputMode::Normal => return InputMode::Editing,
};
}
} }
/// Renders the user interface widgets /// Renders the user interface widgets
@ -32,7 +24,6 @@ pub fn render(app: &mut App, frame: &mut Frame) {
match app.selected_tab { match app.selected_tab {
SelectedTab::Queue => draw_queue(frame, app, layout[0]), 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::Playlists => draw_playlist_viewer(frame, app, layout[0]),
SelectedTab::DirectoryBrowser => draw_directory_browser(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 => { InputMode::Editing => {
draw_search_bar(frame, app, layout[1]); draw_search_bar(frame, app, layout[1]);
} }
InputMode::PlaylistRename => {
draw_rename_playlist(frame, app, layout[1]);
}
} }
if app.playlist_popup { 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); frame.render_stateful_widget(table, size, &mut state);
} }
// Draw search bar // Draw search bar
fn draw_search_bar(frame: &mut Frame, app: &mut App, size: Rect) { fn draw_search_bar(frame: &mut Frame, app: &mut App, size: Rect) {
match app.inputmode { // Make the cursor visible and ask ratatui to put it at the specified coordinates after
InputMode::Normal => // rendering
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here #[allow(clippy::cast_possible_truncation)]
{} frame.set_cursor(
// Draw the cursor at the current position in the input field.
InputMode::Editing => { // This position is can be controlled via the left and right arrow key
// Make the cursor visible and ask ratatui to put it at the specified coordinates after size.x + app.search_cursor_pos as u16 + 2,
// rendering // Move one line down, from the border to the input line
#[allow(clippy::cast_possible_truncation)] size.y + 1,
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,
);
}
}
let input = Paragraph::new("/".to_string() + &app.search_input) let input = Paragraph::new("/".to_string() + &app.search_input)
.style(Style::default()) .style(Style::default())
@ -399,7 +384,7 @@ fn draw_playlist_viewer(frame: &mut Frame, app: &mut App, area: Rect) {
let table = Table::new( let table = Table::new(
rows, rows,
vec![ vec![
Constraint::Percentage(40), Constraint::Min(40),
Constraint::Percentage(40), Constraint::Percentage(40),
Constraint::Percentage(20), 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); 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 { fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::vertical([ let popup_layout = Layout::vertical([
Constraint::Percentage((100 - percent_y) / 2), Constraint::Percentage((100 - percent_y) / 2),