diff --git a/src/tui/app/machine/browse.rs b/src/tui/app/machine/browse.rs index 7ebebba..951d430 100644 --- a/src/tui/app/machine/browse.rs +++ b/src/tui/app/machine/browse.rs @@ -3,9 +3,9 @@ use std::{sync::mpsc, thread, time}; use musichoard::collection::musicbrainz::IMusicBrainzRef; use crate::tui::app::{ - machine::{matches::AppMatchesInfo, App, AppInner, AppMachine}, + machine::{App, AppInner, AppMachine}, selection::{Delta, ListSelection}, - AppPublic, AppState, IAppInteractBrowse, + AppMatchesInfo, AppPublic, AppState, IAppInteractBrowse, }; pub struct AppBrowse; @@ -139,8 +139,8 @@ mod tests { use crate::tui::{ app::{ machine::tests::{inner, inner_with_mb, music_hoard}, - AppPublicAlbumMatches, AppPublicArtistMatches, AppPublicMatchesInfo, Category, - IAppAccess, IAppInteract, IAppInteractMatches, + AppAlbumMatches, AppArtistMatches, AppMatchesInfo, Category, IAppAccess, IAppInteract, + IAppInteractMatches, MatchOption, }, lib::interface::musicbrainz::{self, Match, MockIMusicBrainz}, testmod::COLLECTION, @@ -263,11 +263,14 @@ mod tests { let public_matches = public.state.unwrap_matches(); - let expected = Some(AppPublicMatchesInfo::Album(AppPublicAlbumMatches { - matching: &album_1, - list: &matches_1, + let mut matches_1: Vec> = + matches_1.into_iter().map(Into::into).collect(); + matches_1.push(MatchOption::CannotHaveMbid); + let expected = Some(AppMatchesInfo::Album(AppAlbumMatches { + matching: album_1.clone(), + list: matches_1.clone(), })); - assert_eq!(public_matches.matches, expected); + assert_eq!(public_matches.matches, expected.as_ref()); let mut app = app.unwrap_matches().select(); @@ -276,11 +279,14 @@ mod tests { let public_matches = public.state.unwrap_matches(); - let expected = Some(AppPublicMatchesInfo::Album(AppPublicAlbumMatches { - matching: &album_4, - list: &matches_4, + let mut matches_4: Vec> = + matches_4.into_iter().map(Into::into).collect(); + matches_4.push(MatchOption::CannotHaveMbid); + let expected = Some(AppMatchesInfo::Album(AppAlbumMatches { + matching: album_4.clone(), + list: matches_4.clone(), })); - assert_eq!(public_matches.matches, expected); + assert_eq!(public_matches.matches, expected.as_ref()); let app = app.unwrap_matches().select(); app.unwrap_browse(); @@ -324,11 +330,13 @@ mod tests { let public_matches = public.state.unwrap_matches(); - let expected = Some(AppPublicMatchesInfo::Artist(AppPublicArtistMatches { - matching: &artist, - list: &matches, + let mut matches: Vec> = matches.into_iter().map(Into::into).collect(); + matches.push(MatchOption::CannotHaveMbid); + let expected = Some(AppMatchesInfo::Artist(AppArtistMatches { + matching: artist.clone(), + list: matches.clone(), })); - assert_eq!(public_matches.matches, expected); + assert_eq!(public_matches.matches, expected.as_ref()); let app = app.unwrap_matches().select(); app.unwrap_browse(); diff --git a/src/tui/app/machine/matches.rs b/src/tui/app/machine/matches.rs index 80265d4..adeaea8 100644 --- a/src/tui/app/machine/matches.rs +++ b/src/tui/app/machine/matches.rs @@ -1,80 +1,32 @@ use std::{cmp, sync::mpsc}; -use musichoard::collection::{album::Album, artist::Artist}; - -use crate::tui::{ - app::{ - machine::{App, AppInner, AppMachine}, - AppPublic, AppPublicAlbumMatches, AppPublicArtistMatches, AppPublicMatches, - AppPublicMatchesInfo, AppState, IAppInteractMatches, WidgetState, - }, - lib::interface::musicbrainz::Match, +use crate::tui::app::{ + machine::{App, AppInner, AppMachine}, + AppAlbumMatches, AppArtistMatches, AppMatchesInfo, AppPublic, AppPublicMatches, AppState, + IAppInteractMatches, MatchOption, WidgetState, }; -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct AppArtistMatchesInfo { - pub matching: Artist, - pub list: Vec>, -} - -impl AppArtistMatchesInfo { - fn is_empty(&self) -> bool { - self.list.is_empty() - } - +impl AppArtistMatches { fn len(&self) -> usize { self.list.len() } -} -impl<'app> From<&'app AppArtistMatchesInfo> for AppPublicArtistMatches<'app> { - fn from(value: &'app AppArtistMatchesInfo) -> Self { - AppPublicArtistMatches { - matching: &value.matching, - list: &value.list, - } + fn push_cannot_have_mbid(&mut self) { + self.list.push(MatchOption::CannotHaveMbid) } } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct AppAlbumMatchesInfo { - pub matching: Album, - pub list: Vec>, -} - -impl AppAlbumMatchesInfo { - fn is_empty(&self) -> bool { - self.list.is_empty() - } - +impl AppAlbumMatches { fn len(&self) -> usize { self.list.len() } -} -impl<'app> From<&'app AppAlbumMatchesInfo> for AppPublicAlbumMatches<'app> { - fn from(value: &'app AppAlbumMatchesInfo) -> Self { - AppPublicAlbumMatches { - matching: &value.matching, - list: &value.list, - } + fn push_cannot_have_mbid(&mut self) { + self.list.push(MatchOption::CannotHaveMbid) } } -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum AppMatchesInfo { - Artist(AppArtistMatchesInfo), - Album(AppAlbumMatchesInfo), -} - impl AppMatchesInfo { - fn is_empty(&self) -> bool { - match self { - Self::Artist(a) => a.is_empty(), - Self::Album(a) => a.is_empty(), - } - } - fn len(&self) -> usize { match self { Self::Artist(a) => a.len(), @@ -82,20 +34,10 @@ impl AppMatchesInfo { } } - pub fn artist(matching: Artist, list: Vec>) -> Self { - AppMatchesInfo::Artist(AppArtistMatchesInfo { matching, list }) - } - - pub fn album(matching: Album, list: Vec>) -> Self { - AppMatchesInfo::Album(AppAlbumMatchesInfo { matching, list }) - } -} - -impl<'app> From<&'app AppMatchesInfo> for AppPublicMatchesInfo<'app> { - fn from(value: &'app AppMatchesInfo) -> Self { - match value { - AppMatchesInfo::Artist(a) => AppPublicMatchesInfo::Artist(a.into()), - AppMatchesInfo::Album(a) => AppPublicMatchesInfo::Album(a.into()), + fn push_cannot_have_mbid(&mut self) { + match self { + Self::Artist(a) => a.push_cannot_have_mbid(), + Self::Album(a) => a.push_cannot_have_mbid(), } } } @@ -165,13 +107,7 @@ impl IAppInteractMatches for AppMachine { fn select(mut self) -> Self::APP { self.state.next_matches_info(); match self.state.current { - Some(ref matches_info) => { - self.state.state = WidgetState::default(); - if !matches_info.is_empty() { - self.state.state.list.select(Some(0)); - } - self.into() - } + Some(_) => self.into(), None => AppMachine::browse(self.inner).into(), } } @@ -193,11 +129,10 @@ impl IAppInteractMatchesPrivate for AppMatches { fn next_matches_info(&mut self) { // FIXME: try_recv might not be appropriate for asynchronous version. (self.current, self.state) = match self.matches_rx.try_recv() { - Ok(next_match) => { + Ok(mut next_match) => { + next_match.push_cannot_have_mbid(); let mut state = WidgetState::default(); - if !next_match.is_empty() { - state.list.select(Some(0)); - } + state.list.select(Some(0)); (Some(next_match), state) } Err(_) => (None, WidgetState::default()), @@ -209,13 +144,16 @@ impl IAppInteractMatchesPrivate for AppMatches { mod tests { use mpsc::Receiver; use musichoard::collection::{ - album::{AlbumDate, AlbumId, AlbumPrimaryType, AlbumSecondaryType}, - artist::ArtistId, + album::{Album, AlbumDate, AlbumId, AlbumPrimaryType, AlbumSecondaryType}, + artist::{Artist, ArtistId}, }; - use crate::tui::app::{ - machine::tests::{inner, music_hoard}, - IAppAccess, + use crate::tui::{ + app::{ + machine::tests::{inner, music_hoard}, + IAppAccess, + }, + lib::interface::musicbrainz::Match, }; use super::*; @@ -287,6 +225,12 @@ mod tests { rx } + fn push_cannot_have_mbid(matches_info_vec: &mut [AppMatchesInfo]) { + for matches_info in matches_info_vec.iter_mut() { + matches_info.push_cannot_have_mbid(); + } + } + #[test] fn create_empty() { let matches = AppMachine::matches(inner(music_hoard(vec![])), receiver(vec![])); @@ -306,11 +250,12 @@ mod tests { #[test] fn create_nonempty() { - let matches_info_vec = album_matches_info_vec(); + let mut matches_info_vec = album_matches_info_vec(); let matches = AppMachine::matches( inner(music_hoard(vec![])), receiver(matches_info_vec.clone()), ); + push_cannot_have_mbid(&mut matches_info_vec); let mut widget_state = WidgetState::default(); widget_state.list.select(Some(0)); @@ -322,15 +267,16 @@ mod tests { let public = app.get(); let public_matches = public.state.unwrap_matches(); - assert_eq!(public_matches.matches, Some((&matches_info_vec[0]).into())); + assert_eq!(public_matches.matches, Some(&matches_info_vec[0])); assert_eq!(public_matches.state, &widget_state); } - fn matches_flow(matches_info_vec: Vec) { + fn matches_flow(mut matches_info_vec: Vec) { let matches = AppMachine::matches( inner(music_hoard(vec![])), receiver(matches_info_vec.clone()), ); + push_cannot_have_mbid(&mut matches_info_vec); let mut widget_state = WidgetState::default(); widget_state.list.select(Some(0)); @@ -348,10 +294,16 @@ mod tests { assert_eq!(matches.state.current.as_ref(), Some(&matches_info_vec[0])); assert_eq!(matches.state.state.list.selected(), Some(1)); + // Next is CannotHaveMBID let matches = matches.next_match().unwrap_matches(); assert_eq!(matches.state.current.as_ref(), Some(&matches_info_vec[0])); - assert_eq!(matches.state.state.list.selected(), Some(1)); + assert_eq!(matches.state.state.list.selected(), Some(2)); + + let matches = matches.next_match().unwrap_matches(); + + assert_eq!(matches.state.current.as_ref(), Some(&matches_info_vec[0])); + assert_eq!(matches.state.state.list.selected(), Some(2)); let matches = matches.select().unwrap_matches(); @@ -374,11 +326,12 @@ mod tests { #[test] fn matches_abort() { - let matches_info_vec = album_matches_info_vec(); + let mut matches_info_vec = album_matches_info_vec(); let matches = AppMachine::matches( inner(music_hoard(vec![])), receiver(matches_info_vec.clone()), ); + push_cannot_have_mbid(&mut matches_info_vec); let mut widget_state = WidgetState::default(); widget_state.list.select(Some(0)); diff --git a/src/tui/app/mod.rs b/src/tui/app/mod.rs index 90c49fe..3279b17 100644 --- a/src/tui/app/mod.rs +++ b/src/tui/app/mod.rs @@ -129,26 +129,50 @@ pub struct AppPublicInner<'app> { pub selection: &'app mut Selection, } -#[derive(Debug, PartialEq, Eq)] -pub struct AppPublicArtistMatches<'app> { - pub matching: &'app Artist, - pub list: &'app [Match], +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum MatchOption { + Match(Match), + CannotHaveMbid, } -#[derive(Debug, PartialEq, Eq)] -pub struct AppPublicAlbumMatches<'app> { - pub matching: &'app Album, - pub list: &'app [Match], +impl From> for MatchOption { + fn from(value: Match) -> Self { + MatchOption::Match(value) + } } -#[derive(Debug, PartialEq, Eq)] -pub enum AppPublicMatchesInfo<'app> { - Artist(AppPublicArtistMatches<'app>), - Album(AppPublicAlbumMatches<'app>), +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AppArtistMatches { + pub matching: Artist, + pub list: Vec>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AppAlbumMatches { + pub matching: Album, + pub list: Vec>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum AppMatchesInfo { + Artist(AppArtistMatches), + Album(AppAlbumMatches), +} + +impl AppMatchesInfo { + pub fn artist>>(matching: Artist, list: Vec) -> Self { + let list: Vec> = list.into_iter().map(Into::into).collect(); + AppMatchesInfo::Artist(AppArtistMatches { matching, list }) + } + + pub fn album>>(matching: Album, list: Vec) -> Self { + let list: Vec> = list.into_iter().map(Into::into).collect(); + AppMatchesInfo::Album(AppAlbumMatches { matching, list }) + } } pub struct AppPublicMatches<'app> { - pub matches: Option>, + pub matches: Option<&'app AppMatchesInfo>, pub state: &'app mut WidgetState, } diff --git a/src/tui/ui/display.rs b/src/tui/ui/display.rs index f566c56..78f0927 100644 --- a/src/tui/ui/display.rs +++ b/src/tui/ui/display.rs @@ -4,7 +4,7 @@ use musichoard::collection::{ track::{TrackFormat, TrackQuality}, }; -use crate::tui::{app::AppPublicMatchesInfo, lib::interface::musicbrainz::Match}; +use crate::tui::app::{AppMatchesInfo, MatchOption}; pub struct UiDisplay; @@ -114,40 +114,50 @@ impl UiDisplay { "Matching nothing" } - pub fn display_matching_info(matches: Option<&AppPublicMatchesInfo>) -> String { + pub fn display_matching_info(matches: Option<&AppMatchesInfo>) -> String { match matches.as_ref() { Some(kind) => match kind { - AppPublicMatchesInfo::Artist(m) => UiDisplay::display_artist_matching(m.matching), - AppPublicMatchesInfo::Album(m) => UiDisplay::display_album_matching(m.matching), + AppMatchesInfo::Artist(m) => UiDisplay::display_artist_matching(&m.matching), + AppMatchesInfo::Album(m) => UiDisplay::display_album_matching(&m.matching), }, None => UiDisplay::display_nothing_matching().to_string(), } } - pub fn display_artist_match(match_artist: &Match) -> String { - format!( - "{}{} ({}%)", - &match_artist.item.id.name, - &match_artist - .disambiguation - .as_ref() - .map(|d| format!(" ({d})")) - .unwrap_or_default(), - match_artist.score, - ) + pub fn display_artist_match(match_option: &MatchOption) -> String { + match match_option { + MatchOption::Match(match_artist) => format!( + "{}{} ({}%)", + &match_artist.item.id.name, + &match_artist + .disambiguation + .as_ref() + .map(|d| format!(" ({d})")) + .unwrap_or_default(), + match_artist.score, + ), + MatchOption::CannotHaveMbid => Self::display_cannot_have_mbid().to_string(), + } } - pub fn display_album_match(match_album: &Match) -> String { - format!( - "{:010} | {} [{}] ({}%)", - UiDisplay::display_album_date(&match_album.item.date), - &match_album.item.id.title, - UiDisplay::display_type( - &match_album.item.primary_type, - &match_album.item.secondary_types + pub fn display_album_match(match_option: &MatchOption) -> String { + match match_option { + MatchOption::Match(match_album) => format!( + "{:010} | {} [{}] ({}%)", + UiDisplay::display_album_date(&match_album.item.date), + &match_album.item.id.title, + UiDisplay::display_type( + &match_album.item.primary_type, + &match_album.item.secondary_types + ), + match_album.score, ), - match_album.score, - ) + MatchOption::CannotHaveMbid => Self::display_cannot_have_mbid().to_string(), + } + } + + fn display_cannot_have_mbid() -> &'static str { + "-- Cannot have a MusicBrainz Identifier --" } } diff --git a/src/tui/ui/matches.rs b/src/tui/ui/matches.rs index 301ed17..91bd158 100644 --- a/src/tui/ui/matches.rs +++ b/src/tui/ui/matches.rs @@ -2,8 +2,7 @@ use musichoard::collection::{album::Album, artist::Artist}; use ratatui::widgets::{List, ListItem}; use crate::tui::{ - app::{AppPublicMatchesInfo, WidgetState}, - lib::interface::musicbrainz::Match, + app::{AppMatchesInfo, MatchOption, WidgetState}, ui::display::UiDisplay, }; @@ -14,11 +13,11 @@ pub struct MatchesState<'a, 'b> { } impl<'a, 'b> MatchesState<'a, 'b> { - pub fn new(matches: Option, state: &'b mut WidgetState) -> Self { + pub fn new(matches: Option<&AppMatchesInfo>, state: &'b mut WidgetState) -> Self { match matches { Some(info) => match info { - AppPublicMatchesInfo::Artist(m) => Self::artists(m.matching, m.list, state), - AppPublicMatchesInfo::Album(m) => Self::albums(m.matching, m.list, state), + AppMatchesInfo::Artist(m) => Self::artists(&m.matching, &m.list, state), + AppMatchesInfo::Album(m) => Self::albums(&m.matching, &m.list, state), }, None => Self::empty(state), } @@ -32,7 +31,11 @@ impl<'a, 'b> MatchesState<'a, 'b> { } } - fn artists(matching: &Artist, matches: &[Match], state: &'b mut WidgetState) -> Self { + fn artists( + matching: &Artist, + matches: &[MatchOption], + state: &'b mut WidgetState, + ) -> Self { let matching = UiDisplay::display_artist_matching(matching); let list = List::new( @@ -50,7 +53,11 @@ impl<'a, 'b> MatchesState<'a, 'b> { } } - fn albums(matching: &Album, matches: &[Match], state: &'b mut WidgetState) -> Self { + fn albums( + matching: &Album, + matches: &[MatchOption], + state: &'b mut WidgetState, + ) -> Self { let matching = UiDisplay::display_album_matching(matching); let list = List::new( diff --git a/src/tui/ui/minibuffer.rs b/src/tui/ui/minibuffer.rs index e8e0610..3e6e0f1 100644 --- a/src/tui/ui/minibuffer.rs +++ b/src/tui/ui/minibuffer.rs @@ -59,7 +59,7 @@ impl Minibuffer<'_> { }, AppState::Matches(public) => Minibuffer { paragraphs: vec![ - Paragraph::new(UiDisplay::display_matching_info(public.matches.as_ref())), + Paragraph::new(UiDisplay::display_matching_info(public.matches)), Paragraph::new("q: abort"), ], columns: 2, diff --git a/src/tui/ui/mod.rs b/src/tui/ui/mod.rs index 47b2939..8e5fa70 100644 --- a/src/tui/ui/mod.rs +++ b/src/tui/ui/mod.rs @@ -14,10 +14,7 @@ use ratatui::{layout::Rect, widgets::Paragraph, Frame}; use musichoard::collection::{album::Album, Collection}; use crate::tui::{ - app::{ - AppPublicMatchesInfo, AppPublicState, AppState, Category, IAppAccess, Selection, - WidgetState, - }, + app::{AppMatchesInfo, AppPublicState, AppState, Category, IAppAccess, Selection, WidgetState}, ui::{ browse::{ AlbumArea, AlbumState, ArtistArea, ArtistState, FrameArea, TrackArea, TrackState, @@ -135,7 +132,7 @@ impl Ui { } fn render_matches_overlay( - matches: Option, + matches: Option<&AppMatchesInfo>, state: &mut WidgetState, frame: &mut Frame, ) { @@ -185,10 +182,7 @@ mod tests { }; use crate::tui::{ - app::{ - AppPublic, AppPublicAlbumMatches, AppPublicArtistMatches, AppPublicInner, - AppPublicMatches, Delta, - }, + app::{AppPublic, AppPublicInner, AppPublicMatches, Delta, MatchOption}, lib::interface::musicbrainz::Match, testmod::COLLECTION, tests::terminal, @@ -196,33 +190,6 @@ mod tests { use super::*; - impl<'app> AppPublicArtistMatches<'app> { - fn get(&self) -> AppPublicArtistMatches<'app> { - AppPublicArtistMatches { - matching: self.matching, - list: self.list, - } - } - } - - impl<'app> AppPublicAlbumMatches<'app> { - fn get(&self) -> AppPublicAlbumMatches<'app> { - AppPublicAlbumMatches { - matching: self.matching, - list: self.list, - } - } - } - - impl<'app> AppPublicMatchesInfo<'app> { - fn get(&self) -> AppPublicMatchesInfo<'app> { - match self { - Self::Artist(a) => Self::Artist(a.get()), - Self::Album(a) => Self::Album(a.get()), - } - } - } - // Automock does not support returning types with generic lifetimes. impl<'app> IAppAccess for AppPublic<'app> { fn get(&mut self) -> AppPublic { @@ -237,7 +204,7 @@ mod tests { AppState::Reload(()) => AppState::Reload(()), AppState::Search(s) => AppState::Search(s), AppState::Matches(ref mut m) => AppState::Matches(AppPublicMatches { - matches: m.matches.as_mut().map(|k| k.get()), + matches: m.matches, state: m.state, }), AppState::Error(s) => AppState::Error(s), @@ -257,18 +224,16 @@ mod tests { } } - fn artist_matches<'app>( - matching: &'app Artist, - list: &'app [Match], - ) -> AppPublicMatchesInfo<'app> { - AppPublicMatchesInfo::Artist(AppPublicArtistMatches { matching, list }) + fn artist_matches(matching: Artist, list: Vec>) -> AppMatchesInfo { + let mut list: Vec> = list.into_iter().map(Into::into).collect(); + list.push(MatchOption::CannotHaveMbid); + AppMatchesInfo::artist(matching, list) } - fn album_matches<'app>( - matching: &'app Album, - list: &'app [Match], - ) -> AppPublicMatchesInfo<'app> { - AppPublicMatchesInfo::Album(AppPublicAlbumMatches { matching, list }) + fn album_matches(matching: Album, list: Vec>) -> AppMatchesInfo { + let mut list: Vec> = list.into_iter().map(Into::into).collect(); + list.push(MatchOption::CannotHaveMbid); + AppMatchesInfo::album(matching, list) } fn draw_test_suite(collection: &Collection, selection: &mut Selection) { @@ -364,21 +329,21 @@ mod tests { let mut terminal = terminal(); let artist = Artist::new(ArtistId::new("an artist")); - let artist_match = Match { score: 80, item: artist.clone(), disambiguation: None, }; + let list = vec![artist_match.clone(), artist_match.clone()]; + let artist_matches = artist_matches(artist, list); - let list = [artist_match.clone(), artist_match.clone()]; let mut widget_state = WidgetState::default(); widget_state.list.select(Some(0)); let mut app = AppPublic { inner: public_inner(collection, &mut selection), state: AppState::Matches(AppPublicMatches { - matches: Some(artist_matches(&artist, &list)), + matches: Some(&artist_matches), state: &mut widget_state, }), }; @@ -398,21 +363,21 @@ mod tests { Some(AlbumPrimaryType::Album), vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation], ); - let album_match = Match { score: 80, item: album.clone(), disambiguation: None, }; + let list = vec![album_match.clone(), album_match.clone()]; + let album_matches = album_matches(album, list); - let list = [album_match.clone(), album_match.clone()]; let mut widget_state = WidgetState::default(); widget_state.list.select(Some(0)); let mut app = AppPublic { inner: public_inner(collection, &mut selection), state: AppState::Matches(AppPublicMatches { - matches: Some(album_matches(&album, &list)), + matches: Some(&album_matches), state: &mut widget_state, }), };