Add shortcut to reload database and/or library #116
@ -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()?;
|
||||||
|
197
src/tui/ui.rs
197
src/tui/ui.rs
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user