rmptuif665c4e
better event handling with tick, search
+107 −33
6 files changed
- M Cargo.lock +7 −0
- M Cargo.toml +1 −0
- M src/app.rs +8 −3
- M src/connection.rs +27 −0
- M src/handler.rs +26 −6
- M src/ui.rs +38 −24
| @@ -388,10 +388,17 @@ dependencies = [ | |||
| 388 | 388 | "crossterm", | |
| 389 | 389 | "mpd", | |
| 390 | 390 | "ratatui", | |
| 391 | + | "rust-fuzzy-search", | |
| 391 | 392 | "rust_fzf", | |
| 392 | 393 | "simple-dmenu", | |
| 393 | 394 | ] | |
| 394 | 395 | ||
| 396 | + | [[package]] | |
| 397 | + | name = "rust-fuzzy-search" | |
| 398 | + | version = "0.1.1" | |
| 399 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 400 | + | checksum = "a157657054ffe556d8858504af8a672a054a6e0bd9e8ee531059100c0fa11bb2" | |
| 401 | + | ||
| 395 | 402 | [[package]] | |
| 396 | 403 | name = "rust_fzf" | |
| 397 | 404 | version = "0.3.1" | |
| @@ -12,3 +12,4 @@ rust_fzf = "0.3.1" | |||
| 12 | 12 | simple-dmenu = "0.1.0" | |
| 13 | 13 | ratatui = "0.26.2" | |
| 14 | 14 | crossterm = "0.27.0" | |
| 15 | + | rust-fuzzy-search = "0.1.1" | |
| @@ -1,3 +1,5 @@ | |||
| 1 | + | use std::time::Duration; | |
| 2 | + | ||
| 1 | 3 | use crate::connection::Connection; | |
| 2 | 4 | use crate::list::ContentList; | |
| 3 | 5 | use mpd::Client; | |
| @@ -56,7 +58,11 @@ impl App { | |||
| 56 | 58 | }) | |
| 57 | 59 | } | |
| 58 | 60 | ||
| 59 | − | pub fn tick(&self) {} | |
| 61 | + | pub fn tick(&mut self) { | |
| 62 | + | self.conn.update_state(); | |
| 63 | + | self.conn.update_progress(); | |
| 64 | + | self.update_queue(); | |
| 65 | + | } | |
| 60 | 66 | ||
| 61 | 67 | pub fn quit(&mut self) { | |
| 62 | 68 | self.running = false; | |
| @@ -76,8 +82,7 @@ impl App { | |||
| 76 | 82 | // }); | |
| 77 | 83 | conn.conn.queue().unwrap().into_iter().for_each(|x| { | |
| 78 | 84 | vec.push(x.file); | |
| 79 | − | } | |
| 80 | − | ); | |
| 85 | + | }); | |
| 81 | 86 | } | |
| 82 | 87 | ||
| 83 | 88 | pub fn update_queue(&mut self) { | |
| @@ -2,6 +2,7 @@ use mpd::song::Song; | |||
| 2 | 2 | use mpd::{Client, State}; | |
| 3 | 3 | use simple_dmenu::dmenu; | |
| 4 | 4 | use std::process::Command; | |
| 5 | + | use std::time::Duration; | |
| 5 | 6 | ||
| 6 | 7 | pub type Result<T> = core::result::Result<T, Error>; | |
| 7 | 8 | pub type Error = Box<dyn std::error::Error>; | |
| @@ -11,6 +12,8 @@ pub struct Connection { | |||
| 11 | 12 | pub conn: Client, | |
| 12 | 13 | pub songs_filenames: Vec<String>, | |
| 13 | 14 | pub state: String, | |
| 15 | + | pub elapsed: Duration, | |
| 16 | + | pub total_duration: Duration, | |
| 14 | 17 | } | |
| 15 | 18 | ||
| 16 | 19 | impl Connection { | |
| @@ -24,10 +27,14 @@ impl Connection { | |||
| 24 | 27 | .map(|x| x.file) | |
| 25 | 28 | .collect(); | |
| 26 | 29 | ||
| 30 | + | let (elapsed, total) = conn.status().unwrap().time.unwrap_or_default(); | |
| 31 | + | ||
| 27 | 32 | Ok(Self { | |
| 28 | 33 | conn, | |
| 29 | 34 | songs_filenames, | |
| 30 | 35 | state: "Stopped".to_string(), | |
| 36 | + | elapsed, | |
| 37 | + | total_duration: total, | |
| 31 | 38 | }) | |
| 32 | 39 | } | |
| 33 | 40 | ||
| @@ -64,6 +71,26 @@ impl Connection { | |||
| 64 | 71 | self.state.clone() | |
| 65 | 72 | } | |
| 66 | 73 | ||
| 74 | + | pub fn update_progress(&mut self) { | |
| 75 | + | let (elapsed, total) = self.conn.status().unwrap().time.unwrap_or_default(); | |
| 76 | + | self.elapsed = elapsed; | |
| 77 | + | self.total_duration = total; | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | pub fn get_progress_ratio(&self) -> f64 { | |
| 81 | + | let total = self.total_duration.as_secs_f64(); | |
| 82 | + | if total == 0.0 { | |
| 83 | + | 0.0 | |
| 84 | + | } else { | |
| 85 | + | let ratio = self.elapsed.as_secs_f64() / self.total_duration.as_secs_f64(); | |
| 86 | + | if ratio > 1.0 || ratio == 0.0 { | |
| 87 | + | 1.0 | |
| 88 | + | } else { | |
| 89 | + | ratio | |
| 90 | + | } | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | ||
| 67 | 94 | /// push the given song to queue | |
| 68 | 95 | pub fn push(&mut self, song: &Song) -> Result<()> { | |
| 69 | 96 | if self.conn.queue().unwrap().is_empty() { | |
| @@ -2,6 +2,10 @@ use std::time::Duration; | |||
| 2 | 2 | ||
| 3 | 3 | use crate::app::{App, AppResult, SelectedTab}; | |
| 4 | 4 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; | |
| 5 | + | use mpd::{Query, Term}; | |
| 6 | + | use ratatui::style::Modifier; | |
| 7 | + | use rust_fuzzy_search; | |
| 8 | + | use simple_dmenu::dmenu; | |
| 5 | 9 | ||
| 6 | 10 | pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |
| 7 | 11 | match key_event.code { | |
| @@ -39,7 +43,6 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |||
| 39 | 43 | app.queue_list.list.get(app.queue_list.index).unwrap(), | |
| 40 | 44 | ); | |
| 41 | 45 | app.conn.push(&song)?; | |
| 42 | − | ||
| 43 | 46 | } | |
| 44 | 47 | SelectedTab::Playlists => { | |
| 45 | 48 | app.conn | |
| @@ -52,19 +55,16 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |||
| 52 | 55 | // Toggle Pause | |
| 53 | 56 | KeyCode::Char('p') => { | |
| 54 | 57 | app.conn.toggle_pause(); | |
| 55 | − | app.conn.update_state(); | |
| 56 | 58 | } | |
| 57 | 59 | ||
| 58 | 60 | // Pause | |
| 59 | 61 | KeyCode::Char('s') => { | |
| 60 | 62 | app.conn.pause(); | |
| 61 | − | app.conn.update_state(); | |
| 62 | 63 | } | |
| 63 | 64 | ||
| 64 | 65 | // Clear Queue | |
| 65 | 66 | KeyCode::Char('x') => { | |
| 66 | 67 | app.conn.conn.clear()?; | |
| 67 | − | app.conn.update_state(); | |
| 68 | 68 | // app.update_queue(); | |
| 69 | 69 | } | |
| 70 | 70 | ||
| @@ -73,7 +73,6 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |||
| 73 | 73 | app.conn.play_dmenu()?; | |
| 74 | 74 | } | |
| 75 | 75 | ||
| 76 | − | ||
| 77 | 76 | // add to queue | |
| 78 | 77 | KeyCode::Char('a') => { | |
| 79 | 78 | let song = app.conn.get_song_with_only_filename( | |
| @@ -82,7 +81,6 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |||
| 82 | 81 | app.conn.conn.push(&song)?; | |
| 83 | 82 | } | |
| 84 | 83 | ||
| 85 | − | ||
| 86 | 84 | KeyCode::Right => { | |
| 87 | 85 | app.conn | |
| 88 | 86 | .push_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?; | |
| @@ -138,6 +136,28 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { | |||
| 138 | 136 | // Delete highlighted song from the queue | |
| 139 | 137 | KeyCode::Char('d') => { | |
| 140 | 138 | app.conn.conn.delete(app.queue_list.index as u32)?; | |
| 139 | + | app.update_queue(); | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | KeyCode::Char('U') => { | |
| 143 | + | app.conn.conn.update()?; | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | KeyCode::Char('L') => { | |
| 147 | + | let str = dmenu!(prompt "Search: "); | |
| 148 | + | let list = app | |
| 149 | + | .conn | |
| 150 | + | .songs_filenames | |
| 151 | + | .iter() | |
| 152 | + | .map(|f| f.as_str()) | |
| 153 | + | .collect::<Vec<&str>>(); | |
| 154 | + | let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&str, &list) | |
| 155 | + | .get(0) | |
| 156 | + | .unwrap() | |
| 157 | + | .clone(); | |
| 158 | + | ||
| 159 | + | let song = app.conn.get_song_with_only_filename(filename); | |
| 160 | + | app.conn.push(&song)?; | |
| 141 | 161 | } | |
| 142 | 162 | ||
| 143 | 163 | _ => {} | |
| @@ -1,7 +1,6 @@ | |||
| 1 | 1 | use crate::app::{App, AppResult, SelectedTab}; | |
| 2 | 2 | use ratatui::{ | |
| 3 | 3 | prelude::*, | |
| 4 | − | style::palette::tailwind, | |
| 5 | 4 | widgets::{block::Title, *}, | |
| 6 | 5 | }; | |
| 7 | 6 | ||
| @@ -16,8 +15,9 @@ pub fn render(app: &mut App, frame: &mut Frame) { | |||
| 16 | 15 | let layout = Layout::default() | |
| 17 | 16 | .direction(Direction::Vertical) | |
| 18 | 17 | .constraints(vec![ | |
| 19 | − | Constraint::Percentage(5), | |
| 20 | − | Constraint::Percentage(88), | |
| 18 | + | Constraint::Percentage(0), | |
| 19 | + | // Constraint::Percentage(88), | |
| 20 | + | Constraint::Percentage(93), | |
| 21 | 21 | Constraint::Percentage(7), | |
| 22 | 22 | ]) | |
| 23 | 23 | .split(frame.size()); | |
| @@ -37,30 +37,35 @@ pub fn render(app: &mut App, frame: &mut Frame) { | |||
| 37 | 37 | // draw_playlists(frame, app, inner_layout[1]); | |
| 38 | 38 | draw_progress_bar(frame, app, layout[2]); | |
| 39 | 39 | ||
| 40 | − | let highlight_style = (Color::default(), tailwind::YELLOW.c700); | |
| 41 | − | let tab = Tabs::new(vec!["Songs List", "Play Queue", "Playlists"]) | |
| 42 | − | .block(Block::default().title("Tabs").borders(Borders::ALL)) | |
| 43 | − | .style(Style::default().white()) | |
| 44 | − | .highlight_style(highlight_style) | |
| 45 | − | .divider(" ") | |
| 46 | − | .select(app.selected_tab.clone() as usize) | |
| 47 | − | .padding("", ""); | |
| 48 | − | frame.render_widget(tab, layout[0]); | |
| 40 | + | // let highlight_style = (Color::default(), tailwind::YELLOW.c700); | |
| 41 | + | // let tab = Tabs::new(vec!["Songs List", "Play Queue", "Playlists"]) | |
| 42 | + | // .block(Block::default().title("Tabs").borders(Borders::ALL)) | |
| 43 | + | // .style(Style::default().white()) | |
| 44 | + | // .highlight_style(highlight_style) | |
| 45 | + | // .divider(" ") | |
| 46 | + | // .select(app.selected_tab.clone() as usize) | |
| 47 | + | // .padding("", ""); | |
| 48 | + | // frame.render_widget(tab, layout[0]); | |
| 49 | 49 | ||
| 50 | 50 | match app.selected_tab { | |
| 51 | 51 | SelectedTab::SongList => draw_song_list(frame, app, layout[1]), | |
| 52 | − | SelectedTab::Queue => draw_queue(frame, app, layout[1]), | |
| 53 | − | SelectedTab::Playlists => draw_playlists(frame, app, layout[1]), | |
| 52 | + | SelectedTab::Queue => draw_queue(frame, app, layout[1]), | |
| 53 | + | SelectedTab::Playlists => draw_playlists(frame, app, layout[1]), | |
| 54 | 54 | } | |
| 55 | 55 | } | |
| 56 | 56 | ||
| 57 | 57 | /// draws list of songs | |
| 58 | 58 | fn draw_song_list(frame: &mut Frame, app: &mut App, size: Rect) { | |
| 59 | 59 | let mut song_state = ListState::default(); | |
| 60 | + | let total_songs = app.conn.conn.stats().unwrap().songs.to_string(); | |
| 60 | 61 | let list = List::new(app.conn.songs_filenames.clone()) | |
| 61 | 62 | .block( | |
| 62 | 63 | Block::default() | |
| 63 | 64 | .title("Song List".green().bold()) | |
| 65 | + | .title( | |
| 66 | + | Title::from(format!("Total Songs: {}", total_songs).bold().green()) | |
| 67 | + | .alignment(Alignment::Right), | |
| 68 | + | ) | |
| 64 | 69 | .borders(Borders::ALL), | |
| 65 | 70 | ) | |
| 66 | 71 | .highlight_style(Style::new().add_modifier(Modifier::REVERSED)) | |
| @@ -74,16 +79,13 @@ fn draw_song_list(frame: &mut Frame, app: &mut App, size: Rect) { | |||
| 74 | 79 | /// draws playing queue | |
| 75 | 80 | fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) { | |
| 76 | 81 | let mut queue_state = ListState::default(); | |
| 77 | − | let title = Block::default() | |
| 78 | − | .title(Title::from("Play Queue".green().bold())) | |
| 79 | − | .title("Shift + ▲ ▼ to scroll, Shift + Enter to play".yellow()); | |
| 82 | + | let title = Block::default().title(Title::from("Play Queue".green().bold())); | |
| 80 | 83 | let list = List::new(app.queue_list.list.clone()) | |
| 81 | 84 | .block(title.borders(Borders::ALL)) | |
| 82 | 85 | .highlight_style(Style::new().add_modifier(Modifier::REVERSED)) | |
| 83 | 86 | .highlight_symbol(">>") | |
| 84 | 87 | .repeat_highlight_symbol(true); | |
| 85 | 88 | ||
| 86 | − | app.update_queue(); | |
| 87 | 89 | queue_state.select(Some(app.queue_list.index)); | |
| 88 | 90 | frame.render_stateful_widget(list, size, &mut queue_state); | |
| 89 | 91 | } | |
| @@ -92,9 +94,7 @@ fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) { | |||
| 92 | 94 | fn draw_playlists(frame: &mut Frame, app: &mut App, size: Rect) { | |
| 93 | 95 | let mut state = ListState::default(); | |
| 94 | 96 | ||
| 95 | − | let title = Block::default() | |
| 96 | − | .title(Title::from("Playlist".green().bold())) | |
| 97 | − | .title("▲ ▼ to scroll, Use ► to add playlist to queue".yellow()); | |
| 97 | + | let title = Block::default().title(Title::from("Playlist".green().bold())); | |
| 98 | 98 | let list = List::new(app.pl_list.list.clone()) | |
| 99 | 99 | .block(title.borders(Borders::ALL)) | |
| 100 | 100 | .highlight_style(Style::new().add_modifier(Modifier::REVERSED)) | |
| @@ -118,8 +118,22 @@ fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) { | |||
| 118 | 118 | ||
| 119 | 119 | let title = Block::default() | |
| 120 | 120 | .title(Title::from(format!("{}: ", state).red().bold())) | |
| 121 | − | .title(Title::from(song.green().bold())); | |
| 122 | − | // .title(Title::from(app.conn.conn.status().unwrap_or_default().volume.to_string().yellow())).title_alignment(Alignment::Right); | |
| 121 | + | .title(Title::from(song.green().bold())) | |
| 122 | + | .title( | |
| 123 | + | Title::from( | |
| 124 | + | format!( | |
| 125 | + | "{}/{}", | |
| 126 | + | app.conn.elapsed.as_secs(), | |
| 127 | + | app.conn.total_duration.as_secs() | |
| 128 | + | ) | |
| 129 | + | .cyan() | |
| 130 | + | .bold(), | |
| 131 | + | ) | |
| 132 | + | .alignment(Alignment::Right), | |
| 133 | + | ) | |
| 134 | + | .borders(Borders::ALL); | |
| 135 | + | ||
| 136 | + | // .title(Title::from(app.conn.conn.status().unwrap_or_default().volume.to_string().yellow())).title_alignment(Alignment::Right); | |
| 123 | 137 | let progress_bar = LineGauge::default() | |
| 124 | 138 | .block(title.borders(Borders::ALL)) | |
| 125 | 139 | .gauge_style( | |
| @@ -129,7 +143,7 @@ fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) { | |||
| 129 | 143 | .add_modifier(Modifier::BOLD), | |
| 130 | 144 | ) | |
| 131 | 145 | .line_set(symbols::line::THICK) | |
| 132 | − | .ratio(0.2); | |
| 146 | + | .ratio(app.conn.get_progress_ratio()); | |
| 133 | 147 | ||
| 134 | 148 | frame.render_widget(progress_bar, size); | |
| 135 | 149 | } | |