feat(locallibrary): Albums tab — server-paginated chunked grid

First Local Library data slice. The Albums tab browses the metadata-grouped
local albums via the performant server-paginated path (the same query Tauri's
chunked store uses), reusing the shared AlbumCollectionView grid — no new
rendering. Scales past the documented ~16K-row Tracks freeze by never
materializing the full set.

- local_library.rs: map_local_album (LocalAlbum -> the plain album_map
  AlbumCard, Send-safe; the non-Send slint::Image conversion via to_item
  happens on the UI thread), local_quality (Hz -> kHz), sort_params; a
  blocking fetch_albums_page over LibraryDatabase::get_albums_metadata_page
  (spawn_blocking); apply/append + reload/ensure/load_more driven by an
  ALBUMS_GEN generation guard so a search/sort/retry can't be clobbered by a
  stale in-flight page.
- LocalLibraryView.slint: Albums body reusing AlbumCollectionView (grid,
  show-favorite: false), a search + sort toolbar (both server-side), auto
  load-more on scroll-near-bottom (DiscoverBrowse pattern), and loading /
  error+retry / empty states.
- Local covers: ArtworkTarget::LocalAlbumCard + spawn_local_loads resolve the
  on-disk cover path through the source-aware ArtworkRef::LocalFile pipeline;
  the target carries the generation so a cover decoded after a reload is
  dropped instead of landing on a replaced row.
- state.slint: LocalLibraryState album fields + LocalLibraryActions callbacks.
- main.rs: navigate_local_library lazily loads page 1 on first visit; the
  on_albums_* callbacks wire search/sort/load-more/retry (open-album /
  open-artist / album-action are TODO no-ops until the detail + playback slices).

List/group/A-Z/quality filters, album detail, and playback are later slices.
include_qobuz_downloads mirrors the show_in_library setting default (true).
Built green (cargo check -p qbz-slint); runtime smoke-test pending.

cc
This commit is contained in:
vicrodh committed 2026-05-28 22:44:18 -06:00
1 parent 6974721b7a
commit 835a6cad85
5 files changed
+677 -11

No files matched your search

+36
View File
@@ -344,6 +344,25 @@ export global LocalLibraryState {
in property <int> artist-count: 0;
in property <int> track-count: 0;
in property <int> folder-count: 0;
// --- Albums tab (server-paginated, metadata-grouped local albums) ----
// `albums` is the accumulated paged set (also the artwork target). Sort
// + search are pushed to SQL (the performant path); paging fetches the
// next chunk on scroll. List / group / A-Z / filters land with the next
// slice — this slice ships the chunked grid.
in property <[AlbumCardItem]> albums: [];
in property <int> albums-total: 0;
in property <bool> albums-loading: false; // initial page fetch
in property <bool> albums-loading-more: false; // subsequent pages
in property <bool> albums-has-more: false;
in property <bool> albums-load-failed: false;
in-out property <int> albums-next-offset: 0;
in-out property <string> albums-search: "";
// "artist-asc" | "artist-desc" | "title-asc" | "title-desc"
// | "year-desc" | "year-asc"
in-out property <string> albums-sort: "artist-asc";
// "grid" | "list" — the list view toggle lands with the next slice.
in-out property <string> albums-view-mode: "grid";
}
export global LocalLibraryActions {
@@ -352,6 +371,23 @@ export global LocalLibraryActions {
callback select-tab(string /* "albums"|"artists"|"folders"|"tracks" */);
// Open Settings > Local Library (the gear button — management lives there).
callback open-settings();
// --- Albums tab ---
// Re-query page 1 with the current search text (server-side LIKE).
callback albums-search(string /* query */);
// Set the sort key + re-query page 1 (server-side ORDER BY).
callback albums-set-sort(string /* sort key */);
// Fetch + append the next page (driven by scroll-near-bottom).
callback albums-load-more();
// Retry after a load error.
callback albums-retry();
// Open a local album's detail view (lands with the album-detail slice).
callback open-album(string /* album id */);
// Open an artist (lands with the Artists slice).
callback open-artist(string /* artist id */);
// Per-card action (play / queue / play-next) — lands with the playback
// slice. Wired now so the shared AlbumCollectionView contract is honored.
callback album-action(string /* album id */, string /* action */);
}
// Global toast state, driven by crate::toast. The Toast overlay (mounted