First draft of reload state
Some checks failed
Cargo CI / Build and Test (pull_request) Failing after 1m6s
Cargo CI / Lint (pull_request) Failing after 43s

This commit is contained in:
Wojciech Kozlowski 2024-01-28 23:34:51 +01:00
parent e22fd6837e
commit 883d6170a8
2 changed files with 215 additions and 28 deletions

View File

@ -5,9 +5,11 @@ use mockall::automock;
use crate::tui::{ use crate::tui::{
event::{Event, EventError, EventReceiver}, event::{Event, EventError, EventReceiver},
ui::{IUi, IUiBrowse, IUiInfo, UiState}, ui::{IUi, IUiBrowse, IUiError, IUiInfo, UiState},
}; };
use super::ui::IUiReload;
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait IEventHandler<UI: IUi> { pub trait IEventHandler<UI: IUi> {
fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError>; fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError>;
@ -23,6 +25,14 @@ trait IEventHandlerPrivate<UI: IUi> {
ui: &mut <UI as IUi>::IS, ui: &mut <UI as IUi>::IS,
key_event: KeyEvent, key_event: KeyEvent,
) -> Result<(), EventError>; ) -> Result<(), EventError>;
fn handle_reload_key_event(
ui: &mut <UI as IUi>::RS,
key_event: KeyEvent,
) -> Result<(), EventError>;
fn handle_error_key_event(
ui: &mut <UI as IUi>::ES,
key_event: KeyEvent,
) -> Result<(), EventError>;
fn quit(ui: &mut UI) -> Result<(), EventError>; fn quit(ui: &mut UI) -> Result<(), EventError>;
} }
@ -68,6 +78,12 @@ impl<UI: IUi> IEventHandlerPrivate<UI> for EventHandler {
UiState::Info(info) => { UiState::Info(info) => {
<Self as IEventHandlerPrivate<UI>>::handle_info_key_event(info, key_event)?; <Self as IEventHandlerPrivate<UI>>::handle_info_key_event(info, key_event)?;
} }
UiState::Reload(reload) => {
<Self as IEventHandlerPrivate<UI>>::handle_reload_key_event(reload, key_event)?;
}
UiState::Error(error) => {
<Self as IEventHandlerPrivate<UI>>::handle_error_key_event(error, key_event)?;
}
}, },
}; };
Ok(()) Ok(())
@ -86,6 +102,8 @@ impl<UI: IUi> IEventHandlerPrivate<UI> for EventHandler {
KeyCode::Down => ui.increment_selection(), KeyCode::Down => ui.increment_selection(),
// Toggle overlay. // Toggle overlay.
KeyCode::Char('m') | KeyCode::Char('M') => ui.show_info_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. // Othey keys.
_ => {} _ => {}
} }
@ -107,6 +125,32 @@ impl<UI: IUi> IEventHandlerPrivate<UI> for EventHandler {
Ok(()) Ok(())
} }
fn handle_reload_key_event(
ui: &mut <UI as IUi>::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 <UI as IUi>::ES,
_key_event: KeyEvent,
) -> Result<(), EventError> {
// Any key dismisses the error.
ui.dismiss_error();
Ok(())
}
fn quit(ui: &mut UI) -> Result<(), EventError> { fn quit(ui: &mut UI) -> Result<(), EventError> {
ui.quit(); ui.quit();
ui.save()?; ui.save()?;

View File

@ -10,7 +10,7 @@ use ratatui::{
backend::Backend, backend::Backend,
layout::{Alignment, Rect}, layout::{Alignment, Rect},
style::{Color, Style}, style::{Color, Style},
widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph}, widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap},
Frame, Frame,
}; };
@ -35,12 +35,14 @@ impl From<musichoard::Error> for UiError {
} }
} }
pub enum UiState<BS, IS> { pub enum UiState<BS, IS, RS, ES> {
Browse(BS), Browse(BS),
Info(IS), Info(IS),
Reload(RS),
Error(ES),
} }
impl<BS, IS> UiState<BS, IS> { impl<BS, IS, RS, ES> UiState<BS, IS, RS, ES> {
fn is_browse(&self) -> bool { fn is_browse(&self) -> bool {
matches!(self, UiState::Browse(_)) matches!(self, UiState::Browse(_))
} }
@ -48,20 +50,30 @@ impl<BS, IS> UiState<BS, IS> {
fn is_info(&self) -> bool { fn is_info(&self) -> bool {
matches!(self, UiState::Info(_)) 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 { pub trait IUi {
type BS: IUiBrowse; type BS: IUiBrowse;
type IS: IUiInfo; type IS: IUiInfo;
type RS: IUiReload;
type ES: IUiError;
fn is_running(&self) -> bool; fn is_running(&self) -> bool;
fn quit(&mut self); fn quit(&mut self);
fn save(&mut self) -> Result<(), UiError>; fn save(&mut self) -> Result<(), UiError>;
fn render<B: Backend>(&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<B: Backend>(&mut self, frame: &mut Frame<'_, B>);
} }
pub trait IUiBrowse { pub trait IUiBrowse {
@ -71,12 +83,24 @@ pub trait IUiBrowse {
fn decrement_selection(&mut self); fn decrement_selection(&mut self);
fn show_info_overlay(&mut self); fn show_info_overlay(&mut self);
fn show_reload_menu(&mut self);
} }
pub trait IUiInfo { pub trait IUiInfo {
fn hide_info_overlay(&mut self); 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 { struct TrackSelection {
state: ListState, state: ListState,
} }
@ -383,25 +407,59 @@ impl FrameArea {
} }
} }
struct OverlayArea { enum OverlaySize {
artist: Rect, MarginFactor(u16),
Value(u16),
} }
impl OverlayArea { impl Default for OverlaySize {
fn new(frame: Rect) -> Self { fn default() -> Self {
let margin_factor = 8; OverlaySize::MarginFactor(8)
}
}
let width_margin = frame.width / margin_factor; impl OverlaySize {
let height_margin = frame.height / margin_factor; 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 { #[derive(Default)]
x: width_margin, struct OverlayBuilder {
y: height_margin, width: OverlaySize,
width: frame.width - (2 * width_margin), height: OverlaySize,
height: frame.height - (2 * height_margin), }
};
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<MH> {
running: bool, running: bool,
music_hoard: MH, music_hoard: MH,
selection: Selection, selection: Selection,
state: UiState<(), ()>, state: UiState<(), (), (), String>,
} }
impl<MH: IMusicHoard> Ui<MH> { impl<MH: IMusicHoard> Ui<MH> {
@ -707,20 +765,53 @@ impl<MH: IMusicHoard> Ui<MH> {
Self::render_track_column(track_state, areas.track, frame); Self::render_track_column(track_state, areas.track, frame);
} }
fn render_overlay<B: Backend>(&mut self, frame: &mut Frame<'_, B>) { fn render_info_overlay<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
let areas = OverlayArea::new(frame.size()); let area = OverlayBuilder::default().build(frame.size());
let artists = self.music_hoard.get_collection(); let artists = self.music_hoard.get_collection();
let artist_selection = &mut self.selection.artist; let artist_selection = &mut self.selection.artist;
let artist_overlay = ArtistOverlay::new(artists, &artist_selection.state); 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<B: Backend>(&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<S: AsRef<str>, 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<MH: IMusicHoard> IUi for Ui<MH> { impl<MH: IMusicHoard> IUi for Ui<MH> {
type BS = Self; type BS = Self;
type IS = Self; type IS = Self;
type RS = Self;
type ES = Self;
fn is_running(&self) -> bool { fn is_running(&self) -> bool {
self.running self.running
@ -734,17 +825,23 @@ impl<MH: IMusicHoard> IUi for Ui<MH> {
Ok(self.music_hoard.save_to_database()?) 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 { match self.state {
UiState::Browse(_) => UiState::Browse(self), UiState::Browse(_) => UiState::Browse(self),
UiState::Info(_) => UiState::Info(self), UiState::Info(_) => UiState::Info(self),
UiState::Reload(_) => UiState::Reload(self),
UiState::Error(_) => UiState::Error(self),
} }
} }
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>) { fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
self.render_collection(frame); self.render_collection(frame);
if self.state.is_info() { match self.state {
self.render_overlay(frame); 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<MH: IMusicHoard> IUiBrowse for Ui<MH> {
assert!(self.state.is_browse()); assert!(self.state.is_browse());
self.state = UiState::Info(()); self.state = UiState::Info(());
} }
fn show_reload_menu(&mut self) {
assert!(self.state.is_browse());
self.state = UiState::Reload(());
}
} }
impl<MH: IMusicHoard> IUiInfo for Ui<MH> { impl<MH: IMusicHoard> IUiInfo for Ui<MH> {
@ -781,6 +883,47 @@ impl<MH: IMusicHoard> IUiInfo for Ui<MH> {
} }
} }
impl<MH: IMusicHoard> IUiReload for Ui<MH> {
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<MH: IMusicHoard> IUiReloadPrivate for Ui<MH> {
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<MH: IMusicHoard> IUiError for Ui<MH> {
fn dismiss_error(&mut self) {
assert!(self.state.is_error());
self.state = UiState::Browse(());
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tui::lib::MockIMusicHoard; use crate::tui::lib::MockIMusicHoard;