Compare commits

..

4 Commits

Author SHA1 Message Date
a8443c378c Lints
All checks were successful
Cargo CI / Build and Test (pull_request) Successful in 2m3s
Cargo CI / Lint (pull_request) Successful in 1m6s
2024-09-29 21:29:52 +02:00
03c1b2b9ae Complete unit tests 2024-09-29 21:29:20 +02:00
66b273bef4 Paging of match page 2024-09-29 21:17:17 +02:00
77a97659d1 Complete support for paging 2024-09-29 21:06:29 +02:00
15 changed files with 231 additions and 95 deletions

View File

@ -3,7 +3,7 @@ use std::{thread, time};
use musichoard::{ use musichoard::{
collection::musicbrainz::Mbid, collection::musicbrainz::Mbid,
external::musicbrainz::{ external::musicbrainz::{
api::{browse::BrowseReleaseGroupRequest, MusicBrainzClient, PageSettings}, api::{browse::BrowseReleaseGroupRequest, MusicBrainzClient, NextPage, PageSettings},
http::MusicBrainzHttp, http::MusicBrainzHttp,
}, },
}; };
@ -66,20 +66,19 @@ fn main() {
println!("{rg:?}\n"); println!("{rg:?}\n");
} }
let offset = response.release_group_offset; let offset = response.page.release_group_offset;
let count = response.release_groups.len(); let count = response.release_groups.len();
response_counts.push(count); response_counts.push(count);
let total = response.release_group_count; let total = response.page.release_group_count;
println!("Release group offset : {offset}"); println!("Release group offset : {offset}");
println!("Release groups in this response: {count}"); println!("Release groups in this response: {count}");
println!("Release groups in total : {total}"); println!("Release groups in total : {total}");
let next_offset = offset + count; match response.page.next_page_offset(count) {
if next_offset == total { NextPage::Offset(next_offset) => paging.with_offset(next_offset),
break; NextPage::Complete => break,
} }
paging.with_offset(next_offset);
thread::sleep(time::Duration::from_secs(1)); thread::sleep(time::Duration::from_secs(1));
} }

View File

