Provide a keyboard shortcut to sync all existing albums with MusicBrainz #167
@ -8,7 +8,7 @@ use musichoard::{
|
|||||||
|
|
||||||
use crate::tui::{
|
use crate::tui::{
|
||||||
app::{
|
app::{
|
||||||
machine::{matches::AppMatchInfo, App, AppInner, AppMachine},
|
machine::{matches::AppMatchesInfo, App, AppInner, AppMachine},
|
||||||
selection::{Delta, ListSelection},
|
selection::{Delta, ListSelection},
|
||||||
AppPublic, AppState, IAppInteractBrowse,
|
AppPublic, AppState, IAppInteractBrowse,
|
||||||
},
|
},
|
||||||
@ -115,10 +115,6 @@ impl<MH: IMusicHoard> IAppInteractBrowse for AppMachine<MH, AppBrowse> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if artist.albums.is_empty() {
|
|
||||||
return AppMachine::error(self.inner, "cannot fetch: this artist has no albums").into();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut artist_album_matches = vec![];
|
let mut artist_album_matches = vec![];
|
||||||
let mut album_iter = artist.albums.iter().peekable();
|
let mut album_iter = artist.albums.iter().peekable();
|
||||||
while let Some(album) = album_iter.next() {
|
while let Some(album) = album_iter.next() {
|
||||||
@ -127,7 +123,7 @@ impl<MH: IMusicHoard> IAppInteractBrowse for AppMachine<MH, AppBrowse> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match api.search_release_group(arid, album) {
|
match api.search_release_group(arid, album) {
|
||||||
Ok(matches) => artist_album_matches.push(AppMatchInfo {
|
Ok(matches) => artist_album_matches.push(AppMatchesInfo {
|
||||||
matching: album.clone(),
|
matching: album.clone(),
|
||||||
matches,
|
matches,
|
||||||
}),
|
}),
|
||||||
|
@ -10,31 +10,33 @@ use crate::tui::{
|
|||||||
lib::IMusicHoard,
|
lib::IMusicHoard,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct AppMatchInfo {
|
pub struct AppMatchesInfo {
|
||||||
pub matching: Album,
|
pub matching: Album,
|
||||||
pub matches: Vec<Match<Album>>,
|
pub matches: Vec<Match<Album>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppMatches {
|
pub struct AppMatches {
|
||||||
matches: Vec<AppMatchInfo>,
|
matches_info_vec: Vec<AppMatchesInfo>,
|
||||||
index: usize,
|
index: Option<usize>,
|
||||||
state: WidgetState,
|
state: WidgetState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<MH: IMusicHoard> AppMachine<MH, AppMatches> {
|
impl<MH: IMusicHoard> AppMachine<MH, AppMatches> {
|
||||||
pub fn matches(inner: AppInner<MH>, matches: Vec<AppMatchInfo>) -> Self {
|
pub fn matches(inner: AppInner<MH>, matches_info_vec: Vec<AppMatchesInfo>) -> Self {
|
||||||
assert!(!matches.is_empty());
|
let mut index = None;
|
||||||
|
|
||||||
let mut state = WidgetState::default();
|
let mut state = WidgetState::default();
|
||||||
if !matches[0].matches.is_empty() {
|
if let Some(matches_info) = matches_info_vec.first() {
|
||||||
state.list.select(Some(0));
|
index = Some(0);
|
||||||
|
if !matches_info.matches.is_empty() {
|
||||||
|
state.list.select(Some(0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AppMachine {
|
AppMachine {
|
||||||
inner,
|
inner,
|
||||||
state: AppMatches {
|
state: AppMatches {
|
||||||
matches,
|
matches_info_vec,
|
||||||
index: 0,
|
index,
|
||||||
state,
|
state,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -49,11 +51,19 @@ impl<MH: IMusicHoard> From<AppMachine<MH, AppMatches>> for App<MH> {
|
|||||||
|
|
||||||
impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppMatches>> for AppPublic<'a> {
|
impl<'a, MH: IMusicHoard> From<&'a mut AppMachine<MH, AppMatches>> for AppPublic<'a> {
|
||||||
fn from(machine: &'a mut AppMachine<MH, AppMatches>) -> Self {
|
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 {
|
AppPublic {
|
||||||
inner: (&mut machine.inner).into(),
|
inner: (&mut machine.inner).into(),
|
||||||
state: AppState::Matches(AppPublicMatches {
|
state: AppState::Matches(AppPublicMatches {
|
||||||
matching: &machine.state.matches[machine.state.index].matching,
|
matching,
|
||||||
matches: &machine.state.matches[machine.state.index].matches,
|
matches,
|
||||||
state: &mut machine.state.state,
|
state: &mut machine.state.state,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@ -64,8 +74,8 @@ impl<MH: IMusicHoard> IAppInteractMatches for AppMachine<MH, AppMatches> {
|
|||||||
type APP = App<MH>;
|
type APP = App<MH>;
|
||||||
|
|
||||||
fn prev_match(mut self) -> Self::APP {
|
fn prev_match(mut self) -> Self::APP {
|
||||||
if let Some(index) = self.state.state.list.selected() {
|
if let Some(list_index) = self.state.state.list.selected() {
|
||||||
let result = index.saturating_sub(1);
|
let result = list_index.saturating_sub(1);
|
||||||
self.state.state.list.select(Some(result));
|
self.state.state.list.select(Some(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,11 +83,14 @@ impl<MH: IMusicHoard> IAppInteractMatches for AppMachine<MH, AppMatches> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn next_match(mut self) -> Self::APP {
|
fn next_match(mut self) -> Self::APP {
|
||||||
if let Some(index) = self.state.state.list.selected() {
|
if let Some(list_index) = self.state.state.list.selected() {
|
||||||
let result = index.saturating_add(1);
|
let result = list_index.saturating_add(1);
|
||||||
let to = cmp::min(
|
let to = cmp::min(
|
||||||
result,
|
result,
|
||||||
self.state.matches[self.state.index].matches.len() - 1,
|
self.state.matches_info_vec[self.state.index.unwrap()]
|
||||||
|
.matches
|
||||||
|
.len()
|
||||||
|
.saturating_sub(1),
|
||||||
);
|
);
|
||||||
self.state.state.list.select(Some(to));
|
self.state.state.list.select(Some(to));
|
||||||
}
|
}
|
||||||
@ -86,16 +99,18 @@ impl<MH: IMusicHoard> IAppInteractMatches for AppMachine<MH, AppMatches> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn select(mut self) -> Self::APP {
|
fn select(mut self) -> Self::APP {
|
||||||
self.state.index = self.state.index.saturating_add(1);
|
self.state.index = self.state.index.map(|i| i.saturating_add(1));
|
||||||
if self.state.index < self.state.matches.len() {
|
self.state.state = WidgetState::default();
|
||||||
self.state.state = WidgetState::default();
|
if let Some(index) = self.state.index {
|
||||||
if !self.state.matches[self.state.index].matches.is_empty() {
|
if let Some(matches_info) = self.state.matches_info_vec.get(index) {
|
||||||
self.state.state.list.select(Some(0));
|
if !matches_info.matches.is_empty() {
|
||||||
|
self.state.state.list.select(Some(0));
|
||||||
|
}
|
||||||
|
return self.into();
|
||||||
}
|
}
|
||||||
self.into()
|
|
||||||
} else {
|
|
||||||
AppMachine::browse(self.inner).into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppMachine::browse(self.inner).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn abort(self) -> Self::APP {
|
fn abort(self) -> Self::APP {
|
||||||
|
@ -131,8 +131,8 @@ pub struct AppPublicInner<'app> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppPublicMatches<'app> {
|
pub struct AppPublicMatches<'app> {
|
||||||
pub matching: &'app Album,
|
pub matching: Option<&'app Album>,
|
||||||
pub matches: &'app [Match<Album>],
|
pub matches: Option<&'app [Match<Album>]>,
|
||||||
pub state: &'app mut WidgetState,
|
pub state: &'app mut WidgetState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -531,11 +531,7 @@ impl Minibuffer<'_> {
|
|||||||
},
|
},
|
||||||
AppState::Matches(public) => Minibuffer {
|
AppState::Matches(public) => Minibuffer {
|
||||||
paragraphs: vec![
|
paragraphs: vec![
|
||||||
Paragraph::new(format!(
|
Paragraph::new(Minibuffer::display_matching_info(public.matching)),
|
||||||
"Matching: {} | {}",
|
|
||||||
AlbumState::display_album_date(&public.matching.date),
|
|
||||||
&public.matching.id.title
|
|
||||||
)),
|
|
||||||
Paragraph::new("q: abort"),
|
Paragraph::new("q: abort"),
|
||||||
],
|
],
|
||||||
columns: 2,
|
columns: 2,
|
||||||
@ -562,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;
|
||||||
@ -819,34 +826,38 @@ 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(
|
fn render_matches_overlay(
|
||||||
matching: &Album,
|
matching: Option<&Album>,
|
||||||
matches: &[Match<Album>],
|
matches: Option<&[Match<Album>]>,
|
||||||
state: &mut WidgetState,
|
state: &mut WidgetState,
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
) {
|
) {
|
||||||
let area = OverlayBuilder::default().build(frame.size());
|
let area = OverlayBuilder::default().build(frame.size());
|
||||||
|
let matching_string = Minibuffer::display_matching_info(matching);
|
||||||
let list = List::new(
|
let list = matches.map(|m| Ui::build_match_list(m)).unwrap_or_default();
|
||||||
matches
|
|
||||||
.iter()
|
|
||||||
.map(|m| {
|
|
||||||
ListItem::new(format!(
|
|
||||||
"{:010} | {} [{}] ({}%)",
|
|
||||||
AlbumState::display_album_date(&m.item.date),
|
|
||||||
&m.item.id.title,
|
|
||||||
AlbumState::display_type(&m.item.primary_type, &m.item.secondary_types),
|
|
||||||
m.score,
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.collect::<Vec<ListItem>>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let matching_string = format!(
|
|
||||||
"Matching: {} | {}",
|
|
||||||
AlbumState::display_album_date(&matching.date),
|
|
||||||
&matching.id.title
|
|
||||||
);
|
|
||||||
Self::render_overlay_list_widget(&matching_string, list, state, true, area, frame)
|
Self::render_overlay_list_widget(&matching_string, list, state, true, area, frame)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -923,8 +934,8 @@ mod tests {
|
|||||||
AppState::Search(s) => AppState::Search(s),
|
AppState::Search(s) => AppState::Search(s),
|
||||||
AppState::Matches(ref mut m) => AppState::Matches(AppPublicMatches {
|
AppState::Matches(ref mut m) => AppState::Matches(AppPublicMatches {
|
||||||
matching: m.matching,
|
matching: m.matching,
|
||||||
matches: &m.matches,
|
matches: m.matches,
|
||||||
state: &mut m.state,
|
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),
|
||||||
|
Loading…
Reference in New Issue
Block a user