Provide search functionality through the TUI #134

Merged
wojtek merged 35 commits from 24---provide-search-functionality-through-the-tui into main 2024-02-18 22:12:42 +01:00
9 changed files with 1236 additions and 1102 deletions
Showing only changes of commit 954199b001 - Show all commits

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
pub mod app;
pub mod selection;
mod state;
use musichoard::collection::Collection;

108
src/tui/app/state/browse.rs Normal file
View File

@ -0,0 +1,108 @@
use crate::tui::{
app::{
app::App,
selection::{Delta, ListSelection},
state::{critical::AppCritical, AppInner, AppMachine},
AppPublic, AppState, IAppInteractBrowse,
},
lib::IMusicHoard,
};
pub struct AppBrowse;
impl<MH: IMusicHoard> AppMachine<MH, AppBrowse> {
pub fn browse(inner: AppInner<MH>) -> Self {
AppMachine {
inner,
state: AppBrowse,
}
}
pub fn new(mut music_hoard: MH) -> Result<Self, AppMachine<MH, AppCritical>> {
let init_result = Self::init(&mut music_hoard);
let inner = AppInner::new(music_hoard);
match init_result {
Ok(()) => Ok(AppMachine::browse(inner)),
Err(err) => Err(AppMachine::critical(inner, err.to_string())),
}
}
fn init(music_hoard: &mut MH) -> Result<(), musichoard::Error> {
music_hoard.load_from_database()?;
music_hoard.rescan_library()?;
Ok(())
}
}
impl<MH: IMusicHoard> From<AppMachine<MH, AppBrowse>> for App<MH> {
fn from(machine: AppMachine<MH, AppBrowse>) -> Self {
AppState::Browse(machine)
}
}
impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppBrowse>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<MH, AppBrowse>) -> Self {
AppPublic {
inner: (&mut machine.inner).into(),
state: AppState::Browse(()),
}
}
}
impl<MH: IMusicHoard> IAppInteractBrowse for AppMachine<MH, AppBrowse> {
type APP = App<MH>;
fn save_and_quit(mut self) -> Self::APP {
match self.inner.music_hoard.save_to_database() {
Ok(_) => {
self.inner.running = false;
self.into()
}
Err(err) => AppMachine::error(self.inner, err.to_string()).into(),
}
}
fn increment_category(mut self) -> Self::APP {
self.inner.selection.increment_category();
self.into()
}
fn decrement_category(mut self) -> Self::APP {
self.inner.selection.decrement_category();
self.into()
}
fn increment_selection(mut self, delta: Delta) -> Self::APP {
self.inner
.selection
.increment_selection(self.inner.music_hoard.get_collection(), delta);
self.into()
}
fn decrement_selection(mut self, delta: Delta) -> Self::APP {
self.inner
.selection
.decrement_selection(self.inner.music_hoard.get_collection(), delta);
self.into()
}
fn show_info_overlay(self) -> Self::APP {
AppMachine::info(self.inner).into()
}
fn show_reload_menu(self) -> Self::APP {
AppMachine::reload(self.inner).into()
}
fn begin_search(mut self) -> Self::APP {
let orig = ListSelection::get(&self.inner.selection);
self.inner
.selection
.reset_artist(self.inner.music_hoard.get_collection());
AppMachine::search(self.inner, orig).into()
}
fn no_op(self) -> Self::APP {
self.into()
}
}

View File

