Add support for MusicBrainz's Browse API #228
@ -1,7 +1,7 @@
|
||||
use crate::tui::app::{
|
||||
machine::{App, AppInner, AppMachine},
|
||||
selection::{Delta, ListSelection},
|
||||
AppPublicState, AppState, IAppInteractBrowse,
|
||||
selection::ListSelection,
|
||||
AppPublicState, AppState, IAppInteractBrowse, Delta
|
||||
};
|
||||
|
||||
pub struct BrowseState;
|
||||
|
@ -9,8 +9,8 @@ use musichoard::collection::{
|
||||
use crate::tui::{
|
||||
app::{
|
||||
machine::{fetch_state::FetchState, input::Input, App, AppInner, AppMachine},
|
||||
AlbumMatches, AppPublicState, AppState, ArtistMatches, IAppInteractMatch, ListOption,
|
||||
MatchOption, MatchStateInfo, MatchStatePublic, WidgetState,
|
||||
AlbumMatches, AppPublicState, AppState, ArtistMatches, Delta, IAppInteractMatch,
|
||||
ListOption, MatchOption, MatchStateInfo, MatchStatePublic, WidgetState,
|
||||
},
|
||||
lib::interface::musicbrainz::api::{Lookup, Match},
|
||||
};
|
||||
@ -243,19 +243,19 @@ impl<'a> From<&'a mut MatchState> for AppPublicState<'a> {
|
||||
impl IAppInteractMatch for AppMachine<MatchState> {
|
||||
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() {
|
||||
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.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 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.state.list.select(Some(to));
|
||||
@ -473,38 +473,38 @@ mod tests {
|
||||
assert_eq!(matches.state.current, matches_info);
|
||||
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.state.list.selected(), Some(0));
|
||||
|
||||
let mut matches = matches;
|
||||
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.state.list.selected(), Some(ii));
|
||||
}
|
||||
|
||||
// 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.state.list.selected(), Some(len));
|
||||
|
||||
// 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.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.state.list.selected(), Some(len + 1));
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
@ -619,10 +619,10 @@ mod tests {
|
||||
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.
|
||||
let matches = matches.next_match().unwrap_match();
|
||||
let matches = matches.next_match().unwrap_match();
|
||||
let matches = matches.next_match().unwrap_match();
|
||||
let matches = matches.next_match().unwrap_match();
|
||||
let matches = matches.increment_match(Delta::Line).unwrap_match();
|
||||
let matches = matches.increment_match(Delta::Line).unwrap_match();
|
||||
let matches = matches.increment_match(Delta::Line).unwrap_match();
|
||||
let matches = matches.increment_match(Delta::Line).unwrap_match();
|
||||
|
||||
let app = matches.select();
|
||||
|
||||
@ -657,8 +657,8 @@ mod tests {
|
||||
);
|
||||
|
||||
// There are no matches which means that the second option should be manual input.
|
||||
let matches = matches.next_match().unwrap_match();
|
||||
let matches = matches.next_match().unwrap_match();
|
||||
let matches = matches.increment_match(Delta::Line).unwrap_match();
|
||||
let matches = matches.increment_match(Delta::Line).unwrap_match();
|
||||
|
||||
let mut app = matches.select();
|
||||
app = input_mbid(app);
|
||||
@ -691,8 +691,8 @@ mod tests {
|
||||
);
|
||||
|
||||
// There are no matches which means that the second option should be manual input.
|
||||
let matches = matches.next_match().unwrap_match();
|
||||
let matches = matches.next_match().unwrap_match();
|
||||
let matches = matches.increment_match(Delta::Line).unwrap_match();
|
||||
let matches = matches.increment_match(Delta::Line).unwrap_match();
|
||||
|
||||
let mut app = matches.select();
|
||||
app = input_mbid(app);
|
||||
|
@ -2,7 +2,8 @@ mod machine;
|
||||
mod selection;
|
||||
|
||||
pub use machine::App;
|
||||
pub use selection::{Category, Delta, Selection, WidgetState};
|
||||
use ratatui::widgets::ListState;
|
||||
pub use selection::{Category, Selection};
|
||||
|
||||
use musichoard::collection::{
|
||||
album::AlbumMeta,
|
||||
@ -124,8 +125,8 @@ pub trait IAppEventFetch {
|
||||
pub trait IAppInteractMatch {
|
||||
type APP: IApp;
|
||||
|
||||
fn prev_match(self) -> Self::APP;
|
||||
fn next_match(self) -> Self::APP;
|
||||
fn decrement_match(self, delta: Delta) -> Self::APP;
|
||||
fn increment_match(self, delta: Delta) -> Self::APP;
|
||||
fn select(self) -> Self::APP;
|
||||
|
||||
fn abort(self) -> Self::APP;
|
||||
@ -159,6 +160,40 @@ pub trait IAppInteractError {
|
||||
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
|
||||
// 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.
|
||||
|
@ -5,9 +5,11 @@ use musichoard::collection::{
|
||||
track::Track,
|
||||
};
|
||||
|
||||
use crate::tui::app::selection::{
|
||||
track::{KeySelectTrack, TrackSelection},
|
||||
Delta, SelectionState, WidgetState,
|
||||
use crate::tui::app::{
|
||||
selection::{
|
||||
track::{KeySelectTrack, TrackSelection},
|
||||
SelectionState,
|
||||
}, Delta, WidgetState
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
@ -6,9 +6,11 @@ use musichoard::collection::{
|
||||
track::Track,
|
||||
};
|
||||
|
||||
use crate::tui::app::selection::{
|
||||
album::{AlbumSelection, KeySelectAlbum},
|
||||
Delta, SelectionState, WidgetState,
|
||||
use crate::tui::app::{
|
||||
selection::{
|
||||
album::{AlbumSelection, KeySelectAlbum},
|
||||
SelectionState,
|
||||
}, Delta, WidgetState
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
@ -5,7 +5,10 @@ mod track;
|
||||
use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection};
|
||||
use ratatui::widgets::ListState;
|
||||
|
||||
use artist::{ArtistSelection, KeySelectArtist};
|
||||
use crate::tui::app::{
|
||||
selection::artist::{ArtistSelection, KeySelectArtist},
|
||||
Delta, WidgetState,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Category {
|
||||
@ -24,40 +27,6 @@ pub struct SelectionState<'a, T> {
|
||||
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 {
|
||||
pub fn new(artists: &[Artist]) -> Self {
|
||||
Selection {
|
||||
|
@ -2,7 +2,7 @@ use std::cmp;
|
||||
|
||||
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)]
|
||||
pub struct TrackSelection {
|
||||
|
@ -212,8 +212,10 @@ impl<APP: IApp> IEventHandlerPrivate<APP> for EventHandler {
|
||||
// Abort.
|
||||
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => app.abort(),
|
||||
// Select.
|
||||
KeyCode::Up => app.prev_match(),
|
||||
KeyCode::Down => app.next_match(),
|
||||
KeyCode::Up => app.decrement_match(Delta::Line),
|
||||
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(),
|
||||
// Othey keys.
|
||||
_ => app.no_op(),
|
||||
|
Loading…
Reference in New Issue
Block a user