feat(slint): searchType filter dropdown for per-type tabs
The Albums / Tracks / Artists tabs get a QbzSelect filter (No filter / Main artist / Performer / Composer / Label / Release name). Changing it re-queries that category from offset 0 with the search_type and replaces the list; load-more carries the active filter forward. The filter resets on every new search. search_type_for_filter is unit-tested. cc
This commit is contained in:
4 files changed
+116
-8
No files matched your search
@@ -609,6 +609,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
};
|
||||
let st = w.global::<SearchState>();
|
||||
let query = st.get_query().to_string();
|
||||
let filter = search::search_type_for_filter(st.get_filter_index());
|
||||
let offset = match category {
|
||||
search::SearchCategory::Albums => st.get_albums().row_count(),
|
||||
search::SearchCategory::Tracks => st.get_tracks().row_count(),
|
||||
@@ -619,7 +620,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let weak = weak.clone();
|
||||
let image_cache = image_cache.clone();
|
||||
handle.spawn(async move {
|
||||
match search::load_more(&runtime, &query, category, offset).await {
|
||||
match search::load_more(&runtime, &query, category, filter, offset).await {
|
||||
Ok(more) => {
|
||||
let jobs = search::artwork_jobs_for_more(&more, offset as usize);
|
||||
let _ = weak.upgrade_in_event_loop(move |w| {
|
||||
@@ -633,6 +634,42 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
});
|
||||
}
|
||||
|
||||
// Change the searchType filter on a per-type tab: re-query that
|
||||
// category from offset 0 and replace its list.
|
||||
{
|
||||
let runtime = app_runtime.clone();
|
||||
let weak = window.as_weak();
|
||||
let handle = tokio_rt.handle().clone();
|
||||
let image_cache = image_cache.clone();
|
||||
window.global::<SearchActions>().on_filter_changed(move |index| {
|
||||
let Some(w) = weak.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let st = w.global::<SearchState>();
|
||||
st.set_filter_index(index);
|
||||
let Some(category) = search::category_for_tab(st.get_tab()) else {
|
||||
return;
|
||||
};
|
||||
let query = st.get_query().to_string();
|
||||
let search_type = search::search_type_for_filter(index);
|
||||
let runtime = runtime.clone();
|
||||
let weak = weak.clone();
|
||||
let image_cache = image_cache.clone();
|
||||
handle.spawn(async move {
|
||||
match search::load_more(&runtime, &query, category, search_type, 0).await {
|
||||
Ok(more) => {
|
||||
let jobs = search::artwork_jobs_for_more(&more, 0);
|
||||
let _ = weak.upgrade_in_event_loop(move |w| {
|
||||
search::replace_category(&w, more);
|
||||
});
|
||||
artwork::spawn_loads(jobs, weak.clone(), image_cache);
|
||||
}
|
||||
Err(e) => log::error!("[qbz-slint] search filter failed: {e}"),
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// History navigation — back / forward / settings, all recorded by the
|
||||
// nav module so the [<] [>] pair and the mouse buttons stay in sync.
|
||||
{
|
||||
|
||||
@@ -375,6 +375,7 @@ pub fn reset_search(window: &AppWindow) {
|
||||
state.set_artists_total(0);
|
||||
state.set_playlists_total(0);
|
||||
state.set_most_popular_kind("".into());
|
||||
state.set_filter_index(0);
|
||||
state.set_loading(true);
|
||||
}
|
||||
|
||||
@@ -401,6 +402,19 @@ pub fn category_for_tab(tab: i32) -> Option<SearchCategory> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a filter-dropdown index to the Qobuz `search_type` value.
|
||||
/// Index 0 (No filter) maps to `None`.
|
||||
pub fn search_type_for_filter(index: i32) -> Option<String> {
|
||||
match index {
|
||||
1 => Some("MainArtist".into()),
|
||||
2 => Some("Performer".into()),
|
||||
3 => Some("Composer".into()),
|
||||
4 => Some("Label".into()),
|
||||
5 => Some("ReleaseName".into()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A page of additional rows fetched by load-more, ready to append.
|
||||
pub enum MoreRows {
|
||||
Albums(Vec<AlbumRow>),
|
||||
@@ -417,16 +431,18 @@ pub async fn load_more<A>(
|
||||
runtime: &Arc<AppRuntime<A>>,
|
||||
query: &str,
|
||||
category: SearchCategory,
|
||||
search_type: Option<String>,
|
||||
offset: u32,
|
||||
) -> Result<MoreRows, String>
|
||||
where
|
||||
A: FrontendAdapter + Send + Sync + 'static,
|
||||
{
|
||||
let core = runtime.core();
|
||||
let search_type = search_type.as_deref();
|
||||
match category {
|
||||
SearchCategory::Albums => {
|
||||
let page = core
|
||||
.search_albums(query, PAGE_SIZE, offset, None)
|
||||
.search_albums(query, PAGE_SIZE, offset, search_type)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(MoreRows::Albums(
|
||||
@@ -435,7 +451,7 @@ where
|
||||
}
|
||||
SearchCategory::Tracks => {
|
||||
let page = core
|
||||
.search_tracks(query, PAGE_SIZE, offset, None)
|
||||
.search_tracks(query, PAGE_SIZE, offset, search_type)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(MoreRows::Tracks(
|
||||
@@ -444,7 +460,7 @@ where
|
||||
}
|
||||
SearchCategory::Artists => {
|
||||
let page = core
|
||||
.search_artists(query, PAGE_SIZE, offset, None)
|
||||
.search_artists(query, PAGE_SIZE, offset, search_type)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(MoreRows::Artists(
|
||||
@@ -516,6 +532,30 @@ pub fn append_results(window: &AppWindow, more: MoreRows) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace one category's `SearchState` list wholesale — used when the
|
||||
/// searchType filter changes and the category is re-queried from offset 0.
|
||||
pub fn replace_category(window: &AppWindow, more: MoreRows) {
|
||||
let state = window.global::<SearchState>();
|
||||
match more {
|
||||
MoreRows::Albums(rows) => {
|
||||
let items: Vec<AlbumCardItem> = rows.into_iter().map(album_item).collect();
|
||||
state.set_albums(ModelRc::new(VecModel::from(items)));
|
||||
}
|
||||
MoreRows::Tracks(rows) => {
|
||||
let items: Vec<SearchTrackItem> = rows.into_iter().map(track_item).collect();
|
||||
state.set_tracks(ModelRc::new(VecModel::from(items)));
|
||||
}
|
||||
MoreRows::Artists(rows) => {
|
||||
let items: Vec<SlimItem> = rows.into_iter().map(artist_item).collect();
|
||||
state.set_artists(ModelRc::new(VecModel::from(items)));
|
||||
}
|
||||
MoreRows::Playlists(rows) => {
|
||||
let items: Vec<SearchPlaylistItem> = rows.into_iter().map(playlist_item).collect();
|
||||
state.set_playlists(ModelRc::new(VecModel::from(items)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Artwork jobs ====================
|
||||
|
||||
/// Build artwork download jobs for a freshly applied `SearchData` — one
|
||||
@@ -620,6 +660,15 @@ mod tests {
|
||||
assert_eq!(category_for_tab(9), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_type_for_filter_maps_dropdown_index() {
|
||||
assert_eq!(search_type_for_filter(0), None);
|
||||
assert_eq!(search_type_for_filter(1), Some("MainArtist".to_string()));
|
||||
assert_eq!(search_type_for_filter(3), Some("Composer".to_string()));
|
||||
assert_eq!(search_type_for_filter(5), Some("ReleaseName".to_string()));
|
||||
assert_eq!(search_type_for_filter(99), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mmss_pads_seconds() {
|
||||
assert_eq!(mmss(5), "0:05");
|
||||
|
||||
@@ -15,6 +15,7 @@ import { SearchTrackRow } from "SearchTrackRow.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { ContextMenu } from "../primitives/ContextMenu.slint";
|
||||
import { ContextMenuItem } from "../primitives/ContextMenuItem.slint";
|
||||
import { QbzSelect } from "../primitives/QbzSelect.slint";
|
||||
|
||||
// One tab in the results tab strip.
|
||||
component SearchTab inherits Rectangle {
|
||||
@@ -375,6 +376,26 @@ export component SearchResultsView inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// searchType filter — shown on the Albums / Tracks / Artists tabs.
|
||||
if SearchState.tab >= 1 && SearchState.tab <= 3: HorizontalLayout {
|
||||
alignment: start;
|
||||
QbzSelect {
|
||||
menu-width: 180px;
|
||||
options: [
|
||||
@tr("No filter"),
|
||||
@tr("Main artist"),
|
||||
@tr("Performer"),
|
||||
@tr("Composer"),
|
||||
@tr("Label"),
|
||||
@tr("Release name"),
|
||||
];
|
||||
current-index: SearchState.filter-index;
|
||||
selected(i) => {
|
||||
SearchActions.filter-changed(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scrollable results region.
|
||||
Flickable {
|
||||
vertical-stretch: 1;
|
||||
|
||||
@@ -372,8 +372,9 @@ export global SearchState {
|
||||
in property <AlbumCardItem> most-popular-album;
|
||||
in property <SlimItem> most-popular-artist;
|
||||
in property <SearchTrackItem> most-popular-track;
|
||||
// Active searchType filter: "" | "MainArtist" | "Performer" | ...
|
||||
in property <string> filter-type;
|
||||
// Active searchType filter, as a dropdown index (0 = no filter).
|
||||
// The controller owns the index -> API-value mapping.
|
||||
in property <int> filter-index: 0;
|
||||
}
|
||||
|
||||
// Search actions wired to the Rust search controller.
|
||||
@@ -384,8 +385,8 @@ export global SearchActions {
|
||||
callback live(string);
|
||||
// User picked a results tab (0..4).
|
||||
callback tab-changed(int);
|
||||
// searchType filter changed.
|
||||
callback filter-changed(string);
|
||||
// searchType filter changed — the new dropdown index.
|
||||
callback filter-changed(int);
|
||||
// Request the next page for the given tab.
|
||||
callback load-more(int);
|
||||
}
|
||||
Reference in new issue
Block a user