From 573c479d4b3de16b78720a92c4e700678061ebf8 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Tue, 19 May 2026 12:17:20 -0600 Subject: [PATCH] feat(slint): load-more pagination for per-type search tabs The Load more button now fetches the next 20-item page for the active category via the per-type QbzCore search methods and appends onto the existing VecModel, so already-loaded rows and their artwork are untouched. category_for_tab maps the tab index to a category and is unit-tested. cc --- crates/qbz-slint/src/main.rs | 38 +++++++++ crates/qbz-slint/src/search.rs | 150 ++++++++++++++++++++++++++++++++- 2 files changed, 187 insertions(+), 1 deletion(-) diff --git a/crates/qbz-slint/src/main.rs b/crates/qbz-slint/src/main.rs index fd13114f4..f481a4209 100644 --- a/crates/qbz-slint/src/main.rs +++ b/crates/qbz-slint/src/main.rs @@ -31,6 +31,8 @@ mod ui_prefs; use std::sync::Arc; +use slint::Model; + use adapter::SlintAdapter; use commands::AppCommand; use qbz_app::shell::AppRuntime; @@ -589,6 +591,42 @@ fn main() -> Result<(), Box> { }); } + // Load more results for the active per-type tab. The offset is the + // count already loaded into that category's list. + { + let runtime = app_runtime.clone(); + let weak = window.as_weak(); + let handle = tokio_rt.handle().clone(); + window.global::().on_load_more(move |tab| { + let Some(w) = weak.upgrade() else { + return; + }; + let Some(category) = search::category_for_tab(tab) else { + return; + }; + let st = w.global::(); + let query = st.get_query().to_string(); + let offset = match category { + search::SearchCategory::Albums => st.get_albums().row_count(), + search::SearchCategory::Tracks => st.get_tracks().row_count(), + search::SearchCategory::Artists => st.get_artists().row_count(), + search::SearchCategory::Playlists => st.get_playlists().row_count(), + } as u32; + let runtime = runtime.clone(); + let weak = weak.clone(); + handle.spawn(async move { + match search::load_more(&runtime, &query, category, offset).await { + Ok(more) => { + let _ = weak.upgrade_in_event_loop(move |w| { + search::append_results(&w, more); + }); + } + Err(e) => log::error!("[qbz-slint] search load-more failed: {e}"), + } + }); + }); + } + // History navigation — back / forward / settings, all recorded by the // nav module so the [<] [>] pair and the mouse buttons stay in sync. { diff --git a/crates/qbz-slint/src/search.rs b/crates/qbz-slint/src/search.rs index a2ba8ea6c..c72bc46d3 100644 --- a/crates/qbz-slint/src/search.rs +++ b/crates/qbz-slint/src/search.rs @@ -10,7 +10,7 @@ use std::sync::Arc; use qbz_app::shell::AppRuntime; use qbz_core::FrontendAdapter; use qbz_models::{Album, Artist, MostPopularItem, Playlist, SearchAllResults, Track}; -use slint::{ComponentHandle, ModelRc, VecModel}; +use slint::{ComponentHandle, Model, ModelRc, VecModel}; use crate::{AlbumCardItem, AppWindow, SearchPlaylistItem, SearchState, SearchTrackItem, SlimItem}; @@ -377,10 +377,158 @@ pub fn reset_search(window: &AppWindow) { state.set_loading(true); } +// ==================== Load-more (pagination) ==================== + +/// Which category a load-more request targets. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum SearchCategory { + Albums, + Tracks, + Artists, + Playlists, +} + +/// Map a results-tab index to the category whose list it paginates. +/// Tab 0 (All) has no single category. +pub fn category_for_tab(tab: i32) -> Option { + match tab { + 1 => Some(SearchCategory::Albums), + 2 => Some(SearchCategory::Tracks), + 3 => Some(SearchCategory::Artists), + 4 => Some(SearchCategory::Playlists), + _ => None, + } +} + +/// A page of additional rows fetched by load-more, ready to append. +pub enum MoreRows { + Albums(Vec), + Tracks(Vec), + Artists(Vec), + Playlists(Vec), +} + +/// Load-more page size (matches the Tauri search page size). +const PAGE_SIZE: u32 = 20; + +/// Fetch the next page for one category, starting at `offset`. +pub async fn load_more( + runtime: &Arc>, + query: &str, + category: SearchCategory, + offset: u32, +) -> Result +where + A: FrontendAdapter + Send + Sync + 'static, +{ + let core = runtime.core(); + match category { + SearchCategory::Albums => { + let page = core + .search_albums(query, PAGE_SIZE, offset, None) + .await + .map_err(|e| e.to_string())?; + Ok(MoreRows::Albums( + page.items.into_iter().map(map_album).collect(), + )) + } + SearchCategory::Tracks => { + let page = core + .search_tracks(query, PAGE_SIZE, offset, None) + .await + .map_err(|e| e.to_string())?; + Ok(MoreRows::Tracks( + page.items.into_iter().map(map_track).collect(), + )) + } + SearchCategory::Artists => { + let page = core + .search_artists(query, PAGE_SIZE, offset, None) + .await + .map_err(|e| e.to_string())?; + Ok(MoreRows::Artists( + page.items.iter().map(map_artist).collect(), + )) + } + SearchCategory::Playlists => { + let page = core + .search_playlists(query, PAGE_SIZE, offset) + .await + .map_err(|e| e.to_string())?; + Ok(MoreRows::Playlists( + page.items.into_iter().map(map_playlist).collect(), + )) + } + } +} + +/// Append fetched rows to the matching `SearchState` list. Pushes onto the +/// existing `VecModel` so already-loaded rows (and any resolved artwork) +/// are untouched. Runs on the Slint event loop. +pub fn append_results(window: &AppWindow, more: MoreRows) { + let state = window.global::(); + match more { + MoreRows::Albums(rows) => { + if let Some(vm) = state + .get_albums() + .as_any() + .downcast_ref::>() + { + for row in rows { + vm.push(album_item(row)); + } + } + } + MoreRows::Tracks(rows) => { + if let Some(vm) = state + .get_tracks() + .as_any() + .downcast_ref::>() + { + for row in rows { + vm.push(track_item(row)); + } + } + } + MoreRows::Artists(rows) => { + if let Some(vm) = state + .get_artists() + .as_any() + .downcast_ref::>() + { + for row in rows { + vm.push(artist_item(row)); + } + } + } + MoreRows::Playlists(rows) => { + if let Some(vm) = state + .get_playlists() + .as_any() + .downcast_ref::>() + { + for row in rows { + vm.push(playlist_item(row)); + } + } + } + } +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn category_for_tab_maps_per_type_tabs() { + assert_eq!(category_for_tab(0), None); + assert_eq!(category_for_tab(1), Some(SearchCategory::Albums)); + assert_eq!(category_for_tab(2), Some(SearchCategory::Tracks)); + assert_eq!(category_for_tab(3), Some(SearchCategory::Artists)); + assert_eq!(category_for_tab(4), Some(SearchCategory::Playlists)); + assert_eq!(category_for_tab(9), None); + } + #[test] fn mmss_pads_seconds() { assert_eq!(mmss(5), "0:05");