Add shortcut to reload database and/or library #116

Merged
wojtek merged 14 commits from 105---add-shortcut-to-reload-database-and/or-library into main 2024-02-03 14:32:13 +01:00
9 changed files with 634 additions and 401 deletions
Showing only changes of commit 4b9e6ff130 - Show all commits

View File

@ -21,6 +21,12 @@ pub struct AlbumId {
pub title: String, pub title: String,
} }
impl Album {
pub fn get_sort_key(&self) -> &AlbumId {
&self.id
}
}
impl PartialOrd for Album { impl PartialOrd for Album {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other)) Some(self.cmp(other))

View File

@ -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) self.sort.as_ref().unwrap_or(&self.id)
} }

View File

@ -24,6 +24,12 @@ pub struct Quality {
pub bitrate: u32, pub bitrate: u32,
} }
impl Track {
pub fn get_sort_key(&self) -> &TrackId {
&self.id
}
}
/// The track file format. /// The track file format.
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
pub enum Format { pub enum Format {

View File

@ -1,7 +1,11 @@
use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection}; #![allow(clippy::module_inception)]
use ratatui::widgets::ListState;
use crate::tui::lib::IMusicHoard; use musichoard::collection::Collection;
use crate::tui::{
app::selection::{ActiveSelection, Selection},
lib::IMusicHoard,
};
pub enum AppState<BS, IS, RS, ES> { pub enum AppState<BS, IS, RS, ES> {
Browse(BS), Browse(BS),
@ -82,233 +86,6 @@ pub struct AppPublic<'app> {
pub state: &'app AppState<(), (), (), String>, 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<MH: IMusicHoard> { pub struct App<MH: IMusicHoard> {
running: bool, running: bool,
music_hoard: MH, music_hoard: MH,
@ -322,7 +99,7 @@ impl<MH: IMusicHoard> App<MH> {
Ok(()) => AppState::Browse(()), Ok(()) => AppState::Browse(()),
Err(err) => AppState::Error(err.to_string()), 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 { App {
running: true, running: true,
music_hoard, music_hoard,
@ -413,15 +190,18 @@ impl<MH: IMusicHoard> IAppInteractInfo for App<MH> {
impl<MH: IMusicHoard> IAppInteractReload for App<MH> { impl<MH: IMusicHoard> IAppInteractReload for App<MH> {
fn reload_library(&mut self) { fn reload_library(&mut self) {
let previous = ActiveSelection::get(self.music_hoard.get_collection(), &self.selection);
let result = self.music_hoard.rescan_library(); let result = self.music_hoard.rescan_library();
self.refresh(result); self.refresh(previous, result);
} }
fn reload_database(&mut self) { fn reload_database(&mut self) {
let previous = ActiveSelection::get(self.music_hoard.get_collection(), &self.selection);
let result = self.music_hoard.load_from_database(); 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) { fn go_back(&mut self) {
assert!(self.state.is_reload()); assert!(self.state.is_reload());
self.state = AppState::Browse(()); self.state = AppState::Browse(());
@ -429,15 +209,16 @@ impl<MH: IMusicHoard> IAppInteractReload for App<MH> {
} }
trait IAppInteractReloadPrivate { trait IAppInteractReloadPrivate {
fn refresh(&mut self, result: Result<(), musichoard::Error>); fn refresh(&mut self, previous: ActiveSelection, result: Result<(), musichoard::Error>);
} }
impl<MH: IMusicHoard> IAppInteractReloadPrivate for App<MH> { impl<MH: IMusicHoard> IAppInteractReloadPrivate for App<MH> {
fn refresh(&mut self, result: Result<(), musichoard::Error>) { fn refresh(&mut self, previous: ActiveSelection, result: Result<(), musichoard::Error>) {
assert!(self.state.is_reload()); assert!(self.state.is_reload());
match result { match result {
Ok(()) => { Ok(()) => {
self.selection = Selection::new(Some(self.music_hoard.get_collection())); self.selection
.select(self.music_hoard.get_collection(), previous);
self.state = AppState::Browse(()) self.state = AppState::Browse(())
} }
Err(err) => self.state = AppState::Error(err.to_string()), Err(err) => self.state = AppState::Error(err.to_string()),
@ -464,7 +245,7 @@ impl<MH: IMusicHoard> IAppAccess for App<MH> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tui::{lib::MockIMusicHoard, testmod::COLLECTION}; use crate::tui::{app::selection::Category, lib::MockIMusicHoard, testmod::COLLECTION};
use super::*; use super::*;
@ -484,158 +265,6 @@ mod tests {
music_hoard 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] #[test]
fn running_quit() { fn running_quit() {
let mut app = App::new(music_hoard(COLLECTION.to_owned())); 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); 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] #[test]
fn info_overlay() { fn info_overlay() {
let mut app = App::new(music_hoard(COLLECTION.to_owned())); let mut app = App::new(music_hoard(COLLECTION.to_owned()));

2
src/tui/app/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod app;
pub mod selection;

589
src/tui/app/selection.rs Normal file
View File

@ -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<ActiveArtist>) -> 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<ActiveAlbum>,
) -> 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<ActiveAlbum>) -> 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<ActiveTrack>,
) -> 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<ActiveTrack>) -> 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<ActiveArtist>,
}
struct ActiveArtist {
artist_id: ArtistId,
album: Option<ActiveAlbum>,
}
struct ActiveAlbum {
album_id: AlbumId,
track: Option<ActiveTrack>,
}
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<Self> {
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<Self> {
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<Self> {
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));
}
}

View File

@ -4,7 +4,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use mockall::automock; use mockall::automock;
use crate::tui::{ use crate::tui::{
app::{ app::app::{
AppState, IAppInteract, IAppInteractBrowse, IAppInteractError, IAppInteractInfo, AppState, IAppInteract, IAppInteractBrowse, IAppInteractError, IAppInteractInfo,
IAppInteractReload, IAppInteractReload,
}, },

View File

@ -5,7 +5,7 @@ mod lib;
mod listener; mod listener;
mod ui; mod ui;
pub use app::App; pub use app::app::App;
pub use event::EventChannel; pub use event::EventChannel;
pub use handler::EventHandler; pub use handler::EventHandler;
pub use listener::EventListener; pub use listener::EventListener;
@ -19,7 +19,7 @@ use std::io;
use std::marker::PhantomData; use std::marker::PhantomData;
use crate::tui::{ use crate::tui::{
app::{IAppAccess, IAppInteract}, app::app::{IAppAccess, IAppInteract},
event::EventError, event::EventError,
handler::IEventHandler, handler::IEventHandler,
listener::IEventListener, listener::IEventListener,
@ -178,8 +178,8 @@ mod tests {
use musichoard::collection::Collection; use musichoard::collection::Collection;
use crate::tui::{ use crate::tui::{
app::App, handler::MockIEventHandler, lib::MockIMusicHoard, listener::MockIEventListener, app::app::App, handler::MockIEventHandler, lib::MockIMusicHoard,
ui::Ui, listener::MockIEventListener, ui::Ui,
}; };
use super::*; use super::*;

View File

@ -12,7 +12,10 @@ use ratatui::{
Frame, Frame,
}; };
use crate::tui::app::{AppState, Category, IAppAccess, Selection}; use crate::tui::app::{
app::{AppState, IAppAccess},
selection::{Category, Selection},
};
pub trait IUi { pub trait IUi {
fn render<APP: IAppAccess, B: Backend>(app: &mut APP, frame: &mut Frame<'_, B>); fn render<APP: IAppAccess, B: Backend>(app: &mut APP, frame: &mut Frame<'_, B>);
@ -507,7 +510,7 @@ impl IUi for Ui {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tui::{app::AppPublic, testmod::COLLECTION, tests::terminal}; use crate::tui::{app::app::AppPublic, testmod::COLLECTION, tests::terminal};
use super::*; use super::*;
@ -546,7 +549,7 @@ mod tests {
#[test] #[test]
fn empty() { fn empty() {
let artists: Vec<Artist> = vec![]; let artists: Vec<Artist> = vec![];
let mut selection = Selection::new(Some(&artists)); let mut selection = Selection::new(&artists);
draw_test_suite(&artists, &mut selection); draw_test_suite(&artists, &mut selection);
} }
@ -554,7 +557,7 @@ mod tests {
#[test] #[test]
fn collection() { fn collection() {
let artists = &COLLECTION; let artists = &COLLECTION;
let mut selection = Selection::new(Some(artists)); let mut selection = Selection::new(artists);
draw_test_suite(artists, &mut selection); draw_test_suite(artists, &mut selection);