Add support for MusicBrainz's Browse API #228
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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.
|
||||||
|
@ -5,9 +5,11 @@ use musichoard::collection::{
|
|||||||
track::Track,
|
track::Track,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::tui::app::selection::{
|
use crate::tui::app::{
|
||||||
track::{KeySelectTrack, TrackSelection},
|
selection::{
|
||||||
Delta, SelectionState, WidgetState,
|
track::{KeySelectTrack, TrackSelection},
|
||||||
|
SelectionState,
|
||||||
|
}, Delta, WidgetState
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
@ -6,9 +6,11 @@ use musichoard::collection::{
|
|||||||
track::Track,
|
track::Track,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::tui::app::selection::{
|
use crate::tui::app::{
|
||||||
album::{AlbumSelection, KeySelectAlbum},
|
selection::{
|
||||||
Delta, SelectionState, WidgetState,
|
album::{AlbumSelection, KeySelectAlbum},
|
||||||
|
SelectionState,
|
||||||
|
}, Delta, WidgetState
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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(),
|
||||||
|
Loading…
Reference in New Issue
Block a user