diff --git a/src/tui/app/app.rs b/src/tui/app/app.rs index e3a4928..5b0d8bb 100644 --- a/src/tui/app/app.rs +++ b/src/tui/app/app.rs @@ -1,22 +1,16 @@ -// FIXME: Split file apart #![allow(clippy::module_inception)] use crate::tui::{ app::{ - selection::{Delta, IdSelection, ListSelection, Selection}, - AppPublic, AppPublicInner, AppState, IAppAccess, IAppInteract, IAppInteractBrowse, - IAppInteractCritical, IAppInteractError, IAppInteractInfo, IAppInteractReload, - IAppInteractSearch, + state::{ + browse::AppBrowse, critical::AppCritical, error::AppError, info::AppInfo, + reload::AppReload, search::AppSearch, AppInner, AppMachine, + }, + AppPublic, AppState, IAppAccess, IAppInteract, }, lib::IMusicHoard, }; -struct AppInner { - running: bool, - music_hoard: MH, - selection: Selection, -} - pub type App = AppState< AppMachine, AppMachine, @@ -26,174 +20,33 @@ pub type App = AppState< AppMachine, >; -pub struct AppMachine { - inner: AppInner, - state: STATE, -} - -pub struct AppBrowse; - -impl AppMachine { - fn browse(inner: AppInner) -> Self { - AppMachine { - inner, - state: AppBrowse, - } - } -} - -impl From> for App { - fn from(value: AppMachine) -> Self { - AppState::Browse(value) - } -} - -pub struct AppInfo; - -impl AppMachine { - fn info(inner: AppInner) -> Self { - AppMachine { - inner, - state: AppInfo, - } - } -} - -impl From> for App { - fn from(value: AppMachine) -> Self { - AppState::Info(value) - } -} - -pub struct AppReload; - -impl AppMachine { - fn reload(inner: AppInner) -> Self { - AppMachine { - inner, - state: AppReload, - } - } -} - -impl From> for App { - fn from(value: AppMachine) -> Self { - AppState::Reload(value) - } -} - -pub struct AppSearch { - string: String, - orig: ListSelection, - memo: Vec, -} - -struct AppSearchMemo { - index: Option, - char: bool, -} - -impl AppMachine { - fn search(inner: AppInner, orig: ListSelection) -> Self { - AppMachine { - inner, - state: AppSearch { - string: String::new(), - orig, - memo: vec![], - }, - } - } -} - -impl From> for App { - fn from(value: AppMachine) -> Self { - AppState::Search(value) - } -} - -pub struct AppError { - string: String, -} - -impl AppMachine { - fn error>(inner: AppInner, string: S) -> Self { - AppMachine { - inner, - state: AppError { - string: string.into(), - }, - } - } -} - -impl From> for App { - fn from(value: AppMachine) -> Self { - AppState::Error(value) - } -} - -pub struct AppCritical { - string: String, -} - -impl AppMachine { - fn critical>(inner: AppInner, string: S) -> Self { - AppMachine { - inner, - state: AppCritical { - string: string.into(), - }, - } - } -} - -impl From> for App { - fn from(value: AppMachine) -> Self { - AppState::Critical(value) - } -} - impl App { - pub fn new(mut music_hoard: MH) -> Self { - let init_result = Self::init(&mut music_hoard); - let selection = Selection::new(music_hoard.get_collection()); - let inner = AppInner { - running: true, - music_hoard, - selection, - }; - match init_result { - Ok(()) => AppMachine::browse(inner).into(), - Err(err) => AppMachine::critical(inner, err.to_string()).into(), + pub fn new(music_hoard: MH) -> Self { + match AppMachine::new(music_hoard) { + Ok(browse) => browse.into(), + Err(critical) => critical.into(), } } - fn init(music_hoard: &mut MH) -> Result<(), musichoard::Error> { - music_hoard.load_from_database()?; - music_hoard.rescan_library()?; - Ok(()) - } - fn inner_ref(&self) -> &AppInner { match self { - AppState::Browse(browse) => &browse.inner, - AppState::Info(info) => &info.inner, - AppState::Reload(reload) => &reload.inner, - AppState::Search(search) => &search.inner, - AppState::Error(error) => &error.inner, - AppState::Critical(critical) => &critical.inner, + AppState::Browse(browse) => browse.inner_ref(), + AppState::Info(info) => info.inner_ref(), + AppState::Reload(reload) => reload.inner_ref(), + AppState::Search(search) => search.inner_ref(), + AppState::Error(error) => error.inner_ref(), + AppState::Critical(critical) => critical.inner_ref(), } } fn inner_mut(&mut self) -> &mut AppInner { match self { - AppState::Browse(browse) => &mut browse.inner, - AppState::Info(info) => &mut info.inner, - AppState::Reload(reload) => &mut reload.inner, - AppState::Search(search) => &mut search.inner, - AppState::Error(error) => &mut error.inner, - AppState::Critical(critical) => &mut critical.inner, + AppState::Browse(browse) => browse.inner_mut(), + AppState::Info(info) => info.inner_mut(), + AppState::Reload(reload) => reload.inner_mut(), + AppState::Search(search) => search.inner_mut(), + AppState::Error(error) => error.inner_mut(), + AppState::Critical(critical) => critical.inner_mut(), } } } @@ -207,11 +60,11 @@ impl IAppInteract for App { type CS = AppMachine; fn is_running(&self) -> bool { - self.inner_ref().running + self.inner_ref().is_running() } fn force_quit(mut self) -> Self { - self.inner_mut().running = false; + self.inner_mut().stop(); self } @@ -220,941 +73,15 @@ impl IAppInteract for App { } } -impl IAppInteractBrowse for AppMachine { - type APP = App; - - 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() - } -} - -impl IAppInteractInfo for AppMachine { - type APP = App; - - fn hide_info_overlay(self) -> Self::APP { - AppMachine::browse(self.inner).into() - } - - fn no_op(self) -> Self::APP { - self.into() - } -} - -impl IAppInteractReload for AppMachine { - type APP = App; - - 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 { - fn refresh(self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App; -} - -impl IAppInteractReloadPrivate for AppMachine { - fn refresh(mut self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App { - 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(), - } - } -} - -impl IAppInteractSearch for AppMachine { - type APP = App; - - 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() - } -} - -impl IAppInteractError for AppMachine { - type APP = App; - - fn dismiss_error(self) -> Self::APP { - AppMachine::browse(self.inner).into() - } -} - -impl IAppInteractCritical for AppMachine { - type APP = App; - - fn no_op(self) -> Self::APP { - self.into() - } -} - impl IAppAccess for App { fn get(&mut self) -> AppPublic { match self { - AppState::Browse(generic) => AppPublic { - inner: (&mut generic.inner).into(), - state: AppState::Browse(()), - }, - AppState::Info(generic) => AppPublic { - inner: (&mut generic.inner).into(), - state: AppState::Info(()), - }, - AppState::Reload(generic) => AppPublic { - inner: (&mut generic.inner).into(), - state: AppState::Reload(()), - }, - AppState::Search(search) => AppPublic { - inner: (&mut search.inner).into(), - state: AppState::Search(&search.state.string), - }, - AppState::Error(error) => AppPublic { - inner: (&mut error.inner).into(), - state: AppState::Error(&error.state.string), - }, - AppState::Critical(critical) => AppPublic { - inner: (&mut critical.inner).into(), - state: AppState::Error(&critical.state.string), - }, + AppState::Browse(browse) => browse.into(), + AppState::Info(info) => info.into(), + AppState::Reload(reload) => reload.into(), + AppState::Search(search) => search.into(), + AppState::Error(error) => error.into(), + AppState::Critical(critical) => critical.into(), } } } - -impl<'app, MH: IMusicHoard> From<&'app mut AppInner> for AppPublicInner<'app> { - fn from(inner: &'app mut AppInner) -> Self { - AppPublicInner { - collection: inner.music_hoard.get_collection(), - selection: &mut inner.selection, - } - } -} - -#[cfg(test)] -mod tests { - use musichoard::collection::Collection; - - use crate::tui::{ - app::{selection::Category, AppPublicState}, - lib::MockIMusicHoard, - testmod::COLLECTION, - }; - - use super::*; - - impl AppState { - 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); - } -} diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index d0f4caf..53bff54 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -1,5 +1,6 @@ pub mod app; pub mod selection; +mod state; use musichoard::collection::Collection; diff --git a/src/tui/app/state/browse.rs b/src/tui/app/state/browse.rs new file mode 100644 index 0000000..5b572d5 --- /dev/null +++ b/src/tui/app/state/browse.rs @@ -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 AppMachine { + pub fn browse(inner: AppInner) -> Self { + AppMachine { + inner, + state: AppBrowse, + } + } + + pub fn new(mut music_hoard: MH) -> Result> { + 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 From> for App { + fn from(machine: AppMachine) -> Self { + AppState::Browse(machine) + } +} + +impl<'a, MH: IMusicHoard> From<&'a mut AppMachine> for AppPublic<'a> { + fn from(machine: &'a mut AppMachine) -> Self { + AppPublic { + inner: (&mut machine.inner).into(), + state: AppState::Browse(()), + } + } +} + +impl IAppInteractBrowse for AppMachine { + type APP = App; + + 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() + } +} diff --git a/src/tui/app/state/critical.rs b/src/tui/app/state/critical.rs new file mode 100644 index 0000000..7149df6 --- /dev/null +++ b/src/tui/app/state/critical.rs @@ -0,0 +1,46 @@ +use crate::tui::{ + app::{ + app::App, + state::{AppInner, AppMachine}, + AppPublic, AppState, IAppInteractCritical, + }, + lib::IMusicHoard, +}; + +pub struct AppCritical { + string: String, +} + +impl AppMachine { + pub fn critical>(inner: AppInner, string: S) -> Self { + AppMachine { + inner, + state: AppCritical { + string: string.into(), + }, + } + } +} + +impl From> for App { + fn from(machine: AppMachine) -> Self { + AppState::Critical(machine) + } +} + +impl<'a, MH: IMusicHoard> From<&'a mut AppMachine> for AppPublic<'a> { + fn from(machine: &'a mut AppMachine) -> Self { + AppPublic { + inner: (&mut machine.inner).into(), + state: AppState::Critical(&machine.state.string), + } + } +} + +impl IAppInteractCritical for AppMachine { + type APP = App; + + fn no_op(self) -> Self::APP { + self.into() + } +} diff --git a/src/tui/app/state/error.rs b/src/tui/app/state/error.rs new file mode 100644 index 0000000..b0b0fdd --- /dev/null +++ b/src/tui/app/state/error.rs @@ -0,0 +1,46 @@ +use crate::tui::{ + app::{ + app::App, + state::{AppInner, AppMachine}, + AppPublic, AppState, IAppInteractError, + }, + lib::IMusicHoard, +}; + +pub struct AppError { + string: String, +} + +impl AppMachine { + pub fn error>(inner: AppInner, string: S) -> Self { + AppMachine { + inner, + state: AppError { + string: string.into(), + }, + } + } +} + +impl From> for App { + fn from(machine: AppMachine) -> Self { + AppState::Error(machine) + } +} + +impl<'a, MH: IMusicHoard> From<&'a mut AppMachine> for AppPublic<'a> { + fn from(machine: &'a mut AppMachine) -> Self { + AppPublic { + inner: (&mut machine.inner).into(), + state: AppState::Error(&machine.state.string), + } + } +} + +impl IAppInteractError for AppMachine { + type APP = App; + + fn dismiss_error(self) -> Self::APP { + AppMachine::browse(self.inner).into() + } +} diff --git a/src/tui/app/state/info.rs b/src/tui/app/state/info.rs new file mode 100644 index 0000000..2bd0e0c --- /dev/null +++ b/src/tui/app/state/info.rs @@ -0,0 +1,46 @@ +use crate::tui::{ + app::{ + app::App, + state::{AppInner, AppMachine}, + AppPublic, AppState, IAppInteractInfo, + }, + lib::IMusicHoard, +}; + +pub struct AppInfo; + +impl AppMachine { + pub fn info(inner: AppInner) -> Self { + AppMachine { + inner, + state: AppInfo, + } + } +} + +impl From> for App { + fn from(machine: AppMachine) -> Self { + AppState::Info(machine) + } +} + +impl<'a, MH: IMusicHoard> From<&'a mut AppMachine> for AppPublic<'a> { + fn from(machine: &'a mut AppMachine) -> Self { + AppPublic { + inner: (&mut machine.inner).into(), + state: AppState::Info(()), + } + } +} + +impl IAppInteractInfo for AppMachine { + type APP = App; + + fn hide_info_overlay(self) -> Self::APP { + AppMachine::browse(self.inner).into() + } + + fn no_op(self) -> Self::APP { + self.into() + } +} diff --git a/src/tui/app/state/mod.rs b/src/tui/app/state/mod.rs new file mode 100644 index 0000000..45eaf49 --- /dev/null +++ b/src/tui/app/state/mod.rs @@ -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 { + inner: AppInner, + state: STATE, +} + +impl AppMachine { + pub fn inner_ref(&self) -> &AppInner { + &self.inner + } + + pub fn inner_mut(&mut self) -> &mut AppInner { + &mut self.inner + } +} + +pub struct AppInner { + running: bool, + music_hoard: MH, + selection: Selection, +} + +impl AppInner { + 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> for AppPublicInner<'a> { + fn from(inner: &'a mut AppInner) -> 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 AppState { + 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); + } +} diff --git a/src/tui/app/state/reload.rs b/src/tui/app/state/reload.rs new file mode 100644 index 0000000..e4f37d8 --- /dev/null +++ b/src/tui/app/state/reload.rs @@ -0,0 +1,82 @@ +use crate::tui::{ + app::{ + app::App, + selection::IdSelection, + state::{AppInner, AppMachine}, + AppPublic, AppState, IAppInteractReload, + }, + lib::IMusicHoard, +}; + +pub struct AppReload; + +impl AppMachine { + pub fn reload(inner: AppInner) -> Self { + AppMachine { + inner, + state: AppReload, + } + } +} + +impl From> for App { + fn from(machine: AppMachine) -> Self { + AppState::Reload(machine) + } +} +impl<'a, MH: IMusicHoard> From<&'a mut AppMachine> for AppPublic<'a> { + fn from(machine: &'a mut AppMachine) -> Self { + AppPublic { + inner: (&mut machine.inner).into(), + state: AppState::Reload(()), + } + } +} + +impl IAppInteractReload for AppMachine { + type APP = App; + + 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 { + fn refresh(self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App; +} + +impl IAppInteractReloadPrivate for AppMachine { + fn refresh(mut self, previous: IdSelection, result: Result<(), musichoard::Error>) -> App { + 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(), + } + } +} diff --git a/src/tui/app/state/search.rs b/src/tui/app/state/search.rs new file mode 100644 index 0000000..f3a783c --- /dev/null +++ b/src/tui/app/state/search.rs @@ -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, +} + +struct AppSearchMemo { + index: Option, + char: bool, +} + +impl AppMachine { + pub fn search(inner: AppInner, orig: ListSelection) -> Self { + AppMachine { + inner, + state: AppSearch { + string: String::new(), + orig, + memo: vec![], + }, + } + } +} + +impl From> for App { + fn from(machine: AppMachine) -> Self { + AppState::Search(machine) + } +} + +impl<'a, MH: IMusicHoard> From<&'a mut AppMachine> for AppPublic<'a> { + fn from(machine: &'a mut AppMachine) -> Self { + AppPublic { + inner: (&mut machine.inner).into(), + state: AppState::Search(&machine.state.string), + } + } +} + +impl IAppInteractSearch for AppMachine { + type APP = App; + + 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() + } +}