Provide a keyboard shortcut to sync all existing albums with MusicBrainz (#167)
Closes #166 Reviewed-on: #167
This commit is contained in:
parent
d8fd952456
commit
cf7e23c38c
@ -2,15 +2,11 @@
|
|||||||
|
|
||||||
use std::{fmt, num};
|
use std::{fmt, num};
|
||||||
|
|
||||||
// TODO: #[cfg(test)]
|
|
||||||
// TODO: use mockall::automock;
|
|
||||||
|
|
||||||
use uuid::{self, Uuid};
|
use uuid::{self, Uuid};
|
||||||
|
|
||||||
use crate::collection::album::Album;
|
use crate::collection::album::Album;
|
||||||
|
|
||||||
/// Trait for interacting with the MusicBrainz API.
|
/// Trait for interacting with the MusicBrainz API.
|
||||||
// TODO: #[cfg_attr(test, automock)]
|
|
||||||
pub trait IMusicBrainz {
|
pub trait IMusicBrainz {
|
||||||
fn lookup_artist_release_groups(&mut self, mbid: &Mbid) -> Result<Vec<Album>, Error>;
|
fn lookup_artist_release_groups(&mut self, mbid: &Mbid) -> Result<Vec<Album>, Error>;
|
||||||
fn search_release_group(
|
fn search_release_group(
|
||||||
@ -20,7 +16,7 @@ pub trait IMusicBrainz {
|
|||||||
) -> Result<Vec<Match<Album>>, Error>;
|
) -> Result<Vec<Match<Album>>, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct Match<T> {
|
pub struct Match<T> {
|
||||||
pub score: u8,
|
pub score: u8,
|
||||||
pub item: T,
|
pub item: T,
|
||||||
|
11
src/external/musicbrainz/api/mod.rs
vendored
11
src/external/musicbrainz/api/mod.rs
vendored
@ -150,6 +150,7 @@ struct SearchReleaseGroup {
|
|||||||
title: String,
|
title: String,
|
||||||
first_release_date: String,
|
first_release_date: String,
|
||||||
primary_type: SerdeAlbumPrimaryType,
|
primary_type: SerdeAlbumPrimaryType,
|
||||||
|
secondary_types: Option<Vec<SerdeAlbumSecondaryType>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<SearchReleaseGroup> for Match<Album> {
|
impl TryFrom<SearchReleaseGroup> for Match<Album> {
|
||||||
@ -160,7 +161,10 @@ impl TryFrom<SearchReleaseGroup> for Match<Album> {
|
|||||||
entity.title,
|
entity.title,
|
||||||
AlbumDate::from_mb_date(&entity.first_release_date)?,
|
AlbumDate::from_mb_date(&entity.first_release_date)?,
|
||||||
Some(entity.primary_type.into()),
|
Some(entity.primary_type.into()),
|
||||||
vec![],
|
entity
|
||||||
|
.secondary_types
|
||||||
|
.map(|v| v.into_iter().map(|st| st.into()).collect())
|
||||||
|
.unwrap_or_default(),
|
||||||
);
|
);
|
||||||
let mbref = MbAlbumRef::from_uuid_str(entity.id)
|
let mbref = MbAlbumRef::from_uuid_str(entity.id)
|
||||||
.map_err(|err| Error::MbidParse(err.to_string()))?;
|
.map_err(|err| Error::MbidParse(err.to_string()))?;
|
||||||
@ -229,7 +233,7 @@ pub enum SerdeAlbumSecondaryTypeDef {
|
|||||||
FieldRecording,
|
FieldRecording,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct SerdeAlbumSecondaryType(
|
pub struct SerdeAlbumSecondaryType(
|
||||||
#[serde(with = "SerdeAlbumSecondaryTypeDef")] AlbumSecondaryType,
|
#[serde(with = "SerdeAlbumSecondaryTypeDef")] AlbumSecondaryType,
|
||||||
);
|
);
|
||||||
@ -320,6 +324,7 @@ mod tests {
|
|||||||
title: String::from("an album"),
|
title: String::from("an album"),
|
||||||
first_release_date: String::from("1986-04"),
|
first_release_date: String::from("1986-04"),
|
||||||
primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album),
|
primary_type: SerdeAlbumPrimaryType(AlbumPrimaryType::Album),
|
||||||
|
secondary_types: Some(vec![SerdeAlbumSecondaryType(AlbumSecondaryType::Live)]),
|
||||||
};
|
};
|
||||||
let response = ResponseSearchReleaseGroup {
|
let response = ResponseSearchReleaseGroup {
|
||||||
release_groups: vec![release_group],
|
release_groups: vec![release_group],
|
||||||
@ -350,7 +355,7 @@ mod tests {
|
|||||||
AlbumId::new("an album"),
|
AlbumId::new("an album"),
|
||||||
(1986, 4),
|
(1986, 4),
|
||||||
Some(AlbumPrimaryType::Album),
|
Some(AlbumPrimaryType::Album),
|
||||||
vec![],
|
vec![AlbumSecondaryType::Live],
|
||||||
);
|
);
|
||||||
album.set_musicbrainz_ref(
|
album.set_musicbrainz_ref(
|
||||||
MbAlbumRef::from_uuid_str("11111111-1111-1111-1111-111111111111").unwrap(),
|
MbAlbumRef::from_uuid_str("11111111-1111-1111-1111-111111111111").unwrap(),
|
||||||
|
13
src/main.rs
13
src/main.rs
@ -16,6 +16,7 @@ use musichoard::{
|
|||||||
executor::{ssh::BeetsLibrarySshExecutor, BeetsLibraryProcessExecutor},
|
executor::{ssh::BeetsLibrarySshExecutor, BeetsLibraryProcessExecutor},
|
||||||
BeetsLibrary,
|
BeetsLibrary,
|
||||||
},
|
},
|
||||||
|
musicbrainz::api::{client::MusicBrainzApiClient, MusicBrainzApi},
|
||||||
},
|
},
|
||||||
interface::{
|
interface::{
|
||||||
database::{IDatabase, NullDatabase},
|
database::{IDatabase, NullDatabase},
|
||||||
@ -26,6 +27,12 @@ use musichoard::{
|
|||||||
|
|
||||||
use tui::{App, EventChannel, EventHandler, EventListener, Tui, Ui};
|
use tui::{App, EventChannel, EventHandler, EventListener, Tui, Ui};
|
||||||
|
|
||||||
|
const MUSICHOARD_HTTP_USER_AGENT: &str = concat!(
|
||||||
|
"MusicHoard/",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
" ( musichoard@thenineworlds.net )"
|
||||||
|
);
|
||||||
|
|
||||||
#[derive(StructOpt)]
|
#[derive(StructOpt)]
|
||||||
struct Opt {
|
struct Opt {
|
||||||
#[structopt(flatten)]
|
#[structopt(flatten)]
|
||||||
@ -74,7 +81,11 @@ fn with<Database: IDatabase, Library: ILibrary>(builder: MusicHoardBuilder<Datab
|
|||||||
let listener = EventListener::new(channel.sender());
|
let listener = EventListener::new(channel.sender());
|
||||||
let handler = EventHandler::new(channel.receiver());
|
let handler = EventHandler::new(channel.receiver());
|
||||||
|
|
||||||
let app = App::new(music_hoard);
|
let client = MusicBrainzApiClient::new(MUSICHOARD_HTTP_USER_AGENT)
|
||||||
|
.expect("failed to initialise HTTP client");
|
||||||
|
let api = Box::new(MusicBrainzApi::new(client));
|
||||||
|
|
||||||
|
let app = App::new(music_hoard, api);
|
||||||
let ui = Ui;
|
let ui = Ui;
|
||||||
|
|
||||||
// Run the TUI application.
|
// Run the TUI application.
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
|
use std::{thread, time};
|
||||||
|
|
||||||
|
use musichoard::collection::musicbrainz::IMusicBrainzRef;
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{
|
app::{
|
||||||
machine::{App, AppInner, AppMachine},
|
machine::{matches::AppMatchesInfo, App, AppInner, AppMachine},
|
||||||
selection::{Delta, ListSelection},
|
selection::{Delta, ListSelection},
|
||||||
AppPublic, AppState, IAppInteractBrowse,
|
AppPublic, AppState, IAppInteractBrowse,
|
||||||
},
|
},
|
||||||
@ -81,6 +85,45 @@ impl<MH: IMusicHoard> IAppInteractBrowse for AppMachine<MH, AppBrowse> {
|
|||||||
AppMachine::search(self.inner, orig).into()
|
AppMachine::search(self.inner, orig).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fetch_musicbrainz(mut self) -> Self::APP {
|
||||||
|
let coll = self.inner.music_hoard.get_collection();
|
||||||
|
let artist = match self.inner.selection.state_artist(coll) {
|
||||||
|
Some(artist_state) => &coll[artist_state.index],
|
||||||
|
None => {
|
||||||
|
return AppMachine::error(self.inner, "cannot fetch: no artist selected").into()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let arid = match artist.musicbrainz {
|
||||||
|
Some(ref mbid) => mbid.mbid(),
|
||||||
|
None => {
|
||||||
|
return AppMachine::error(self.inner, "cannot fetch: missing artist MBID").into()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut artist_album_matches = vec![];
|
||||||
|
let mut album_iter = artist.albums.iter().peekable();
|
||||||
|
while let Some(album) = album_iter.next() {
|
||||||
|
if album.musicbrainz.is_some() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.inner.mb_api.search_release_group(arid, album) {
|
||||||
|
Ok(matches) => artist_album_matches.push(AppMatchesInfo {
|
||||||
|
matching: album.clone(),
|
||||||
|
matches,
|
||||||
|
}),
|
||||||
|
Err(err) => return AppMachine::error(self.inner, err.to_string()).into(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if album_iter.peek().is_some() {
|
||||||
|
thread::sleep(time::Duration::from_secs(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppMachine::matches(self.inner, artist_album_matches).into()
|
||||||
|
}
|
||||||
|
|
||||||
fn no_op(self) -> Self::APP {
|
fn no_op(self) -> Self::APP {
|
||||||
self.into()
|
self.into()
|
||||||
}
|
}
|
||||||
@ -88,10 +131,17 @@ impl<MH: IMusicHoard> IAppInteractBrowse for AppMachine<MH, AppBrowse> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use mockall::{predicate, Sequence};
|
||||||
|
use musichoard::collection::album::Album;
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{
|
app::{
|
||||||
machine::tests::{inner, music_hoard},
|
machine::tests::{inner, inner_with_mb, music_hoard},
|
||||||
Category, IAppInteract,
|
Category, IAppAccess, IAppInteract, IAppInteractMatches,
|
||||||
|
},
|
||||||
|
lib::external::musicbrainz::{
|
||||||
|
self,
|
||||||
|
api::{Match, Mbid, MockIMusicBrainz},
|
||||||
},
|
},
|
||||||
testmod::COLLECTION,
|
testmod::COLLECTION,
|
||||||
};
|
};
|
||||||
@ -168,6 +218,104 @@ mod tests {
|
|||||||
app.unwrap_search();
|
app.unwrap_search();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_musicbrainz() {
|
||||||
|
let mut mb_api = Box::new(MockIMusicBrainz::new());
|
||||||
|
|
||||||
|
let arid: Mbid = "11111111-1111-1111-1111-111111111111".try_into().unwrap();
|
||||||
|
let album_1 = COLLECTION[1].albums[0].clone();
|
||||||
|
let album_4 = COLLECTION[1].albums[3].clone();
|
||||||
|
|
||||||
|
let album_match_1_1 = Match::new(100, album_1.clone());
|
||||||
|
let album_match_1_2 = Match::new(50, album_4.clone());
|
||||||
|
let album_match_4_1 = Match::new(100, album_4.clone());
|
||||||
|
let album_match_4_2 = Match::new(30, album_1.clone());
|
||||||
|
let matches_1 = vec![album_match_1_1.clone(), album_match_1_2.clone()];
|
||||||
|
let matches_4 = vec![album_match_4_1.clone(), album_match_4_2.clone()];
|
||||||
|
|
||||||
|
let result_1: Result<Vec<Match<Album>>, musicbrainz::api::Error> = Ok(matches_1.clone());
|
||||||
|
let result_4: Result<Vec<Match<Album>>, musicbrainz::api::Error> = Ok(matches_4.clone());
|
||||||
|
|
||||||
|
// Other albums have an MBID and so they will be skipped.
|
||||||
|
let mut seq = Sequence::new();
|
||||||
|
|
||||||
|
mb_api
|
||||||
|
.expect_search_release_group()
|
||||||
|
.with(predicate::eq(arid.clone()), predicate::eq(album_1.clone()))
|
||||||
|
.times(1)
|
||||||
|
.in_sequence(&mut seq)
|
||||||
|
.return_once(|_, _| result_1);
|
||||||
|
mb_api
|
||||||
|
.expect_search_release_group()
|
||||||
|
.with(predicate::eq(arid.clone()), predicate::eq(album_4.clone()))
|
||||||
|
.times(1)
|
||||||
|
.in_sequence(&mut seq)
|
||||||
|
.return_once(|_, _| result_4);
|
||||||
|
|
||||||
|
let browse = AppMachine::browse(inner_with_mb(music_hoard(COLLECTION.to_owned()), mb_api));
|
||||||
|
|
||||||
|
// Use the second artist for this test.
|
||||||
|
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
|
||||||
|
let mut app = browse.fetch_musicbrainz();
|
||||||
|
|
||||||
|
let public = app.get();
|
||||||
|
assert!(matches!(public.state, AppState::Matches(_)));
|
||||||
|
|
||||||
|
let public_matches = public.state.unwrap_matches();
|
||||||
|
|
||||||
|
assert_eq!(public_matches.matching, Some(&album_1));
|
||||||
|
assert_eq!(public_matches.matches, Some(matches_1.as_slice()));
|
||||||
|
|
||||||
|
let mut app = app.unwrap_matches().select();
|
||||||
|
|
||||||
|
let public = app.get();
|
||||||
|
assert!(matches!(public.state, AppState::Matches(_)));
|
||||||
|
|
||||||
|
let public_matches = public.state.unwrap_matches();
|
||||||
|
|
||||||
|
assert_eq!(public_matches.matching, Some(&album_4));
|
||||||
|
assert_eq!(public_matches.matches, Some(matches_4.as_slice()));
|
||||||
|
|
||||||
|
let app = app.unwrap_matches().select();
|
||||||
|
app.unwrap_browse();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_musicbrainz_no_artist() {
|
||||||
|
let browse = AppMachine::browse(inner(music_hoard(vec![])));
|
||||||
|
let app = browse.fetch_musicbrainz();
|
||||||
|
app.unwrap_error();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_musicbrainz_no_mbid() {
|
||||||
|
let browse = AppMachine::browse(inner(music_hoard(COLLECTION.to_owned())));
|
||||||
|
|
||||||
|
// Use the fourth artist for this test as they have no MBID.
|
||||||
|
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
|
||||||
|
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
|
||||||
|
let browse = browse.increment_selection(Delta::Line).unwrap_browse();
|
||||||
|
let app = browse.fetch_musicbrainz();
|
||||||
|
app.unwrap_error();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_musicbrainz_api_error() {
|
||||||
|
let mut mb_api = Box::new(MockIMusicBrainz::new());
|
||||||
|
|
||||||
|
let error = Err(musicbrainz::api::Error::RateLimit);
|
||||||
|
|
||||||
|
mb_api
|
||||||
|
.expect_search_release_group()
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_, _| error);
|
||||||
|
|
||||||
|
let browse = AppMachine::browse(inner_with_mb(music_hoard(COLLECTION.to_owned()), mb_api));
|
||||||
|
|
||||||
|
let app = browse.fetch_musicbrainz();
|
||||||
|
app.unwrap_error();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_op() {
|
fn no_op() {
|
||||||
let browse = AppMachine::browse(inner(music_hoard(vec![])));
|
let browse = AppMachine::browse(inner(music_hoard(vec![])));
|
||||||
|
295
src/tui/app/machine/matches.rs
Normal file
295
src/tui/app/machine/matches.rs
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
use std::cmp;
|
||||||
|
|
||||||
|
use musichoard::{collection::album::Album, interface::musicbrainz::Match};
|
||||||
|
|
||||||
|
use crate::tui::{
|
||||||
|
app::{
|
||||||
|
machine::{App, AppInner, AppMachine},
|
||||||
|
AppPublic, AppPublicMatches, AppState, IAppInteractMatches, WidgetState,
|
||||||
|
},
|
||||||
|
lib::IMusicHoard,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct AppMatchesInfo {
|
||||||
|
pub matching: Album,
|
||||||
|
pub matches: Vec<Match<Album>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AppMatches {
|
||||||
|
matches_info_vec: Vec<AppMatchesInfo>,
|
||||||
|
index: Option<usize>,
|
||||||
|
state: WidgetState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<MH: IMusicHoard> AppMachine<MH, AppMatches> {
|
||||||
|
pub fn matches(inner: AppInner<MH>, matches_info_vec: Vec<AppMatchesInfo>) -> Self {
|
||||||
|
let mut index = None;
|
||||||
|
let mut state = WidgetState::default();
|
||||||
|
if let Some(matches_info) = matches_info_vec.first() {
|
||||||
|
index = Some(0);
|
||||||
|
if !matches_info.matches.is_empty() {
|
||||||
|
state.list.select(Some(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppMachine {
|
||||||
|
inner,
|
||||||
|
state: AppMatches {
|
||||||
|
matches_info_vec,
|
||||||
|
index,
|
||||||
|
state,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<MH: IMusicHoard> From<AppMachine<MH, AppMatches>> for App<MH> {
|
||||||
|
fn from(machine: AppMachine<MH, AppMatches>) -> Self {
|
||||||
|
AppState::Matches(machine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppMatches>> for AppPublic<'a> {
|
||||||
|
fn from(machine: &'a mut AppMachine<MH, AppMatches>) -> Self {
|
||||||
|
let (matching, matches) = match machine.state.index {
|
||||||
|
Some(index) => (
|
||||||
|
Some(&machine.state.matches_info_vec[index].matching),
|
||||||
|
Some(machine.state.matches_info_vec[index].matches.as_slice()),
|
||||||
|
),
|
||||||
|
None => (None, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
AppPublic {
|
||||||
|
inner: (&mut machine.inner).into(),
|
||||||
|
state: AppState::Matches(AppPublicMatches {
|
||||||
|
matching,
|
||||||
|
matches,
|
||||||
|
state: &mut machine.state.state,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<MH: IMusicHoard> IAppInteractMatches for AppMachine<MH, AppMatches> {
|
||||||
|
type APP = App<MH>;
|
||||||
|
|
||||||
|
fn prev_match(mut self) -> Self::APP {
|
||||||
|
if let Some(list_index) = self.state.state.list.selected() {
|
||||||
|
let result = list_index.saturating_sub(1);
|
||||||
|
self.state.state.list.select(Some(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_match(mut self) -> Self::APP {
|
||||||
|
if let Some(list_index) = self.state.state.list.selected() {
|
||||||
|
let result = list_index.saturating_add(1);
|
||||||
|
let to = cmp::min(
|
||||||
|
result,
|
||||||
|
self.state.matches_info_vec[self.state.index.unwrap()]
|
||||||
|
.matches
|
||||||
|
.len()
|
||||||
|
.saturating_sub(1),
|
||||||
|
);
|
||||||
|
self.state.state.list.select(Some(to));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select(mut self) -> Self::APP {
|
||||||
|
self.state.index = self.state.index.map(|i| i.saturating_add(1));
|
||||||
|
self.state.state = WidgetState::default();
|
||||||
|
if let Some(index) = self.state.index {
|
||||||
|
if let Some(matches_info) = self.state.matches_info_vec.get(index) {
|
||||||
|
if !matches_info.matches.is_empty() {
|
||||||
|
self.state.state.list.select(Some(0));
|
||||||
|
}
|
||||||
|
return self.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppMachine::browse(self.inner).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn abort(self) -> Self::APP {
|
||||||
|
AppMachine::browse(self.inner).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn no_op(self) -> Self::APP {
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use musichoard::collection::album::{AlbumDate, AlbumId, AlbumPrimaryType, AlbumSecondaryType};
|
||||||
|
|
||||||
|
use crate::tui::app::{
|
||||||
|
machine::tests::{inner, music_hoard},
|
||||||
|
IAppAccess,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn matches_info_vec() -> Vec<AppMatchesInfo> {
|
||||||
|
let album_1 = Album::new(
|
||||||
|
AlbumId::new("Album 1"),
|
||||||
|
AlbumDate::new(Some(1990), Some(5), None),
|
||||||
|
Some(AlbumPrimaryType::Album),
|
||||||
|
vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation],
|
||||||
|
);
|
||||||
|
|
||||||
|
let album_1_1 = album_1.clone();
|
||||||
|
let album_match_1_1 = Match {
|
||||||
|
score: 100,
|
||||||
|
item: album_1_1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut album_1_2 = album_1.clone();
|
||||||
|
album_1_2.id.title.push_str(" extra title part");
|
||||||
|
album_1_2.secondary_types.pop();
|
||||||
|
let album_match_1_2 = Match {
|
||||||
|
score: 100,
|
||||||
|
item: album_1_2,
|
||||||
|
};
|
||||||
|
|
||||||
|
let matches_info_1 = AppMatchesInfo {
|
||||||
|
matching: album_1.clone(),
|
||||||
|
matches: vec![album_match_1_1.clone(), album_match_1_2.clone()],
|
||||||
|
};
|
||||||
|
|
||||||
|
let album_2 = Album::new(
|
||||||
|
AlbumId::new("Album 2"),
|
||||||
|
AlbumDate::new(Some(2001), None, None),
|
||||||
|
Some(AlbumPrimaryType::Album),
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
|
||||||
|
let album_2_1 = album_1.clone();
|
||||||
|
let album_match_2_1 = Match {
|
||||||
|
score: 100,
|
||||||
|
item: album_2_1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let matches_info_2 = AppMatchesInfo {
|
||||||
|
matching: album_2.clone(),
|
||||||
|
matches: vec![album_match_2_1.clone()],
|
||||||
|
};
|
||||||
|
|
||||||
|
vec![matches_info_1, matches_info_2]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_empty() {
|
||||||
|
let matches = AppMachine::matches(inner(music_hoard(vec![])), vec![]);
|
||||||
|
|
||||||
|
let widget_state = WidgetState::default();
|
||||||
|
|
||||||
|
assert_eq!(matches.state.matches_info_vec, vec![]);
|
||||||
|
assert_eq!(matches.state.index, None);
|
||||||
|
assert_eq!(matches.state.state, widget_state);
|
||||||
|
|
||||||
|
let mut app = matches.no_op();
|
||||||
|
let public = app.get();
|
||||||
|
let public_matches = public.state.unwrap_matches();
|
||||||
|
|
||||||
|
assert_eq!(public_matches.matching, None);
|
||||||
|
assert_eq!(public_matches.matches, None);
|
||||||
|
assert_eq!(public_matches.state, &widget_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_nonempty() {
|
||||||
|
let matches_info_vec = matches_info_vec();
|
||||||
|
let matches = AppMachine::matches(inner(music_hoard(vec![])), matches_info_vec.clone());
|
||||||
|
|
||||||
|
let mut widget_state = WidgetState::default();
|
||||||
|
widget_state.list.select(Some(0));
|
||||||
|
|
||||||
|
assert_eq!(matches.state.matches_info_vec, matches_info_vec);
|
||||||
|
assert_eq!(matches.state.index, Some(0));
|
||||||
|
assert_eq!(matches.state.state, widget_state);
|
||||||
|
|
||||||
|
let mut app = matches.no_op();
|
||||||
|
let public = app.get();
|
||||||
|
let public_matches = public.state.unwrap_matches();
|
||||||
|
|
||||||
|
assert_eq!(public_matches.matching, Some(&matches_info_vec[0].matching));
|
||||||
|
assert_eq!(
|
||||||
|
public_matches.matches,
|
||||||
|
Some(matches_info_vec[0].matches.as_slice())
|
||||||
|
);
|
||||||
|
assert_eq!(public_matches.state, &widget_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn matches_flow() {
|
||||||
|
let matches_info_vec = matches_info_vec();
|
||||||
|
let matches = AppMachine::matches(inner(music_hoard(vec![])), matches_info_vec.clone());
|
||||||
|
|
||||||
|
let mut widget_state = WidgetState::default();
|
||||||
|
widget_state.list.select(Some(0));
|
||||||
|
|
||||||
|
assert_eq!(matches.state.matches_info_vec, matches_info_vec);
|
||||||
|
assert_eq!(matches.state.index, Some(0));
|
||||||
|
assert_eq!(matches.state.state, widget_state);
|
||||||
|
|
||||||
|
let matches = matches.prev_match().unwrap_matches();
|
||||||
|
|
||||||
|
assert_eq!(matches.state.index, Some(0));
|
||||||
|
assert_eq!(matches.state.state.list.selected(), Some(0));
|
||||||
|
|
||||||
|
let matches = matches.next_match().unwrap_matches();
|
||||||
|
|
||||||
|
assert_eq!(matches.state.index, Some(0));
|
||||||
|
assert_eq!(matches.state.state.list.selected(), Some(1));
|
||||||
|
|
||||||
|
let matches = matches.next_match().unwrap_matches();
|
||||||
|
|
||||||
|
assert_eq!(matches.state.index, Some(0));
|
||||||
|
assert_eq!(matches.state.state.list.selected(), Some(1));
|
||||||
|
|
||||||
|
let matches = matches.select().unwrap_matches();
|
||||||
|
|
||||||
|
assert_eq!(matches.state.index, Some(1));
|
||||||
|
assert_eq!(matches.state.state.list.selected(), Some(0));
|
||||||
|
|
||||||
|
// And it's done
|
||||||
|
matches.select().unwrap_browse();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn matches_abort() {
|
||||||
|
let matches_info_vec = matches_info_vec();
|
||||||
|
let matches = AppMachine::matches(inner(music_hoard(vec![])), matches_info_vec.clone());
|
||||||
|
|
||||||
|
let mut widget_state = WidgetState::default();
|
||||||
|
widget_state.list.select(Some(0));
|
||||||
|
|
||||||
|
assert_eq!(matches.state.matches_info_vec, matches_info_vec);
|
||||||
|
assert_eq!(matches.state.index, Some(0));
|
||||||
|
assert_eq!(matches.state.state, widget_state);
|
||||||
|
|
||||||
|
matches.abort().unwrap_browse();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn matches_select_empty() {
|
||||||
|
let matches = AppMachine::matches(inner(music_hoard(vec![])), vec![]);
|
||||||
|
|
||||||
|
assert_eq!(matches.state.matches_info_vec, vec![]);
|
||||||
|
assert_eq!(matches.state.index, None);
|
||||||
|
|
||||||
|
matches.select().unwrap_browse();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_op() {
|
||||||
|
let matches = AppMachine::matches(inner(music_hoard(vec![])), vec![]);
|
||||||
|
let app = matches.no_op();
|
||||||
|
app.unwrap_matches();
|
||||||
|
}
|
||||||
|
}
|
@ -2,18 +2,20 @@ mod browse;
|
|||||||
mod critical;
|
mod critical;
|
||||||
mod error;
|
mod error;
|
||||||
mod info;
|
mod info;
|
||||||
|
mod matches;
|
||||||
mod reload;
|
mod reload;
|
||||||
mod search;
|
mod search;
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{selection::Selection, AppPublic, AppPublicInner, AppState, IAppAccess, IAppInteract},
|
app::{selection::Selection, AppPublic, AppPublicInner, AppState, IAppAccess, IAppInteract},
|
||||||
lib::IMusicHoard,
|
lib::{external::musicbrainz::api::IMusicBrainz, IMusicHoard},
|
||||||
};
|
};
|
||||||
|
|
||||||
use browse::AppBrowse;
|
use browse::AppBrowse;
|
||||||
use critical::AppCritical;
|
use critical::AppCritical;
|
||||||
use error::AppError;
|
use error::AppError;
|
||||||
use info::AppInfo;
|
use info::AppInfo;
|
||||||
|
use matches::AppMatches;
|
||||||
use reload::AppReload;
|
use reload::AppReload;
|
||||||
use search::AppSearch;
|
use search::AppSearch;
|
||||||
|
|
||||||
@ -22,6 +24,7 @@ pub type App<MH> = AppState<
|
|||||||
AppMachine<MH, AppInfo>,
|
AppMachine<MH, AppInfo>,
|
||||||
AppMachine<MH, AppReload>,
|
AppMachine<MH, AppReload>,
|
||||||
AppMachine<MH, AppSearch>,
|
AppMachine<MH, AppSearch>,
|
||||||
|
AppMachine<MH, AppMatches>,
|
||||||
AppMachine<MH, AppError>,
|
AppMachine<MH, AppError>,
|
||||||
AppMachine<MH, AppCritical>,
|
AppMachine<MH, AppCritical>,
|
||||||
>;
|
>;
|
||||||
@ -34,13 +37,14 @@ pub struct AppMachine<MH: IMusicHoard, STATE> {
|
|||||||
pub struct AppInner<MH: IMusicHoard> {
|
pub struct AppInner<MH: IMusicHoard> {
|
||||||
running: bool,
|
running: bool,
|
||||||
music_hoard: MH,
|
music_hoard: MH,
|
||||||
|
mb_api: Box<dyn IMusicBrainz>,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<MH: IMusicHoard> App<MH> {
|
impl<MH: IMusicHoard> App<MH> {
|
||||||
pub fn new(mut music_hoard: MH) -> Self {
|
pub fn new(mut music_hoard: MH, mb_api: Box<dyn IMusicBrainz>) -> Self {
|
||||||
let init_result = Self::init(&mut music_hoard);
|
let init_result = Self::init(&mut music_hoard);
|
||||||
let inner = AppInner::new(music_hoard);
|
let inner = AppInner::new(music_hoard, mb_api);
|
||||||
match init_result {
|
match init_result {
|
||||||
Ok(()) => AppMachine::browse(inner).into(),
|
Ok(()) => AppMachine::browse(inner).into(),
|
||||||
Err(err) => AppMachine::critical(inner, err.to_string()).into(),
|
Err(err) => AppMachine::critical(inner, err.to_string()).into(),
|
||||||
@ -56,6 +60,7 @@ impl<MH: IMusicHoard> App<MH> {
|
|||||||
match self {
|
match self {
|
||||||
AppState::Browse(browse) => &browse.inner,
|
AppState::Browse(browse) => &browse.inner,
|
||||||
AppState::Info(info) => &info.inner,
|
AppState::Info(info) => &info.inner,
|
||||||
|
AppState::Matches(matches) => &matches.inner,
|
||||||
AppState::Reload(reload) => &reload.inner,
|
AppState::Reload(reload) => &reload.inner,
|
||||||
AppState::Search(search) => &search.inner,
|
AppState::Search(search) => &search.inner,
|
||||||
AppState::Error(error) => &error.inner,
|
AppState::Error(error) => &error.inner,
|
||||||
@ -67,6 +72,7 @@ impl<MH: IMusicHoard> App<MH> {
|
|||||||
match self {
|
match self {
|
||||||
AppState::Browse(browse) => &mut browse.inner,
|
AppState::Browse(browse) => &mut browse.inner,
|
||||||
AppState::Info(info) => &mut info.inner,
|
AppState::Info(info) => &mut info.inner,
|
||||||
|
AppState::Matches(matches) => &mut matches.inner,
|
||||||
AppState::Reload(reload) => &mut reload.inner,
|
AppState::Reload(reload) => &mut reload.inner,
|
||||||
AppState::Search(search) => &mut search.inner,
|
AppState::Search(search) => &mut search.inner,
|
||||||
AppState::Error(error) => &mut error.inner,
|
AppState::Error(error) => &mut error.inner,
|
||||||
@ -80,6 +86,7 @@ impl<MH: IMusicHoard> IAppInteract for App<MH> {
|
|||||||
type IS = AppMachine<MH, AppInfo>;
|
type IS = AppMachine<MH, AppInfo>;
|
||||||
type RS = AppMachine<MH, AppReload>;
|
type RS = AppMachine<MH, AppReload>;
|
||||||
type SS = AppMachine<MH, AppSearch>;
|
type SS = AppMachine<MH, AppSearch>;
|
||||||
|
type MS = AppMachine<MH, AppMatches>;
|
||||||
type ES = AppMachine<MH, AppError>;
|
type ES = AppMachine<MH, AppError>;
|
||||||
type CS = AppMachine<MH, AppCritical>;
|
type CS = AppMachine<MH, AppCritical>;
|
||||||
|
|
||||||
@ -92,7 +99,9 @@ impl<MH: IMusicHoard> IAppInteract for App<MH> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn state(self) -> AppState<Self::BS, Self::IS, Self::RS, Self::SS, Self::ES, Self::CS> {
|
fn state(
|
||||||
|
self,
|
||||||
|
) -> AppState<Self::BS, Self::IS, Self::RS, Self::SS, Self::MS, Self::ES, Self::CS> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,6 +111,7 @@ impl<MH: IMusicHoard> IAppAccess for App<MH> {
|
|||||||
match self {
|
match self {
|
||||||
AppState::Browse(browse) => browse.into(),
|
AppState::Browse(browse) => browse.into(),
|
||||||
AppState::Info(info) => info.into(),
|
AppState::Info(info) => info.into(),
|
||||||
|
AppState::Matches(matches) => matches.into(),
|
||||||
AppState::Reload(reload) => reload.into(),
|
AppState::Reload(reload) => reload.into(),
|
||||||
AppState::Search(search) => search.into(),
|
AppState::Search(search) => search.into(),
|
||||||
AppState::Error(error) => error.into(),
|
AppState::Error(error) => error.into(),
|
||||||
@ -111,11 +121,12 @@ impl<MH: IMusicHoard> IAppAccess for App<MH> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<MH: IMusicHoard> AppInner<MH> {
|
impl<MH: IMusicHoard> AppInner<MH> {
|
||||||
pub fn new(music_hoard: MH) -> Self {
|
pub fn new(music_hoard: MH, mb_api: Box<dyn IMusicBrainz>) -> Self {
|
||||||
let selection = Selection::new(music_hoard.get_collection());
|
let selection = Selection::new(music_hoard.get_collection());
|
||||||
AppInner {
|
AppInner {
|
||||||
running: true,
|
running: true,
|
||||||
music_hoard,
|
music_hoard,
|
||||||
|
mb_api,
|
||||||
selection,
|
selection,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -136,12 +147,12 @@ mod tests {
|
|||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{AppState, IAppInteract, IAppInteractBrowse},
|
app::{AppState, IAppInteract, IAppInteractBrowse},
|
||||||
lib::MockIMusicHoard,
|
lib::{external::musicbrainz::api::MockIMusicBrainz, MockIMusicHoard},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
impl<BS, IS, RS, SS, ES, CS> AppState<BS, IS, RS, SS, ES, CS> {
|
impl<BS, IS, RS, SS, MS, ES, CS> AppState<BS, IS, RS, SS, MS, ES, CS> {
|
||||||
pub fn unwrap_browse(self) -> BS {
|
pub fn unwrap_browse(self) -> BS {
|
||||||
match self {
|
match self {
|
||||||
AppState::Browse(browse) => browse,
|
AppState::Browse(browse) => browse,
|
||||||
@ -170,6 +181,13 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn unwrap_matches(self) -> MS {
|
||||||
|
match self {
|
||||||
|
AppState::Matches(matches) => matches,
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn unwrap_error(self) -> ES {
|
pub fn unwrap_error(self) -> ES {
|
||||||
match self {
|
match self {
|
||||||
AppState::Error(error) => error,
|
AppState::Error(error) => error,
|
||||||
@ -203,21 +221,32 @@ mod tests {
|
|||||||
music_hoard
|
music_hoard
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mb_api() -> Box<MockIMusicBrainz> {
|
||||||
|
Box::new(MockIMusicBrainz::new())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn inner(music_hoard: MockIMusicHoard) -> AppInner<MockIMusicHoard> {
|
pub fn inner(music_hoard: MockIMusicHoard) -> AppInner<MockIMusicHoard> {
|
||||||
AppInner::new(music_hoard)
|
AppInner::new(music_hoard, mb_api())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inner_with_mb(
|
||||||
|
music_hoard: MockIMusicHoard,
|
||||||
|
mb_api: Box<MockIMusicBrainz>,
|
||||||
|
) -> AppInner<MockIMusicHoard> {
|
||||||
|
AppInner::new(music_hoard, mb_api)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn state_browse() {
|
fn state_browse() {
|
||||||
let mut app = App::new(music_hoard_init(vec![]));
|
let mut app = App::new(music_hoard_init(vec![]), mb_api());
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
let state = app.state();
|
let state = app.state();
|
||||||
matches!(state, AppState::Browse(_));
|
assert!(matches!(state, AppState::Browse(_)));
|
||||||
app = state;
|
app = state;
|
||||||
|
|
||||||
let public = app.get();
|
let public = app.get();
|
||||||
matches!(public.state, AppState::Browse(_));
|
assert!(matches!(public.state, AppState::Browse(_)));
|
||||||
|
|
||||||
let app = app.force_quit();
|
let app = app.force_quit();
|
||||||
assert!(!app.is_running());
|
assert!(!app.is_running());
|
||||||
@ -225,17 +254,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn state_info() {
|
fn state_info() {
|
||||||
let mut app = App::new(music_hoard_init(vec![]));
|
let mut app = App::new(music_hoard_init(vec![]), mb_api());
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
app = app.unwrap_browse().show_info_overlay();
|
app = app.unwrap_browse().show_info_overlay();
|
||||||
|
|
||||||
let state = app.state();
|
let state = app.state();
|
||||||
matches!(state, AppState::Info(_));
|
assert!(matches!(state, AppState::Info(_)));
|
||||||
app = state;
|
app = state;
|
||||||
|
|
||||||
let public = app.get();
|
let public = app.get();
|
||||||
matches!(public.state, AppState::Info(_));
|
assert!(matches!(public.state, AppState::Info(_)));
|
||||||
|
|
||||||
let app = app.force_quit();
|
let app = app.force_quit();
|
||||||
assert!(!app.is_running());
|
assert!(!app.is_running());
|
||||||
@ -243,17 +272,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn state_reload() {
|
fn state_reload() {
|
||||||
let mut app = App::new(music_hoard_init(vec![]));
|
let mut app = App::new(music_hoard_init(vec![]), mb_api());
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
app = app.unwrap_browse().show_reload_menu();
|
app = app.unwrap_browse().show_reload_menu();
|
||||||
|
|
||||||
let state = app.state();
|
let state = app.state();
|
||||||
matches!(state, AppState::Reload(_));
|
assert!(matches!(state, AppState::Reload(_)));
|
||||||
app = state;
|
app = state;
|
||||||
|
|
||||||
let public = app.get();
|
let public = app.get();
|
||||||
matches!(public.state, AppState::Reload(_));
|
assert!(matches!(public.state, AppState::Reload(_)));
|
||||||
|
|
||||||
let app = app.force_quit();
|
let app = app.force_quit();
|
||||||
assert!(!app.is_running());
|
assert!(!app.is_running());
|
||||||
@ -261,17 +290,35 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn state_search() {
|
fn state_search() {
|
||||||
let mut app = App::new(music_hoard_init(vec![]));
|
let mut app = App::new(music_hoard_init(vec![]), mb_api());
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
app = app.unwrap_browse().begin_search();
|
app = app.unwrap_browse().begin_search();
|
||||||
|
|
||||||
let state = app.state();
|
let state = app.state();
|
||||||
matches!(state, AppState::Search(_));
|
assert!(matches!(state, AppState::Search(_)));
|
||||||
app = state;
|
app = state;
|
||||||
|
|
||||||
let public = app.get();
|
let public = app.get();
|
||||||
matches!(public.state, AppState::Search(""));
|
assert!(matches!(public.state, AppState::Search("")));
|
||||||
|
|
||||||
|
let app = app.force_quit();
|
||||||
|
assert!(!app.is_running());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn state_matches() {
|
||||||
|
let mut app = App::new(music_hoard_init(vec![]), mb_api());
|
||||||
|
assert!(app.is_running());
|
||||||
|
|
||||||
|
app = AppMachine::matches(app.unwrap_browse().inner, vec![]).into();
|
||||||
|
|
||||||
|
let state = app.state();
|
||||||
|
assert!(matches!(state, AppState::Matches(_)));
|
||||||
|
app = state;
|
||||||
|
|
||||||
|
let public = app.get();
|
||||||
|
assert!(matches!(public.state, AppState::Matches(_)));
|
||||||
|
|
||||||
let app = app.force_quit();
|
let app = app.force_quit();
|
||||||
assert!(!app.is_running());
|
assert!(!app.is_running());
|
||||||
@ -279,17 +326,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn state_error() {
|
fn state_error() {
|
||||||
let mut app = App::new(music_hoard_init(vec![]));
|
let mut app = App::new(music_hoard_init(vec![]), mb_api());
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
app = AppMachine::error(app.unwrap_browse().inner, "get rekt").into();
|
app = AppMachine::error(app.unwrap_browse().inner, "get rekt").into();
|
||||||
|
|
||||||
let state = app.state();
|
let state = app.state();
|
||||||
matches!(state, AppState::Error(_));
|
assert!(matches!(state, AppState::Error(_)));
|
||||||
app = state;
|
app = state;
|
||||||
|
|
||||||
let public = app.get();
|
let public = app.get();
|
||||||
matches!(public.state, AppState::Error("get rekt"));
|
assert!(matches!(public.state, AppState::Error("get rekt")));
|
||||||
|
|
||||||
app = app.force_quit();
|
app = app.force_quit();
|
||||||
assert!(!app.is_running());
|
assert!(!app.is_running());
|
||||||
@ -297,17 +344,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn state_critical() {
|
fn state_critical() {
|
||||||
let mut app = App::new(music_hoard_init(vec![]));
|
let mut app = App::new(music_hoard_init(vec![]), mb_api());
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
|
|
||||||
app = AppMachine::critical(app.unwrap_browse().inner, "get rekt").into();
|
app = AppMachine::critical(app.unwrap_browse().inner, "get rekt").into();
|
||||||
|
|
||||||
let state = app.state();
|
let state = app.state();
|
||||||
matches!(state, AppState::Critical(_));
|
assert!(matches!(state, AppState::Critical(_)));
|
||||||
app = state;
|
app = state;
|
||||||
|
|
||||||
let public = app.get();
|
let public = app.get();
|
||||||
matches!(public.state, AppState::Critical("get rekt"));
|
assert!(matches!(public.state, AppState::Critical("get rekt")));
|
||||||
|
|
||||||
app = app.force_quit();
|
app = app.force_quit();
|
||||||
assert!(!app.is_running());
|
assert!(!app.is_running());
|
||||||
@ -323,7 +370,7 @@ mod tests {
|
|||||||
.return_once(|| Err(musichoard::Error::LibraryError(String::from("get rekt"))));
|
.return_once(|| Err(musichoard::Error::LibraryError(String::from("get rekt"))));
|
||||||
music_hoard.expect_get_collection().return_const(vec![]);
|
music_hoard.expect_get_collection().return_const(vec![]);
|
||||||
|
|
||||||
let app = App::new(music_hoard);
|
let app = App::new(music_hoard, mb_api());
|
||||||
assert!(app.is_running());
|
assert!(app.is_running());
|
||||||
app.unwrap_critical();
|
app.unwrap_critical();
|
||||||
}
|
}
|
||||||
|
@ -4,13 +4,17 @@ mod selection;
|
|||||||
pub use machine::App;
|
pub use machine::App;
|
||||||
pub use selection::{Category, Delta, Selection, WidgetState};
|
pub use selection::{Category, Delta, Selection, WidgetState};
|
||||||
|
|
||||||
use musichoard::collection::Collection;
|
use musichoard::{
|
||||||
|
collection::{album::Album, Collection},
|
||||||
|
interface::musicbrainz::Match,
|
||||||
|
};
|
||||||
|
|
||||||
pub enum AppState<BS, IS, RS, SS, ES, CS> {
|
pub enum AppState<BS, IS, RS, SS, MS, ES, CS> {
|
||||||
Browse(BS),
|
Browse(BS),
|
||||||
Info(IS),
|
Info(IS),
|
||||||
Reload(RS),
|
Reload(RS),
|
||||||
Search(SS),
|
Search(SS),
|
||||||
|
Matches(MS),
|
||||||
Error(ES),
|
Error(ES),
|
||||||
Critical(CS),
|
Critical(CS),
|
||||||
}
|
}
|
||||||
@ -20,6 +24,7 @@ pub trait IAppInteract {
|
|||||||
type IS: IAppInteractInfo<APP = Self>;
|
type IS: IAppInteractInfo<APP = Self>;
|
||||||
type RS: IAppInteractReload<APP = Self>;
|
type RS: IAppInteractReload<APP = Self>;
|
||||||
type SS: IAppInteractSearch<APP = Self>;
|
type SS: IAppInteractSearch<APP = Self>;
|
||||||
|
type MS: IAppInteractMatches<APP = Self>;
|
||||||
type ES: IAppInteractError<APP = Self>;
|
type ES: IAppInteractError<APP = Self>;
|
||||||
type CS: IAppInteractCritical<APP = Self>;
|
type CS: IAppInteractCritical<APP = Self>;
|
||||||
|
|
||||||
@ -27,7 +32,9 @@ pub trait IAppInteract {
|
|||||||
fn force_quit(self) -> Self;
|
fn force_quit(self) -> Self;
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
fn state(self) -> AppState<Self::BS, Self::IS, Self::RS, Self::SS, Self::ES, Self::CS>;
|
fn state(
|
||||||
|
self,
|
||||||
|
) -> AppState<Self::BS, Self::IS, Self::RS, Self::SS, Self::MS, Self::ES, Self::CS>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait IAppInteractBrowse {
|
pub trait IAppInteractBrowse {
|
||||||
@ -46,6 +53,8 @@ pub trait IAppInteractBrowse {
|
|||||||
|
|
||||||
fn begin_search(self) -> Self::APP;
|
fn begin_search(self) -> Self::APP;
|
||||||
|
|
||||||
|
fn fetch_musicbrainz(self) -> Self::APP;
|
||||||
|
|
||||||
fn no_op(self) -> Self::APP;
|
fn no_op(self) -> Self::APP;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,6 +88,18 @@ pub trait IAppInteractSearch {
|
|||||||
fn no_op(self) -> Self::APP;
|
fn no_op(self) -> Self::APP;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait IAppInteractMatches {
|
||||||
|
type APP: IAppInteract;
|
||||||
|
|
||||||
|
fn prev_match(self) -> Self::APP;
|
||||||
|
fn next_match(self) -> Self::APP;
|
||||||
|
fn select(self) -> Self::APP;
|
||||||
|
|
||||||
|
fn abort(self) -> Self::APP;
|
||||||
|
|
||||||
|
fn no_op(self) -> Self::APP;
|
||||||
|
}
|
||||||
|
|
||||||
pub trait IAppInteractError {
|
pub trait IAppInteractError {
|
||||||
type APP: IAppInteract;
|
type APP: IAppInteract;
|
||||||
|
|
||||||
@ -109,9 +130,16 @@ pub struct AppPublicInner<'app> {
|
|||||||
pub selection: &'app mut Selection,
|
pub selection: &'app mut Selection,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type AppPublicState<'app> = AppState<(), (), (), &'app str, &'app str, &'app str>;
|
pub struct AppPublicMatches<'app> {
|
||||||
|
pub matching: Option<&'app Album>,
|
||||||
|
pub matches: Option<&'app [Match<Album>]>,
|
||||||
|
pub state: &'app mut WidgetState,
|
||||||
|
}
|
||||||
|
|
||||||
impl<BS, IS, RS, SS, ES, CS> AppState<BS, IS, RS, SS, ES, CS> {
|
pub type AppPublicState<'app> =
|
||||||
|
AppState<(), (), (), &'app str, AppPublicMatches<'app>, &'app str, &'app str>;
|
||||||
|
|
||||||
|
impl<BS, IS, RS, SS, MS, ES, CS> AppState<BS, IS, RS, SS, MS, ES, CS> {
|
||||||
pub fn is_search(&self) -> bool {
|
pub fn is_search(&self) -> bool {
|
||||||
matches!(self, AppState::Search(_))
|
matches!(self, AppState::Search(_))
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ use mockall::automock;
|
|||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{
|
app::{
|
||||||
AppState, Delta, IAppInteract, IAppInteractBrowse, IAppInteractCritical, IAppInteractError,
|
AppState, Delta, IAppInteract, IAppInteractBrowse, IAppInteractCritical, IAppInteractError,
|
||||||
IAppInteractInfo, IAppInteractReload, IAppInteractSearch,
|
IAppInteractInfo, IAppInteractMatches, IAppInteractReload, IAppInteractSearch,
|
||||||
},
|
},
|
||||||
event::{Event, EventError, EventReceiver},
|
event::{Event, EventError, EventReceiver},
|
||||||
};
|
};
|
||||||
@ -22,6 +22,7 @@ trait IEventHandlerPrivate<APP: IAppInteract> {
|
|||||||
fn handle_info_key_event(app: <APP as IAppInteract>::IS, key_event: KeyEvent) -> APP;
|
fn handle_info_key_event(app: <APP as IAppInteract>::IS, key_event: KeyEvent) -> APP;
|
||||||
fn handle_reload_key_event(app: <APP as IAppInteract>::RS, key_event: KeyEvent) -> APP;
|
fn handle_reload_key_event(app: <APP as IAppInteract>::RS, key_event: KeyEvent) -> APP;
|
||||||
fn handle_search_key_event(app: <APP as IAppInteract>::SS, key_event: KeyEvent) -> APP;
|
fn handle_search_key_event(app: <APP as IAppInteract>::SS, key_event: KeyEvent) -> APP;
|
||||||
|
fn handle_matches_key_event(app: <APP as IAppInteract>::MS, key_event: KeyEvent) -> APP;
|
||||||
fn handle_error_key_event(app: <APP as IAppInteract>::ES, key_event: KeyEvent) -> APP;
|
fn handle_error_key_event(app: <APP as IAppInteract>::ES, key_event: KeyEvent) -> APP;
|
||||||
fn handle_critical_key_event(app: <APP as IAppInteract>::CS, key_event: KeyEvent) -> APP;
|
fn handle_critical_key_event(app: <APP as IAppInteract>::CS, key_event: KeyEvent) -> APP;
|
||||||
}
|
}
|
||||||
@ -69,6 +70,9 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
|
|||||||
AppState::Search(search) => {
|
AppState::Search(search) => {
|
||||||
<Self as IEventHandlerPrivate<APP>>::handle_search_key_event(search, key_event)
|
<Self as IEventHandlerPrivate<APP>>::handle_search_key_event(search, key_event)
|
||||||
}
|
}
|
||||||
|
AppState::Matches(matches) => {
|
||||||
|
<Self as IEventHandlerPrivate<APP>>::handle_matches_key_event(matches, key_event)
|
||||||
|
}
|
||||||
AppState::Error(error) => {
|
AppState::Error(error) => {
|
||||||
<Self as IEventHandlerPrivate<APP>>::handle_error_key_event(error, key_event)
|
<Self as IEventHandlerPrivate<APP>>::handle_error_key_event(error, key_event)
|
||||||
}
|
}
|
||||||
@ -102,6 +106,7 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
|
|||||||
app.no_op()
|
app.no_op()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
KeyCode::Char('f') | KeyCode::Char('F') => app.fetch_musicbrainz(),
|
||||||
// Othey keys.
|
// Othey keys.
|
||||||
_ => app.no_op(),
|
_ => app.no_op(),
|
||||||
}
|
}
|
||||||
@ -156,6 +161,19 @@ impl<APP: IAppInteract> IEventHandlerPrivate<APP> for EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_matches_key_event(app: <APP as IAppInteract>::MS, key_event: KeyEvent) -> APP {
|
||||||
|
match key_event.code {
|
||||||
|
// Abort.
|
||||||
|
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => app.abort(),
|
||||||
|
// Select.
|
||||||
|
KeyCode::Up => app.prev_match(),
|
||||||
|
KeyCode::Down => app.next_match(),
|
||||||
|
KeyCode::Enter => app.select(),
|
||||||
|
// Othey keys.
|
||||||
|
_ => app.no_op(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_error_key_event(app: <APP as IAppInteract>::ES, _key_event: KeyEvent) -> APP {
|
fn handle_error_key_event(app: <APP as IAppInteract>::ES, _key_event: KeyEvent) -> APP {
|
||||||
// Any key dismisses the error.
|
// Any key dismisses the error.
|
||||||
app.dismiss_error()
|
app.dismiss_error()
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use musichoard::{
|
use musichoard::{
|
||||||
collection::Collection, interface::database::IDatabase, interface::library::ILibrary,
|
collection::Collection, interface, IMusicHoardBase, IMusicHoardDatabase, IMusicHoardLibrary,
|
||||||
IMusicHoardBase, IMusicHoardDatabase, IMusicHoardLibrary, MusicHoard,
|
MusicHoard,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -14,7 +14,9 @@ pub trait IMusicHoard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GRCOV_EXCL_START
|
// GRCOV_EXCL_START
|
||||||
impl<Database: IDatabase, Library: ILibrary> IMusicHoard for MusicHoard<Database, Library> {
|
impl<Database: interface::database::IDatabase, Library: interface::library::ILibrary> IMusicHoard
|
||||||
|
for MusicHoard<Database, Library>
|
||||||
|
{
|
||||||
fn rescan_library(&mut self) -> Result<(), musichoard::Error> {
|
fn rescan_library(&mut self) -> Result<(), musichoard::Error> {
|
||||||
<Self as IMusicHoardLibrary>::rescan_library(self)
|
<Self as IMusicHoardLibrary>::rescan_library(self)
|
||||||
}
|
}
|
||||||
@ -28,3 +30,45 @@ impl<Database: IDatabase, Library: ILibrary> IMusicHoard for MusicHoard<Database
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// GRCOV_EXCL_STOP
|
// GRCOV_EXCL_STOP
|
||||||
|
|
||||||
|
pub mod external {
|
||||||
|
pub mod musicbrainz {
|
||||||
|
pub mod api {
|
||||||
|
use musichoard::{
|
||||||
|
collection::album::Album,
|
||||||
|
external::musicbrainz::api::{IMusicBrainzApiClient, MusicBrainzApi},
|
||||||
|
interface,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use mockall::automock;
|
||||||
|
|
||||||
|
pub type Match<T> = interface::musicbrainz::Match<T>;
|
||||||
|
pub type Mbid = interface::musicbrainz::Mbid;
|
||||||
|
pub type Error = interface::musicbrainz::Error;
|
||||||
|
|
||||||
|
#[cfg_attr(test, automock)]
|
||||||
|
pub trait IMusicBrainz {
|
||||||
|
fn search_release_group(
|
||||||
|
&mut self,
|
||||||
|
arid: &Mbid,
|
||||||
|
album: &Album,
|
||||||
|
) -> Result<Vec<Match<Album>>, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GRCOV_EXCL_START
|
||||||
|
impl<Mbc: IMusicBrainzApiClient> IMusicBrainz for MusicBrainzApi<Mbc> {
|
||||||
|
fn search_release_group(
|
||||||
|
&mut self,
|
||||||
|
arid: &Mbid,
|
||||||
|
album: &Album,
|
||||||
|
) -> Result<Vec<Match<Album>>, Error> {
|
||||||
|
<Self as interface::musicbrainz::IMusicBrainz>::search_release_group(
|
||||||
|
self, arid, album,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// GRCOV_EXCL_STOP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -173,6 +173,7 @@ mod testmod;
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::{io, thread};
|
use std::{io, thread};
|
||||||
|
|
||||||
|
use lib::external::musicbrainz::api::MockIMusicBrainz;
|
||||||
use ratatui::{backend::TestBackend, Terminal};
|
use ratatui::{backend::TestBackend, Terminal};
|
||||||
|
|
||||||
use musichoard::collection::Collection;
|
use musichoard::collection::Collection;
|
||||||
@ -201,7 +202,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn app(collection: Collection) -> App<MockIMusicHoard> {
|
fn app(collection: Collection) -> App<MockIMusicHoard> {
|
||||||
App::new(music_hoard(collection))
|
App::new(music_hoard(collection), Box::new(MockIMusicBrainz::new()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn listener() -> MockIEventListener {
|
fn listener() -> MockIEventListener {
|
||||||
|
125
src/tui/ui.rs
125
src/tui/ui.rs
@ -1,11 +1,14 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use musichoard::collection::{
|
use musichoard::{
|
||||||
album::{Album, AlbumDate, AlbumPrimaryType, AlbumSecondaryType, AlbumSeq, AlbumStatus},
|
collection::{
|
||||||
artist::Artist,
|
album::{Album, AlbumDate, AlbumPrimaryType, AlbumSecondaryType, AlbumSeq, AlbumStatus},
|
||||||
musicbrainz::IMusicBrainzRef,
|
artist::Artist,
|
||||||
track::{Track, TrackFormat, TrackQuality},
|
musicbrainz::IMusicBrainzRef,
|
||||||
Collection,
|
track::{Track, TrackFormat, TrackQuality},
|
||||||
|
Collection,
|
||||||
|
},
|
||||||
|
interface::musicbrainz::Match,
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Rect},
|
layout::{Alignment, Rect},
|
||||||
@ -501,6 +504,7 @@ impl Minibuffer<'_> {
|
|||||||
Paragraph::new("m: show info overlay"),
|
Paragraph::new("m: show info overlay"),
|
||||||
Paragraph::new("g: show reload menu"),
|
Paragraph::new("g: show reload menu"),
|
||||||
Paragraph::new("ctrl+s: search artist"),
|
Paragraph::new("ctrl+s: search artist"),
|
||||||
|
Paragraph::new("f: fetch musicbrainz"),
|
||||||
],
|
],
|
||||||
columns,
|
columns,
|
||||||
},
|
},
|
||||||
@ -525,6 +529,13 @@ impl Minibuffer<'_> {
|
|||||||
],
|
],
|
||||||
columns,
|
columns,
|
||||||
},
|
},
|
||||||
|
AppState::Matches(public) => Minibuffer {
|
||||||
|
paragraphs: vec![
|
||||||
|
Paragraph::new(Minibuffer::display_matching_info(public.matching)),
|
||||||
|
Paragraph::new("q: abort"),
|
||||||
|
],
|
||||||
|
columns: 2,
|
||||||
|
},
|
||||||
AppState::Error(_) => Minibuffer {
|
AppState::Error(_) => Minibuffer {
|
||||||
paragraphs: vec![Paragraph::new(
|
paragraphs: vec![Paragraph::new(
|
||||||
"Press any key to dismiss the error message...",
|
"Press any key to dismiss the error message...",
|
||||||
@ -547,6 +558,17 @@ impl Minibuffer<'_> {
|
|||||||
|
|
||||||
mb
|
mb
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn display_matching_info(matching: Option<&Album>) -> String {
|
||||||
|
match matching {
|
||||||
|
Some(matching) => format!(
|
||||||
|
"Matching: {} | {}",
|
||||||
|
AlbumState::display_album_date(&matching.date),
|
||||||
|
&matching.id.title
|
||||||
|
),
|
||||||
|
None => String::from("Matching: nothing"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ReloadMenu;
|
struct ReloadMenu;
|
||||||
@ -621,6 +643,18 @@ impl Ui {
|
|||||||
state.height = area.height.saturating_sub(2) as usize;
|
state.height = area.height.saturating_sub(2) as usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_overlay_list_widget(
|
||||||
|
title: &str,
|
||||||
|
list: List,
|
||||||
|
state: &mut WidgetState,
|
||||||
|
active: bool,
|
||||||
|
area: Rect,
|
||||||
|
frame: &mut Frame,
|
||||||
|
) {
|
||||||
|
frame.render_widget(Clear, area);
|
||||||
|
Self::render_list_widget(title, list, state, active, area, frame);
|
||||||
|
}
|
||||||
|
|
||||||
fn render_info_widget(
|
fn render_info_widget(
|
||||||
title: &str,
|
title: &str,
|
||||||
paragraph: Paragraph,
|
paragraph: Paragraph,
|
||||||
@ -792,6 +826,41 @@ impl Ui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn display_match_string(match_album: &Match<Album>) -> String {
|
||||||
|
format!(
|
||||||
|
"{:010} | {} [{}] ({}%)",
|
||||||
|
AlbumState::display_album_date(&match_album.item.date),
|
||||||
|
&match_album.item.id.title,
|
||||||
|
AlbumState::display_type(
|
||||||
|
&match_album.item.primary_type,
|
||||||
|
&match_album.item.secondary_types
|
||||||
|
),
|
||||||
|
match_album.score,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_match_list(matches: &[Match<Album>]) -> List {
|
||||||
|
List::new(
|
||||||
|
matches
|
||||||
|
.iter()
|
||||||
|
.map(Ui::display_match_string)
|
||||||
|
.map(ListItem::new)
|
||||||
|
.collect::<Vec<ListItem>>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_matches_overlay(
|
||||||
|
matching: Option<&Album>,
|
||||||
|
matches: Option<&[Match<Album>]>,
|
||||||
|
state: &mut WidgetState,
|
||||||
|
frame: &mut Frame,
|
||||||
|
) {
|
||||||
|
let area = OverlayBuilder::default().build(frame.size());
|
||||||
|
let matching_string = Minibuffer::display_matching_info(matching);
|
||||||
|
let list = matches.map(|m| Ui::build_match_list(m)).unwrap_or_default();
|
||||||
|
Self::render_overlay_list_widget(&matching_string, list, state, true, area, frame)
|
||||||
|
}
|
||||||
|
|
||||||
fn render_reload_overlay(frame: &mut Frame) {
|
fn render_reload_overlay(frame: &mut Frame) {
|
||||||
let area = OverlayBuilder::default()
|
let area = OverlayBuilder::default()
|
||||||
.with_width(OverlaySize::Value(39))
|
.with_width(OverlaySize::Value(39))
|
||||||
@ -827,6 +896,9 @@ impl IUi for Ui {
|
|||||||
Self::render_main_frame(collection, selection, &state, frame);
|
Self::render_main_frame(collection, selection, &state, frame);
|
||||||
match state {
|
match state {
|
||||||
AppState::Info(_) => Self::render_info_overlay(collection, selection, frame),
|
AppState::Info(_) => Self::render_info_overlay(collection, selection, frame),
|
||||||
|
AppState::Matches(public) => {
|
||||||
|
Self::render_matches_overlay(public.matching, public.matches, public.state, frame)
|
||||||
|
}
|
||||||
AppState::Reload(_) => Self::render_reload_overlay(frame),
|
AppState::Reload(_) => Self::render_reload_overlay(frame),
|
||||||
AppState::Error(msg) => Self::render_error_overlay("Error", msg, frame),
|
AppState::Error(msg) => Self::render_error_overlay("Error", msg, frame),
|
||||||
AppState::Critical(msg) => Self::render_error_overlay("Critical Error", msg, frame),
|
AppState::Critical(msg) => Self::render_error_overlay("Critical Error", msg, frame),
|
||||||
@ -837,10 +909,10 @@ impl IUi for Ui {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use musichoard::collection::artist::ArtistId;
|
use musichoard::collection::{album::AlbumId, artist::ArtistId};
|
||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{AppPublic, AppPublicInner, Delta},
|
app::{AppPublic, AppPublicInner, AppPublicMatches, Delta},
|
||||||
testmod::COLLECTION,
|
testmod::COLLECTION,
|
||||||
tests::terminal,
|
tests::terminal,
|
||||||
};
|
};
|
||||||
@ -860,6 +932,11 @@ mod tests {
|
|||||||
AppState::Info(()) => AppState::Info(()),
|
AppState::Info(()) => AppState::Info(()),
|
||||||
AppState::Reload(()) => AppState::Reload(()),
|
AppState::Reload(()) => AppState::Reload(()),
|
||||||
AppState::Search(s) => AppState::Search(s),
|
AppState::Search(s) => AppState::Search(s),
|
||||||
|
AppState::Matches(ref mut m) => AppState::Matches(AppPublicMatches {
|
||||||
|
matching: m.matching,
|
||||||
|
matches: m.matches,
|
||||||
|
state: m.state,
|
||||||
|
}),
|
||||||
AppState::Error(s) => AppState::Error(s),
|
AppState::Error(s) => AppState::Error(s),
|
||||||
AppState::Critical(s) => AppState::Critical(s),
|
AppState::Critical(s) => AppState::Critical(s),
|
||||||
},
|
},
|
||||||
@ -870,6 +947,18 @@ mod tests {
|
|||||||
fn draw_test_suite(collection: &Collection, selection: &mut Selection) {
|
fn draw_test_suite(collection: &Collection, selection: &mut Selection) {
|
||||||
let mut terminal = terminal();
|
let mut terminal = terminal();
|
||||||
|
|
||||||
|
let album = Album::new(
|
||||||
|
AlbumId::new("An Album"),
|
||||||
|
AlbumDate::new(Some(1990), Some(5), None),
|
||||||
|
Some(AlbumPrimaryType::Album),
|
||||||
|
vec![AlbumSecondaryType::Live, AlbumSecondaryType::Compilation],
|
||||||
|
);
|
||||||
|
|
||||||
|
let album_match = Match {
|
||||||
|
score: 80,
|
||||||
|
item: album.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
let mut app = AppPublic {
|
let mut app = AppPublic {
|
||||||
inner: AppPublicInner {
|
inner: AppPublicInner {
|
||||||
collection,
|
collection,
|
||||||
@ -888,6 +977,26 @@ mod tests {
|
|||||||
app.state = AppState::Search("");
|
app.state = AppState::Search("");
|
||||||
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||||
|
|
||||||
|
let album_matches = [album_match.clone(), album_match.clone()];
|
||||||
|
let mut widget_state = WidgetState::default();
|
||||||
|
widget_state.list.select(Some(0));
|
||||||
|
|
||||||
|
app.state = AppState::Matches(AppPublicMatches {
|
||||||
|
matching: Some(&album),
|
||||||
|
matches: Some(&album_matches),
|
||||||
|
state: &mut widget_state,
|
||||||
|
});
|
||||||
|
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||||
|
|
||||||
|
let mut widget_state = WidgetState::default();
|
||||||
|
|
||||||
|
app.state = AppState::Matches(AppPublicMatches {
|
||||||
|
matching: None,
|
||||||
|
matches: None,
|
||||||
|
state: &mut widget_state,
|
||||||
|
});
|
||||||
|
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||||
|
|
||||||
app.state = AppState::Error("get rekt scrub");
|
app.state = AppState::Error("get rekt scrub");
|
||||||
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap();
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user