From 4b9e6ff1308ba83a13f021519ab816062e9d66c0 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Thu, 1 Feb 2024 20:02:47 +0100 Subject: [PATCH] Working refresh --- src/core/collection/album.rs | 6 + src/core/collection/artist.rs | 2 +- src/core/collection/track.rs | 6 + src/tui/{ => app}/app.rs | 409 ++--------------------- src/tui/app/mod.rs | 2 + src/tui/app/selection.rs | 589 ++++++++++++++++++++++++++++++++++ src/tui/handler.rs | 2 +- src/tui/mod.rs | 8 +- src/tui/ui.rs | 11 +- 9 files changed, 634 insertions(+), 401 deletions(-) rename src/tui/{ => app}/app.rs (60%) create mode 100644 src/tui/app/mod.rs create mode 100644 src/tui/app/selection.rs diff --git a/src/core/collection/album.rs b/src/core/collection/album.rs index 5eb412e..926d2b2 100644 --- a/src/core/collection/album.rs +++ b/src/core/collection/album.rs @@ -21,6 +21,12 @@ pub struct AlbumId { pub title: String, } +impl Album { + pub fn get_sort_key(&self) -> &AlbumId { + &self.id + } +} + impl PartialOrd for Album { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) diff --git a/src/core/collection/artist.rs b/src/core/collection/artist.rs index bef3684..35d25dd 100644 --- a/src/core/collection/artist.rs +++ b/src/core/collection/artist.rs @@ -102,7 +102,7 @@ impl Artist { } } - fn get_sort_key(&self) -> &ArtistId { + pub fn get_sort_key(&self) -> &ArtistId { self.sort.as_ref().unwrap_or(&self.id) } diff --git a/src/core/collection/track.rs b/src/core/collection/track.rs index 3b35cf0..0fc4e1b 100644 --- a/src/core/collection/track.rs +++ b/src/core/collection/track.rs @@ -24,6 +24,12 @@ pub struct Quality { pub bitrate: u32, } +impl Track { + pub fn get_sort_key(&self) -> &TrackId { + &self.id + } +} + /// The track file format. #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] pub enum Format { diff --git a/src/tui/app.rs b/src/tui/app/app.rs similarity index 60% rename from src/tui/app.rs rename to src/tui/app/app.rs index cb4e80d..ffbcb34 100644 --- a/src/tui/app.rs +++ b/src/tui/app/app.rs @@ -1,7 +1,11 @@ -use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection}; -use ratatui::widgets::ListState; +#![allow(clippy::module_inception)] -use crate::tui::lib::IMusicHoard; +use musichoard::collection::Collection; + +use crate::tui::{ + app::selection::{ActiveSelection, Selection}, + lib::IMusicHoard, +}; pub enum AppState { Browse(BS), @@ -82,233 +86,6 @@ pub struct AppPublic<'app> { pub state: &'app AppState<(), (), (), String>, } -pub struct ArtistSelection { - pub state: ListState, - pub album: AlbumSelection, -} - -pub struct AlbumSelection { - pub state: ListState, - pub track: TrackSelection, -} - -pub struct TrackSelection { - pub state: ListState, -} - -impl ArtistSelection { - fn initialise(artists: Option<&[Artist]>) -> Self { - let mut state = ListState::default(); - let album: AlbumSelection; - if let Some(artists) = artists { - state.select(if !artists.is_empty() { Some(0) } else { None }); - album = AlbumSelection::initialise(artists.first().map(|a| a.albums.as_slice())); - } else { - state.select(None); - album = AlbumSelection::initialise(None); - } - ArtistSelection { state, album } - } - - fn increment(&mut self, artists: &[Artist]) { - if let Some(index) = self.state.selected() { - if let Some(result) = index.checked_add(1) { - if result < artists.len() { - self.state.select(Some(result)); - self.album = AlbumSelection::initialise(Some(&artists[result].albums)); - } - } - } - } - - fn increment_album(&mut self, artists: &[Artist]) { - if let Some(index) = self.state.selected() { - self.album.increment(&artists[index].albums); - } - } - - fn increment_track(&mut self, artists: &[Artist]) { - if let Some(index) = self.state.selected() { - self.album.increment_track(&artists[index].albums); - } - } - - fn decrement(&mut self, artists: &[Artist]) { - if let Some(index) = self.state.selected() { - if let Some(result) = index.checked_sub(1) { - self.state.select(Some(result)); - self.album = AlbumSelection::initialise(Some(&artists[result].albums)); - } - } - } - - fn decrement_album(&mut self, artists: &[Artist]) { - if let Some(index) = self.state.selected() { - self.album.decrement(&artists[index].albums); - } - } - - fn decrement_track(&mut self, artists: &[Artist]) { - if let Some(index) = self.state.selected() { - self.album.decrement_track(&artists[index].albums); - } - } -} - -impl AlbumSelection { - fn initialise(albums: Option<&[Album]>) -> Self { - let mut state = ListState::default(); - let track: TrackSelection; - if let Some(albums) = albums { - state.select(if !albums.is_empty() { Some(0) } else { None }); - track = TrackSelection::initialise(albums.first().map(|a| a.tracks.as_slice())); - } else { - state.select(None); - track = TrackSelection::initialise(None); - } - AlbumSelection { state, track } - } - - fn increment(&mut self, albums: &[Album]) { - if let Some(index) = self.state.selected() { - if let Some(result) = index.checked_add(1) { - if result < albums.len() { - self.state.select(Some(result)); - self.track = TrackSelection::initialise(Some(&albums[result].tracks)); - } - } - } - } - - fn increment_track(&mut self, albums: &[Album]) { - if let Some(index) = self.state.selected() { - self.track.increment(&albums[index].tracks); - } - } - - fn decrement(&mut self, albums: &[Album]) { - if let Some(index) = self.state.selected() { - if let Some(result) = index.checked_sub(1) { - self.state.select(Some(result)); - self.track = TrackSelection::initialise(Some(&albums[result].tracks)); - } - } - } - - fn decrement_track(&mut self, albums: &[Album]) { - if let Some(index) = self.state.selected() { - self.track.decrement(&albums[index].tracks); - } - } -} - -impl TrackSelection { - fn initialise(tracks: Option<&[Track]>) -> Self { - let mut state = ListState::default(); - if let Some(tracks) = tracks { - state.select(if !tracks.is_empty() { Some(0) } else { None }); - } else { - state.select(None); - }; - TrackSelection { state } - } - - fn increment(&mut self, tracks: &[Track]) { - if let Some(index) = self.state.selected() { - if let Some(result) = index.checked_add(1) { - if result < tracks.len() { - self.state.select(Some(result)); - } - } - } - } - - fn decrement(&mut self, _tracks: &[Track]) { - if let Some(index) = self.state.selected() { - if let Some(result) = index.checked_sub(1) { - self.state.select(Some(result)); - } - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum Category { - Artist, - Album, - Track, -} - -pub struct Selection { - pub active: Category, - pub artist: ArtistSelection, -} - -impl Selection { - pub fn new(artists: Option<&[Artist]>) -> Self { - Selection { - active: Category::Artist, - artist: ArtistSelection::initialise(artists), - } - } - - pub fn increment_category(&mut self) { - self.active = match self.active { - Category::Artist => Category::Album, - Category::Album => Category::Track, - Category::Track => Category::Track, - }; - } - - pub fn decrement_category(&mut self) { - self.active = match self.active { - Category::Artist => Category::Artist, - Category::Album => Category::Artist, - Category::Track => Category::Album, - }; - } - - pub fn increment_selection(&mut self, collection: &Collection) { - match self.active { - Category::Artist => self.increment_artist(collection), - Category::Album => self.increment_album(collection), - Category::Track => self.increment_track(collection), - } - } - - pub fn decrement_selection(&mut self, collection: &Collection) { - match self.active { - Category::Artist => self.decrement_artist(collection), - Category::Album => self.decrement_album(collection), - Category::Track => self.decrement_track(collection), - } - } - - fn increment_artist(&mut self, artists: &[Artist]) { - self.artist.increment(artists); - } - - fn decrement_artist(&mut self, artists: &[Artist]) { - self.artist.decrement(artists); - } - - fn increment_album(&mut self, artists: &[Artist]) { - self.artist.increment_album(artists); - } - - fn decrement_album(&mut self, artists: &[Artist]) { - self.artist.decrement_album(artists); - } - - fn increment_track(&mut self, artists: &[Artist]) { - self.artist.increment_track(artists); - } - - fn decrement_track(&mut self, artists: &[Artist]) { - self.artist.decrement_track(artists); - } -} - pub struct App { running: bool, music_hoard: MH, @@ -322,7 +99,7 @@ impl App { Ok(()) => AppState::Browse(()), Err(err) => AppState::Error(err.to_string()), }; - let selection = Selection::new(Some(music_hoard.get_collection())); + let selection = Selection::new(music_hoard.get_collection()); App { running: true, music_hoard, @@ -413,15 +190,18 @@ impl IAppInteractInfo for App { impl IAppInteractReload for App { fn reload_library(&mut self) { + let previous = ActiveSelection::get(self.music_hoard.get_collection(), &self.selection); let result = self.music_hoard.rescan_library(); - self.refresh(result); + self.refresh(previous, result); } fn reload_database(&mut self) { + let previous = ActiveSelection::get(self.music_hoard.get_collection(), &self.selection); let result = self.music_hoard.load_from_database(); - self.refresh(result); + self.refresh(previous, result); } + // FIXME: Rename to hide_reload_menu fn go_back(&mut self) { assert!(self.state.is_reload()); self.state = AppState::Browse(()); @@ -429,15 +209,16 @@ impl IAppInteractReload for App { } trait IAppInteractReloadPrivate { - fn refresh(&mut self, result: Result<(), musichoard::Error>); + fn refresh(&mut self, previous: ActiveSelection, result: Result<(), musichoard::Error>); } impl IAppInteractReloadPrivate for App { - fn refresh(&mut self, result: Result<(), musichoard::Error>) { + fn refresh(&mut self, previous: ActiveSelection, result: Result<(), musichoard::Error>) { assert!(self.state.is_reload()); match result { Ok(()) => { - self.selection = Selection::new(Some(self.music_hoard.get_collection())); + self.selection + .select(self.music_hoard.get_collection(), previous); self.state = AppState::Browse(()) } Err(err) => self.state = AppState::Error(err.to_string()), @@ -464,7 +245,7 @@ impl IAppAccess for App { #[cfg(test)] mod tests { - use crate::tui::{lib::MockIMusicHoard, testmod::COLLECTION}; + use crate::tui::{app::selection::Category, lib::MockIMusicHoard, testmod::COLLECTION}; use super::*; @@ -484,158 +265,6 @@ mod tests { music_hoard } - #[test] - fn track_selection() { - let tracks = &COLLECTION[0].albums[0].tracks; - assert!(tracks.len() > 1); - - let empty = TrackSelection::initialise(None); - assert_eq!(empty.state.selected(), None); - - let empty = TrackSelection::initialise(Some(&[])); - assert_eq!(empty.state.selected(), None); - - let mut sel = TrackSelection::initialise(Some(tracks)); - assert_eq!(sel.state.selected(), Some(0)); - - sel.decrement(tracks); - assert_eq!(sel.state.selected(), Some(0)); - - sel.increment(tracks); - assert_eq!(sel.state.selected(), Some(1)); - - sel.decrement(tracks); - assert_eq!(sel.state.selected(), Some(0)); - - for _ in 0..(tracks.len() + 5) { - sel.increment(tracks); - } - assert_eq!(sel.state.selected(), Some(tracks.len() - 1)); - - // Artifical test case to verify upper limit. - sel.state.select(Some(std::usize::MAX)); - assert_eq!(sel.state.selected(), Some(std::usize::MAX)); - - sel.increment(&[]); - assert_eq!(sel.state.selected(), Some(std::usize::MAX)); - } - - #[test] - fn album_selection() { - let albums = &COLLECTION[0].albums; - assert!(albums.len() > 1); - - let empty = AlbumSelection::initialise(None); - assert_eq!(empty.state.selected(), None); - - let empty = AlbumSelection::initialise(Some(&[])); - assert_eq!(empty.state.selected(), None); - - let mut sel = AlbumSelection::initialise(Some(albums)); - assert_eq!(sel.state.selected(), Some(0)); - assert_eq!(sel.track.state.selected(), Some(0)); - - sel.increment_track(albums); - assert_eq!(sel.state.selected(), Some(0)); - assert_eq!(sel.track.state.selected(), Some(1)); - - // Verify that decrement that doesn't change index does not reset track. - sel.decrement(albums); - assert_eq!(sel.state.selected(), Some(0)); - assert_eq!(sel.track.state.selected(), Some(1)); - - sel.increment(albums); - assert_eq!(sel.state.selected(), Some(1)); - assert_eq!(sel.track.state.selected(), Some(0)); - - sel.decrement(albums); - assert_eq!(sel.state.selected(), Some(0)); - assert_eq!(sel.track.state.selected(), Some(0)); - - for _ in 0..(albums.len() + 5) { - sel.increment(albums); - } - assert_eq!(sel.state.selected(), Some(albums.len() - 1)); - assert_eq!(sel.track.state.selected(), Some(0)); - - sel.increment_track(albums); - assert_eq!(sel.state.selected(), Some(albums.len() - 1)); - assert_eq!(sel.track.state.selected(), Some(1)); - - // Verify that increment that doesn't change index does not reset track. - sel.increment(albums); - assert_eq!(sel.state.selected(), Some(albums.len() - 1)); - assert_eq!(sel.track.state.selected(), Some(1)); - - // Artifical test case to verify upper limit. - sel.state.select(Some(std::usize::MAX)); - sel.track.state.select(Some(1)); - assert_eq!(sel.state.selected(), Some(std::usize::MAX)); - assert_eq!(sel.track.state.selected(), Some(1)); - - sel.increment(&[]); - assert_eq!(sel.state.selected(), Some(std::usize::MAX)); - assert_eq!(sel.track.state.selected(), Some(1)); - } - - #[test] - fn artist_selection() { - let artists = &COLLECTION; - assert!(artists.len() > 1); - - let empty = ArtistSelection::initialise(None); - assert_eq!(empty.state.selected(), None); - - let empty = ArtistSelection::initialise(Some(&[])); - assert_eq!(empty.state.selected(), None); - - let mut sel = ArtistSelection::initialise(Some(artists)); - assert_eq!(sel.state.selected(), Some(0)); - assert_eq!(sel.album.state.selected(), Some(0)); - - sel.increment_album(artists); - assert_eq!(sel.state.selected(), Some(0)); - assert_eq!(sel.album.state.selected(), Some(1)); - - // Verify that decrement that doesn't change index does not reset album. - sel.decrement(artists); - assert_eq!(sel.state.selected(), Some(0)); - assert_eq!(sel.album.state.selected(), Some(1)); - - sel.increment(artists); - assert_eq!(sel.state.selected(), Some(1)); - assert_eq!(sel.album.state.selected(), Some(0)); - - sel.decrement(artists); - assert_eq!(sel.state.selected(), Some(0)); - assert_eq!(sel.album.state.selected(), Some(0)); - - for _ in 0..(artists.len() + 5) { - sel.increment(artists); - } - assert_eq!(sel.state.selected(), Some(artists.len() - 1)); - assert_eq!(sel.album.state.selected(), Some(0)); - - sel.increment_album(artists); - assert_eq!(sel.state.selected(), Some(artists.len() - 1)); - assert_eq!(sel.album.state.selected(), Some(1)); - - // Verify that increment that doesn't change index does not reset album. - sel.increment(artists); - assert_eq!(sel.state.selected(), Some(artists.len() - 1)); - assert_eq!(sel.album.state.selected(), Some(1)); - - // Artifical test case to verify upper limit. - sel.state.select(Some(std::usize::MAX)); - sel.album.state.select(Some(1)); - assert_eq!(sel.state.selected(), Some(std::usize::MAX)); - assert_eq!(sel.album.state.selected(), Some(1)); - - sel.increment(&[]); - assert_eq!(sel.state.selected(), Some(std::usize::MAX)); - assert_eq!(sel.album.state.selected(), Some(1)); - } - #[test] fn running_quit() { let mut app = App::new(music_hoard(COLLECTION.to_owned())); @@ -939,8 +568,6 @@ mod tests { assert_eq!(app.selection.artist.album.track.state.selected(), None); } - // This is UI so the only sensible unit test is to run the code through various app states. - #[test] fn info_overlay() { let mut app = App::new(music_hoard(COLLECTION.to_owned())); diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs new file mode 100644 index 0000000..57892fa --- /dev/null +++ b/src/tui/app/mod.rs @@ -0,0 +1,2 @@ +pub mod app; +pub mod selection; diff --git a/src/tui/app/selection.rs b/src/tui/app/selection.rs new file mode 100644 index 0000000..6926df8 --- /dev/null +++ b/src/tui/app/selection.rs @@ -0,0 +1,589 @@ +use musichoard::collection::{ + album::{Album, AlbumId}, + artist::{Artist, ArtistId}, + track::{Track, TrackId}, + Collection, +}; +use ratatui::widgets::ListState; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Category { + Artist, + Album, + Track, +} + +pub struct Selection { + pub active: Category, + pub artist: ArtistSelection, +} + +#[derive(Clone, Debug)] +pub struct ArtistSelection { + pub state: ListState, + pub album: AlbumSelection, +} + +impl PartialEq for ArtistSelection { + fn eq(&self, other: &Self) -> bool { + self.state.selected().eq(&other.state.selected()) && self.album.eq(&other.album) + } +} + +#[derive(Clone, Debug)] +pub struct AlbumSelection { + pub state: ListState, + pub track: TrackSelection, +} + +impl PartialEq for AlbumSelection { + fn eq(&self, other: &Self) -> bool { + self.state.selected().eq(&other.state.selected()) && self.track.eq(&other.track) + } +} + +#[derive(Clone, Debug)] +pub struct TrackSelection { + pub state: ListState, +} + +impl PartialEq for TrackSelection { + fn eq(&self, other: &Self) -> bool { + self.state.selected().eq(&other.state.selected()) + } +} + +impl Selection { + pub fn new(artists: &[Artist]) -> Self { + Selection { + active: Category::Artist, + artist: ArtistSelection::initialise(artists), + } + } + + pub fn select(&mut self, artists: &[Artist], selected: ActiveSelection) { + self.artist = ArtistSelection::reinitialise(artists, selected.artist); + } + + pub fn increment_category(&mut self) { + self.active = match self.active { + Category::Artist => Category::Album, + Category::Album => Category::Track, + Category::Track => Category::Track, + }; + } + + pub fn decrement_category(&mut self) { + self.active = match self.active { + Category::Artist => Category::Artist, + Category::Album => Category::Artist, + Category::Track => Category::Album, + }; + } + + pub fn increment_selection(&mut self, collection: &Collection) { + match self.active { + Category::Artist => self.increment_artist(collection), + Category::Album => self.increment_album(collection), + Category::Track => self.increment_track(collection), + } + } + + pub fn decrement_selection(&mut self, collection: &Collection) { + match self.active { + Category::Artist => self.decrement_artist(collection), + Category::Album => self.decrement_album(collection), + Category::Track => self.decrement_track(collection), + } + } + + fn increment_artist(&mut self, artists: &[Artist]) { + self.artist.increment(artists); + } + + fn decrement_artist(&mut self, artists: &[Artist]) { + self.artist.decrement(artists); + } + + fn increment_album(&mut self, artists: &[Artist]) { + self.artist.increment_album(artists); + } + + fn decrement_album(&mut self, artists: &[Artist]) { + self.artist.decrement_album(artists); + } + + fn increment_track(&mut self, artists: &[Artist]) { + self.artist.increment_track(artists); + } + + fn decrement_track(&mut self, artists: &[Artist]) { + self.artist.decrement_track(artists); + } +} + +impl ArtistSelection { + fn initialise(artists: &[Artist]) -> Self { + Self::reinitialise(artists, None) + } + + fn reinitialise(artists: &[Artist], active: Option) -> Self { + if let Some(active) = active { + let result = artists.binary_search_by(|a| a.get_sort_key().cmp(&active.artist_id)); + match result { + Ok(index) => Self::reinitialise_with_index(artists, index, active.album), + Err(index) => Self::reinitialise_with_index(artists, index, None), + } + } else { + Self::reinitialise_with_index(artists, 0, None) + } + } + + fn reinitialise_with_index( + artists: &[Artist], + mut index: usize, + mut active_album: Option, + ) -> Self { + let mut state = ListState::default(); + let album: AlbumSelection; + if artists.is_empty() { + album = AlbumSelection::initialise(&[]); + } else { + if index >= artists.len() { + index = artists.len() - 1; + active_album = None; + } + state.select(Some(index)); + album = AlbumSelection::reinitialise(&artists[index].albums, active_album); + } + ArtistSelection { state, album } + } + + fn increment(&mut self, artists: &[Artist]) { + if let Some(index) = self.state.selected() { + if let Some(result) = index.checked_add(1) { + if result < artists.len() { + self.state.select(Some(result)); + self.album = AlbumSelection::initialise(&artists[result].albums); + } + } + } + } + + fn increment_album(&mut self, artists: &[Artist]) { + if let Some(index) = self.state.selected() { + self.album.increment(&artists[index].albums); + } + } + + fn increment_track(&mut self, artists: &[Artist]) { + if let Some(index) = self.state.selected() { + self.album.increment_track(&artists[index].albums); + } + } + + fn decrement(&mut self, artists: &[Artist]) { + if let Some(index) = self.state.selected() { + if let Some(result) = index.checked_sub(1) { + self.state.select(Some(result)); + self.album = AlbumSelection::initialise(&artists[result].albums); + } + } + } + + fn decrement_album(&mut self, artists: &[Artist]) { + if let Some(index) = self.state.selected() { + self.album.decrement(&artists[index].albums); + } + } + + fn decrement_track(&mut self, artists: &[Artist]) { + if let Some(index) = self.state.selected() { + self.album.decrement_track(&artists[index].albums); + } + } +} + +impl AlbumSelection { + fn initialise(albums: &[Album]) -> Self { + Self::reinitialise(albums, None) + } + + fn reinitialise(albums: &[Album], album: Option) -> Self { + if let Some(album) = album { + let result = albums.binary_search_by(|a| a.get_sort_key().cmp(&album.album_id)); + match result { + Ok(index) => Self::reinitialise_with_index(albums, index, album.track), + Err(index) => Self::reinitialise_with_index(albums, index, None), + } + } else { + Self::reinitialise_with_index(albums, 0, None) + } + } + + fn reinitialise_with_index( + albums: &[Album], + mut index: usize, + mut active_track: Option, + ) -> Self { + let mut state = ListState::default(); + let track: TrackSelection; + if albums.is_empty() { + track = TrackSelection::initialise(&[]); + } else { + if index >= albums.len() { + index = albums.len() - 1; + active_track = None; + } + state.select(Some(index)); + track = TrackSelection::reinitialise(&albums[index].tracks, active_track); + } + AlbumSelection { state, track } + } + + fn increment(&mut self, albums: &[Album]) { + if let Some(index) = self.state.selected() { + if let Some(result) = index.checked_add(1) { + if result < albums.len() { + self.state.select(Some(result)); + self.track = TrackSelection::initialise(&albums[result].tracks); + } + } + } + } + + fn increment_track(&mut self, albums: &[Album]) { + if let Some(index) = self.state.selected() { + self.track.increment(&albums[index].tracks); + } + } + + fn decrement(&mut self, albums: &[Album]) { + if let Some(index) = self.state.selected() { + if let Some(result) = index.checked_sub(1) { + self.state.select(Some(result)); + self.track = TrackSelection::initialise(&albums[result].tracks); + } + } + } + + fn decrement_track(&mut self, albums: &[Album]) { + if let Some(index) = self.state.selected() { + self.track.decrement(&albums[index].tracks); + } + } +} + +impl TrackSelection { + fn initialise(tracks: &[Track]) -> Self { + Self::reinitialise(tracks, None) + } + + fn reinitialise(tracks: &[Track], track: Option) -> Self { + if let Some(track) = track { + let result = tracks.binary_search_by(|t| t.get_sort_key().cmp(&track.track_id)); + match result { + Ok(index) | Err(index) => Self::reinitialise_with_index(tracks, index), + } + } else { + Self::reinitialise_with_index(tracks, 0) + } + } + + fn reinitialise_with_index(tracks: &[Track], mut index: usize) -> Self { + let mut state = ListState::default(); + if !tracks.is_empty() { + if index >= tracks.len() { + index = tracks.len() - 1; + } + state.select(Some(index)); + } + TrackSelection { state } + } + + fn increment(&mut self, tracks: &[Track]) { + if let Some(index) = self.state.selected() { + if let Some(result) = index.checked_add(1) { + if result < tracks.len() { + self.state.select(Some(result)); + } + } + } + } + + fn decrement(&mut self, _tracks: &[Track]) { + if let Some(index) = self.state.selected() { + if let Some(result) = index.checked_sub(1) { + self.state.select(Some(result)); + } + } + } +} + +pub struct ActiveSelection { + artist: Option, +} + +struct ActiveArtist { + artist_id: ArtistId, + album: Option, +} + +struct ActiveAlbum { + album_id: AlbumId, + track: Option, +} + +struct ActiveTrack { + track_id: TrackId, +} + +impl ActiveSelection { + pub fn get(collection: &Collection, selection: &Selection) -> Self { + ActiveSelection { + artist: ActiveArtist::get(collection, &selection.artist), + } + } +} + +impl ActiveArtist { + fn get(artists: &[Artist], selection: &ArtistSelection) -> Option { + selection.state.selected().map(|index| { + let artist = &artists[index]; + ActiveArtist { + artist_id: artist.get_sort_key().clone(), + album: ActiveAlbum::get(&artist.albums, &selection.album), + } + }) + } +} + +impl ActiveAlbum { + fn get(albums: &[Album], selection: &AlbumSelection) -> Option { + selection.state.selected().map(|index| { + let album = &albums[index]; + ActiveAlbum { + album_id: album.get_sort_key().clone(), + track: ActiveTrack::get(&album.tracks, &selection.track), + } + }) + } +} + +impl ActiveTrack { + fn get(tracks: &[Track], selection: &TrackSelection) -> Option { + selection.state.selected().map(|index| { + let track = &tracks[index]; + ActiveTrack { + track_id: track.get_sort_key().clone(), + } + }) + } +} + +#[cfg(test)] +mod tests { + use crate::tui::testmod::COLLECTION; + + use super::*; + + #[test] + fn track_selection() { + let tracks = &COLLECTION[0].albums[0].tracks; + assert!(tracks.len() > 1); + + let empty = TrackSelection::initialise(&[]); + assert_eq!(empty.state.selected(), None); + + let mut sel = TrackSelection::initialise(tracks); + assert_eq!(sel.state.selected(), Some(0)); + + sel.decrement(tracks); + assert_eq!(sel.state.selected(), Some(0)); + + sel.increment(tracks); + assert_eq!(sel.state.selected(), Some(1)); + + sel.decrement(tracks); + assert_eq!(sel.state.selected(), Some(0)); + + for _ in 0..(tracks.len() + 5) { + sel.increment(tracks); + } + assert_eq!(sel.state.selected(), Some(tracks.len() - 1)); + + // Re-initialise. + let previous = sel.clone(); + let active_track = ActiveTrack::get(tracks, &sel); + sel = TrackSelection::reinitialise(tracks, active_track); + assert_eq!(sel, previous); + + // Re-initialise out-of-bounds. + let mut previous = sel.clone(); + previous.decrement(tracks); + let active_track = ActiveTrack::get(tracks, &sel); + sel = TrackSelection::reinitialise(&tracks[..(tracks.len() - 1)], active_track); + assert_eq!(sel, previous); + + // Re-initialise empty. + let previous = TrackSelection::initialise(&[]); + let active_track = ActiveTrack::get(tracks, &sel); + sel = TrackSelection::reinitialise(&[], active_track); + assert_eq!(sel, previous); + + // Artifical test case to verify upper limit. + sel.state.select(Some(std::usize::MAX)); + assert_eq!(sel.state.selected(), Some(std::usize::MAX)); + + sel.increment(&[]); + assert_eq!(sel.state.selected(), Some(std::usize::MAX)); + } + + #[test] + fn album_selection() { + let albums = &COLLECTION[0].albums; + assert!(albums.len() > 1); + + let empty = AlbumSelection::initialise(&[]); + assert_eq!(empty.state.selected(), None); + + let mut sel = AlbumSelection::initialise(albums); + assert_eq!(sel.state.selected(), Some(0)); + assert_eq!(sel.track.state.selected(), Some(0)); + + sel.increment_track(albums); + assert_eq!(sel.state.selected(), Some(0)); + assert_eq!(sel.track.state.selected(), Some(1)); + + // Verify that decrement that doesn't change index does not reset track. + sel.decrement(albums); + assert_eq!(sel.state.selected(), Some(0)); + assert_eq!(sel.track.state.selected(), Some(1)); + + sel.increment(albums); + assert_eq!(sel.state.selected(), Some(1)); + assert_eq!(sel.track.state.selected(), Some(0)); + + sel.decrement(albums); + assert_eq!(sel.state.selected(), Some(0)); + assert_eq!(sel.track.state.selected(), Some(0)); + + for _ in 0..(albums.len() + 5) { + sel.increment(albums); + } + assert_eq!(sel.state.selected(), Some(albums.len() - 1)); + assert_eq!(sel.track.state.selected(), Some(0)); + + sel.increment_track(albums); + assert_eq!(sel.state.selected(), Some(albums.len() - 1)); + assert_eq!(sel.track.state.selected(), Some(1)); + + // Verify that increment that doesn't change index does not reset track. + sel.increment(albums); + assert_eq!(sel.state.selected(), Some(albums.len() - 1)); + assert_eq!(sel.track.state.selected(), Some(1)); + + // Re-initialise. + let previous = sel.clone(); + let active_album = ActiveAlbum::get(albums, &sel); + sel = AlbumSelection::reinitialise(albums, active_album); + assert_eq!(sel, previous); + + // Re-initialise out-of-bounds. + let mut previous = sel.clone(); + previous.decrement(albums); + let active_album = ActiveAlbum::get(albums, &sel); + sel = AlbumSelection::reinitialise(&albums[..(albums.len() - 1)], active_album); + assert_eq!(sel, previous); + + // Re-initialise empty. + let previous = AlbumSelection::initialise(&[]); + let active_album = ActiveAlbum::get(albums, &sel); + sel = AlbumSelection::reinitialise(&[], active_album); + assert_eq!(sel, previous); + + // Artifical test case to verify upper limit. + sel.state.select(Some(std::usize::MAX)); + sel.track.state.select(Some(1)); + assert_eq!(sel.state.selected(), Some(std::usize::MAX)); + assert_eq!(sel.track.state.selected(), Some(1)); + + sel.increment(&[]); + assert_eq!(sel.state.selected(), Some(std::usize::MAX)); + assert_eq!(sel.track.state.selected(), Some(1)); + } + + #[test] + fn artist_selection() { + let artists = &COLLECTION; + assert!(artists.len() > 1); + + let empty = ArtistSelection::initialise(&[]); + assert_eq!(empty.state.selected(), None); + + let mut sel = ArtistSelection::initialise(artists); + assert_eq!(sel.state.selected(), Some(0)); + assert_eq!(sel.album.state.selected(), Some(0)); + + sel.increment_album(artists); + assert_eq!(sel.state.selected(), Some(0)); + assert_eq!(sel.album.state.selected(), Some(1)); + + // Verify that decrement that doesn't change index does not reset album. + sel.decrement(artists); + assert_eq!(sel.state.selected(), Some(0)); + assert_eq!(sel.album.state.selected(), Some(1)); + + sel.increment(artists); + assert_eq!(sel.state.selected(), Some(1)); + assert_eq!(sel.album.state.selected(), Some(0)); + + sel.decrement(artists); + assert_eq!(sel.state.selected(), Some(0)); + assert_eq!(sel.album.state.selected(), Some(0)); + + for _ in 0..(artists.len() + 5) { + sel.increment(artists); + } + assert_eq!(sel.state.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.state.selected(), Some(0)); + + sel.increment_album(artists); + assert_eq!(sel.state.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.state.selected(), Some(1)); + + // Verify that increment that doesn't change index does not reset album. + sel.increment(artists); + assert_eq!(sel.state.selected(), Some(artists.len() - 1)); + assert_eq!(sel.album.state.selected(), Some(1)); + + // Re-initialise. + let previous = sel.clone(); + let active_artist = ActiveArtist::get(artists, &sel); + sel = ArtistSelection::reinitialise(artists, active_artist); + assert_eq!(sel, previous); + + // Re-initialise out-of-bounds. + let mut previous = sel.clone(); + previous.decrement(artists); + let active_artist = ActiveArtist::get(artists, &sel); + sel = ArtistSelection::reinitialise(&artists[..(artists.len() - 1)], active_artist); + assert_eq!(sel, previous); + + // Re-initialise empty. + let previous = ArtistSelection::initialise(&[]); + let active_artist = ActiveArtist::get(artists, &sel); + sel = ArtistSelection::reinitialise(&[], active_artist); + assert_eq!(sel, previous); + + // Artifical test case to verify upper limit. + sel.state.select(Some(std::usize::MAX)); + sel.album.state.select(Some(1)); + assert_eq!(sel.state.selected(), Some(std::usize::MAX)); + assert_eq!(sel.album.state.selected(), Some(1)); + + sel.increment(&[]); + assert_eq!(sel.state.selected(), Some(std::usize::MAX)); + assert_eq!(sel.album.state.selected(), Some(1)); + } +} diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 585fe6f..6eb39c5 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -4,7 +4,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use mockall::automock; use crate::tui::{ - app::{ + app::app::{ AppState, IAppInteract, IAppInteractBrowse, IAppInteractError, IAppInteractInfo, IAppInteractReload, }, diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 21c2b1d..1b4b663 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -5,7 +5,7 @@ mod lib; mod listener; mod ui; -pub use app::App; +pub use app::app::App; pub use event::EventChannel; pub use handler::EventHandler; pub use listener::EventListener; @@ -19,7 +19,7 @@ use std::io; use std::marker::PhantomData; use crate::tui::{ - app::{IAppAccess, IAppInteract}, + app::app::{IAppAccess, IAppInteract}, event::EventError, handler::IEventHandler, listener::IEventListener, @@ -178,8 +178,8 @@ mod tests { use musichoard::collection::Collection; use crate::tui::{ - app::App, handler::MockIEventHandler, lib::MockIMusicHoard, listener::MockIEventListener, - ui::Ui, + app::app::App, handler::MockIEventHandler, lib::MockIMusicHoard, + listener::MockIEventListener, ui::Ui, }; use super::*; diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 35279db..5265afa 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -12,7 +12,10 @@ use ratatui::{ Frame, }; -use crate::tui::app::{AppState, Category, IAppAccess, Selection}; +use crate::tui::app::{ + app::{AppState, IAppAccess}, + selection::{Category, Selection}, +}; pub trait IUi { fn render(app: &mut APP, frame: &mut Frame<'_, B>); @@ -507,7 +510,7 @@ impl IUi for Ui { #[cfg(test)] mod tests { - use crate::tui::{app::AppPublic, testmod::COLLECTION, tests::terminal}; + use crate::tui::{app::app::AppPublic, testmod::COLLECTION, tests::terminal}; use super::*; @@ -546,7 +549,7 @@ mod tests { #[test] fn empty() { let artists: Vec = vec![]; - let mut selection = Selection::new(Some(&artists)); + let mut selection = Selection::new(&artists); draw_test_suite(&artists, &mut selection); } @@ -554,7 +557,7 @@ mod tests { #[test] fn collection() { let artists = &COLLECTION; - let mut selection = Selection::new(Some(artists)); + let mut selection = Selection::new(artists); draw_test_suite(artists, &mut selection);