diff --git a/src/tui/handler.rs b/src/tui/handler.rs index c7bb360..1f66e0f 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -5,9 +5,11 @@ use mockall::automock; use crate::tui::{ event::{Event, EventError, EventReceiver}, - ui::{IUi, IUiBrowse, IUiInfo, UiState}, + ui::{IUi, IUiBrowse, IUiError, IUiInfo, UiState}, }; +use super::ui::IUiReload; + #[cfg_attr(test, automock)] pub trait IEventHandler { fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError>; @@ -23,6 +25,14 @@ trait IEventHandlerPrivate { ui: &mut ::IS, key_event: KeyEvent, ) -> Result<(), EventError>; + fn handle_reload_key_event( + ui: &mut ::RS, + key_event: KeyEvent, + ) -> Result<(), EventError>; + fn handle_error_key_event( + ui: &mut ::ES, + key_event: KeyEvent, + ) -> Result<(), EventError>; fn quit(ui: &mut UI) -> Result<(), EventError>; } @@ -68,6 +78,12 @@ impl IEventHandlerPrivate for EventHandler { UiState::Info(info) => { >::handle_info_key_event(info, key_event)?; } + UiState::Reload(reload) => { + >::handle_reload_key_event(reload, key_event)?; + } + UiState::Error(error) => { + >::handle_error_key_event(error, key_event)?; + } }, }; Ok(()) @@ -86,6 +102,8 @@ impl IEventHandlerPrivate for EventHandler { KeyCode::Down => ui.increment_selection(), // Toggle overlay. KeyCode::Char('m') | KeyCode::Char('M') => ui.show_info_overlay(), + // Toggle Reload + KeyCode::Char('g') | KeyCode::Char('G') => ui.show_reload_menu(), // Othey keys. _ => {} } @@ -107,6 +125,32 @@ impl IEventHandlerPrivate for EventHandler { Ok(()) } + fn handle_reload_key_event( + ui: &mut ::RS, + key_event: KeyEvent, + ) -> Result<(), EventError> { + match key_event.code { + // Reload keys. + KeyCode::Char('l') | KeyCode::Char('L') => ui.reload_library(), + KeyCode::Char('d') | KeyCode::Char('D') => ui.reload_database(), + // Return. + KeyCode::Char('g') | KeyCode::Char('G') => ui.go_back(), + // Othey keys. + _ => {} + } + + Ok(()) + } + + fn handle_error_key_event( + ui: &mut ::ES, + _key_event: KeyEvent, + ) -> Result<(), EventError> { + // Any key dismisses the error. + ui.dismiss_error(); + Ok(()) + } + fn quit(ui: &mut UI) -> Result<(), EventError> { ui.quit(); ui.save()?; diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 85655be..b56c324 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -10,7 +10,7 @@ use ratatui::{ backend::Backend, layout::{Alignment, Rect}, style::{Color, Style}, - widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph}, + widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap}, Frame, }; @@ -35,12 +35,14 @@ impl From for UiError { } } -pub enum UiState { +pub enum UiState { Browse(BS), Info(IS), + Reload(RS), + Error(ES), } -impl UiState { +impl UiState { fn is_browse(&self) -> bool { matches!(self, UiState::Browse(_)) } @@ -48,20 +50,30 @@ impl UiState { fn is_info(&self) -> bool { matches!(self, UiState::Info(_)) } + + fn is_reload(&self) -> bool { + matches!(self, UiState::Reload(_)) + } + + fn is_error(&self) -> bool { + matches!(self, UiState::Error(_)) + } } pub trait IUi { type BS: IUiBrowse; type IS: IUiInfo; + type RS: IUiReload; + type ES: IUiError; fn is_running(&self) -> bool; fn quit(&mut self); fn save(&mut self) -> Result<(), UiError>; - fn render(&mut self, frame: &mut Frame<'_, B>); + fn state(&mut self) -> UiState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES>; - fn state(&mut self) -> UiState<&mut Self::BS, &mut Self::IS>; + fn render(&mut self, frame: &mut Frame<'_, B>); } pub trait IUiBrowse { @@ -71,12 +83,24 @@ pub trait IUiBrowse { fn decrement_selection(&mut self); fn show_info_overlay(&mut self); + + fn show_reload_menu(&mut self); } pub trait IUiInfo { fn hide_info_overlay(&mut self); } +pub trait IUiReload { + fn reload_library(&mut self); + fn reload_database(&mut self); + fn go_back(&mut self); +} + +pub trait IUiError { + fn dismiss_error(&mut self); +} + struct TrackSelection { state: ListState, } @@ -383,25 +407,59 @@ impl FrameArea { } } -struct OverlayArea { - artist: Rect, +enum OverlaySize { + MarginFactor(u16), + Value(u16), } -impl OverlayArea { - fn new(frame: Rect) -> Self { - let margin_factor = 8; +impl Default for OverlaySize { + fn default() -> Self { + OverlaySize::MarginFactor(8) + } +} - let width_margin = frame.width / margin_factor; - let height_margin = frame.height / margin_factor; +impl OverlaySize { + fn get(&self, full: u16) -> (u16, u16) { + match self { + OverlaySize::MarginFactor(margin_factor) => { + let margin = full / margin_factor; + (margin, full - (2 * margin)) + } + OverlaySize::Value(value) => { + let margin = (full - value) / 2; + (margin, *value) + } + } + } +} - let artist = Rect { - x: width_margin, - y: height_margin, - width: frame.width - (2 * width_margin), - height: frame.height - (2 * height_margin), - }; +#[derive(Default)] +struct OverlayBuilder { + width: OverlaySize, + height: OverlaySize, +} - OverlayArea { artist } +impl OverlayBuilder { + fn with_width(mut self, width: OverlaySize) -> OverlayBuilder { + self.width = width; + self + } + + fn with_height(mut self, height: OverlaySize) -> OverlayBuilder { + self.height = height; + self + } + + fn build(self, frame: Rect) -> Rect { + let (x, width) = self.width.get(frame.width); + let (y, height) = self.height.get(frame.height); + + Rect { + x, + y, + width, + height, + } } } @@ -559,7 +617,7 @@ pub struct Ui { running: bool, music_hoard: MH, selection: Selection, - state: UiState<(), ()>, + state: UiState<(), (), (), String>, } impl Ui { @@ -707,20 +765,53 @@ impl Ui { Self::render_track_column(track_state, areas.track, frame); } - fn render_overlay(&mut self, frame: &mut Frame<'_, B>) { - let areas = OverlayArea::new(frame.size()); + fn render_info_overlay(&mut self, frame: &mut Frame<'_, B>) { + let area = OverlayBuilder::default().build(frame.size()); let artists = self.music_hoard.get_collection(); let artist_selection = &mut self.selection.artist; - let artist_overlay = ArtistOverlay::new(artists, &artist_selection.state); - Self::render_overlay_widget("Artist", artist_overlay.properties, areas.artist, frame); + + Self::render_overlay_widget("Artist", artist_overlay.properties, area, frame); + } + + fn render_reload_overlay(&mut self, frame: &mut Frame<'_, B>) { + let area = OverlayBuilder::default() + .with_width(OverlaySize::Value(39)) + .with_height(OverlaySize::Value(4)) + .build(frame.size()); + + let reload_text = Paragraph::new( + "d: database\n\ + l: library", + ) + .alignment(Alignment::Center); + + Self::render_overlay_widget("Reload", reload_text, area, frame); + } + + fn render_error_overlay, B: Backend>( + &mut self, + frame: &mut Frame<'_, B>, + msg: S, + ) { + let area = OverlayBuilder::default() + .with_height(OverlaySize::Value(4)) + .build(frame.size()); + + let error_text = Paragraph::new(msg.as_ref()) + .alignment(Alignment::Center) + .wrap(Wrap { trim: true }); + + Self::render_overlay_widget("Error", error_text, area, frame); } } impl IUi for Ui { type BS = Self; type IS = Self; + type RS = Self; + type ES = Self; fn is_running(&self) -> bool { self.running @@ -734,17 +825,23 @@ impl IUi for Ui { Ok(self.music_hoard.save_to_database()?) } - fn state(&mut self) -> UiState<&mut Self::BS, &mut Self::IS> { + fn state(&mut self) -> UiState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES> { match self.state { UiState::Browse(_) => UiState::Browse(self), UiState::Info(_) => UiState::Info(self), + UiState::Reload(_) => UiState::Reload(self), + UiState::Error(_) => UiState::Error(self), } } fn render(&mut self, frame: &mut Frame<'_, B>) { self.render_collection(frame); - if self.state.is_info() { - self.render_overlay(frame); + match self.state { + UiState::Info(_) => self.render_info_overlay(frame), + UiState::Reload(_) => self.render_reload_overlay(frame), + // FIXME: Remove the clone + UiState::Error(ref msg) => self.render_error_overlay(frame, msg.clone()), + _ => {} } } } @@ -772,6 +869,11 @@ impl IUiBrowse for Ui { assert!(self.state.is_browse()); self.state = UiState::Info(()); } + + fn show_reload_menu(&mut self) { + assert!(self.state.is_browse()); + self.state = UiState::Reload(()); + } } impl IUiInfo for Ui { @@ -781,6 +883,47 @@ impl IUiInfo for Ui { } } +impl IUiReload for Ui { + fn reload_library(&mut self) { + let result = self.music_hoard.rescan_library(); + self.refresh(result); + } + + fn reload_database(&mut self) { + let result = self.music_hoard.load_from_database(); + self.refresh(result); + } + + fn go_back(&mut self) { + assert!(self.state.is_reload()); + self.state = UiState::Browse(()); + } +} + +trait IUiReloadPrivate { + fn refresh(&mut self, result: Result<(), musichoard::Error>); +} + +impl IUiReloadPrivate for Ui { + fn refresh(&mut self, result: Result<(), musichoard::Error>) { + assert!(self.state.is_reload()); + match result { + Ok(()) => { + self.selection = Selection::new(Some(self.music_hoard.get_collection())); + self.state = UiState::Browse(()) + } + Err(err) => self.state = UiState::Error(err.to_string()), + } + } +} + +impl IUiError for Ui { + fn dismiss_error(&mut self) { + assert!(self.state.is_error()); + self.state = UiState::Browse(()); + } +} + #[cfg(test)] mod tests { use crate::tui::lib::MockIMusicHoard;