Working refresh
This commit is contained in:
parent
00f8c4477e
commit
4b9e6ff130
@ -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))
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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
2
src/tui/app/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod app;
|
||||||
|
pub mod selection;
|
589
src/tui/app/selection.rs
Normal file
589
src/tui/app/selection.rs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -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::*;
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user