From d9d59454226c6560b6596653c27036b6ee6c39c0 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Sun, 17 Mar 2024 20:17:41 +0100 Subject: [PATCH] Display all the extra album info (#173) Closes #172 Reviewed-on: https://git.thenineworlds.net/wojtek/musichoard/pulls/173 --- src/tui/ui.rs | 201 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 178 insertions(+), 23 deletions(-) diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 8949e13..314f7f2 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use musichoard::collection::{ - album::{Album, AlbumDate, AlbumSeq, AlbumStatus}, + album::{Album, AlbumDate, AlbumPrimaryType, AlbumSecondaryType, AlbumSeq, AlbumStatus}, artist::Artist, musicbrainz::IMusicBrainzRef, track::{Track, TrackFormat, TrackQuality}, @@ -198,15 +198,18 @@ impl<'a, 'b> ArtistState<'a, 'b> { } } +struct InfoOverlay; + +impl InfoOverlay { + const ITEM_INDENT: &'static str = " "; + const LIST_INDENT: &'static str = " - "; +} + struct ArtistOverlay<'a> { properties: Paragraph<'a>, } impl<'a> ArtistOverlay<'a> { - fn opt_opt_to_str + ?Sized>(opt: Option>) -> &str { - opt.flatten().map(|item| item.as_ref()).unwrap_or("") - } - fn opt_hashmap_to_string, T: AsRef>( opt_map: Option<&HashMap>>, item_indent: &str, @@ -254,8 +257,8 @@ impl<'a> ArtistOverlay<'a> { fn new(artists: &'a [Artist], state: &ListState) -> ArtistOverlay<'a> { let artist = state.selected().map(|i| &artists[i]); - let item_indent = " "; - let list_indent = " - "; + let item_indent = InfoOverlay::ITEM_INDENT; + let list_indent = InfoOverlay::LIST_INDENT; let double_item_indent = format!("{item_indent}{item_indent}"); let double_list_indent = format!("{item_indent}{list_indent}"); @@ -265,7 +268,9 @@ impl<'a> ArtistOverlay<'a> { MusicBrainz: {}\n{item_indent}\ Properties: {}", artist.map(|a| a.id.name.as_str()).unwrap_or(""), - Self::opt_opt_to_str(artist.map(|a| a.musicbrainz.as_ref().map(|mb| mb.url()))), + artist + .and_then(|a| a.musicbrainz.as_ref().map(|mb| mb.url().as_str())) + .unwrap_or(""), Self::opt_hashmap_to_string( artist.map(|a| &a.properties), &double_item_indent, @@ -296,14 +301,15 @@ impl<'a, 'b> AlbumState<'a, 'b> { let album = state.list.selected().map(|i| &albums[i]); let info = Paragraph::new(format!( "Title: {}\n\ - Date: {}{}\n\ + Date: {}\n\ + Type: {}\n\ Status: {}", album.map(|a| a.id.title.as_str()).unwrap_or(""), album - .map(|a| Self::display_album_date(&a.date)) + .map(|a| Self::display_date(&a.date, &a.seq)) .unwrap_or_default(), album - .map(|a| Self::display_album_seq(&a.seq)) + .map(|a| Self::display_type(&a.primary_type, &a.secondary_types)) .unwrap_or_default(), album .map(|a| Self::display_album_status(&a.get_status())) @@ -329,6 +335,14 @@ impl<'a, 'b> AlbumState<'a, 'b> { ListItem::new(line) } + fn display_date(date: &AlbumDate, seq: &AlbumSeq) -> String { + if seq.0 > 0 { + format!("{} ({})", Self::display_album_date(date), seq.0) + } else { + Self::display_album_date(date) + } + } + fn display_album_date(date: &AlbumDate) -> String { match date.year { Some(year) => match date.month { @@ -342,14 +356,57 @@ impl<'a, 'b> AlbumState<'a, 'b> { } } - fn display_album_seq(seq: &AlbumSeq) -> String { - if seq.0 > 0 { - format!(" ({})", seq.0) - } else { - String::new() + fn display_type( + primary: &Option, + secondary: &Vec, + ) -> String { + match primary { + Some(ref primary) => { + if secondary.is_empty() { + Self::display_primary_type(primary).to_string() + } else { + format!( + "{} ({})", + Self::display_primary_type(primary), + Self::display_secondary_types(secondary) + ) + } + } + None => String::default(), } } + fn display_primary_type(value: &AlbumPrimaryType) -> &'static str { + match value { + AlbumPrimaryType::Album => "Album", + AlbumPrimaryType::Single => "Single", + AlbumPrimaryType::Ep => "EP", + AlbumPrimaryType::Broadcast => "Broadcast", + AlbumPrimaryType::Other => "Other", + } + } + + fn display_secondary_types(values: &Vec) -> String { + let mut types: Vec<&'static str> = vec![]; + for value in values { + match value { + AlbumSecondaryType::Compilation => types.push("Compilation"), + AlbumSecondaryType::Soundtrack => types.push("Soundtrack"), + AlbumSecondaryType::Spokenword => types.push("Spokenword"), + AlbumSecondaryType::Interview => types.push("Interview"), + AlbumSecondaryType::Audiobook => types.push("Audiobook"), + AlbumSecondaryType::AudioDrama => types.push("Audio drama"), + AlbumSecondaryType::Live => types.push("Live"), + AlbumSecondaryType::Remix => types.push("Remix"), + AlbumSecondaryType::DjMix => types.push("DJ-mix"), + AlbumSecondaryType::MixtapeStreet => types.push("Mixtape/Street"), + AlbumSecondaryType::Demo => types.push("Demo"), + AlbumSecondaryType::FieldRecording => types.push("Field recording"), + } + } + types.join(", ") + } + fn display_album_status(status: &AlbumStatus) -> &'static str { match status { AlbumStatus::None => "None", @@ -361,6 +418,29 @@ impl<'a, 'b> AlbumState<'a, 'b> { } } +struct AlbumOverlay<'a> { + properties: Paragraph<'a>, +} + +impl<'a> AlbumOverlay<'a> { + fn new(albums: &'a [Album], state: &ListState) -> AlbumOverlay<'a> { + let album = state.selected().map(|i| &albums[i]); + + let item_indent = InfoOverlay::ITEM_INDENT; + + let properties = Paragraph::new(format!( + "Album: {}\n\n{item_indent}\ + MusicBrainz: {}", + album.map(|a| a.id.title.as_str()).unwrap_or(""), + album + .and_then(|a| a.musicbrainz.as_ref().map(|mb| mb.url().as_str())) + .unwrap_or(""), + )); + + AlbumOverlay { properties } + } +} + struct TrackState<'a, 'b> { active: bool, list: List<'a>, @@ -698,9 +778,18 @@ impl Ui { fn render_info_overlay(artists: &Collection, selection: &mut Selection, frame: &mut Frame) { let area = OverlayBuilder::default().build(frame.size()); - let artist_overlay = ArtistOverlay::new(artists, &selection.widget_state_artist().list); - - Self::render_overlay_widget("Artist", artist_overlay.properties, area, false, frame); + if selection.category() == Category::Artist { + let artist_overlay = ArtistOverlay::new(artists, &selection.widget_state_artist().list); + Self::render_overlay_widget("Artist", artist_overlay.properties, area, false, frame); + } else { + let no_albums: Vec = vec![]; + let albums = selection + .state_album(artists) + .map(|st| st.list) + .unwrap_or_else(|| &no_albums); + let album_overlay = AlbumOverlay::new(albums, &selection.widget_state_album().list); + Self::render_overlay_widget("Album", album_overlay.properties, area, false, frame); + } } fn render_reload_overlay(frame: &mut Frame) { @@ -818,10 +907,76 @@ mod tests { } #[test] - fn display_album_seq() { - assert_eq!(AlbumState::display_album_seq(&AlbumSeq::default()), ""); - assert_eq!(AlbumState::display_album_seq(&AlbumSeq(0)), ""); - assert_eq!(AlbumState::display_album_seq(&AlbumSeq(5)), " (5)"); + fn display_date() { + let date: AlbumDate = 1990.into(); + assert_eq!( + AlbumState::display_date(&date, &AlbumSeq::default()), + "1990" + ); + assert_eq!(AlbumState::display_date(&date, &AlbumSeq(0)), "1990"); + assert_eq!(AlbumState::display_date(&date, &AlbumSeq(5)), "1990 (5)"); + } + + #[test] + fn display_primary_type() { + assert_eq!( + AlbumState::display_primary_type(&AlbumPrimaryType::Album), + "Album" + ); + assert_eq!( + AlbumState::display_primary_type(&AlbumPrimaryType::Single), + "Single" + ); + assert_eq!( + AlbumState::display_primary_type(&AlbumPrimaryType::Ep), + "EP" + ); + assert_eq!( + AlbumState::display_primary_type(&AlbumPrimaryType::Broadcast), + "Broadcast" + ); + assert_eq!( + AlbumState::display_primary_type(&AlbumPrimaryType::Other), + "Other" + ); + } + + #[test] + fn display_secondary_types() { + assert_eq!( + AlbumState::display_secondary_types(&vec![ + AlbumSecondaryType::Compilation, + AlbumSecondaryType::Soundtrack, + AlbumSecondaryType::Spokenword, + AlbumSecondaryType::Interview, + AlbumSecondaryType::Audiobook, + AlbumSecondaryType::AudioDrama, + AlbumSecondaryType::Live, + AlbumSecondaryType::Remix, + AlbumSecondaryType::DjMix, + AlbumSecondaryType::MixtapeStreet, + AlbumSecondaryType::Demo, + AlbumSecondaryType::FieldRecording, + ]), + "Compilation, Soundtrack, Spokenword, Interview, Audiobook, Audio drama, Live, Remix, \ + DJ-mix, Mixtape/Street, Demo, Field recording" + ); + } + + #[test] + fn display_type() { + assert_eq!(AlbumState::display_type(&None, &vec![]), ""); + assert_eq!( + AlbumState::display_type(&Some(AlbumPrimaryType::Album), &vec![]), + "Album" + ); + assert_eq!( + AlbumState::display_type( + &Some(AlbumPrimaryType::Album), + &vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation] + ), + "Album (Live, Compilation)" + ); } #[test]