Add support for MusicBrainz's Browse API #228

Merged
wojtek merged 9 commits from 160---provide-a-keyboard-shortcut-to-pull-all-release-groups-of-an-artist into main 2024-09-29 21:33:43 +02:00
8 changed files with 79 additions and 69 deletions
Showing only changes of commit 66b273bef4 - Show all commits

View File

@ -1,7 +1,7 @@
use crate::tui::app::{ use crate::tui::app::{
machine::{App, AppInner, AppMachine}, machine::{App, AppInner, AppMachine},
selection::{Delta, ListSelection}, selection::ListSelection,
AppPublicState, AppState, IAppInteractBrowse, AppPublicState, AppState, IAppInteractBrowse, Delta
}; };
pub struct BrowseState; pub struct BrowseState;

View File

@ -9,8 +9,8 @@ use musichoard::collection::{
use crate::tui::{ use crate::tui::{
app::{ app::{
machine::{fetch_state::FetchState, input::Input, App, AppInner, AppMachine}, machine::{fetch_state::FetchState, input::Input, App, AppInner, AppMachine},
AlbumMatches, AppPublicState, AppState, ArtistMatches, IAppInteractMatch, ListOption, AlbumMatches, AppPublicState, AppState, ArtistMatches, Delta, IAppInteractMatch,
MatchOption, MatchStateInfo, MatchStatePublic, WidgetState, ListOption, MatchOption, MatchStateInfo, MatchStatePublic, WidgetState,
}, },
lib::interface::musicbrainz::api::{Lookup, Match}, lib::interface::musicbrainz::api::{Lookup, Match},
}; };
@ -243,19 +243,19 @@ impl<'a> From<&'a mut MatchState> for AppPublicState<'a> {
impl IAppInteractMatch for AppMachine<MatchState> { impl IAppInteractMatch for AppMachine<MatchState> {
type APP = App; type APP = App;
fn prev_match(mut self) -> Self::APP { fn decrement_match(mut self, delta: Delta) -> Self::APP {
if let Some(index) = self.state.state.list.selected() { if let Some(index) = self.state.state.list.selected() {
let result = index.saturating_sub(1); let result = index.saturating_sub(delta.as_usize(&self.state.state));
self.state.state.list.select(Some(result)); self.state.state.list.select(Some(result));
} }
self.into() self.into()
} }
fn next_match(mut self) -> Self::APP { fn increment_match(mut self, delta: Delta) -> Self::APP {
let index = self.state.state.list.selected().unwrap(); let index = self.state.state.list.selected().unwrap();
let to = cmp::min( let to = cmp::min(
index.saturating_add(1), index.saturating_add(delta.as_usize(&self.state.state)),
self.state.current.len().saturating_sub(1), self.state.current.len().saturating_sub(1),
); );
self.state.state.list.select(Some(to)); self.state.state.list.select(Some(to));
@ -473,38 +473,38 @@ mod tests {
assert_eq!(matches.state.current, matches_info); assert_eq!(matches.state.current, matches_info);
assert_eq!(matches.state.state, widget_state); assert_eq!(matches.state.state, widget_state);
let matches = matches.prev_match().unwrap_match(); let matches = matches.decrement_match(Delta::Line).unwrap_match();
assert_eq!(matches.state.current, matches_info); assert_eq!(matches.state.current, matches_info);
assert_eq!(matches.state.state.list.selected(), Some(0)); assert_eq!(matches.state.state.list.selected(), Some(0));
let mut matches = matches; let mut matches = matches;
for ii in 1..len { for ii in 1..len {
matches = matches.next_match().unwrap_match(); matches = matches.increment_match(Delta::Line).unwrap_match();
assert_eq!(matches.state.current, matches_info); assert_eq!(matches.state.current, matches_info);
assert_eq!(matches.state.state.list.selected(), Some(ii)); assert_eq!(matches.state.state.list.selected(), Some(ii));
} }
// Next is CannotHaveMBID // Next is CannotHaveMBID
let matches = matches.next_match().unwrap_match(); let matches = matches.increment_match(Delta::Line).unwrap_match();
assert_eq!(matches.state.current, matches_info); assert_eq!(matches.state.current, matches_info);
assert_eq!(matches.state.state.list.selected(), Some(len)); assert_eq!(matches.state.state.list.selected(), Some(len));
// Next is ManualInputMbid // Next is ManualInputMbid
let matches = matches.next_match().unwrap_match(); let matches = matches.increment_match(Delta::Line).unwrap_match();
assert_eq!(matches.state.current, matches_info); assert_eq!(matches.state.current, matches_info);
assert_eq!(matches.state.state.list.selected(), Some(len + 1)); assert_eq!(matches.state.state.list.selected(), Some(len + 1));
let matches = matches.next_match().unwrap_match(); let matches = matches.increment_match(Delta::Line).unwrap_match();
assert_eq!(matches.state.current, matches_info); assert_eq!(matches.state.current, matches_info);
assert_eq!(matches.state.state.list.selected(), Some(len + 1)); assert_eq!(matches.state.state.list.selected(), Some(len + 1));
// Go prev_match first as selecting on manual input does not go back to fetch. // Go prev_match first as selecting on manual input does not go back to fetch.
let matches = matches.prev_match().unwrap_match(); let matches = matches.decrement_match(Delta::Line).unwrap_match();
matches.select().unwrap_fetch(); matches.select().unwrap_fetch();
} }
@ -619,10 +619,10 @@ mod tests {
AppMachine::match_state(inner(music_hoard(vec![])), match_state(album_match())); AppMachine::match_state(inner(music_hoard(vec![])), match_state(album_match()));
// album_match has two matches which means that the fourth option should be manual input. // album_match has two matches which means that the fourth option should be manual input.
let matches = matches.next_match().unwrap_match(); let matches = matches.increment_match(Delta::Line).unwrap_match();
let matches = matches.next_match().unwrap_match(); let matches = matches.increment_match(Delta::Line).unwrap_match();
let matches = matches.next_match().unwrap_match(); let matches = matches.increment_match(Delta::Line).unwrap_match();
let matches = matches.next_match().unwrap_match(); let matches = matches.increment_match(Delta::Line).unwrap_match();
let app = matches.select(); let app = matches.select();
@ -657,8 +657,8 @@ mod tests {
); );
// There are no matches which means that the second option should be manual input. // There are no matches which means that the second option should be manual input.
let matches = matches.next_match().unwrap_match(); let matches = matches.increment_match(Delta::Line).unwrap_match();
let matches = matches.next_match().unwrap_match(); let matches = matches.increment_match(Delta::Line).unwrap_match();
let mut app = matches.select(); let mut app = matches.select();
app = input_mbid(app); app = input_mbid(app);
@ -691,8 +691,8 @@ mod tests {
); );
// There are no matches which means that the second option should be manual input. // There are no matches which means that the second option should be manual input.
let matches = matches.next_match().unwrap_match(); let matches = matches.increment_match(Delta::Line).unwrap_match();
let matches = matches.next_match().unwrap_match(); let matches = matches.increment_match(Delta::Line).unwrap_match();
let mut app = matches.select(); let mut app = matches.select();
app = input_mbid(app); app = input_mbid(app);

View File

@ -2,7 +2,8 @@ mod machine;
mod selection; mod selection;
pub use machine::App; pub use machine::App;
pub use selection::{Category, Delta, Selection, WidgetState}; use ratatui::widgets::ListState;
pub use selection::{Category, Selection};
use musichoard::collection::{ use musichoard::collection::{
album::AlbumMeta, album::AlbumMeta,
@ -124,8 +125,8 @@ pub trait IAppEventFetch {
pub trait IAppInteractMatch { pub trait IAppInteractMatch {
type APP: IApp; type APP: IApp;
fn prev_match(self) -> Self::APP; fn decrement_match(self, delta: Delta) -> Self::APP;
fn next_match(self) -> Self::APP; fn increment_match(self, delta: Delta) -> Self::APP;
fn select(self) -> Self::APP; fn select(self) -> Self::APP;
fn abort(self) -> Self::APP; fn abort(self) -> Self::APP;
@ -159,6 +160,40 @@ pub trait IAppInteractError {
fn dismiss_error(self) -> Self::APP; fn dismiss_error(self) -> Self::APP;
} }
#[derive(Clone, Debug, Default)]
pub struct WidgetState {
pub list: ListState,
pub height: usize,
}
impl PartialEq for WidgetState {
fn eq(&self, other: &Self) -> bool {
self.list.selected().eq(&other.list.selected()) && self.height.eq(&other.height)
}
}
impl WidgetState {
#[must_use]
pub const fn with_selected(mut self, selected: Option<usize>) -> Self {
self.list = self.list.with_selected(selected);
self
}
}
pub enum Delta {
Line,
Page,
}
impl Delta {
fn as_usize(&self, state: &WidgetState) -> usize {
match self {
Delta::Line => 1,
Delta::Page => state.height.saturating_sub(1),
}
}
}
// It would be preferable to have a getter for each field separately. However, the selection field // It would be preferable to have a getter for each field separately. However, the selection field
// needs to be mutably accessible requiring a mutable borrow of the entire struct if behind a trait. // needs to be mutably accessible requiring a mutable borrow of the entire struct if behind a trait.
// This in turn complicates simultaneous field access since only a single mutable borrow is allowed. // This in turn complicates simultaneous field access since only a single mutable borrow is allowed.

View File

@ -5,9 +5,11 @@ use musichoard::collection::{
track::Track, track::Track,
}; };
use crate::tui::app::selection::{ use crate::tui::app::{
selection::{
track::{KeySelectTrack, TrackSelection}, track::{KeySelectTrack, TrackSelection},
Delta, SelectionState, WidgetState, SelectionState,
}, Delta, WidgetState
}; };
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]

View File

@ -6,9 +6,11 @@ use musichoard::collection::{
track::Track, track::Track,
}; };
use crate::tui::app::selection::{ use crate::tui::app::{
selection::{
album::{AlbumSelection, KeySelectAlbum}, album::{AlbumSelection, KeySelectAlbum},
Delta, SelectionState, WidgetState, SelectionState,
}, Delta, WidgetState
}; };
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]

View File

@ -5,7 +5,10 @@ mod track;
use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection}; use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection};
use ratatui::widgets::ListState; use ratatui::widgets::ListState;
use artist::{ArtistSelection, KeySelectArtist}; use crate::tui::app::{
selection::artist::{ArtistSelection, KeySelectArtist},
Delta, WidgetState,
};
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Category { pub enum Category {
@ -24,40 +27,6 @@ pub struct SelectionState<'a, T> {
pub index: usize, pub index: usize,
} }
#[derive(Clone, Debug, Default)]
pub struct WidgetState {
pub list: ListState,
pub height: usize,
}
impl PartialEq for WidgetState {
fn eq(&self, other: &Self) -> bool {
self.list.selected().eq(&other.list.selected()) && self.height.eq(&other.height)
}
}
impl WidgetState {
#[must_use]
pub const fn with_selected(mut self, selected: Option<usize>) -> Self {
self.list = self.list.with_selected(selected);
self
}
}
pub enum Delta {
Line,
Page,
}
impl Delta {
fn as_usize(&self, state: &WidgetState) -> usize {
match self {
Delta::Line => 1,
Delta::Page => state.height.saturating_sub(1),
}
}
}
impl Selection { impl Selection {
pub fn new(artists: &[Artist]) -> Self { pub fn new(artists: &[Artist]) -> Self {
Selection { Selection {

View File

@ -2,7 +2,7 @@ use std::cmp;
use musichoard::collection::track::{Track, TrackId, TrackNum}; use musichoard::collection::track::{Track, TrackId, TrackNum};
use crate::tui::app::selection::{Delta, SelectionState, WidgetState}; use crate::tui::app::{selection::SelectionState, Delta, WidgetState};
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct TrackSelection { pub struct TrackSelection {

View File

@ -212,8 +212,10 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
// Abort. // Abort.
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => app.abort(), KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => app.abort(),
// Select. // Select.
KeyCode::Up => app.prev_match(), KeyCode::Up => app.decrement_match(Delta::Line),
KeyCode::Down => app.next_match(), KeyCode::Down => app.increment_match(Delta::Line),
KeyCode::PageUp => app.decrement_match(Delta::Page),
KeyCode::PageDown => app.increment_match(Delta::Page),
KeyCode::Enter => app.select(), KeyCode::Enter => app.select(),
// Othey keys. // Othey keys.
_ => app.no_op(), _ => app.no_op(),