@ -0,0 +1,46 @@
use crate::tui::{
app::{
app::App,
state::{AppInner, AppMachine},
AppPublic, AppState, IAppInteractCritical,
},
lib::IMusicHoard,
};
pub struct AppCritical {
string: String,
}
impl<MH: IMusicHoard> AppMachine<MH, AppCritical> {
pub fn critical<S: Into<String>>(inner: AppInner<MH>, string: S) -> Self {
AppMachine {
inner,
state: AppCritical {
string: string.into(),
},
}
}
}
impl<MH: IMusicHoard> From<AppMachine<MH, AppCritical>> for App<MH> {
fn from(machine: AppMachine<MH, AppCritical>) -> Self {
AppState::Critical(machine)
}
}
impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppCritical>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<MH, AppCritical>) -> Self {
AppPublic {
inner: (&mut machine.inner).into(),
state: AppState::Critical(&machine.state.string),
}
}
}
impl<MH: IMusicHoard> IAppInteractCritical for AppMachine<MH, AppCritical> {
type APP = App<MH>;
fn no_op(self) -> Self::APP {
self.into()
}
}

View File

@ -0,0 +1,46 @@
use crate::tui::{
app::{
app::App,
state::{AppInner, AppMachine},
AppPublic, AppState, IAppInteractError,
},
lib::IMusicHoard,
};
pub struct AppError {
string: String,
}
impl<MH: IMusicHoard> AppMachine<MH, AppError> {
pub fn error<S: Into<String>>(inner: AppInner<MH>, string: S) -> Self {
AppMachine {
inner,
state: AppError {
string: string.into(),
},
}
}
}
impl<MH: IMusicHoard> From<AppMachine<MH, AppError>> for App<MH> {
fn from(machine: AppMachine<MH, AppError>) -> Self {
AppState::Error(machine)
}
}
impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppError>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<MH, AppError>) -> Self {
AppPublic {
inner: (&mut machine.inner).into(),
state: AppState::Error(&machine.state.string),
}
}
}
impl<MH: IMusicHoard> IAppInteractError for AppMachine<MH, AppError> {
type APP = App<MH>;
fn dismiss_error(self) -> Self::APP {
AppMachine::browse(self.inner).into()
}
}

46
src/tui/app/state/info.rs Normal file
View File

@ -0,0 +1,46 @@
use crate::tui::{
app::{
app::App,
state::{AppInner, AppMachine},
AppPublic, AppState, IAppInteractInfo,
},
lib::IMusicHoard,
};
pub struct AppInfo;
impl<MH: IMusicHoard> AppMachine<MH, AppInfo> {
pub fn info(inner: AppInner<MH>) -> Self {
AppMachine {
inner,
state: AppInfo,
}
}
}
impl<MH: IMusicHoard> From<AppMachine<MH, AppInfo>> for App<MH> {
fn from(machine: AppMachine<MH, AppInfo>) -> Self {
AppState::Info(machine)
}
}
impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppInfo>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<MH, AppInfo>) -> Self {
AppPublic {
inner: (&mut machine.inner).into(),
state: AppState::Info(()),
}
}
}
impl<MH: IMusicHoard> IAppInteractInfo for AppMachine<MH, AppInfo> {
type APP = App<MH>;
fn hide_info_overlay(self) -> Self::APP {
AppMachine::browse(self.inner).into()
}
fn no_op(self) -> Self::APP {
self.into()
}
}

778
src/tui/app/state/mod.rs Normal file
View File

