rmptui59eed99
add to playlist feature
+159 −46
4 files changed
- M src/app.rs +19 −14
- M src/connection.rs +23 −0
- M src/handler.rs +74 −27
- M src/ui.rs +43 −5
| @@ -22,6 +22,10 @@ pub struct App { | |||
| 22 | 22 | pub inputmode: InputMode, | |
| 23 | 23 | pub search_input: String, | |
| 24 | 24 | pub cursor_position: usize, | |
| 25 | + | ||
| 26 | + | // add to playlists | |
| 27 | + | pub playlist_popup: bool, | |
| 28 | + | pub append_list: ContentList<String>, | |
| 25 | 29 | } | |
| 26 | 30 | ||
| 27 | 31 | #[derive(Debug, PartialEq, Clone)] | |
| @@ -38,6 +42,7 @@ impl App { | |||
| 38 | 42 | let mut pl_list = ContentList::new(); | |
| 39 | 43 | pl_list.list = Self::get_playlist(&mut conn.conn)?; | |
| 40 | 44 | ||
| 45 | + | let append_list = Self::get_append_list(&mut conn.conn)?; | |
| 41 | 46 | Self::get_queue(&mut conn, &mut queue_list.list); | |
| 42 | 47 | ||
| 43 | 48 | let browser = FileBrowser::new(); | |
| @@ -51,6 +56,8 @@ impl App { | |||
| 51 | 56 | inputmode: InputMode::Normal, | |
| 52 | 57 | search_input: String::new(), | |
| 53 | 58 | cursor_position: 0, | |
| 59 | + | playlist_popup: false, | |
| 60 | + | append_list, | |
| 54 | 61 | }) | |
| 55 | 62 | } | |
| 56 | 63 | ||
| @@ -91,6 +98,16 @@ impl App { | |||
| 91 | 98 | Ok(list) | |
| 92 | 99 | } | |
| 93 | 100 | ||
| 101 | + | pub fn get_append_list(conn: &mut Client) -> AppResult<ContentList<String>> { | |
| 102 | + | let mut list = ContentList::new(); | |
| 103 | + | list.list.push("Current Playlist".to_string()); | |
| 104 | + | for item in Self::get_playlist(conn)? { | |
| 105 | + | list.list.push(item.to_string()); | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | Ok(list) | |
| 109 | + | } | |
| 110 | + | ||
| 94 | 111 | pub fn update_playlist(&mut self) -> AppResult<()> { | |
| 95 | 112 | Self::get_playlist(&mut self.conn.conn)?; | |
| 96 | 113 | Ok(()) | |
| @@ -105,21 +122,9 @@ impl App { | |||
| 105 | 122 | } | |
| 106 | 123 | ||
| 107 | 124 | pub fn search_song(&mut self) -> AppResult<()> { | |
| 108 | − | let list = self | |
| 109 | − | .conn | |
| 110 | − | .songs_filenames | |
| 111 | − | .iter() | |
| 112 | − | .map(|f| f.as_str()) | |
| 113 | − | .collect::<Vec<&str>>(); | |
| 114 | − | let (filename, _) = | |
| 115 | − | rust_fuzzy_search::fuzzy_search_sorted(self.search_input.as_str(), &list) | |
| 116 | − | .get(0) | |
| 117 | − | .unwrap() | |
| 118 | − | .clone(); | |
| 119 | − | ||
| 120 | − | let song = self.conn.get_song_with_only_filename(filename); | |
| 125 | + | let filename = self.conn.get_full_path(&self.search_input)?; | |
| 126 | + | let song = self.conn.get_song_with_only_filename(&filename); | |
| 121 | 127 | self.conn.push(&song)?; | |
| 122 | − | ||
| 123 | 128 | Ok(()) | |
| 124 | 129 | } | |
| 125 | 130 | ||
| @@ -4,6 +4,8 @@ use simple_dmenu::dmenu; | |||
| 4 | 4 | use std::process::Command; | |
| 5 | 5 | use std::time::Duration; | |
| 6 | 6 | ||
| 7 | + | use crate::app::AppResult; | |
| 8 | + | ||
| 7 | 9 | pub type Result<T> = core::result::Result<T, Error>; | |
| 8 | 10 | pub type Error = Box<dyn std::error::Error>; | |
| 9 | 11 | ||
| @@ -143,6 +145,12 @@ impl Connection { | |||
| 143 | 145 | Ok(()) | |
| 144 | 146 | } | |
| 145 | 147 | ||
| 148 | + | /// Add given song to playlist | |
| 149 | + | pub fn add_to_playlist(&mut self, playlist: &str, song: &Song) -> Result<()> { | |
| 150 | + | self.conn.pl_push(playlist, song)?; | |
| 151 | + | Ok(()) | |
| 152 | + | } | |
| 153 | + | ||
| 146 | 154 | /// Given a filename, get instance of Song with only filename | |
| 147 | 155 | pub fn get_song_with_only_filename(&self, filename: &str) -> Song { | |
| 148 | 156 | Song { | |
| @@ -158,6 +166,21 @@ impl Connection { | |||
| 158 | 166 | } | |
| 159 | 167 | } | |
| 160 | 168 | ||
| 169 | + | /// Given a song name from a directory, it returns the full path of the song in the database | |
| 170 | + | pub fn get_full_path(&self, short_path: &str) -> AppResult<String> { | |
| 171 | + | let list = self | |
| 172 | + | .songs_filenames | |
| 173 | + | .iter() | |
| 174 | + | .map(|f| f.as_str()) | |
| 175 | + | .collect::<Vec<&str>>(); | |
| 176 | + | let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&short_path, &list) | |
| 177 | + | .get(0) | |
| 178 | + | .unwrap() | |
| 179 | + | .clone(); | |
| 180 | + | ||
| 181 | + | Ok(filename.to_string()) | |
| 182 | + | } | |
| 183 | + | ||
| 161 | 184 | /// Print status to stdout | |
| 162 | 185 | pub fn status(&mut self) { | |
| 163 | 186 | let current_song = self.conn.currentsong(); | |
| @@ -6,7 +6,6 @@ use crate::{ | |||
| 6 | 6 | }; | |
| 7 | 7 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; | |
| 8 | 8 | use rust_fuzzy_search::{self, fuzzy_search_sorted}; | |
| 9 | − | use simple_dmenu::dmenu; | |
| 10 | 9 | ||
| 11 | 10 | pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |
| 12 | 11 | if app.inputmode == InputMode::Editing { | |
| @@ -68,6 +67,52 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |||
| 68 | 67 | app.move_cursor_right(); | |
| 69 | 68 | } | |
| 70 | 69 | ||
| 70 | + | _ => {} | |
| 71 | + | } | |
| 72 | + | } else if app.playlist_popup { | |
| 73 | + | match key_event.code { | |
| 74 | + | KeyCode::Char('q') | KeyCode::Esc => { | |
| 75 | + | app.playlist_popup = false; | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | KeyCode::Char('j') | KeyCode::Down => app.append_list.next(), | |
| 79 | + | KeyCode::Char('k') | KeyCode::Up => app.append_list.prev(), | |
| 80 | + | ||
| 81 | + | KeyCode::Enter => { | |
| 82 | + | let pl_index = app.append_list.index; | |
| 83 | + | let pl_name = app.append_list.list.get(pl_index).unwrap(); | |
| 84 | + | ||
| 85 | + | let s_index: usize; | |
| 86 | + | let mut short_path: String = String::new(); | |
| 87 | + | match app.selected_tab { | |
| 88 | + | SelectedTab::Queue => { | |
| 89 | + | s_index = app.queue_list.index; | |
| 90 | + | short_path = app.queue_list.list.get(s_index).unwrap().to_string(); | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | SelectedTab::DirectoryBrowser => { | |
| 94 | + | let (t, f) = app.browser.filetree.get(app.browser.selected).unwrap(); | |
| 95 | + | if t == "file" { | |
| 96 | + | short_path = f.to_string(); | |
| 97 | + | } | |
| 98 | + | } | |
| 99 | + | _ => {} | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | let full_path = app.conn.get_full_path(&short_path)?; | |
| 103 | + | let song = app.conn.get_song_with_only_filename(&full_path); | |
| 104 | + | ||
| 105 | + | if pl_name == "Current Playlist" { | |
| 106 | + | app.conn.conn.push(&song)?; | |
| 107 | + | app.update_queue(); | |
| 108 | + | } else { | |
| 109 | + | app.conn.add_to_playlist(pl_name, &song)?; | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | // hide the playlist popup | |
| 113 | + | app.playlist_popup = false; | |
| 114 | + | app.append_list.index = 0; | |
| 115 | + | } | |
| 71 | 116 | _ => {} | |
| 72 | 117 | } | |
| 73 | 118 | } else { | |
| @@ -160,32 +205,34 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |||
| 160 | 205 | ||
| 161 | 206 | // add to queue | |
| 162 | 207 | KeyCode::Char('a') => { | |
| 163 | − | let list = app | |
| 164 | − | .conn | |
| 165 | − | .songs_filenames | |
| 166 | − | .iter() | |
| 167 | − | .map(|f| f.as_str()) | |
| 168 | − | .collect::<Vec<&str>>(); | |
| 169 | − | ||
| 170 | − | let files: Vec<String> = app | |
| 171 | − | .browser | |
| 172 | − | .filetree | |
| 173 | − | .clone() | |
| 174 | − | .into_iter() | |
| 175 | − | .map(|(_, f)| f) | |
| 176 | − | .collect(); | |
| 177 | − | ||
| 178 | − | let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted( | |
| 179 | − | &files.get(app.browser.selected).unwrap(), | |
| 180 | − | &list, | |
| 181 | − | ) | |
| 182 | − | .get(0) | |
| 183 | − | .unwrap() | |
| 184 | − | .clone(); | |
| 185 | − | ||
| 186 | − | let song = app.conn.get_song_with_only_filename(filename); | |
| 187 | − | ||
| 188 | − | app.conn.conn.push(&song)?; | |
| 208 | + | // let list = app | |
| 209 | + | // .conn | |
| 210 | + | // .songs_filenames | |
| 211 | + | // .iter() | |
| 212 | + | // .map(|f| f.as_str()) | |
| 213 | + | // .collect::<Vec<&str>>(); | |
| 214 | + | // | |
| 215 | + | // let files: Vec<String> = app | |
| 216 | + | // .browser | |
| 217 | + | // .filetree | |
| 218 | + | // .clone() | |
| 219 | + | // .into_iter() | |
| 220 | + | // .map(|(_, f)| f) | |
| 221 | + | // .collect(); | |
| 222 | + | // | |
| 223 | + | // let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted( | |
| 224 | + | // &files.get(app.browser.selected).unwrap(), | |
| 225 | + | // &list, | |
| 226 | + | // ) | |
| 227 | + | // .get(0) | |
| 228 | + | // .unwrap() | |
| 229 | + | // .clone(); | |
| 230 | + | // | |
| 231 | + | // let song = app.conn.get_song_with_only_filename(filename); | |
| 232 | + | // | |
| 233 | + | // app.conn.conn.push(&song)?; | |
| 234 | + | ||
| 235 | + | app.playlist_popup = true; | |
| 189 | 236 | } | |
| 190 | 237 | ||
| 191 | 238 | KeyCode::Right => { | |
| @@ -21,11 +21,6 @@ impl InputMode { | |||
| 21 | 21 | ||
| 22 | 22 | /// Renders the user interface widgets | |
| 23 | 23 | pub fn render(app: &mut App, frame: &mut Frame) { | |
| 24 | − | // This is where you add new widgets. | |
| 25 | − | // See the following resources: | |
| 26 | − | // - https://docs.rs/ratatui/latest/ratatui/widgets/index.html | |
| 27 | − | // - https://github.com/ratatui-org/ratatui/tree/master/examples | |
| 28 | − | ||
| 29 | 24 | // Layout | |
| 30 | 25 | let layout = Layout::default() | |
| 31 | 26 | .direction(Direction::Vertical) | |
| @@ -46,6 +41,10 @@ pub fn render(app: &mut App, frame: &mut Frame) { | |||
| 46 | 41 | draw_search_bar(frame, app, layout[1]); | |
| 47 | 42 | } | |
| 48 | 43 | } | |
| 44 | + | ||
| 45 | + | if app.playlist_popup { | |
| 46 | + | draw_add_to_playlist(frame, app, layout[0]); | |
| 47 | + | } | |
| 49 | 48 | } | |
| 50 | 49 | ||
| 51 | 50 | /// Draws the file tree browser | |
| @@ -238,3 +237,42 @@ fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) { | |||
| 238 | 237 | ||
| 239 | 238 | frame.render_widget(progress_bar, size); | |
| 240 | 239 | } | |
| 240 | + | ||
| 241 | + | fn draw_add_to_playlist(frame: &mut Frame, app: &mut App, area: Rect) { | |
| 242 | + | let area = centered_rect(40, 50, area); | |
| 243 | + | let mut state = ListState::default(); | |
| 244 | + | let title = Block::default() | |
| 245 | + | .title(Title::from("Add Selected Item to: ")) | |
| 246 | + | .title(Title::from("<Esc> to Cancel".green().bold()).alignment(Alignment::Right)); | |
| 247 | + | let list = List::new(app.append_list.list.clone()) | |
| 248 | + | .block(title.borders(Borders::ALL)) | |
| 249 | + | .highlight_style( | |
| 250 | + | Style::new() | |
| 251 | + | .fg(Color::Cyan) | |
| 252 | + | .bg(Color::Black) | |
| 253 | + | .add_modifier(Modifier::BOLD) | |
| 254 | + | .add_modifier(Modifier::REVERSED), | |
| 255 | + | ) | |
| 256 | + | .highlight_symbol(">>") | |
| 257 | + | .repeat_highlight_symbol(true); | |
| 258 | + | ||
| 259 | + | state.select(Some(app.append_list.index)); | |
| 260 | + | frame.render_widget(Clear, area); //this clears out the background | |
| 261 | + | frame.render_stateful_widget(list, area, &mut state); | |
| 262 | + | } | |
| 263 | + | ||
| 264 | + | fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { | |
| 265 | + | let popup_layout = Layout::vertical([ | |
| 266 | + | Constraint::Percentage((100 - percent_y) / 2), | |
| 267 | + | Constraint::Percentage(percent_y), | |
| 268 | + | Constraint::Percentage((100 - percent_y) / 2), | |
| 269 | + | ]) | |
| 270 | + | .split(r); | |
| 271 | + | ||
| 272 | + | Layout::horizontal([ | |
| 273 | + | Constraint::Percentage((100 - percent_x) / 2), | |
| 274 | + | Constraint::Percentage(percent_x), | |
| 275 | + | Constraint::Percentage((100 - percent_x) / 2), | |
| 276 | + | ]) | |
| 277 | + | .split(popup_layout[1])[1] | |
| 278 | + | } | |