add rename playlists feature
This commit is contained in:
parent
e95e1bbb36
commit
32a3c60471
|
|
@ -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
|
||||
|
|
|
|||
126
src/app.rs
126
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<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();
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()?,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
65
src/ui.rs
65
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),
|
||||
|
|
|
|||
Reference in New Issue