@ -6,14 +6,31 @@ use crate::{
collection::musicbrainz::Mbid, collection::musicbrainz::Mbid,
external::musicbrainz::{ external::musicbrainz::{
api::{ api::{
Error, MbReleaseGroupMeta, MusicBrainzClient, PageSettings, SerdeMbReleaseGroupMeta, ApiDisplay, Error, MbReleaseGroupMeta, MusicBrainzClient, NextPage, PageSettings,
MB_BASE_URL, SerdeMbReleaseGroupMeta, MB_BASE_URL,
}, },
IMusicBrainzHttp, IMusicBrainzHttp,
}, },
}; };
use super::ApiDisplay; #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all(deserialize = "kebab-case"))]
pub struct BrowseReleaseGroupPage {
pub release_group_offset: usize,
pub release_group_count: usize,
}
impl BrowseReleaseGroupPage {
pub fn next_page_offset(&self, page_count: usize) -> NextPage {
NextPage::next_page_offset(
self.release_group_offset,
self.release_group_count,
page_count,
)
}
}
pub type SerdeBrowseReleaseGroupPage = BrowseReleaseGroupPage;
impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> { impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
pub fn browse_release_group( pub fn browse_release_group(
@ -60,24 +77,22 @@ impl<'a> BrowseReleaseGroupRequest<'a> {
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct BrowseReleaseGroupResponse { pub struct BrowseReleaseGroupResponse {
pub release_group_offset: usize,
pub release_group_count: usize,
pub release_groups: Vec<MbReleaseGroupMeta>, pub release_groups: Vec<MbReleaseGroupMeta>,
pub page: BrowseReleaseGroupPage,
} }
#[derive(Clone, Deserialize)] #[derive(Clone, Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))] #[serde(rename_all(deserialize = "kebab-case"))]
struct DeserializeBrowseReleaseGroupResponse { struct DeserializeBrowseReleaseGroupResponse {
release_group_offset: usize,
release_group_count: usize,
release_groups: Option<Vec<SerdeMbReleaseGroupMeta>>, release_groups: Option<Vec<SerdeMbReleaseGroupMeta>>,
#[serde(flatten)]
page: SerdeBrowseReleaseGroupPage,
} }
impl From<DeserializeBrowseReleaseGroupResponse> for BrowseReleaseGroupResponse { impl From<DeserializeBrowseReleaseGroupResponse> for BrowseReleaseGroupResponse {
fn from(value: DeserializeBrowseReleaseGroupResponse) -> Self { fn from(value: DeserializeBrowseReleaseGroupResponse) -> Self {
BrowseReleaseGroupResponse { BrowseReleaseGroupResponse {
release_group_offset: value.release_group_offset, page: value.page,
release_group_count: value.release_group_count,
release_groups: value release_groups: value
.release_groups .release_groups
.map(|rgs| rgs.into_iter().map(Into::into).collect()) .map(|rgs| rgs.into_iter().map(Into::into).collect())
@ -94,8 +109,8 @@ mod tests {
collection::album::{AlbumPrimaryType, AlbumSecondaryType}, collection::album::{AlbumPrimaryType, AlbumSecondaryType},
external::musicbrainz::{ external::musicbrainz::{
api::{ api::{
SerdeAlbumDate, SerdeAlbumPrimaryType, SerdeAlbumSecondaryType, SerdeMbid, tests::next_page_test, SerdeAlbumDate, SerdeAlbumPrimaryType,
MB_MAX_PAGE_LIMIT, SerdeAlbumSecondaryType, SerdeMbid, MB_MAX_PAGE_LIMIT,
}, },
MockIMusicBrainzHttp, MockIMusicBrainzHttp,
}, },
@ -103,6 +118,16 @@ mod tests {
use super::*; use super::*;
#[test]
fn browse_release_group_next_page() {
let page = BrowseReleaseGroupPage {
release_group_offset: 5,
release_group_count: 45,
};
next_page_test(|val| page.next_page_offset(val));
}
#[test] #[test]
fn browse_release_group() { fn browse_release_group() {
let mbid = "00000000-0000-0000-0000-000000000000"; let mbid = "00000000-0000-0000-0000-000000000000";
@ -120,14 +145,15 @@ mod tests {
)]), )]),
}; };
let de_response = DeserializeBrowseReleaseGroupResponse { let de_response = DeserializeBrowseReleaseGroupResponse {
page: SerdeBrowseReleaseGroupPage {
release_group_offset: de_release_group_offset, release_group_offset: de_release_group_offset,
release_group_count: de_release_group_count, release_group_count: de_release_group_count,
},
release_groups: Some(vec![de_meta.clone()]), release_groups: Some(vec![de_meta.clone()]),
}; };
let response = BrowseReleaseGroupResponse { let response = BrowseReleaseGroupResponse {
release_group_offset: de_release_group_offset, page: de_response.page,
release_group_count: de_release_group_count,
release_groups: vec![de_meta.clone().into()], release_groups: vec![de_meta.clone().into()],
}; };

View File

@ -84,6 +84,23 @@ impl PageSettings {
} }
} }
#[derive(Debug, PartialEq, Eq)]
pub enum NextPage {
Offset(usize),
Complete,
}
impl NextPage {
pub fn next_page_offset(offset: usize, total_count: usize, page_count: usize) -> NextPage {
let next_offset = offset + page_count;
if next_offset < total_count {
NextPage::Offset(next_offset)
} else {
NextPage::Complete
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct MbArtistMeta { pub struct MbArtistMeta {
pub id: Mbid, pub id: Mbid,
@ -342,6 +359,25 @@ mod tests {
assert!(!format!("{unk_err:?}").is_empty()); assert!(!format!("{unk_err:?}").is_empty());
} }
pub fn next_page_test<Fn>(mut f: Fn)
where
Fn: FnMut(usize) -> NextPage,
{
let next = f(20);
assert_eq!(next, NextPage::Offset(25));
let next = f(40);
assert_eq!(next, NextPage::Complete);
let next = f(100);
assert_eq!(next, NextPage::Complete);
}
#[test]
fn next_page() {
next_page_test(|val| NextPage::next_page_offset(5, 45, val));
}
#[test] #[test]
fn format_page_settings() { fn format_page_settings() {
let paging = PageSettings::default(); let paging = PageSettings::default();

View File

@ -3,7 +3,10 @@ use std::fmt;
use serde::Deserialize; use serde::Deserialize;
use crate::external::musicbrainz::api::{ use crate::external::musicbrainz::api::{
search::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin}, search::{
query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin},
SearchPage, SerdeSearchPage,
},
MbArtistMeta, SerdeMbArtistMeta, MbArtistMeta, SerdeMbArtistMeta,
}; };
@ -26,18 +29,22 @@ impl_term!(string, SearchArtist<'a>, String, &'a str);
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct SearchArtistResponse { pub struct SearchArtistResponse {
pub artists: Vec<SearchArtistResponseArtist>, pub artists: Vec<SearchArtistResponseArtist>,
pub page: SearchPage,
} }
#[derive(Clone, Deserialize)] #[derive(Clone, Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))] #[serde(rename_all(deserialize = "kebab-case"))]
pub struct DeserializeSearchArtistResponse { pub struct DeserializeSearchArtistResponse {
artists: Vec<DeserializeSearchArtistResponseArtist>, artists: Vec<DeserializeSearchArtistResponseArtist>,
#[serde(flatten)]
page: SerdeSearchPage,
} }
impl From<DeserializeSearchArtistResponse> for SearchArtistResponse { impl From<DeserializeSearchArtistResponse> for SearchArtistResponse {
fn from(value: DeserializeSearchArtistResponse) -> Self { fn from(value: DeserializeSearchArtistResponse) -> Self {
SearchArtistResponse { SearchArtistResponse {
artists: value.artists.into_iter().map(Into::into).collect(), artists: value.artists.into_iter().map(Into::into).collect(),
page: value.page,
} }
} }
} }
@ -77,6 +84,8 @@ mod tests {
use super::*; use super::*;
fn de_response() -> DeserializeSearchArtistResponse { fn de_response() -> DeserializeSearchArtistResponse {
let de_offset = 24;
let de_count = 124;
let de_artist = DeserializeSearchArtistResponseArtist { let de_artist = DeserializeSearchArtistResponseArtist {
score: 67, score: 67,
meta: SerdeMbArtistMeta { meta: SerdeMbArtistMeta {
@ -88,6 +97,10 @@ mod tests {
}; };
DeserializeSearchArtistResponse { DeserializeSearchArtistResponse {
artists: vec![de_artist.clone()], artists: vec![de_artist.clone()],
page: SerdeSearchPage {
offset: de_offset,
count: de_count,
},
} }
} }
@ -101,6 +114,7 @@ mod tests {
meta: a.meta.into(), meta: a.meta.into(),
}) })
.collect(), .collect(),
page: de_response.page,
} }
} }

View File

@ -9,6 +9,7 @@ pub use release_group::{
}; };
use paste::paste; use paste::paste;
use serde::Deserialize;
use url::form_urlencoded; use url::form_urlencoded;
use crate::external::musicbrainz::{ use crate::external::musicbrainz::{
@ -22,6 +23,23 @@ use crate::external::musicbrainz::{
IMusicBrainzHttp, IMusicBrainzHttp,
}; };
use super::NextPage;
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all(deserialize = "kebab-case"))]
pub struct SearchPage {
pub offset: usize,
pub count: usize,
}
impl SearchPage {
pub fn next_page_offset(&self, page_count: usize) -> NextPage {
NextPage::next_page_offset(self.offset, self.count, page_count)
}
}
pub type SerdeSearchPage = SearchPage;
macro_rules! impl_search_entity { macro_rules! impl_search_entity {
($name:ident, $entity:literal) => { ($name:ident, $entity:literal) => {
paste! { paste! {
@ -46,3 +64,20 @@ impl<Http: IMusicBrainzHttp> MusicBrainzClient<Http> {
impl_search_entity!(Artist, "artist"); impl_search_entity!(Artist, "artist");
impl_search_entity!(ReleaseGroup, "release-group"); impl_search_entity!(ReleaseGroup, "release-group");
} }
#[cfg(test)]
mod tests {
use crate::external::musicbrainz::api::tests::next_page_test;
use super::*;
#[test]
fn search_next_page() {
let page = SearchPage {
offset: 5,
count: 45,
};
next_page_test(|val| page.next_page_offset(val));
}
}

View File

@ -5,7 +5,10 @@ use serde::Deserialize;
use crate::{ use crate::{
collection::{album::AlbumDate, musicbrainz::Mbid}, collection::{album::AlbumDate, musicbrainz::Mbid},
external::musicbrainz::api::{ external::musicbrainz::api::{
search::query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin}, search::{
query::{impl_term, EmptyQuery, EmptyQueryJoin, Query, QueryJoin},
SearchPage, SerdeSearchPage,
},
ApiDisplay, MbReleaseGroupMeta, SerdeMbReleaseGroupMeta, ApiDisplay, MbReleaseGroupMeta, SerdeMbReleaseGroupMeta,
}, },
}; };
@ -50,18 +53,22 @@ impl_term!(rgid, SearchReleaseGroup<'a>, Rgid, &'a Mbid);
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct SearchReleaseGroupResponse { pub struct SearchReleaseGroupResponse {
pub release_groups: Vec<SearchReleaseGroupResponseReleaseGroup>, pub release_groups: Vec<SearchReleaseGroupResponseReleaseGroup>,
pub page: SearchPage,
} }
#[derive(Clone, Deserialize)] #[derive(Clone, Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))] #[serde(rename_all(deserialize = "kebab-case"))]
pub struct DeserializeSearchReleaseGroupResponse { pub struct DeserializeSearchReleaseGroupResponse {
release_groups: Vec<DeserializeSearchReleaseGroupResponseReleaseGroup>, release_groups: Vec<DeserializeSearchReleaseGroupResponseReleaseGroup>,
#[serde(flatten)]
page: SerdeSearchPage,
} }
impl From<DeserializeSearchReleaseGroupResponse> for SearchReleaseGroupResponse { impl From<DeserializeSearchReleaseGroupResponse> for SearchReleaseGroupResponse {
fn from(value: DeserializeSearchReleaseGroupResponse) -> Self { fn from(value: DeserializeSearchReleaseGroupResponse) -> Self {
SearchReleaseGroupResponse { SearchReleaseGroupResponse {
release_groups: value.release_groups.into_iter().map(Into::into).collect(), release_groups: value.release_groups.into_iter().map(Into::into).collect(),
page: value.page,
} }
} }
} }
@ -109,6 +116,8 @@ mod tests {
use super::*; use super::*;
fn de_response() -> DeserializeSearchReleaseGroupResponse { fn de_response() -> DeserializeSearchReleaseGroupResponse {
let de_offset = 26;
let de_count = 126;
let de_release_group = DeserializeSearchReleaseGroupResponseReleaseGroup { let de_release_group = DeserializeSearchReleaseGroupResponseReleaseGroup {
score: 67, score: 67,
meta: SerdeMbReleaseGroupMeta { meta: SerdeMbReleaseGroupMeta {
@ -121,6 +130,10 @@ mod tests {
}; };
DeserializeSearchReleaseGroupResponse { DeserializeSearchReleaseGroupResponse {
release_groups: vec![de_release_group.clone()], release_groups: vec![de_release_group.clone()],
page: SerdeSearchPage {
offset: de_offset,
count: de_count,
},
} }
} }
@ -134,6 +147,7 @@ mod tests {
meta: rg.meta.into(), meta: rg.meta.into(),
}) })
.collect(), .collect(),
page: de_response.page,
} }
} }

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, Delta, IAppInteractBrowse,
}; };
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,12 @@ 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,12 @@ 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(),

View File

@ -57,7 +57,7 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error> { fn search_artist(&mut self, artist: &ArtistMeta) -> Result<Vec<Match<ArtistMeta>>, Error> {
let query = SearchArtistRequest::new().string(&artist.id.name); let query = SearchArtistRequest::new().string(&artist.id.name);
let paging = PageSettings::with_max_limit(); let paging = PageSettings::default();
let mb_response = self.client.search_artist(&query, &paging)?; let mb_response = self.client.search_artist(&query, &paging)?;
Ok(mb_response Ok(mb_response
@ -83,7 +83,7 @@ impl<Http: IMusicBrainzHttp> IMusicBrainz for MusicBrainz<Http> {
.and() .and()
.release_group(&album.id.title); .release_group(&album.id.title);
let paging = PageSettings::with_max_limit(); let paging = PageSettings::default();
let mb_response = self.client.search_release_group(&query, &paging)?; let mb_response = self.client.search_release_group(&query, &paging)?;
Ok(mb_response Ok(mb_response