First draft of reload state
This commit is contained in:
parent
e22fd6837e
commit
883d6170a8
@ -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<UI: IUi> {
|
||||
fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError>;
|
||||
@ -23,6 +25,14 @@ trait IEventHandlerPrivate<UI: IUi> {
|
||||
ui: &mut <UI as IUi>::IS,
|
||||
key_event: KeyEvent,
|
||||
) -> 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>;
|
||||
}
|
||||
|
||||
@ -68,6 +78,12 @@ impl<UI: IUi> IEventHandlerPrivate<UI> for EventHandler {
|
||||
UiState::Info(info) => {
|
||||
<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(())
|
||||
@ -86,6 +102,8 @@ impl<UI: IUi> IEventHandlerPrivate<UI> 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<UI: IUi> IEventHandlerPrivate<UI> for EventHandler {
|
||||
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> {
|
||||
ui.quit();
|
||||
ui.save()?;
|
||||
|
197
src/tui/ui.rs
197
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<musichoard::Error> for UiError {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum UiState<BS, IS> {
|
||||
pub enum UiState<BS, IS, RS, ES> {
|
||||
Browse(BS),
|
||||
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 {
|
||||
matches!(self, UiState::Browse(_))
|
||||
}
|
||||
@ -48,20 +50,30 @@ impl<BS, IS> UiState<BS, IS> {
|
||||
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<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 {
|
||||
@ -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<MH> {
|
||||
running: bool,
|
||||
music_hoard: MH,
|
||||
selection: Selection,
|
||||
state: UiState<(), ()>,
|
||||
state: UiState<(), (), (), String>,
|
||||
}
|
||||
|
||||
impl<MH: IMusicHoard> Ui<MH> {
|
||||
@ -707,20 +765,53 @@ impl<MH: IMusicHoard> Ui<MH> {
|
||||
Self::render_track_column(track_state, areas.track, frame);
|
||||
}
|
||||
|
||||
fn render_overlay<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
|
||||
let areas = OverlayArea::new(frame.size());
|
||||
fn render_info_overlay<B: Backend>(&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<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> {
|
||||
type BS = Self;
|
||||
type IS = Self;
|
||||
type RS = Self;
|
||||
type ES = Self;
|
||||
|
||||
fn is_running(&self) -> bool {
|
||||
self.running
|
||||
@ -734,17 +825,23 @@ impl<MH: IMusicHoard> IUi for Ui<MH> {
|
||||
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<B: Backend>(&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<MH: IMusicHoard> IUiBrowse for Ui<MH> {
|
||||
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<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)]
|
||||
mod tests {
|
||||
use crate::tui::lib::MockIMusicHoard;
|
||||
|
Loading…
Reference in New Issue
Block a user