Add shortcut to reload database and/or library #116
11
src/main.rs
11
src/main.rs
@ -20,7 +20,13 @@ use musichoard::{
|
|||||||
MusicHoardBuilder, NoDatabase, NoLibrary,
|
MusicHoardBuilder, NoDatabase, NoLibrary,
|
||||||
};
|
};
|
||||||
|
|
||||||
use tui::{event::EventChannel, handler::EventHandler, listener::EventListener, ui::Ui, Tui};
|
use tui::{
|
||||||
|
event::EventChannel,
|
||||||
|
handler::EventHandler,
|
||||||
|
listener::EventListener,
|
||||||
|
ui::{render::Renderer, Ui},
|
||||||
|
Tui,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(StructOpt)]
|
#[derive(StructOpt)]
|
||||||
struct Opt {
|
struct Opt {
|
||||||
@ -68,9 +74,10 @@ fn with<LIB: ILibrary, DB: IDatabase>(builder: MusicHoardBuilder<LIB, DB>) {
|
|||||||
let handler = EventHandler::new(channel.receiver());
|
let handler = EventHandler::new(channel.receiver());
|
||||||
|
|
||||||
let ui = Ui::new(music_hoard).expect("failed to initialise ui");
|
let ui = Ui::new(music_hoard).expect("failed to initialise ui");
|
||||||
|
let renderer = Renderer;
|
||||||
|
|
||||||
// Run the TUI application.
|
// Run the TUI application.
|
||||||
Tui::run(terminal, ui, handler, listener).expect("failed to run tui");
|
Tui::run(terminal, ui, renderer, handler, listener).expect("failed to run tui");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_database<LIB: ILibrary>(db_opt: DbOpt, builder: MusicHoardBuilder<LIB, NoDatabase>) {
|
fn with_database<LIB: ILibrary>(db_opt: DbOpt, builder: MusicHoardBuilder<LIB, NoDatabase>) {
|
||||||
|
@ -15,7 +15,7 @@ use std::marker::PhantomData;
|
|||||||
use self::event::EventError;
|
use self::event::EventError;
|
||||||
use self::handler::IEventHandler;
|
use self::handler::IEventHandler;
|
||||||
use self::listener::IEventListener;
|
use self::listener::IEventListener;
|
||||||
use self::ui::IUi;
|
use self::ui::{render::IRender, IUi};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
@ -43,12 +43,12 @@ impl From<EventError> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Tui<B: Backend, UI: IUi> {
|
pub struct Tui<B: Backend, R: IRender, UI: IUi> {
|
||||||
terminal: Terminal<B>,
|
terminal: Terminal<B>,
|
||||||
_phantom: PhantomData<UI>,
|
_phantom: PhantomData<(R, UI)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B: Backend, UI: IUi> Tui<B, UI> {
|
impl<B: Backend, R: IRender, UI: IUi> Tui<B, R, UI> {
|
||||||
fn init(&mut self) -> Result<(), Error> {
|
fn init(&mut self) -> Result<(), Error> {
|
||||||
self.terminal.hide_cursor()?;
|
self.terminal.hide_cursor()?;
|
||||||
self.terminal.clear()?;
|
self.terminal.clear()?;
|
||||||
@ -65,9 +65,14 @@ impl<B: Backend, UI: IUi> Tui<B, UI> {
|
|||||||
self.exit();
|
self.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main_loop(&mut self, mut ui: UI, handler: impl IEventHandler<UI>) -> Result<(), Error> {
|
fn main_loop(
|
||||||
|
&mut self,
|
||||||
|
mut ui: UI,
|
||||||
|
renderer: R,
|
||||||
|
handler: impl IEventHandler<UI>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
while ui.is_running() {
|
while ui.is_running() {
|
||||||
self.terminal.draw(|frame| ui.render(frame))?;
|
self.terminal.draw(|frame| ui.render(&renderer, frame))?;
|
||||||
handler.handle_next_event(&mut ui)?;
|
handler.handle_next_event(&mut ui)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,6 +82,7 @@ impl<B: Backend, UI: IUi> Tui<B, UI> {
|
|||||||
fn main(
|
fn main(
|
||||||
term: Terminal<B>,
|
term: Terminal<B>,
|
||||||
ui: UI,
|
ui: UI,
|
||||||
|
renderer: R,
|
||||||
handler: impl IEventHandler<UI>,
|
handler: impl IEventHandler<UI>,
|
||||||
listener: impl IEventListener,
|
listener: impl IEventListener,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
@ -88,7 +94,7 @@ impl<B: Backend, UI: IUi> Tui<B, UI> {
|
|||||||
tui.init()?;
|
tui.init()?;
|
||||||
|
|
||||||
let listener_handle = listener.spawn();
|
let listener_handle = listener.spawn();
|
||||||
let result = tui.main_loop(ui, handler);
|
let result = tui.main_loop(ui, renderer, handler);
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
@ -136,11 +142,12 @@ impl<B: Backend, UI: IUi> Tui<B, UI> {
|
|||||||
pub fn run(
|
pub fn run(
|
||||||
term: Terminal<B>,
|
term: Terminal<B>,
|
||||||
ui: UI,
|
ui: UI,
|
||||||
|
renderer: R,
|
||||||
handler: impl IEventHandler<UI>,
|
handler: impl IEventHandler<UI>,
|
||||||
listener: impl IEventListener,
|
listener: impl IEventListener,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
Self::enable()?;
|
Self::enable()?;
|
||||||
let result = Self::main(term, ui, handler, listener);
|
let result = Self::main(term, ui, renderer, handler, listener);
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
Self::disable()?;
|
Self::disable()?;
|
||||||
@ -169,7 +176,10 @@ mod tests {
|
|||||||
use musichoard::collection::Collection;
|
use musichoard::collection::Collection;
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
handler::MockIEventHandler, lib::MockIMusicHoard, listener::MockIEventListener, ui::Ui,
|
handler::MockIEventHandler,
|
||||||
|
lib::MockIMusicHoard,
|
||||||
|
listener::MockIEventListener,
|
||||||
|
ui::{render::Renderer, Ui},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -220,11 +230,12 @@ mod tests {
|
|||||||
fn run() {
|
fn run() {
|
||||||
let terminal = terminal();
|
let terminal = terminal();
|
||||||
let ui = ui(COLLECTION.to_owned());
|
let ui = ui(COLLECTION.to_owned());
|
||||||
|
let renderer = Renderer;
|
||||||
|
|
||||||
let listener = listener();
|
let listener = listener();
|
||||||
let handler = handler();
|
let handler = handler();
|
||||||
|
|
||||||
let result = Tui::main(terminal, ui, handler, listener);
|
let result = Tui::main(terminal, ui, renderer, handler, listener);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,6 +243,7 @@ mod tests {
|
|||||||
fn event_error() {
|
fn event_error() {
|
||||||
let terminal = terminal();
|
let terminal = terminal();
|
||||||
let ui = ui(COLLECTION.to_owned());
|
let ui = ui(COLLECTION.to_owned());
|
||||||
|
let renderer = Renderer;
|
||||||
|
|
||||||
let listener = listener();
|
let listener = listener();
|
||||||
|
|
||||||
@ -240,7 +252,7 @@ mod tests {
|
|||||||
.expect_handle_next_event()
|
.expect_handle_next_event()
|
||||||
.return_once(|_| Err(EventError::Recv));
|
.return_once(|_| Err(EventError::Recv));
|
||||||
|
|
||||||
let result = Tui::main(terminal, ui, handler, listener);
|
let result = Tui::main(terminal, ui, renderer, handler, listener);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.unwrap_err(),
|
result.unwrap_err(),
|
||||||
@ -252,6 +264,7 @@ mod tests {
|
|||||||
fn listener_error() {
|
fn listener_error() {
|
||||||
let terminal = terminal();
|
let terminal = terminal();
|
||||||
let ui = ui(COLLECTION.to_owned());
|
let ui = ui(COLLECTION.to_owned());
|
||||||
|
let renderer = Renderer;
|
||||||
|
|
||||||
let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error"));
|
let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error"));
|
||||||
let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| error);
|
let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| error);
|
||||||
@ -265,7 +278,7 @@ mod tests {
|
|||||||
.expect_handle_next_event()
|
.expect_handle_next_event()
|
||||||
.return_once(|_| Err(EventError::Recv));
|
.return_once(|_| Err(EventError::Recv));
|
||||||
|
|
||||||
let result = Tui::main(terminal, ui, handler, listener);
|
let result = Tui::main(terminal, ui, renderer, handler, listener);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
|
|
||||||
let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error"));
|
let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error"));
|
||||||
@ -276,6 +289,7 @@ mod tests {
|
|||||||
fn listener_panic() {
|
fn listener_panic() {
|
||||||
let terminal = terminal();
|
let terminal = terminal();
|
||||||
let ui = ui(COLLECTION.to_owned());
|
let ui = ui(COLLECTION.to_owned());
|
||||||
|
let renderer = Renderer;
|
||||||
|
|
||||||
let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| panic!());
|
let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| panic!());
|
||||||
while !listener_handle.is_finished() {}
|
while !listener_handle.is_finished() {}
|
||||||
@ -288,7 +302,7 @@ mod tests {
|
|||||||
.expect_handle_next_event()
|
.expect_handle_next_event()
|
||||||
.return_once(|_| Err(EventError::Recv));
|
.return_once(|_| Err(EventError::Recv));
|
||||||
|
|
||||||
let result = Tui::main(terminal, ui, handler, listener);
|
let result = Tui::main(terminal, ui, renderer, handler, listener);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert_eq!(result.unwrap_err(), Error::ListenerPanic);
|
assert_eq!(result.unwrap_err(), Error::ListenerPanic);
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,14 @@
|
|||||||
|
pub mod render;
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use musichoard::collection::{
|
use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection};
|
||||||
album::Album,
|
use ratatui::{backend::Backend, widgets::ListState, Frame};
|
||||||
artist::Artist,
|
|
||||||
track::{Format, Track},
|
|
||||||
Collection,
|
|
||||||
};
|
|
||||||
use ratatui::{
|
|
||||||
backend::Backend,
|
|
||||||
layout::{Alignment, Rect},
|
|
||||||
style::{Color, Style},
|
|
||||||
widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap},
|
|
||||||
Frame,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::tui::{lib::IMusicHoard, Error};
|
use crate::tui::{
|
||||||
|
ui::render::IRender,
|
||||||
|
{lib::IMusicHoard, Error},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum UiError {
|
pub enum UiError {
|
||||||
@ -73,7 +67,7 @@ pub trait IUi {
|
|||||||
|
|
||||||
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, &mut Self::RS, &mut Self::ES>;
|
||||||
|
|
||||||
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>);
|
fn render<R: IRender, B: Backend>(&mut self, renderer: &R, frame: &mut Frame<'_, B>);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait IUiBrowse {
|
pub trait IUiBrowse {
|
||||||
@ -258,7 +252,7 @@ pub enum Category {
|
|||||||
Track,
|
Track,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Selection {
|
pub struct Selection {
|
||||||
active: Category,
|
active: Category,
|
||||||
artist: ArtistSelection,
|
artist: ArtistSelection,
|
||||||
}
|
}
|
||||||
@ -328,292 +322,7 @@ impl Selection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ArtistArea {
|
pub struct Ui<MH: IMusicHoard> {
|
||||||
list: Rect,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AlbumArea {
|
|
||||||
list: Rect,
|
|
||||||
info: Rect,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TrackArea {
|
|
||||||
list: Rect,
|
|
||||||
info: Rect,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FrameArea {
|
|
||||||
artist: ArtistArea,
|
|
||||||
album: AlbumArea,
|
|
||||||
track: TrackArea,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FrameArea {
|
|
||||||
fn new(frame: Rect) -> Self {
|
|
||||||
let width_one_third = frame.width / 3;
|
|
||||||
let height_one_third = frame.height / 3;
|
|
||||||
|
|
||||||
let panel_width = width_one_third;
|
|
||||||
let panel_width_last = frame.width - 2 * panel_width;
|
|
||||||
let panel_height_top = frame.height - height_one_third;
|
|
||||||
let panel_height_bottom = height_one_third;
|
|
||||||
|
|
||||||
let artist_list = Rect {
|
|
||||||
x: frame.x,
|
|
||||||
y: frame.y,
|
|
||||||
width: panel_width,
|
|
||||||
height: frame.height,
|
|
||||||
};
|
|
||||||
|
|
||||||
let album_list = Rect {
|
|
||||||
x: artist_list.x + artist_list.width,
|
|
||||||
y: frame.y,
|
|
||||||
width: panel_width,
|
|
||||||
height: panel_height_top,
|
|
||||||
};
|
|
||||||
|
|
||||||
let album_info = Rect {
|
|
||||||
x: album_list.x,
|
|
||||||
y: album_list.y + album_list.height,
|
|
||||||
width: album_list.width,
|
|
||||||
height: panel_height_bottom,
|
|
||||||
};
|
|
||||||
|
|
||||||
let track_list = Rect {
|
|
||||||
x: album_list.x + album_list.width,
|
|
||||||
y: frame.y,
|
|
||||||
width: panel_width_last,
|
|
||||||
height: panel_height_top,
|
|
||||||
};
|
|
||||||
|
|
||||||
let track_info = Rect {
|
|
||||||
x: track_list.x,
|
|
||||||
y: track_list.y + track_list.height,
|
|
||||||
width: track_list.width,
|
|
||||||
height: panel_height_bottom,
|
|
||||||
};
|
|
||||||
|
|
||||||
FrameArea {
|
|
||||||
artist: ArtistArea { list: artist_list },
|
|
||||||
album: AlbumArea {
|
|
||||||
list: album_list,
|
|
||||||
info: album_info,
|
|
||||||
},
|
|
||||||
track: TrackArea {
|
|
||||||
list: track_list,
|
|
||||||
info: track_info,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OverlaySize {
|
|
||||||
MarginFactor(u16),
|
|
||||||
Value(u16),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for OverlaySize {
|
|
||||||
fn default() -> Self {
|
|
||||||
OverlaySize::MarginFactor(8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct OverlayBuilder {
|
|
||||||
width: OverlaySize,
|
|
||||||
height: OverlaySize,
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ArtistState<'a, 'b> {
|
|
||||||
active: bool,
|
|
||||||
list: List<'a>,
|
|
||||||
state: &'b mut ListState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'b> ArtistState<'a, 'b> {
|
|
||||||
fn new(active: bool, artists: &'a [Artist], state: &'b mut ListState) -> ArtistState<'a, 'b> {
|
|
||||||
let list = List::new(
|
|
||||||
artists
|
|
||||||
.iter()
|
|
||||||
.map(|a| ListItem::new(a.id.name.as_str()))
|
|
||||||
.collect::<Vec<ListItem>>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
ArtistState {
|
|
||||||
active,
|
|
||||||
list,
|
|
||||||
state,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ArtistOverlay<'a> {
|
|
||||||
properties: Paragraph<'a>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> ArtistOverlay<'a> {
|
|
||||||
fn opt_opt_to_str<S: AsRef<str>>(opt: Option<Option<&S>>) -> &str {
|
|
||||||
opt.flatten().map(|item| item.as_ref()).unwrap_or("")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn opt_vec_to_string<S: AsRef<str>>(opt_vec: Option<&Vec<S>>, indent: &str) -> String {
|
|
||||||
opt_vec
|
|
||||||
.map(|vec| {
|
|
||||||
if vec.len() < 2 {
|
|
||||||
vec.first()
|
|
||||||
.map(|item| item.as_ref())
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string()
|
|
||||||
} else {
|
|
||||||
let indent = format!("\n{indent}");
|
|
||||||
let list = vec
|
|
||||||
.iter()
|
|
||||||
.map(|item| item.as_ref())
|
|
||||||
.collect::<Vec<&str>>()
|
|
||||||
.join(&indent);
|
|
||||||
format!("{indent}{list}")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| String::from(""))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new(artists: &'a [Artist], state: &ListState) -> ArtistOverlay<'a> {
|
|
||||||
let artist = state.selected().map(|i| &artists[i]);
|
|
||||||
|
|
||||||
let item_indent = " ";
|
|
||||||
let list_indent = " - ";
|
|
||||||
let properties = Paragraph::new(format!(
|
|
||||||
"Artist: {}\n\n{item_indent}\
|
|
||||||
MusicBrainz: {}\n{item_indent}\
|
|
||||||
MusicButler: {}\n{item_indent}\
|
|
||||||
Bandcamp: {}\n{item_indent}\
|
|
||||||
Qobuz: {}",
|
|
||||||
artist.map(|a| a.id.name.as_str()).unwrap_or(""),
|
|
||||||
Self::opt_opt_to_str(artist.map(|a| a.properties.musicbrainz.as_ref())),
|
|
||||||
Self::opt_vec_to_string(artist.map(|a| &a.properties.musicbutler), list_indent),
|
|
||||||
Self::opt_vec_to_string(artist.map(|a| &a.properties.bandcamp), list_indent),
|
|
||||||
Self::opt_opt_to_str(artist.map(|a| a.properties.qobuz.as_ref())),
|
|
||||||
));
|
|
||||||
|
|
||||||
ArtistOverlay { properties }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AlbumState<'a, 'b> {
|
|
||||||
active: bool,
|
|
||||||
list: List<'a>,
|
|
||||||
state: &'b mut ListState,
|
|
||||||
info: Paragraph<'a>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'b> AlbumState<'a, 'b> {
|
|
||||||
fn new(active: bool, albums: &'a [Album], state: &'b mut ListState) -> AlbumState<'a, 'b> {
|
|
||||||
let list = List::new(
|
|
||||||
albums
|
|
||||||
.iter()
|
|
||||||
.map(|a| ListItem::new(a.id.title.as_str()))
|
|
||||||
.collect::<Vec<ListItem>>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let album = state.selected().map(|i| &albums[i]);
|
|
||||||
let info = Paragraph::new(format!(
|
|
||||||
"Title: {}\n\
|
|
||||||
Year: {}",
|
|
||||||
album.map(|a| a.id.title.as_str()).unwrap_or(""),
|
|
||||||
album.map(|a| a.id.year.to_string()).unwrap_or_default(),
|
|
||||||
));
|
|
||||||
|
|
||||||
AlbumState {
|
|
||||||
active,
|
|
||||||
list,
|
|
||||||
state,
|
|
||||||
info,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TrackState<'a, 'b> {
|
|
||||||
active: bool,
|
|
||||||
list: List<'a>,
|
|
||||||
state: &'b mut ListState,
|
|
||||||
info: Paragraph<'a>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'b> TrackState<'a, 'b> {
|
|
||||||
fn new(active: bool, tracks: &'a [Track], state: &'b mut ListState) -> TrackState<'a, 'b> {
|
|
||||||
let list = List::new(
|
|
||||||
tracks
|
|
||||||
.iter()
|
|
||||||
.map(|tr| ListItem::new(tr.id.title.as_str()))
|
|
||||||
.collect::<Vec<ListItem>>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let track = state.selected().map(|i| &tracks[i]);
|
|
||||||
let info = Paragraph::new(format!(
|
|
||||||
"Track: {}\n\
|
|
||||||
Title: {}\n\
|
|
||||||
Artist: {}\n\
|
|
||||||
Quality: {}",
|
|
||||||
track.map(|t| t.id.number.to_string()).unwrap_or_default(),
|
|
||||||
track.map(|t| t.id.title.as_str()).unwrap_or(""),
|
|
||||||
track.map(|t| t.artist.join("; ")).unwrap_or_default(),
|
|
||||||
track
|
|
||||||
.map(|t| match t.quality.format {
|
|
||||||
Format::Flac => "FLAC".to_string(),
|
|
||||||
Format::Mp3 => format!("MP3 {}kbps", t.quality.bitrate),
|
|
||||||
})
|
|
||||||
.unwrap_or_default(),
|
|
||||||
));
|
|
||||||
|
|
||||||
TrackState {
|
|
||||||
active,
|
|
||||||
list,
|
|
||||||
state,
|
|
||||||
info,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Ui<MH> {
|
|
||||||
running: bool,
|
running: bool,
|
||||||
music_hoard: MH,
|
music_hoard: MH,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
@ -622,6 +331,7 @@ pub struct Ui<MH> {
|
|||||||
|
|
||||||
impl<MH: IMusicHoard> Ui<MH> {
|
impl<MH: IMusicHoard> Ui<MH> {
|
||||||
pub fn new(mut music_hoard: MH) -> Result<Self, Error> {
|
pub fn new(mut music_hoard: MH) -> Result<Self, Error> {
|
||||||
|
// FIXME: if either returns an error start in an error state
|
||||||
music_hoard.load_from_database()?;
|
music_hoard.load_from_database()?;
|
||||||
music_hoard.rescan_library()?;
|
music_hoard.rescan_library()?;
|
||||||
let selection = Selection::new(Some(music_hoard.get_collection()));
|
let selection = Selection::new(Some(music_hoard.get_collection()));
|
||||||
@ -632,182 +342,6 @@ impl<MH: IMusicHoard> Ui<MH> {
|
|||||||
state: UiState::Browse(()),
|
state: UiState::Browse(()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn style(_active: bool) -> Style {
|
|
||||||
Style::default().fg(Color::White).bg(Color::Black)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn block_style(active: bool) -> Style {
|
|
||||||
Self::style(active)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlight_style(active: bool) -> Style {
|
|
||||||
if active {
|
|
||||||
Style::default().fg(Color::White).bg(Color::DarkGray)
|
|
||||||
} else {
|
|
||||||
Self::style(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn block<'a>(title: &str, active: bool) -> Block<'a> {
|
|
||||||
Block::default()
|
|
||||||
.title_alignment(Alignment::Center)
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_type(BorderType::Rounded)
|
|
||||||
.style(Self::block_style(active))
|
|
||||||
.title(format!(" {title} "))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_list_widget<B: Backend>(
|
|
||||||
title: &str,
|
|
||||||
list: List,
|
|
||||||
list_state: &mut ListState,
|
|
||||||
active: bool,
|
|
||||||
area: Rect,
|
|
||||||
frame: &mut Frame<'_, B>,
|
|
||||||
) {
|
|
||||||
frame.render_stateful_widget(
|
|
||||||
list.highlight_style(Self::highlight_style(active))
|
|
||||||
.highlight_symbol(">> ")
|
|
||||||
.style(Self::style(active))
|
|
||||||
.block(Self::block(title, active)),
|
|
||||||
area,
|
|
||||||
list_state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_info_widget<B: Backend>(
|
|
||||||
title: &str,
|
|
||||||
paragraph: Paragraph,
|
|
||||||
active: bool,
|
|
||||||
area: Rect,
|
|
||||||
frame: &mut Frame<'_, B>,
|
|
||||||
) {
|
|
||||||
frame.render_widget(
|
|
||||||
paragraph
|
|
||||||
.style(Self::style(active))
|
|
||||||
.block(Self::block(title, active)),
|
|
||||||
area,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_overlay_widget<B: Backend>(
|
|
||||||
title: &str,
|
|
||||||
paragraph: Paragraph,
|
|
||||||
area: Rect,
|
|
||||||
frame: &mut Frame<'_, B>,
|
|
||||||
) {
|
|
||||||
frame.render_widget(Clear, area);
|
|
||||||
frame.render_widget(
|
|
||||||
paragraph
|
|
||||||
.style(Self::style(true))
|
|
||||||
.block(Self::block(title, true)),
|
|
||||||
area,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_artist_column<B: Backend>(st: ArtistState, ar: ArtistArea, fr: &mut Frame<'_, B>) {
|
|
||||||
Self::render_list_widget("Artists", st.list, st.state, st.active, ar.list, fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_album_column<B: Backend>(st: AlbumState, ar: AlbumArea, fr: &mut Frame<'_, B>) {
|
|
||||||
Self::render_list_widget("Albums", st.list, st.state, st.active, ar.list, fr);
|
|
||||||
Self::render_info_widget("Album info", st.info, st.active, ar.info, fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_track_column<B: Backend>(st: TrackState, ar: TrackArea, fr: &mut Frame<'_, B>) {
|
|
||||||
Self::render_list_widget("Tracks", st.list, st.state, st.active, ar.list, fr);
|
|
||||||
Self::render_info_widget("Track info", st.info, st.active, ar.info, fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_collection<B: Backend>(
|
|
||||||
artists: &Collection,
|
|
||||||
selection: &mut Selection,
|
|
||||||
frame: &mut Frame<'_, B>,
|
|
||||||
) {
|
|
||||||
let active = selection.active;
|
|
||||||
let areas = FrameArea::new(frame.size());
|
|
||||||
|
|
||||||
let artist_selection = &mut selection.artist;
|
|
||||||
let artist_state = ArtistState::new(
|
|
||||||
active == Category::Artist,
|
|
||||||
artists,
|
|
||||||
&mut artist_selection.state,
|
|
||||||
);
|
|
||||||
|
|
||||||
Self::render_artist_column(artist_state, areas.artist, frame);
|
|
||||||
|
|
||||||
let no_albums: Vec<Album> = vec![];
|
|
||||||
let albums = artist_selection
|
|
||||||
.state
|
|
||||||
.selected()
|
|
||||||
.map(|i| &artists[i].albums)
|
|
||||||
.unwrap_or_else(|| &no_albums);
|
|
||||||
let album_selection = &mut artist_selection.album;
|
|
||||||
let album_state = AlbumState::new(
|
|
||||||
active == Category::Album,
|
|
||||||
albums,
|
|
||||||
&mut album_selection.state,
|
|
||||||
);
|
|
||||||
|
|
||||||
Self::render_album_column(album_state, areas.album, frame);
|
|
||||||
|
|
||||||
let no_tracks: Vec<Track> = vec![];
|
|
||||||
let tracks = album_selection
|
|
||||||
.state
|
|
||||||
.selected()
|
|
||||||
.map(|i| &albums[i].tracks)
|
|
||||||
.unwrap_or_else(|| &no_tracks);
|
|
||||||
let track_selection = &mut album_selection.track;
|
|
||||||
let track_state = TrackState::new(
|
|
||||||
active == Category::Track,
|
|
||||||
tracks,
|
|
||||||
&mut track_selection.state,
|
|
||||||
);
|
|
||||||
|
|
||||||
Self::render_track_column(track_state, areas.track, frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_info_overlay<B: Backend>(
|
|
||||||
artists: &Collection,
|
|
||||||
selection: &mut Selection,
|
|
||||||
frame: &mut Frame<'_, B>,
|
|
||||||
) {
|
|
||||||
let area = OverlayBuilder::default().build(frame.size());
|
|
||||||
|
|
||||||
let artist_selection = &mut selection.artist;
|
|
||||||
let artist_overlay = ArtistOverlay::new(artists, &artist_selection.state);
|
|
||||||
|
|
||||||
Self::render_overlay_widget("Artist", artist_overlay.properties, area, frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_reload_overlay<B: Backend>(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>(msg: S, frame: &mut Frame<'_, B>) {
|
|
||||||
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 });
|
|
||||||
|
|
||||||
// FIXME: highlight with red
|
|
||||||
Self::render_overlay_widget("Error", error_text, area, frame);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<MH: IMusicHoard> IUi for Ui<MH> {
|
impl<MH: IMusicHoard> IUi for Ui<MH> {
|
||||||
@ -837,15 +371,18 @@ impl<MH: IMusicHoard> IUi for Ui<MH> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
|
// FIXME: rendering logic should be in render.rs - reverse Renderer/Ui relationship so that TUI
|
||||||
|
// calls are renderer.render(ui, frame); Then rename ui -> app and render -> ui as it used to be
|
||||||
|
// originally.
|
||||||
|
fn render<R: IRender, B: Backend>(&mut self, _: &R, frame: &mut Frame<'_, B>) {
|
||||||
let collection: &Collection = &self.music_hoard.get_collection();
|
let collection: &Collection = &self.music_hoard.get_collection();
|
||||||
let selection: &mut Selection = &mut self.selection;
|
let selection: &mut Selection = &mut self.selection;
|
||||||
|
|
||||||
Self::render_collection(collection, selection, frame);
|
R::render_collection(collection, selection, frame);
|
||||||
match self.state {
|
match self.state {
|
||||||
UiState::Info(_) => Self::render_info_overlay(collection, selection, frame),
|
UiState::Info(_) => R::render_info_overlay(collection, selection, frame),
|
||||||
UiState::Reload(_) => Self::render_reload_overlay(frame),
|
UiState::Reload(_) => R::render_reload_overlay(frame),
|
||||||
UiState::Error(ref msg) => Self::render_error_overlay(msg, frame),
|
UiState::Error(ref msg) => R::render_error_overlay(msg, frame),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -934,6 +471,7 @@ mod tests {
|
|||||||
use crate::tui::lib::MockIMusicHoard;
|
use crate::tui::lib::MockIMusicHoard;
|
||||||
use crate::tui::testmod::COLLECTION;
|
use crate::tui::testmod::COLLECTION;
|
||||||
use crate::tui::tests::{terminal, ui};
|
use crate::tui::tests::{terminal, ui};
|
||||||
|
use crate::tui::ui::render::Renderer;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@ -954,7 +492,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_track_selection() {
|
fn track_selection() {
|
||||||
let tracks = &COLLECTION[0].albums[0].tracks;
|
let tracks = &COLLECTION[0].albums[0].tracks;
|
||||||
assert!(tracks.len() > 1);
|
assert!(tracks.len() > 1);
|
||||||
|
|
||||||
@ -990,7 +528,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_album_selection() {
|
fn album_selection() {
|
||||||
let albums = &COLLECTION[0].albums;
|
let albums = &COLLECTION[0].albums;
|
||||||
assert!(albums.len() > 1);
|
assert!(albums.len() > 1);
|
||||||
|
|
||||||
@ -1048,7 +586,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_artist_selection() {
|
fn artist_selection() {
|
||||||
let artists = &COLLECTION;
|
let artists = &COLLECTION;
|
||||||
assert!(artists.len() > 1);
|
assert!(artists.len() > 1);
|
||||||
|
|
||||||
@ -1106,7 +644,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ui_running() {
|
fn running() {
|
||||||
let mut ui = Ui::new(music_hoard(COLLECTION.to_owned())).unwrap();
|
let mut ui = Ui::new(music_hoard(COLLECTION.to_owned())).unwrap();
|
||||||
assert!(ui.is_running());
|
assert!(ui.is_running());
|
||||||
|
|
||||||
@ -1115,7 +653,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ui_save() {
|
fn save() {
|
||||||
let mut music_hoard = music_hoard(COLLECTION.to_owned());
|
let mut music_hoard = music_hoard(COLLECTION.to_owned());
|
||||||
|
|
||||||
music_hoard
|
music_hoard
|
||||||
@ -1130,7 +668,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ui_modifiers() {
|
fn modifiers() {
|
||||||
let mut ui = Ui::new(music_hoard(COLLECTION.to_owned())).unwrap();
|
let mut ui = Ui::new(music_hoard(COLLECTION.to_owned())).unwrap();
|
||||||
assert!(ui.is_running());
|
assert!(ui.is_running());
|
||||||
|
|
||||||
@ -1219,7 +757,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn app_no_tracks() {
|
fn no_tracks() {
|
||||||
let mut collection = COLLECTION.to_owned();
|
let mut collection = COLLECTION.to_owned();
|
||||||
collection[0].albums[0].tracks = vec![];
|
collection[0].albums[0].tracks = vec![];
|
||||||
|
|
||||||
@ -1248,7 +786,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn app_no_albums() {
|
fn no_albums() {
|
||||||
let mut collection = COLLECTION.to_owned();
|
let mut collection = COLLECTION.to_owned();
|
||||||
collection[0].albums = vec![];
|
collection[0].albums = vec![];
|
||||||
|
|
||||||
@ -1290,7 +828,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn app_no_artists() {
|
fn no_artists() {
|
||||||
let mut app = Ui::new(music_hoard(vec![])).unwrap();
|
let mut app = Ui::new(music_hoard(vec![])).unwrap();
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
@ -1343,50 +881,120 @@ mod tests {
|
|||||||
// This is UI so the only sensible unit test is to run the code through various app states.
|
// This is UI so the only sensible unit test is to run the code through various app states.
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty() {
|
fn info_overlay() {
|
||||||
let mut terminal = terminal();
|
let mut terminal = terminal();
|
||||||
let mut ui = ui(vec![]);
|
let renderer = Renderer;
|
||||||
|
|
||||||
terminal.draw(|frame| ui.render(frame)).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn collection() {
|
|
||||||
let mut terminal = terminal();
|
|
||||||
let mut ui = ui(COLLECTION.to_owned());
|
|
||||||
|
|
||||||
terminal.draw(|frame| ui.render(frame)).unwrap();
|
|
||||||
|
|
||||||
// Change the track (which has a different track format).
|
|
||||||
ui.increment_category();
|
|
||||||
ui.increment_category();
|
|
||||||
ui.increment_selection();
|
|
||||||
|
|
||||||
terminal.draw(|frame| ui.render(frame)).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn overlay() {
|
|
||||||
let mut terminal = terminal();
|
|
||||||
let mut ui = ui(COLLECTION.to_owned());
|
let mut ui = ui(COLLECTION.to_owned());
|
||||||
assert!(ui.state().is_browse());
|
assert!(ui.state().is_browse());
|
||||||
|
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
||||||
terminal.draw(|frame| ui.render(frame)).unwrap();
|
|
||||||
|
|
||||||
ui.show_info_overlay();
|
ui.show_info_overlay();
|
||||||
assert!(ui.state().is_info());
|
assert!(ui.state().is_info());
|
||||||
|
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
||||||
terminal.draw(|frame| ui.render(frame)).unwrap();
|
|
||||||
|
|
||||||
// Change the artist (which has a multi-link entry).
|
// Change the artist (which has a multi-link entry).
|
||||||
ui.increment_selection();
|
ui.increment_selection();
|
||||||
|
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
||||||
terminal.draw(|frame| ui.render(frame)).unwrap();
|
|
||||||
|
|
||||||
ui.hide_info_overlay();
|
ui.hide_info_overlay();
|
||||||
assert!(ui.state().is_browse());
|
assert!(ui.state().is_browse());
|
||||||
|
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
terminal.draw(|frame| ui.render(frame)).unwrap();
|
#[test]
|
||||||
|
fn reload_go_back() {
|
||||||
|
let mut terminal = terminal();
|
||||||
|
let renderer = Renderer;
|
||||||
|
let music_hoard = music_hoard(COLLECTION.to_owned());
|
||||||
|
|
||||||
|
let mut ui = Ui::new(music_hoard).unwrap();
|
||||||
|
assert!(ui.state().is_browse());
|
||||||
|
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
||||||
|
|
||||||
|
ui.show_reload_menu();
|
||||||
|
assert!(ui.state().is_reload());
|
||||||
|
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
||||||
|
|
||||||
|
ui.go_back();
|
||||||
|
assert!(ui.state().is_browse());
|
||||||
|
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reload_database() {
|
||||||
|
let mut terminal = terminal();
|
||||||
|
let renderer = Renderer;
|
||||||
|
let mut music_hoard = music_hoard(COLLECTION.to_owned());
|
||||||
|
|
||||||
|
music_hoard
|
||||||
|
.expect_load_from_database()
|
||||||
|
.times(1)
|
||||||
|
.return_once(|| Ok(()));
|
||||||
|
|
||||||
|
let mut ui = Ui::new(music_hoard).unwrap();
|
||||||
|
assert!(ui.state().is_browse());
|
||||||
|
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
||||||
|
|
||||||
|
ui.show_reload_menu();
|
||||||
|
assert!(ui.state().is_reload());
|
||||||
|
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
||||||
|
|
||||||
|
ui.reload_database();
|
||||||
|
assert!(ui.state().is_browse());
|
||||||
|
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reload_library() {
|
||||||
|
let mut terminal = terminal();
|
||||||
|
let renderer = Renderer;
|
||||||
|
let mut music_hoard = music_hoard(COLLECTION.to_owned());
|
||||||
|
|
||||||
|
music_hoard
|
||||||
|
.expect_rescan_library()
|
||||||
|
.times(1)
|
||||||
|
.return_once(|| Ok(()));
|
||||||
|
|
||||||
|
let mut ui = Ui::new(music_hoard).unwrap();
|
||||||
|
assert!(ui.state().is_browse());
|
||||||
|
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
||||||
|
|
||||||
|
ui.show_reload_menu();
|
||||||
|
assert!(ui.state().is_reload());
|
||||||
|
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
||||||
|
|
||||||
|
ui.reload_library();
|
||||||
|
assert!(ui.state().is_browse());
|
||||||
|
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reload_error() {
|
||||||
|
let mut terminal = terminal();
|
||||||
|
let renderer = Renderer;
|
||||||
|
let mut music_hoard = music_hoard(COLLECTION.to_owned());
|
||||||
|
|
||||||
|
music_hoard
|
||||||
|
.expect_load_from_database()
|
||||||
|
.times(1)
|
||||||
|
.return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt"))));
|
||||||
|
|
||||||
|
let mut ui = Ui::new(music_hoard).unwrap();
|
||||||
|
assert!(ui.state().is_browse());
|
||||||
|
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
||||||
|
|
||||||
|
ui.show_reload_menu();
|
||||||
|
assert!(ui.state().is_reload());
|
||||||
|
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
||||||
|
|
||||||
|
ui.reload_database();
|
||||||
|
assert!(ui.state().is_error());
|
||||||
|
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
||||||
|
|
||||||
|
ui.dismiss_error();
|
||||||
|
assert!(ui.state().is_browse());
|
||||||
|
terminal.draw(|frame| ui.render(&renderer, frame)).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
573
src/tui/ui/render.rs
Normal file
573
src/tui/ui/render.rs
Normal file
@ -0,0 +1,573 @@
|
|||||||
|
use musichoard::collection::{
|
||||||
|
album::Album,
|
||||||
|
artist::Artist,
|
||||||
|
track::{Format, Track},
|
||||||
|
Collection,
|
||||||
|
};
|
||||||
|
use ratatui::{
|
||||||
|
backend::Backend,
|
||||||
|
layout::{Alignment, Rect},
|
||||||
|
style::{Color, Style},
|
||||||
|
widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::tui::ui::{Category, Selection};
|
||||||
|
|
||||||
|
pub trait IRender {
|
||||||
|
fn render_collection<B: Backend>(
|
||||||
|
artists: &Collection,
|
||||||
|
selection: &mut Selection,
|
||||||
|
frame: &mut Frame<'_, B>,
|
||||||
|
);
|
||||||
|
fn render_info_overlay<B: Backend>(
|
||||||
|
artists: &Collection,
|
||||||
|
selection: &mut Selection,
|
||||||
|
frame: &mut Frame<'_, B>,
|
||||||
|
);
|
||||||
|
fn render_reload_overlay<B: Backend>(frame: &mut Frame<'_, B>);
|
||||||
|
fn render_error_overlay<S: AsRef<str>, B: Backend>(msg: S, frame: &mut Frame<'_, B>);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ArtistArea {
|
||||||
|
list: Rect,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AlbumArea {
|
||||||
|
list: Rect,
|
||||||
|
info: Rect,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TrackArea {
|
||||||
|
list: Rect,
|
||||||
|
info: Rect,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FrameArea {
|
||||||
|
artist: ArtistArea,
|
||||||
|
album: AlbumArea,
|
||||||
|
track: TrackArea,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrameArea {
|
||||||
|
fn new(frame: Rect) -> Self {
|
||||||
|
let width_one_third = frame.width / 3;
|
||||||
|
let height_one_third = frame.height / 3;
|
||||||
|
|
||||||
|
let panel_width = width_one_third;
|
||||||
|
let panel_width_last = frame.width - 2 * panel_width;
|
||||||
|
let panel_height_top = frame.height - height_one_third;
|
||||||
|
let panel_height_bottom = height_one_third;
|
||||||
|
|
||||||
|
let artist_list = Rect {
|
||||||
|
x: frame.x,
|
||||||
|
y: frame.y,
|
||||||
|
width: panel_width,
|
||||||
|
height: frame.height,
|
||||||
|
};
|
||||||
|
|
||||||
|
let album_list = Rect {
|
||||||
|
x: artist_list.x + artist_list.width,
|
||||||
|
y: frame.y,
|
||||||
|
width: panel_width,
|
||||||
|
height: panel_height_top,
|
||||||
|
};
|
||||||
|
|
||||||
|
let album_info = Rect {
|
||||||
|
x: album_list.x,
|
||||||
|
y: album_list.y + album_list.height,
|
||||||
|
width: album_list.width,
|
||||||
|
height: panel_height_bottom,
|
||||||
|
};
|
||||||
|
|
||||||
|
let track_list = Rect {
|
||||||
|
x: album_list.x + album_list.width,
|
||||||
|
y: frame.y,
|
||||||
|
width: panel_width_last,
|
||||||
|
height: panel_height_top,
|
||||||
|
};
|
||||||
|
|
||||||
|
let track_info = Rect {
|
||||||
|
x: track_list.x,
|
||||||
|
y: track_list.y + track_list.height,
|
||||||
|
width: track_list.width,
|
||||||
|
height: panel_height_bottom,
|
||||||
|
};
|
||||||
|
|
||||||
|
FrameArea {
|
||||||
|
artist: ArtistArea { list: artist_list },
|
||||||
|
album: AlbumArea {
|
||||||
|
list: album_list,
|
||||||
|
info: album_info,
|
||||||
|
},
|
||||||
|
track: TrackArea {
|
||||||
|
list: track_list,
|
||||||
|
info: track_info,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OverlaySize {
|
||||||
|
MarginFactor(u16),
|
||||||
|
Value(u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OverlaySize {
|
||||||
|
fn default() -> Self {
|
||||||
|
OverlaySize::MarginFactor(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct OverlayBuilder {
|
||||||
|
width: OverlaySize,
|
||||||
|
height: OverlaySize,
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ArtistState<'a, 'b> {
|
||||||
|
active: bool,
|
||||||
|
list: List<'a>,
|
||||||
|
state: &'b mut ListState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> ArtistState<'a, 'b> {
|
||||||
|
fn new(active: bool, artists: &'a [Artist], state: &'b mut ListState) -> ArtistState<'a, 'b> {
|
||||||
|
let list = List::new(
|
||||||
|
artists
|
||||||
|
.iter()
|
||||||
|
.map(|a| ListItem::new(a.id.name.as_str()))
|
||||||
|
.collect::<Vec<ListItem>>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
ArtistState {
|
||||||
|
active,
|
||||||
|
list,
|
||||||
|
state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ArtistOverlay<'a> {
|
||||||
|
properties: Paragraph<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ArtistOverlay<'a> {
|
||||||
|
fn opt_opt_to_str<S: AsRef<str>>(opt: Option<Option<&S>>) -> &str {
|
||||||
|
opt.flatten().map(|item| item.as_ref()).unwrap_or("")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opt_vec_to_string<S: AsRef<str>>(opt_vec: Option<&Vec<S>>, indent: &str) -> String {
|
||||||
|
opt_vec
|
||||||
|
.map(|vec| {
|
||||||
|
if vec.len() < 2 {
|
||||||
|
vec.first()
|
||||||
|
.map(|item| item.as_ref())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
let indent = format!("\n{indent}");
|
||||||
|
let list = vec
|
||||||
|
.iter()
|
||||||
|
.map(|item| item.as_ref())
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join(&indent);
|
||||||
|
format!("{indent}{list}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| String::from(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(artists: &'a [Artist], state: &ListState) -> ArtistOverlay<'a> {
|
||||||
|
let artist = state.selected().map(|i| &artists[i]);
|
||||||
|
|
||||||
|
let item_indent = " ";
|
||||||
|
let list_indent = " - ";
|
||||||
|
let properties = Paragraph::new(format!(
|
||||||
|
"Artist: {}\n\n{item_indent}\
|
||||||
|
MusicBrainz: {}\n{item_indent}\
|
||||||
|
MusicButler: {}\n{item_indent}\
|
||||||
|
Bandcamp: {}\n{item_indent}\
|
||||||
|
Qobuz: {}",
|
||||||
|
artist.map(|a| a.id.name.as_str()).unwrap_or(""),
|
||||||
|
Self::opt_opt_to_str(artist.map(|a| a.properties.musicbrainz.as_ref())),
|
||||||
|
Self::opt_vec_to_string(artist.map(|a| &a.properties.musicbutler), list_indent),
|
||||||
|
Self::opt_vec_to_string(artist.map(|a| &a.properties.bandcamp), list_indent),
|
||||||
|
Self::opt_opt_to_str(artist.map(|a| a.properties.qobuz.as_ref())),
|
||||||
|
));
|
||||||
|
|
||||||
|
ArtistOverlay { properties }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AlbumState<'a, 'b> {
|
||||||
|
active: bool,
|
||||||
|
list: List<'a>,
|
||||||
|
state: &'b mut ListState,
|
||||||
|
info: Paragraph<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> AlbumState<'a, 'b> {
|
||||||
|
fn new(active: bool, albums: &'a [Album], state: &'b mut ListState) -> AlbumState<'a, 'b> {
|
||||||
|
let list = List::new(
|
||||||
|
albums
|
||||||
|
.iter()
|
||||||
|
.map(|a| ListItem::new(a.id.title.as_str()))
|
||||||
|
.collect::<Vec<ListItem>>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let album = state.selected().map(|i| &albums[i]);
|
||||||
|
let info = Paragraph::new(format!(
|
||||||
|
"Title: {}\n\
|
||||||
|
Year: {}",
|
||||||
|
album.map(|a| a.id.title.as_str()).unwrap_or(""),
|
||||||
|
album.map(|a| a.id.year.to_string()).unwrap_or_default(),
|
||||||
|
));
|
||||||
|
|
||||||
|
AlbumState {
|
||||||
|
active,
|
||||||
|
list,
|
||||||
|
state,
|
||||||
|
info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TrackState<'a, 'b> {
|
||||||
|
active: bool,
|
||||||
|
list: List<'a>,
|
||||||
|
state: &'b mut ListState,
|
||||||
|
info: Paragraph<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> TrackState<'a, 'b> {
|
||||||
|
fn new(active: bool, tracks: &'a [Track], state: &'b mut ListState) -> TrackState<'a, 'b> {
|
||||||
|
let list = List::new(
|
||||||
|
tracks
|
||||||
|
.iter()
|
||||||
|
.map(|tr| ListItem::new(tr.id.title.as_str()))
|
||||||
|
.collect::<Vec<ListItem>>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let track = state.selected().map(|i| &tracks[i]);
|
||||||
|
let info = Paragraph::new(format!(
|
||||||
|
"Track: {}\n\
|
||||||
|
Title: {}\n\
|
||||||
|
Artist: {}\n\
|
||||||
|
Quality: {}",
|
||||||
|
track.map(|t| t.id.number.to_string()).unwrap_or_default(),
|
||||||
|
track.map(|t| t.id.title.as_str()).unwrap_or(""),
|
||||||
|
track.map(|t| t.artist.join("; ")).unwrap_or_default(),
|
||||||
|
track
|
||||||
|
.map(|t| match t.quality.format {
|
||||||
|
Format::Flac => "FLAC".to_string(),
|
||||||
|
Format::Mp3 => format!("MP3 {}kbps", t.quality.bitrate),
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
));
|
||||||
|
|
||||||
|
TrackState {
|
||||||
|
active,
|
||||||
|
list,
|
||||||
|
state,
|
||||||
|
info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Renderer;
|
||||||
|
|
||||||
|
impl Renderer {
|
||||||
|
fn style(_active: bool) -> Style {
|
||||||
|
Style::default().fg(Color::White).bg(Color::Black)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_style(active: bool) -> Style {
|
||||||
|
Self::style(active)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highlight_style(active: bool) -> Style {
|
||||||
|
if active {
|
||||||
|
Style::default().fg(Color::White).bg(Color::DarkGray)
|
||||||
|
} else {
|
||||||
|
Self::style(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block<'a>(title: &str, active: bool) -> Block<'a> {
|
||||||
|
Block::default()
|
||||||
|
.title_alignment(Alignment::Center)
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.style(Self::block_style(active))
|
||||||
|
.title(format!(" {title} "))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_list_widget<B: Backend>(
|
||||||
|
title: &str,
|
||||||
|
list: List,
|
||||||
|
list_state: &mut ListState,
|
||||||
|
active: bool,
|
||||||
|
area: Rect,
|
||||||
|
frame: &mut Frame<'_, B>,
|
||||||
|
) {
|
||||||
|
frame.render_stateful_widget(
|
||||||
|
list.highlight_style(Self::highlight_style(active))
|
||||||
|
.highlight_symbol(">> ")
|
||||||
|
.style(Self::style(active))
|
||||||
|
.block(Self::block(title, active)),
|
||||||
|
area,
|
||||||
|
list_state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_info_widget<B: Backend>(
|
||||||
|
title: &str,
|
||||||
|
paragraph: Paragraph,
|
||||||
|
active: bool,
|
||||||
|
area: Rect,
|
||||||
|
frame: &mut Frame<'_, B>,
|
||||||
|
) {
|
||||||
|
frame.render_widget(
|
||||||
|
paragraph
|
||||||
|
.style(Self::style(active))
|
||||||
|
.block(Self::block(title, active)),
|
||||||
|
area,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_overlay_widget<B: Backend>(
|
||||||
|
title: &str,
|
||||||
|
paragraph: Paragraph,
|
||||||
|
area: Rect,
|
||||||
|
frame: &mut Frame<'_, B>,
|
||||||
|
) {
|
||||||
|
frame.render_widget(Clear, area);
|
||||||
|
frame.render_widget(
|
||||||
|
paragraph
|
||||||
|
.style(Self::style(true))
|
||||||
|
.block(Self::block(title, true)),
|
||||||
|
area,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_artist_column<B: Backend>(st: ArtistState, ar: ArtistArea, fr: &mut Frame<'_, B>) {
|
||||||
|
Self::render_list_widget("Artists", st.list, st.state, st.active, ar.list, fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_album_column<B: Backend>(st: AlbumState, ar: AlbumArea, fr: &mut Frame<'_, B>) {
|
||||||
|
Self::render_list_widget("Albums", st.list, st.state, st.active, ar.list, fr);
|
||||||
|
Self::render_info_widget("Album info", st.info, st.active, ar.info, fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_track_column<B: Backend>(st: TrackState, ar: TrackArea, fr: &mut Frame<'_, B>) {
|
||||||
|
Self::render_list_widget("Tracks", st.list, st.state, st.active, ar.list, fr);
|
||||||
|
Self::render_info_widget("Track info", st.info, st.active, ar.info, fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IRender for Renderer {
|
||||||
|
fn render_collection<B: Backend>(
|
||||||
|
artists: &Collection,
|
||||||
|
selection: &mut Selection,
|
||||||
|
frame: &mut Frame<'_, B>,
|
||||||
|
) {
|
||||||
|
let active = selection.active;
|
||||||
|
let areas = FrameArea::new(frame.size());
|
||||||
|
|
||||||
|
let artist_selection = &mut selection.artist;
|
||||||
|
let artist_state = ArtistState::new(
|
||||||
|
active == Category::Artist,
|
||||||
|
artists,
|
||||||
|
&mut artist_selection.state,
|
||||||
|
);
|
||||||
|
|
||||||
|
Self::render_artist_column(artist_state, areas.artist, frame);
|
||||||
|
|
||||||
|
let no_albums: Vec<Album> = vec![];
|
||||||
|
let albums = artist_selection
|
||||||
|
.state
|
||||||
|
.selected()
|
||||||
|
.map(|i| &artists[i].albums)
|
||||||
|
.unwrap_or_else(|| &no_albums);
|
||||||
|
let album_selection = &mut artist_selection.album;
|
||||||
|
let album_state = AlbumState::new(
|
||||||
|
active == Category::Album,
|
||||||
|
albums,
|
||||||
|
&mut album_selection.state,
|
||||||
|
);
|
||||||
|
|
||||||
|
Self::render_album_column(album_state, areas.album, frame);
|
||||||
|
|
||||||
|
let no_tracks: Vec<Track> = vec![];
|
||||||
|
let tracks = album_selection
|
||||||
|
.state
|
||||||
|
.selected()
|
||||||
|
.map(|i| &albums[i].tracks)
|
||||||
|
.unwrap_or_else(|| &no_tracks);
|
||||||
|
let track_selection = &mut album_selection.track;
|
||||||
|
let track_state = TrackState::new(
|
||||||
|
active == Category::Track,
|
||||||
|
tracks,
|
||||||
|
&mut track_selection.state,
|
||||||
|
);
|
||||||
|
|
||||||
|
Self::render_track_column(track_state, areas.track, frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_info_overlay<B: Backend>(
|
||||||
|
artists: &Collection,
|
||||||
|
selection: &mut Selection,
|
||||||
|
frame: &mut Frame<'_, B>,
|
||||||
|
) {
|
||||||
|
let area = OverlayBuilder::default().build(frame.size());
|
||||||
|
|
||||||
|
let artist_selection = &mut selection.artist;
|
||||||
|
let artist_overlay = ArtistOverlay::new(artists, &artist_selection.state);
|
||||||
|
|
||||||
|
Self::render_overlay_widget("Artist", artist_overlay.properties, area, frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_reload_overlay<B: Backend>(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>(msg: S, frame: &mut Frame<'_, B>) {
|
||||||
|
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 });
|
||||||
|
|
||||||
|
// FIXME: highlight with red
|
||||||
|
Self::render_overlay_widget("Error", error_text, area, frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::tui::{testmod::COLLECTION, tests::terminal};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn draw_test_suite(artists: &Collection, selection: &mut Selection) {
|
||||||
|
let mut terminal = terminal();
|
||||||
|
|
||||||
|
terminal
|
||||||
|
.draw(|frame| Renderer::render_collection(artists, selection, frame))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
terminal
|
||||||
|
.draw(|frame| Renderer::render_info_overlay(artists, selection, frame))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
terminal
|
||||||
|
.draw(|frame| {
|
||||||
|
Renderer::render_collection(artists, selection, frame);
|
||||||
|
Renderer::render_info_overlay(artists, selection, frame);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
terminal
|
||||||
|
.draw(|frame| {
|
||||||
|
Renderer::render_collection(artists, selection, frame);
|
||||||
|
Renderer::render_reload_overlay(frame);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
terminal
|
||||||
|
.draw(|frame| {
|
||||||
|
Renderer::render_collection(artists, selection, frame);
|
||||||
|
Renderer::render_error_overlay("get rekt scrub", frame);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stateless() {
|
||||||
|
let mut terminal = terminal();
|
||||||
|
|
||||||
|
terminal
|
||||||
|
.draw(|frame| Renderer::render_reload_overlay(frame))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
terminal
|
||||||
|
.draw(|frame| Renderer::render_error_overlay("get rekt scrub", frame))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty() {
|
||||||
|
let artists: Vec<Artist> = vec![];
|
||||||
|
let mut selection = Selection::new(Some(&artists));
|
||||||
|
|
||||||
|
draw_test_suite(&artists, &mut selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collection() {
|
||||||
|
let artists: &Collection = &COLLECTION;
|
||||||
|
let mut selection = Selection::new(Some(artists));
|
||||||
|
|
||||||
|
draw_test_suite(&artists, &mut selection);
|
||||||
|
|
||||||
|
// Change the track (which has a different track format).
|
||||||
|
selection.increment_category();
|
||||||
|
selection.increment_category();
|
||||||
|
selection.increment_selection(artists);
|
||||||
|
|
||||||
|
draw_test_suite(&artists, &mut selection);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user