@ -0,0 +1,778 @@
pub mod browse;
pub mod critical;
pub mod error;
pub mod info;
pub mod reload;
pub mod search;
use crate::tui::{
app::{selection::Selection, AppPublicInner},
lib::IMusicHoard,
};
pub struct AppMachine<MH: IMusicHoard, STATE> {
inner: AppInner<MH>,
state: STATE,
}
impl<MH: IMusicHoard, STATE> AppMachine<MH, STATE> {
pub fn inner_ref(&self) -> &AppInner<MH> {
&self.inner
}
pub fn inner_mut(&mut self) -> &mut AppInner<MH> {
&mut self.inner
}
}
pub struct AppInner<MH: IMusicHoard> {
running: bool,
music_hoard: MH,
selection: Selection,
}
impl<MH: IMusicHoard> AppInner<MH> {
pub fn new(music_hoard: MH) -> Self {
let selection = Selection::new(music_hoard.get_collection());
AppInner {
running: true,
music_hoard,
selection,
}
}
pub fn is_running(&self) -> bool {
self.running
}
pub fn stop(&mut self) {
self.running = false;
}
}
impl<'a, MH: IMusicHoard> From<&'a mut AppInner<MH>> for AppPublicInner<'a> {
fn from(inner: &'a mut AppInner<MH>) -> Self {
AppPublicInner {
collection: inner.music_hoard.get_collection(),
selection: &mut inner.selection,
}
}
}
#[cfg(test)]
mod tests {
use musichoard::collection::Collection;
use crate::tui::{
app::{
app::App,
selection::{Category, Delta},
AppPublicState, AppState, IAppInteract, IAppInteractBrowse, IAppInteractError,
IAppInteractInfo, IAppInteractReload, IAppInteractSearch,
},
lib::MockIMusicHoard,
testmod::COLLECTION,
};
use super::*;
impl<BS, IS, RS, SS, ES, CS> AppState<BS, IS, RS, SS, ES, CS> {
fn unwrap_browse(self) -> BS {
match self {
AppState::Browse(browse) => browse,
_ => panic!(),
}
}
fn unwrap_info(self) -> IS {
match self {
AppState::Info(info) => info,
_ => panic!(),
}
}
fn unwrap_reload(self) -> RS {
match self {
AppState::Reload(reload) => reload,
_ => panic!(),
}
}
fn unwrap_search(self) -> SS {
match self {
AppState::Search(search) => search,
_ => panic!(),
}
}
fn unwrap_error(self) -> ES {
match self {
AppState::Error(error) => error,
_ => panic!(),
}
}
fn unwrap_critical(self) -> CS {
match self {
AppState::Critical(critical) => critical,
_ => panic!(),
}
}
}
fn music_hoard(collection: Collection) -> MockIMusicHoard {
let mut music_hoard = MockIMusicHoard::new();
music_hoard
.expect_load_from_database()
.times(1)
.return_once(|| Ok(()));
music_hoard
.expect_rescan_library()
.times(1)
.return_once(|| Ok(()));
music_hoard.expect_get_collection().return_const(collection);
music_hoard
}
#[test]
fn app_is_state() {
let state = AppPublicState::Search("get rekt");
assert!(state.is_search());
}
#[test]
fn running_quit() {
let mut music_hoard = music_hoard(COLLECTION.to_owned());
music_hoard
.expect_save_to_database()
.times(1)
.return_once(|| Ok(()));
let app = App::new(music_hoard);
assert!(app.is_running());
let browse = app.unwrap_browse();
let app = browse.save_and_quit();
assert!(!app.is_running());
}
#[test]
fn error_quit() {
let mut music_hoard = music_hoard(COLLECTION.to_owned());
music_hoard
.expect_save_to_database()
.times(1)
.return_once(|| Ok(()));
let app = App::new(music_hoard);
assert!(app.is_running());
let app = App::Error(AppMachine::error(
app.unwrap_browse().inner,
String::from("get rekt"),
));
let error = app.unwrap_error();
let browse = error.dismiss_error().unwrap_browse();
let app = browse.save_and_quit();
assert!(!app.is_running());
}
#[test]
fn running_force_quit() {
let app = App::new(music_hoard(COLLECTION.to_owned()));
assert!(app.is_running());
let app = app.force_quit();
assert!(!app.is_running());
}
#[test]
fn error_force_quit() {
let app = App::new(music_hoard(COLLECTION.to_owned()));
assert!(app.is_running());
let app = App::Error(AppMachine::error(
app.unwrap_browse().inner,
String::from("get rekt"),
));
let app = app.force_quit();
assert!(!app.is_running());
}
#[test]
fn save() {
let mut music_hoard = music_hoard(COLLECTION.to_owned());
music_hoard
.expect_save_to_database()
.times(1)
.return_once(|| Ok(()));
let browse = App::new(music_hoard).unwrap_browse();
browse.save_and_quit().unwrap_browse();
}
#[test]
fn save_error() {
let mut music_hoard = music_hoard(COLLECTION.to_owned());
music_hoard
.expect_save_to_database()
.times(1)
.return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt"))));
let browse = App::new(music_hoard).unwrap_browse();
browse.save_and_quit().unwrap_error();
}
#[test]
fn init_error() {
let mut music_hoard = MockIMusicHoard::new();
music_hoard
.expect_load_from_database()
.times(1)
.return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt"))));
music_hoard.expect_get_collection().return_const(vec![]);
let app = App::new(music_hoard);
assert!(app.is_running());
app.unwrap_critical();
}
#[test]
fn modifiers() {
let app = App::new(music_hoard(COLLECTION.to_owned()));
assert!(app.is_running());
let browse = app.unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.artist.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
let browse = browse.increment_category().unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Album);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Album);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
let browse = browse.increment_category().unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(1));
let browse = browse.increment_category().unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(1));
let browse = browse.decrement_selection(Delta::Line).unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
let browse = browse.decrement_category().unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Album);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(1));
let browse = browse.decrement_selection(Delta::Line).unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Album);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
let browse = browse.decrement_category().unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.artist.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
let browse = browse.decrement_selection(Delta::Line).unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.artist.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
let browse = browse.increment_category().unwrap_browse();
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
let browse = browse.decrement_category().unwrap_browse();
let browse = browse.decrement_selection(Delta::Line).unwrap_browse();
let browse = browse.decrement_category().unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.artist.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.state.list.selected(), Some(1));
assert_eq!(selection.artist.album.track.state.list.selected(), Some(0));
}
#[test]
fn no_tracks() {
let mut collection = COLLECTION.to_owned();
collection[0].albums[0].tracks = vec![];
let app = App::new(music_hoard(collection));
assert!(app.is_running());
let browse = app.unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.artist.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.track.state.list.selected(), None);
let browse = browse.increment_category().unwrap_browse();
let browse = browse.increment_category().unwrap_browse();
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.artist.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.track.state.list.selected(), None);
let browse = browse.decrement_selection(Delta::Line).unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.artist.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.track.state.list.selected(), None);
}
#[test]
fn no_albums() {
let mut collection = COLLECTION.to_owned();
collection[0].albums = vec![];
let app = App::new(music_hoard(collection));
assert!(app.is_running());
let browse = app.unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.artist.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.state.list.selected(), None);
assert_eq!(selection.artist.album.track.state.list.selected(), None);
let browse = browse.increment_category().unwrap_browse();
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Album);
assert_eq!(selection.artist.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.state.list.selected(), None);
assert_eq!(selection.artist.album.track.state.list.selected(), None);
let browse = browse.decrement_selection(Delta::Line).unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Album);
assert_eq!(selection.artist.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.state.list.selected(), None);
assert_eq!(selection.artist.album.track.state.list.selected(), None);
let browse = browse.increment_category().unwrap_browse();
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.artist.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.state.list.selected(), None);
assert_eq!(selection.artist.album.track.state.list.selected(), None);
let browse = browse.decrement_selection(Delta::Line).unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.artist.state.list.selected(), Some(0));
assert_eq!(selection.artist.album.state.list.selected(), None);
assert_eq!(selection.artist.album.track.state.list.selected(), None);
}
#[test]
fn no_artists() {
let app = App::new(music_hoard(vec![]));
assert!(app.is_running());
let browse = app.unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.artist.state.list.selected(), None);
assert_eq!(selection.artist.album.state.list.selected(), None);
assert_eq!(selection.artist.album.track.state.list.selected(), None);
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.artist.state.list.selected(), None);
assert_eq!(selection.artist.album.state.list.selected(), None);
assert_eq!(selection.artist.album.track.state.list.selected(), None);
let browse = browse.decrement_selection(Delta::Line).unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Artist);
assert_eq!(selection.artist.state.list.selected(), None);
assert_eq!(selection.artist.album.state.list.selected(), None);
assert_eq!(selection.artist.album.track.state.list.selected(), None);
let browse = browse.increment_category().unwrap_browse();
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Album);
assert_eq!(selection.artist.state.list.selected(), None);
assert_eq!(selection.artist.album.state.list.selected(), None);
assert_eq!(selection.artist.album.track.state.list.selected(), None);
let browse = browse.decrement_selection(Delta::Line).unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Album);
assert_eq!(selection.artist.state.list.selected(), None);
assert_eq!(selection.artist.album.state.list.selected(), None);
assert_eq!(selection.artist.album.track.state.list.selected(), None);
let browse = browse.increment_category().unwrap_browse();
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.artist.state.list.selected(), None);
assert_eq!(selection.artist.album.state.list.selected(), None);
assert_eq!(selection.artist.album.track.state.list.selected(), None);
let browse = browse.decrement_selection(Delta::Line).unwrap_browse();
let selection = &browse.inner.selection;
assert_eq!(selection.active, Category::Track);
assert_eq!(selection.artist.state.list.selected(), None);
assert_eq!(selection.artist.album.state.list.selected(), None);
assert_eq!(selection.artist.album.track.state.list.selected(), None);
}
#[test]
fn info_overlay() {
let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse();
let info = browse.show_info_overlay().unwrap_info();
info.hide_info_overlay().unwrap_browse();
}
#[test]
fn reload_hide_menu() {
let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse();
let reload = browse.show_reload_menu().unwrap_reload();
reload.hide_reload_menu().unwrap_browse();
}
#[test]
fn reload_database() {
let mut music_hoard = music_hoard(COLLECTION.to_owned());
music_hoard
.expect_load_from_database()
.times(1)
.return_once(|| Ok(()));
let browse = App::new(music_hoard).unwrap_browse();
let reload = browse.show_reload_menu().unwrap_reload();
reload.reload_database().unwrap_browse();
}
#[test]
fn reload_library() {
let mut music_hoard = music_hoard(COLLECTION.to_owned());
music_hoard
.expect_rescan_library()
.times(1)
.return_once(|| Ok(()));
let browse = App::new(music_hoard).unwrap_browse();
let reload = browse.show_reload_menu().unwrap_reload();
reload.reload_library().unwrap_browse();
}
#[test]
fn reload_error() {
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 browse = App::new(music_hoard).unwrap_browse();
let reload = browse.show_reload_menu().unwrap_reload();
let error = reload.reload_database().unwrap_error();
error.dismiss_error().unwrap_browse();
}
#[test]
fn search() {
let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse();
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
let browse = browse.increment_category().unwrap_browse();
assert_eq!(browse.inner.selection.active, Category::Album);
assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1));
let search = browse.begin_search().unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0));
let search = search.append_character('a').unwrap_search();
let search = search.append_character('l').unwrap_search();
let search = search.append_character('b').unwrap_search();
let search = search.append_character('u').unwrap_search();
let search = search.append_character('m').unwrap_search();
let search = search.append_character('_').unwrap_search();
let search = search.append_character('a').unwrap_search();
let search = search.append_character('r').unwrap_search();
let search = search.append_character('t').unwrap_search();
let search = search.append_character('i').unwrap_search();
let search = search.append_character('s').unwrap_search();
let search = search.append_character('t').unwrap_search();
let search = search.append_character(' ').unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0));
let search = search.append_character('\'').unwrap_search();
let search = search.append_character('c').unwrap_search();
let search = search.append_character('\'').unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2));
let search = search.step_back().unwrap_search();
let search = search.step_back().unwrap_search();
let search = search.step_back().unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0));
let search = search.append_character('\'').unwrap_search();
let search = search.append_character('b').unwrap_search();
let search = search.append_character('\'').unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1));
let browse = search.finish_search().unwrap_browse();
assert_eq!(browse.inner.selection.active, Category::Album);
assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1));
}
#[test]
fn search_next() {
let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse();
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
let browse = browse.increment_category().unwrap_browse();
assert_eq!(browse.inner.selection.active, Category::Album);
assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1));
let search = browse.begin_search().unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0));
let search = search.step_back().unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0));
let search = search.append_character('a').unwrap_search();
let search = search.search_next().unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1));
let search = search.search_next().unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2));
let search = search.search_next().unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3));
let search = search.search_next().unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3));
let search = search.step_back().unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(3));
let search = search.step_back().unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2));
let search = search.step_back().unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(1));
let search = search.step_back().unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0));
let search = search.step_back().unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0));
}
#[test]
fn cancel_search() {
let browse = App::new(music_hoard(COLLECTION.to_owned())).unwrap_browse();
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
let browse = browse.increment_category().unwrap_browse();
assert_eq!(browse.inner.selection.active, Category::Album);
assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1));
let search = browse.begin_search().unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(0));
let search = search.append_character('a').unwrap_search();
let search = search.append_character('l').unwrap_search();
let search = search.append_character('b').unwrap_search();
let search = search.append_character('u').unwrap_search();
let search = search.append_character('m').unwrap_search();
let search = search.append_character('_').unwrap_search();
let search = search.append_character('a').unwrap_search();
let search = search.append_character('r').unwrap_search();
let search = search.append_character('t').unwrap_search();
let search = search.append_character('i').unwrap_search();
let search = search.append_character('s').unwrap_search();
let search = search.append_character('t').unwrap_search();
let search = search.append_character(' ').unwrap_search();
let search = search.append_character('\'').unwrap_search();
let search = search.append_character('c').unwrap_search();
let search = search.append_character('\'').unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), Some(2));
let browse = search.cancel_search().unwrap_browse();
assert_eq!(browse.inner.selection.active, Category::Album);
assert_eq!(browse.inner.selection.artist.state.list.selected(), Some(1));
}
#[test]
fn empty_search() {
let browse = App::new(music_hoard(vec![])).unwrap_browse();
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
let browse = browse.increment_category().unwrap_browse();
assert_eq!(browse.inner.selection.active, Category::Album);
assert_eq!(browse.inner.selection.artist.state.list.selected(), None);
let search = browse.begin_search().unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), None);
let search = search.append_character('a').unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), None);
let search = search.search_next().unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), None);
let search = search.step_back().unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), None);
let search = search.step_back().unwrap_search();
assert_eq!(search.inner.selection.active, Category::Album);
assert_eq!(search.inner.selection.artist.state.list.selected(), None);
let browse = search.cancel_search().unwrap_browse();
assert_eq!(browse.inner.selection.active, Category::Album);
assert_eq!(browse.inner.selection.artist.state.list.selected(), None);
}
}

