Krolyxon
Commit 32a3c60

rmptui32a3c60

add rename playlists feature

Author
krolxon
Date
2024-04-30 22:40 +0530
Parent
e95e1bb
Commit
32a3c604717c99e498a20069bc8895ae9deb0cb9

+181 −65

4 files changed

  1. M README.md +2 −2
  2. M src/app.rs +101 −25
  3. M src/handler.rs +43 −8
  4. M src/ui.rs +35 −30
M README.md View
@@ -39,5 +39,5 @@ A MPD client in Rust
3939 - [x] search for songs
4040 - [x] Human readable time format
4141 - [x] metadata based tree view
42−- [ ] view playlist
43−- [ ] change playlist name
42+- [x] view playlist
43+- [x] change playlist name
M src/app.rs View
@@ -21,9 +21,11 @@ pub struct App {
2121 pub selected_tab: SelectedTab, // Used to switch between tabs
2222
2323 // Search
24− pub inputmode: InputMode, // Defines input mode, Normal or Search
25− pub search_input: String, // Stores the userinput to be searched
26− pub cursor_position: usize, // Stores the cursor position
24+ pub inputmode: InputMode, // Defines input mode, Normal or Search
25+ pub search_input: String, // Stores the userinput to be searched
26+ pub search_cursor_pos: usize, // Stores the cursor position for searching
27+ pub pl_newname_input: String, // Stores the new name of the playlist
28+ pub pl_cursor_pos: usize, // Stores the cursor position for renaming playlist
2729
2830 // playlist variables
2931 // used to show playlist popup
@@ -59,7 +61,9 @@ impl App {
5961 browser,
6062 inputmode: InputMode::Normal,
6163 search_input: String::new(),
62− cursor_position: 0,
64+ pl_newname_input: String::new(),
65+ search_cursor_pos: 0,
66+ pl_cursor_pos: 0,
6367 playlist_popup: false,
6468 append_list,
6569 })
@@ -97,7 +101,7 @@ impl App {
97101 Self::get_queue(&mut self.conn, &mut self.queue_list.list);
98102 }
99103
100− fn get_playlist(conn: &mut Client) -> AppResult<Vec<String>> {
104+ pub fn get_playlist(conn: &mut Client) -> AppResult<Vec<String>> {
101105 let list: Vec<String> = conn.playlists()?.iter().map(|p| p.clone().name).collect();
102106 Ok(list)
103107 }
@@ -220,49 +224,111 @@ impl App {
220224
221225 // Cursor movements
222226 pub fn move_cursor_left(&mut self) {
223− let cursor_moved_left = self.cursor_position.saturating_sub(1);
224− self.cursor_position = self.clamp_cursor(cursor_moved_left);
227+ match self.inputmode {
228+ InputMode::PlaylistRename => {
229+ let cursor_moved_left = self.pl_cursor_pos.saturating_sub(1);
230+ self.pl_cursor_pos = self.clamp_cursor(cursor_moved_left);
231+ }
232+ InputMode::Editing => {
233+ let cursor_moved_left = self.search_cursor_pos.saturating_sub(1);
234+ self.search_cursor_pos = self.clamp_cursor(cursor_moved_left);
235+ }
236+ _ => {}
237+ }
225238 }
226239
227240 pub fn move_cursor_right(&mut self) {
228− let cursor_moved_right = self.cursor_position.saturating_add(1);
229− self.cursor_position = self.clamp_cursor(cursor_moved_right);
241+ match self.inputmode {
242+ InputMode::PlaylistRename => {
243+ let cursor_moved_right = self.pl_cursor_pos.saturating_add(1);
244+ self.pl_cursor_pos = self.clamp_cursor(cursor_moved_right);
245+ }
246+ InputMode::Editing => {
247+ let cursor_moved_right = self.search_cursor_pos.saturating_add(1);
248+ self.search_cursor_pos = self.clamp_cursor(cursor_moved_right);
249+ }
250+ _ => {}
251+ }
230252 }
231253
232254 pub fn enter_char(&mut self, new_char: char) {
233− self.search_input.insert(self.cursor_position, new_char);
234−
235− self.move_cursor_right();
255+ match self.inputmode {
256+ InputMode::PlaylistRename => {
257+ self.pl_newname_input.insert(self.pl_cursor_pos, new_char);
258+ self.move_cursor_right();
259+ }
260+ InputMode::Editing => {
261+ self.search_input.insert(self.search_cursor_pos, new_char);
262+ self.move_cursor_right();
263+ }
264+ _ => {}
265+ }
236266 }
237267
238268 pub fn delete_char(&mut self) {
239− let is_not_cursor_leftmost = self.cursor_position != 0;
269+ let is_not_cursor_leftmost = match self.inputmode {
270+ InputMode::PlaylistRename => self.pl_cursor_pos != 0,
271+ InputMode::Editing => self.search_cursor_pos != 0,
272+ _ => false,
273+ };
274+
240275 if is_not_cursor_leftmost {
241276 // Method "remove" is not used on the saved text for deleting the selected char.
242277 // Reason: Using remove on String works on bytes instead of the chars.
243278 // Using remove would require special care because of char boundaries.
244279
245− let current_index = self.cursor_position;
246− let from_left_to_current_index = current_index - 1;
280+ let current_index = match self.inputmode {
281+ InputMode::Editing => self.search_cursor_pos,
282+ InputMode::PlaylistRename => self.pl_cursor_pos,
283+ _ => 0,
284+ };
247285
248− // Getting all characters before the selected character.
249− let before_char_to_delete = self.search_input.chars().take(from_left_to_current_index);
250− // Getting all characters after selected character.
251− let after_char_to_delete = self.search_input.chars().skip(current_index);
286+ let from_left_to_current_index = current_index - 1;
252287
253− // Put all characters together except the selected one.
254− // By leaving the selected one out, it is forgotten and therefore deleted.
255− self.search_input = before_char_to_delete.chain(after_char_to_delete).collect();
256− self.move_cursor_left();
288+ if self.inputmode == InputMode::PlaylistRename {
289+ // Getting all characters before the selected character.
290+ let before_char_to_delete = self
291+ .pl_newname_input
292+ .chars()
293+ .take(from_left_to_current_index);
294+ // Getting all characters after selected character.
295+ let after_char_to_delete = self.pl_newname_input.chars().skip(current_index);
296+ // Put all characters together except the selected one.
297+ // By leaving the selected one out, it is forgotten and therefore deleted.
298+ self.pl_newname_input = before_char_to_delete.chain(after_char_to_delete).collect();
299+ self.move_cursor_left();
300+ } else if self.inputmode == InputMode::Editing {
301+ // Getting all characters before the selected character.
302+ let before_char_to_delete =
303+ self.search_input.chars().take(from_left_to_current_index);
304+ // Getting all characters after selected character.
305+ let after_char_to_delete = self.search_input.chars().skip(current_index);
306+ // Put all characters together except the selected one.
307+ // By leaving the selected one out, it is forgotten and therefore deleted.
308+ self.search_input = before_char_to_delete.chain(after_char_to_delete).collect();
309+ self.move_cursor_left();
310+ }
257311 }
258312 }
259313
260314 pub fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
261− new_cursor_pos.clamp(0, self.search_input.len())
315+ match self.inputmode {
316+ InputMode::PlaylistRename => new_cursor_pos.clamp(0, self.pl_newname_input.len()),
317+ InputMode::Editing => new_cursor_pos.clamp(0, self.search_input.len()),
318+ _ => 0,
319+ }
262320 }
263321
264322 pub fn reset_cursor(&mut self) {
265− self.cursor_position = 0;
323+ match self.inputmode {
324+ InputMode::Editing => {
325+ self.search_cursor_pos = 0;
326+ }
327+ InputMode::PlaylistRename => {
328+ self.pl_cursor_pos = 0;
329+ }
330+ _ => {}
331+ }
266332 }
267333
268334 /// Given time in seconds, convert it to hh:mm:ss
@@ -277,4 +343,14 @@ impl App {
277343 format!("{:02}:{:02}:{:02}", h, m, s)
278344 }
279345 }
346+
347+ pub fn change_playlist_name(&mut self) -> AppResult<()> {
348+ match self.selected_tab {
349+ SelectedTab::Playlists => {
350+ self.inputmode = InputMode::PlaylistRename;
351+ }
352+ _ => {}
353+ }
354+ Ok(())
355+ }
280356 }
M src/handler.rs View
@@ -92,8 +92,8 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
9292 }
9393
9494 app.search_input.clear();
95− app.inputmode = InputMode::Normal;
9695 app.reset_cursor();
96+ app.inputmode = InputMode::Normal;
9797 }
9898
9999 KeyCode::Backspace => {
@@ -110,10 +110,41 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
110110
111111 _ => {}
112112 }
113+ } else if app.inputmode == InputMode::PlaylistRename {
114+ match key_event.code {
115+ KeyCode::Esc => {
116+ app.pl_newname_input.clear();
117+ app.reset_cursor();
118+ app.inputmode = InputMode::Normal;
119+ }
120+ KeyCode::Char(to_insert) => {
121+ app.enter_char(to_insert);
122+ }
123+ KeyCode::Enter => {
124+ app.conn.conn.pl_rename(app.pl_list.list.get(app.pl_list.index).unwrap(), &app.pl_newname_input)?;
125+ app.pl_list.list = App::get_playlist(&mut app.conn.conn)?;
126+ app.pl_newname_input.clear();
127+ app.reset_cursor();
128+ app.inputmode = InputMode::Normal;
129+ }
130+
131+ KeyCode::Backspace => {
132+ app.delete_char();
133+ }
113134
114− // Playlist popup keybinds
115− //
116− // Keybind for when the "append to playlist" popup is visible
135+ KeyCode::Left => {
136+ app.move_cursor_left();
137+ }
138+
139+ KeyCode::Right => {
140+ app.move_cursor_right();
141+ }
142+
143+ _ => {}
144+ }
145+ // Playlist popup keybinds
146+ //
147+ // Keybind for when the "append to playlist" popup is visible
117148 } else if app.playlist_popup {
118149 match key_event.code {
119150 KeyCode::Char('q') | KeyCode::Esc => {
@@ -358,7 +389,11 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
358389
359390 // Search for songs
360391 KeyCode::Char('/') => {
361− app.inputmode = InputMode::toggle_editing_states(&app.inputmode);
392+ if app.inputmode == InputMode::Normal {
393+ app.inputmode = InputMode::Editing;
394+ } else {
395+ app.inputmode = InputMode::Normal;
396+ }
362397 }
363398
364399 // Remove from Current Playlsit
@@ -366,9 +401,6 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
366401 app.handle_add_or_remove_from_current_playlist()?;
367402 }
368403
369− // Change playlist name
370− KeyCode::Char('e') => if app.selected_tab == SelectedTab::Playlists {},
371−
372404 // go to top of list
373405 KeyCode::Char('g') => match app.selected_tab {
374406 SelectedTab::Queue => app.queue_list.index = 0,
@@ -384,6 +416,9 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
384416 }
385417 SelectedTab::Playlists => app.pl_list.index = app.pl_list.list.len() - 1,
386418 },
419+
420+ // Change playlist name
421+ KeyCode::Char('e') => app.change_playlist_name()?,
387422 _ => {}
388423 }
389424 }
M src/ui.rs View
@@ -11,15 +11,7 @@ use ratatui::{
1111 pub enum InputMode {
1212 Editing,
1313 Normal,
14−}
15−
16−impl InputMode {
17− pub fn toggle_editing_states(state: &InputMode) -> InputMode {
18− match state {
19− InputMode::Editing => return InputMode::Normal,
20− InputMode::Normal => return InputMode::Editing,
21− };
22− }
14+ PlaylistRename,
2315 }
2416
2517 /// Renders the user interface widgets
@@ -32,7 +24,6 @@ pub fn render(app: &mut App, frame: &mut Frame) {
3224
3325 match app.selected_tab {
3426 SelectedTab::Queue => draw_queue(frame, app, layout[0]),
35− // SelectedTab::Playlists => draw_playlists(frame, app, layout[0]),
3627 SelectedTab::Playlists => draw_playlist_viewer(frame, app, layout[0]),
3728 SelectedTab::DirectoryBrowser => draw_directory_browser(frame, app, layout[0]),
3829 }
@@ -44,6 +35,9 @@ pub fn render(app: &mut App, frame: &mut Frame) {
4435 InputMode::Editing => {
4536 draw_search_bar(frame, app, layout[1]);
4637 }
38+ InputMode::PlaylistRename => {
39+ draw_rename_playlist(frame, app, layout[1]);
40+ }
4741 }
4842
4943 if app.playlist_popup {
@@ -254,27 +248,18 @@ fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) {
254248 frame.render_stateful_widget(table, size, &mut state);
255249 }
256250
257−
258251 // Draw search bar
259252 fn draw_search_bar(frame: &mut Frame, app: &mut App, size: Rect) {
260− match app.inputmode {
261− InputMode::Normal =>
262− // Hide the cursor. `Frame` does this by default, so we don't need to do anything here
263− {}
264−
265− InputMode::Editing => {
266− // Make the cursor visible and ask ratatui to put it at the specified coordinates after
267− // rendering
268− #[allow(clippy::cast_possible_truncation)]
269− frame.set_cursor(
270− // Draw the cursor at the current position in the input field.
271− // This position is can be controlled via the left and right arrow key
272− size.x + app.cursor_position as u16 + 2,
273− // Move one line down, from the border to the input line
274− size.y + 1,
275− );
276− }
277− }
253+ // Make the cursor visible and ask ratatui to put it at the specified coordinates after
254+ // rendering
255+ #[allow(clippy::cast_possible_truncation)]
256+ frame.set_cursor(
257+ // Draw the cursor at the current position in the input field.
258+ // This position is can be controlled via the left and right arrow key
259+ size.x + app.search_cursor_pos as u16 + 2,
260+ // Move one line down, from the border to the input line
261+ size.y + 1,
262+ );
278263
279264 let input = Paragraph::new("/".to_string() + &app.search_input)
280265 .style(Style::default())
@@ -399,7 +384,7 @@ fn draw_playlist_viewer(frame: &mut Frame, app: &mut App, area: Rect) {
399384 let table = Table::new(
400385 rows,
401386 vec![
402− Constraint::Percentage(40),
387+ Constraint::Min(40),
403388 Constraint::Percentage(40),
404389 Constraint::Percentage(20),
405390 ],
@@ -439,6 +424,26 @@ fn draw_add_to_playlist(frame: &mut Frame, app: &mut App, area: Rect) {
439424 frame.render_stateful_widget(list, area, &mut state);
440425 }
441426
427+fn draw_rename_playlist(frame: &mut Frame, app: &mut App, area: Rect) {
428+ #[allow(clippy::cast_possible_truncation)]
429+ frame.set_cursor(
430+ // Draw the cursor at the current position in the input field.
431+ // This position is can be controlled via the left and right arrow key
432+ area.x + app.pl_cursor_pos as u16 + 2,
433+ // Move one line down, from the border to the input line
434+ area.y + 1,
435+ );
436+
437+ let input = Paragraph::new("/".to_string() + &app.pl_newname_input)
438+ .style(Style::default())
439+ .block(
440+ Block::default()
441+ .borders(Borders::ALL)
442+ .title("Enter New Name: ".bold().green()),
443+ );
444+ frame.render_widget(input, area);
445+}
446+
442447 fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
443448 let popup_layout = Layout::vertical([
444449 Constraint::Percentage((100 - percent_y) / 2),