View File

@ -0,0 +1,82 @@
use crate::tui::{
app::{
app::App,
selection::IdSelection,
state::{AppInner, AppMachine},
AppPublic, AppState, IAppInteractReload,
},
lib::IMusicHoard,
};
pub struct AppReload;
impl<MH: IMusicHoard> AppMachine<MH, AppReload> {
pub fn reload(inner: AppInner<MH>) -> Self {
AppMachine {
inner,
state: AppReload,
}
}
}
impl<MH: IMusicHoard> From<AppMachine<MH, AppReload>> for App<MH> {
fn from(machine: AppMachine<MH, AppReload>) -> Self {
AppState::Reload(machine)
}
}
impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppReload>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<MH, AppReload>) -> Self {
AppPublic {
inner: (&mut machine.inner).into(),
state: AppState::Reload(()),
}
}
}
impl<MH: IMusicHoard> IAppInteractReload for AppMachine<MH, AppReload> {
type APP = App<MH>;
fn reload_library(mut self) -> Self::APP {
let previous = IdSelection::get(
self.inner.music_hoard.get_collection(),
&self.inner.selection,
);
let result = self.inner.music_hoard.rescan_library();
self.refresh(previous, result)
}
fn reload_database(mut self) -> Self::APP {
let previous = IdSelection::get(
self.inner.music_hoard.get_collection(),
&self.inner.selection,
);
let result = self.inner.music_hoard.load_from_database();
self.refresh(previous, result)
}
fn hide_reload_menu(self) -> Self::APP {
AppMachine::browse(self.inner).into()
}
fn no_op(self) -> Self::APP {
self.into()
}
}
trait IAppInteractReloadPrivate<MH: IMusicHoard> {
fn refresh(self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App<MH>;
}
impl<MH: IMusicHoard> IAppInteractReloadPrivate<MH> for AppMachine<MH, AppReload> {
fn refresh(mut self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App<MH> {
match result {
Ok(()) => {
self.inner
.selection
.select_by_id(self.inner.music_hoard.get_collection(), previous);
AppMachine::browse(self.inner).into()
}
Err(err) => AppMachine::error(self.inner, err.to_string()).into(),
}
}
}

100
src/tui/app/state/search.rs Normal file
View File

@ -0,0 +1,100 @@
use crate::tui::{
app::{
app::App,
selection::ListSelection,
state::{AppInner, AppMachine},
AppState, IAppInteractSearch, AppPublic,
},
lib::IMusicHoard,
};
pub struct AppSearch {
string: String,
orig: ListSelection,
memo: Vec<AppSearchMemo>,
}
struct AppSearchMemo {
index: Option<usize>,
char: bool,
}
impl<MH: IMusicHoard> AppMachine<MH, AppSearch> {
pub fn search(inner: AppInner<MH>, orig: ListSelection) -> Self {
AppMachine {
inner,
state: AppSearch {
string: String::new(),
orig,
memo: vec![],
},
}
}
}
impl<MH: IMusicHoard> From<AppMachine<MH, AppSearch>> for App<MH> {
fn from(machine: AppMachine<MH, AppSearch>) -> Self {
AppState::Search(machine)
}
}
impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppSearch>> for AppPublic<'a> {
fn from(machine: &'a mut AppMachine<MH, AppSearch>) -> Self {
AppPublic {
inner: (&mut machine.inner).into(),
state: AppState::Search(&machine.state.string),
}
}
}
impl<MH: IMusicHoard> IAppInteractSearch for AppMachine<MH, AppSearch> {
type APP = App<MH>;
fn append_character(mut self, ch: char) -> Self::APP {
let collection = self.inner.music_hoard.get_collection();
self.state.string.push(ch);
let index =
self.inner
.selection
.incremental_artist_search(collection, &self.state.string, false);
self.state.memo.push(AppSearchMemo { index, char: true });
self.into()
}
fn search_next(mut self) -> Self::APP {
let collection = self.inner.music_hoard.get_collection();
if !self.state.string.is_empty() {
let index = self.inner.selection.incremental_artist_search(
collection,
&self.state.string,
true,
);
self.state.memo.push(AppSearchMemo { index, char: false });
}
self.into()
}
fn step_back(mut self) -> Self::APP {
let collection = self.inner.music_hoard.get_collection();
if let Some(memo) = self.state.memo.pop() {
if memo.char {
self.state.string.pop();
}
self.inner.selection.select_artist(collection, memo.index);
}
self.into()
}
fn finish_search(self) -> Self::APP {
AppMachine::browse(self.inner).into()
}
fn cancel_search(mut self) -> Self::APP {
self.inner.selection.select_by_list(self.state.orig);
AppMachine::browse(self.inner).into()
}
fn no_op(self) -> Self::APP {
self.into()
}
}