A Local Library album, or the Tracks list, now plays through with auto-advance
across mixed local/offline sources.
The one change to the shared playback path is additive and provably safe for
Qobuz: play_audible gains a guard at the top — when the CURRENT queue track's
source is "local" (and its id matches), it plays the file from disk via
play_local_file_audible (read -> player.play_data + CUE seek); otherwise it
falls through to the EXISTING offline/Qobuz tier-walk, byte-for-byte unchanged.
Qobuz/offline queue tracks never carry source "local", so their behavior cannot
change. Auto-advance, next/previous and play-all all route through play_audible,
so they become source-aware for free. current_track() releases the queue read
lock before play (no nested lock / deadlock).
- qbz-player: QueueManager::current() (additive).
- qbz-core: QbzCore::current_track() (additive).
- qbz-slint playback.rs: source-aware play_audible + play_local_file_audible
(extracted) + play_local_tracks_now + play_local_album
(get_album_tracks_metadata) + play_local_tracks_from (search_with_filter);
replaces the Stage-1 single-track helper.
- main.rs: album card "play" -> play the album; Tracks row "play" -> play from
that row down the searched list. (play-next / queue still stubbed.)
Built green (cargo check). Local-file gapless isn't wired (local tracks aren't
prefetched, so local/mixed transitions use the simple track-ended -> next path);
Qobuz gapless is unchanged. Runtime smoke-test pending: (1) Qobuz streaming
still plays + advances, (2) an offline album advances, (3) a local album/list
advances.
cc
First source-aware playback stage. Clicking a track in the Local Library
Tracks tab now plays it, mirroring Tauri's v2_library_play_track routing:
- offline-cached (qobuz_download) tracks reuse the shared tier-walk
(after_track_change -> play_track_resolved -> offline resolve -> play_data),
keyed by the Qobuz id;
- local user files are read from disk and handed to the player's play_data
seam (which extracts the sample rate + drives the PROTECTED device init,
untouched), with a CUE seek for virtual tracks.
Additive: play_local_library_track + local_queue_track (mirrors Tauri's
local_track_to_queue_track) are new; the shared Qobuz play_audible path is
unchanged (no regression risk to streaming). Single-track play (a 1-track
queue) — album/queue + auto-advance need a source-aware advancement handler
and land in the next stage. Built green (cargo check -p qbz-slint);
runtime smoke-test pending (a local FLAC + an offline track should play).
cc
Adds the Tracks browse tab. Each page is a search_with_filter_page query
(new paged variant of search_with_filter) appended on scroll-near-bottom, so
the list never materializes the whole table — the documented ~16K-row Tracks
freeze. Reuses the shared TrackRow in a virtualized ListView + ListScrollbar
(the Favorites Tracks pattern) and the Albums controller's generation-guard
(TRACKS_GEN) so a search/retry can't be clobbered by a stale in-flight page.
- qbz-library: search_with_filter_page(query, offset, limit, include_qobuz,
exclude_network) — same ordering as search_with_filter, LIMIT/OFFSET.
- local_library.rs: map_local_track (LocalTrack is Send -> crosses
spawn_blocking; TrackItem built on the UI thread), fetch_tracks_page,
apply/append, reload/ensure/load_more (TRACKS_GEN-guarded).
- LocalLibraryView.slint: Tracks body (ListView + TrackRow, show-artwork off
per Tauri's perf default, show-favorite/show-download off) + a search
toolbar + loading/error/empty states.
- state.slint + main.rs: tracks state/actions + lazy load on first visit +
search/load-more/retry wiring.
Group-by, multi-select/bulk, and per-row playback land with the source-aware
playback slice (track-action is a stub, like Albums). Built green
(cargo check -p qbz-slint); runtime smoke-test pending.
cc
Generalize resolve_album_cover_fallback beyond a single track + cover.jpg:
scan several of the album's tracks (CMAF keeps each track in its own folder,
only some of which may carry a cover), check both the track folder and its
parent (multi-disc layouts keep art one level up), and accept the common
local cover filenames (cover/folder/front, jpg/png). Resolves more on-disk
covers without touching the paged query. Albums whose offline bundle never
saved a cover (segments only) still need an offline-cache cover re-fetch.
cc
Two fixes from the first Albums-grid test pass.
Offline/folder covers (the reported "cover exists in the folder but isn't
shown"): some albums — offline-cached CMAF bundles and folder downloads —
have no artwork_path in the index even though a cover.jpg sits in the track
folder (older offline-cache downloads didn't backfill it). Add a frontend-
agnostic LibraryDatabase::resolve_album_cover_fallback(group_key) that finds
a representative track, derives its folder (the path itself for a CMAF bundle
dir, else the parent), and returns <folder>/cover.jpg when it exists. The
Slint Albums mapper calls it whenever the album has no artwork. Verified
against the live index: it resolves the folder covers that exist on disk
(FLAC-layout + CMAF-with-cover). Older CMAF bundles that never downloaded a
cover (segments only) still show the placeholder — those need a cover
re-fetch (offline-cache maintenance), tracked separately.
Source badge: the Local Library album card now shows the always-visible
source badge inside the artwork (bottom-right) — a hard-drive glyph for local
files, the Qobuz logo for offline copies, the Plex logo for Plex. Added
AlbumCard.show-source-badge (default off; threaded through AlbumCollectionView
/AlbumGrid) + AlbumCard.source; album_map now carries the real source and
map_local_album maps user->local / qobuz_download / plex. Small rounded
square, not a pill (ADR-008). Logos copied into the Slint icon assets.
Built green (cargo check -p qbz-slint); runtime smoke-test pending.
cc
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
The shared AlbumCard always rendered the favorite heart (overlay button +
"Add to favorites" menu item). Local Library albums are not Qobuz-favoritable
(Tauri's AlbumCardLibraryLite hides favorite/genre), so the upcoming local
grid needs to suppress it.
Add an additive `show-favorite: bool` (default true) on AlbumCard, threaded
through AlbumCollectionView -> AlbumGrid. The fav overlay button uses
`visible:` (not an `if`) so it stays referenceable by `overlay-on` (keeping
the Qobuz hover-no-flicker behavior intact); the menu item is gated with an
`if`. All existing callers (Discover / Favorites / Label) default to true,
so behavior is unchanged.
cc
The nav-scaffolding view (c50ae171) invented its own chrome: a title+stats
header with no NavButtons (so no back/forward history nav), plus a tab bar
of border-radius:18px pill chips — an ADR-008 violation. Rebuild it 1:1 on
the app's mandatory fixed-chrome header (the same FavoritesView/Discover
use):
- Row 1: NavButtons (back/forward) + "Local Library" title (left), the tab
menu floated right. Tabs are underline tabs (LibTab/LibTabMenu mirroring
FavTab/FavTabMenu) — no pills.
- The settings gear becomes a standalone icon-only circle matching
NavButtons (the one circular shape ADR-008 permits), routing to
Settings > Local Library.
- Row 2 (per-tab actions + toolbar) is established with vertical-stretch:0
so it doesn't inflate while empty; filled per slice (Albums next).
- 1px divider, then a single scrolling content area with a placeholder.
Wiring (select-tab / open-settings, nav, mount) is unchanged — only the
.slint chrome was rebuilt.
cc
Add the frontend-agnostic contract for the source-aware playback context:
- qbz-models: new `source` module with `PlaybackSource` (Qobuz / OfflineCache
/ Local / Plex; parses the existing stringly-typed `source` field, defaults
to Qobuz) and `ArtworkRef` (Remote / LocalFile / PlexThumb / Embedded /
None). `QueueTrack::source_kind()` + `QueueTrack::artwork_ref()` derive both
without changing the wire format. `PlaybackSource::is_qobuz_streamable()`
seeds the Qobuz Connect queue gate (only Qobuz-sourced tracks are castable).
- qbz-slint artwork pipeline: `fetch_and_decode_ref(&ArtworkRef, ...)` resolves
cover bytes per origin — HTTP/Plex through the disk cache, local files read
directly, embedded bytes in place. This fixes the root cause of local/Plex
artwork never reaching the UI (the loader was HTTP-only). The URL-only
`fetch_and_decode` becomes a thin wrapper, so all existing card/row jobs are
unchanged. The now-playing loader now takes an `ArtworkRef` derived from the
queue track, so local-library/Plex covers reach the now-playing bar.
Groundwork for the Local Library Slint port (source-aware playback context
designed in from the start). qbz-models tests pass; qbz-slint cargo check green.
cc
Move the Plex LAN client, hand-rolled XML metadata parsing, and the
SQLite plex_cache.db logic out of src-tauri/src/plex/mod.rs into a new
qbz-plex core crate so it can be shared by every frontend (Tauri, Slint,
TUI) per ADR-006. Plex previously lived entirely in src-tauri (~1715
lines), which blocked the Slint Local Library port from shipping Plex.
The extraction is mechanical: the module was already almost entirely
plain Rust (reqwest + rusqlite + serde + hand-rolled XML). The only
Tauri coupling was a dead plex_play_track command bound to AppState
(unreferenced; the live path is commands_v2::v2_plex_play_track, which
calls plex_resolve_track_media + AppState.player itself). That dead fn
and the vestigial #[tauri::command] attributes are dropped.
src-tauri/src/plex/mod.rs becomes a `pub use qbz_plex::*;` re-export so
every existing crate::plex::* call site compiles unchanged. Full Tauri
workspace cargo check stays green; qbz-plex tests pass.
cc
The Cloudflare proxy funnelled every user's MusicBrainz traffic onto a few
egress IPs, and MusicBrainz rate-limits 1 req/s per IP, so the budget was
shared across the whole userbase and produced 503s that worsen with growth.
MB read access needs no key and authentication does not raise the limit, so
the proxy added only the funnel; going per-IP makes the budget per-user.
- qbz-integrations MusicBrainzConfig default is now use_proxy=false (direct).
- client: handle HTTP 503 + Retry-After (not just 429, which the proxy used
to translate); add parse_retry_after; route search_recording_by_isrc and
search_artist through handle_response_status.
- src-tauri session_lifecycle: MusicBrainz V2 init/teardown pass use_proxy=false.
Slint inherits this via CoreBridge's default-constructed client.
cc
The panel improvised its own title + free-form paragraph + ghost buttons,
breaking the established Settings structure. Rebuilt it like Audio/Playback/
Appearance: a GroupHeader ("OFFLINE CACHE") + SettingRow rows (label +
description + a SecondaryButton control on the right) — Manage offline cache /
Cache folder / Clear offline cache. No panel-level title (the left sub-nav
names the section). Keeps the layout consistent for the offline settings we'll
add here later.
cc
Per user feedback:
- Multi-select: a checkbox on every track row + "Select all"/"Clear" toggle +
a bulk bar ("N selected" / Re-download / Remove). Selection is an in-place
model edit (offline_manager::toggle_select / set_all_selected) with a live
selected-count; bulk reads the checked ids and runs redownload_track /
remove_cached per track.
- Moved the status glyph to the END of the row (albums and tracks) — it read
as "missing art" at the leading edge under the album-grouping model.
- Track rows now show their position number in the leading slot (filling what
looked empty). (Position within the album; the catalog track number isn't in
the cache index.db — a follow-up if the real number is wanted.)
qbz-slint compiles clean.
cc
Closes the v1 gaps in the offline manager toward Tauri parity:
- Album COVERS: rows load the on-disk cover.jpg (per album). Built Send-safely
— the worker passes the cover PATH (string), the image is decoded on the UI
thread (slint::Image is not Send).
- Two-pane layout with an A-Z ARTIST RAIL (left, "All artists" + per-artist
"N albums · M tracks", selectable) + the discography list (right).
- SORT dropdown (A-Z / Recent / Largest / Smallest; Recent uses the DB's
last_accessed_at DESC order) and a "Failed only" toggle. Filter state lives
Rust-side (offline_manager) so rebuild can read it off-thread; mirrored back
onto OfflineManagerState for the UI.
- Limit now PERSISTS to disk (<audio>/offline_limit), restored on activate via
apply_persisted_limit; the manager's edit-limit writes it.
- Manager mutations (remove/redownload/clear) now rebuild on completion
(offline_manager::rebuild made pub, called at the end of each cache op) —
the 500ms timed reload hack is gone.
Deferred (user-accepted): the "track no longer available on Qobuz" case —
offline copy still plays, but warn it can't be re-downloaded / won't work on
Qobuz Connect.
qbz-slint compiles clean.
cc
- offline_cache::cache_album fetches an album's tracks and batches them; wired
to a new ("album","cache") media-action arm + a cloud-download CircleAction in
the AlbumPageView header action row.
- Favorites track rows now enable show-download, so the direct per-row offline
button shows there too (not just album rows / cached tracks).
Rounds out Slice 2: trigger surfaces (per-row button on album+favorites, context
menu everywhere, bulk multi-select, album header), live status (spinner/check/
alert) + seeding, and the unlock padlock. qbz-slint compiles clean.
cc
A cached track now shows its "ready" check when a view is built, persisting
across navigation within the session (was: only live downloads showed state).
- offline_cache.rs: a session-wide CACHED_IDS set (mirrors fav_cache), seeded
from index.db ready rows via load_cached_ids() on login/restore, kept in
sync as downloads complete (mark_cached true) and copies are removed (false).
is_cached(id) reads it.
- The 7 TrackItem builders seed cache-status from is_cached (favorites/search
clone/precompute the id to avoid moving it before the borrow).
- auth.rs calls load_cached_ids after the offline cache activates.
qbz-slint compiles clean.
cc
The queued/downloading state showed a static loader-circle. Split it into its
own `if` arm that continuously rotates via animation-tick (same technique as
LoadingSpinner), isolated so the per-frame tick only drives a row while it is
actually downloading. The idle/ready/failed glyph stays static. Communicates
that the backend is working.
cc
Adds the 6th MultiSelectBar action -> offline_cache::cache_tracks(selected),
mirroring Tauri's bulk caching. Clears the selection after queuing. (Also
retires the cache_tracks dead-code warning.)
cc
The CMAF download opened the SecretBox (OS keyring -> secret-service -> a
synchronous zbus D-Bus round-trip) directly on a tokio async worker, which
panicked ("Cannot start a runtime from within a runtime") and poisoned the
vault mutex — every later attempt then failed and fell back to the legacy
plain-FLAC path. Playback never hit this because load_cmaf_bundle already runs
inside spawn_blocking.
- downloader: wrap the vault open + key wrap in tokio::task::spawn_blocking
(off the async pool), mirroring the playback decrypt path.
- secret_vault: get_or_init recovers a poisoned mutex (into_inner) instead of
wedging the vault for the rest of the session.
Verified at runtime: CMAF bundle now persists (tracks-cmaf/<id>/) and plays
back decrypted; no panic, no legacy fallback.
cc
TrackRow now renders the offline state the controller maintains:
- The offline button is stateful: cloud-download (none, click -> cache) ->
loader-circle accent (queued/downloading) -> circle-check accent (ready,
click -> uncache) -> triangle-alert red (failed, click -> retry). Shown on
surfaces that enable it AND on any row with active cache state.
- The unlock PADLOCK (user-requested, back from Tauri): a pulsing lock glyph
overlays the play cell while the track's CMAF bundle decrypts, driven by the
`unlocking` flag the row_sink toggles on UnlockStart/UnlockEnd. New lucide
lock.svg / lock-open.svg assets.
The context menu's "Make available offline" already routes to caching. Testable
loop: cache a track (album button or context menu) -> watch it download -> play
it offline -> padlock pulses during decrypt. qbz-slint compiles clean.
cc
The Rust half of the trigger/status UI:
- offline_cache.rs: cache_track / cache_tracks / remove_cached + row_sink — a
CacheEventSink that reflects download status (queued/downloading%/ready/failed)
and the unlock padlock onto every visible row, plus terminal toasts. cache_*
pre-flight the limit, insert the queued row, and spawn the shared
spawn_track_cache_download with the core client.
- TrackItem gains cache-status / cache-progress / unlocking; set_row_cache_status
+ set_row_unlocking mirror set_row_favorite (walk the 6 track models + search
hero). All 7 exhaustive initializers updated.
- media-action arms ("track","cache"|"download"|"uncache") dispatch to the
controller; the play path now passes row_sink so UnlockStart/End fire the
padlock state during CMAF decrypt.
Compiles clean. Next: the TrackRow.slint affordance (make-offline button + ring
/ check / warn) and the padlock glyph that render these fields.
cc
- offline.rs: constructs + owns a per-user OfflineCacheState (same index.db +
library.db paths as Tauri, so the offline store is shared across frontends);
activated on login/restore, torn down on logout (hooks in auth.rs).
- playback.rs: play_audible + the gapless trigger now route through QbzCore's
play_track_resolved / fetch_for_gapless_resolved, so an offline-cached track
resolves from disk (decrypt -> play_data) before hitting the network. The
unlock-animation sink is None for now (padlock lands with the Slice 2 UI).
qbz-slint compiles clean. Offline PLAYBACK now works in Slint; the "Make
offline" trigger UI + per-row status + the padlock are next (Slice 2).
cc
The shared tier-walk compose methods both frontends call (player untouched):
- play_track_resolved: offline-first (resolve_offline_bytes -> play_data, warms
L1) else Player::play_track (which already does L1/L2 -> CMAF -> legacy).
- fetch_for_gapless_resolved: L1/L2 (player cache) -> offline -> network; the
offline tier is only tried when the track is NOT in the player cache (the
CMAF decrypt is slow, so a cached copy wins — matches the Tauri gapless order).
Both take the frontend's OfflineCacheState by ref (no duplicate DB handle) and
an optional CacheEventSink for the unlock animation. Compiles green.
cc
Adds qbz_core::offline_resolve::resolve_offline_bytes — the OFFLINE tier of
the playback tier-walk, frontend-agnostic and player-untouched (the user's
locked Slice-1 decision): resolve a track's offline-cached bytes by Qobuz id
(v2 CMAF decrypt via the secret vault, optionally emitting UnlockStart/End
through a sink; v1 plain-FLAC read; None on miss).
qbz-core now depends on qbz-offline-cache (aligns with the long-intended
`offline` feature seam). Both frontends will slot this in before the network
tier: Tauri keeps its hardware/quality gate around the returned bytes; Slint
calls it before Player::play_track. Player's L1/L2 + network path is unchanged.
Next: QbzCore compose methods (play_track_resolved / gapless), wire Slint's
session to construct + own an OfflineCacheState, route the Slint play path
through the resolver, and point Tauri's offline branch at it.
cc
Completes Slice 0. src-tauri now depends on the qbz-offline-cache crate; the
old src-tauri/src/offline_cache/ implementation files are deleted. What remains
in that module is a thin shim: a re-export of the crate plus the Tauri glue.
- offline_cache/mod.rs: pub use qbz_offline_cache::* + tauri_cache_sink (maps
CacheEvent -> the exact legacy offline:caching_*/offline:unlock_* app.emit
shapes, so the Svelte frontend is unchanged) + core_client (resolves the core
qbz_qobuz::QobuzClient from the CoreBridge for the download pipeline).
- Cache/redownload/migration commands: drop the legacy crate::api::QobuzClient
(state.client) + the CoreBridge double-plumb; pass one core client + an
app.emit-backed CacheEventSink. Unused `state: State<AppState>` params dropped.
- Playback + library cached-resolution call sites: pass &tauri_cache_sink(..)
to load_cmaf_bundle_with_ui_events instead of &app_handle.
- session_lifecycle: apply_persisted_limit now reads the persisted value here;
purge takes &library.db directly.
- migration: qobuz_client is the Option-wrapped core client (consistent with the
downloader); None mid-loop is recorded as a per-track failure.
Full src-tauri cargo check passes; behaviour-preserving for the Svelte frontend.
Backend extraction (ADR-006) done — next is Slice 1 (shared tier-walk in core).
cc
Completes the crate extraction (Slice 0 backend half):
- state: OfflineCacheState holder (db/fetcher/cache_dir/limit/semaphore/
library_db + dir-layout helpers + new/init_at/teardown). apply_persisted_limit
now takes Option<u64> (the caller reads it from offline_settings) instead of
borrowing src-tauri's OfflineState, so the struct is frontend-agnostic and
Slint can own one too.
- purge: purge_all_cached_files takes &Mutex<Option<LibraryDatabase>> directly
instead of src-tauri's LibraryState wrapper.
The crate is now self-contained (13 modules, no Tauri). Next: re-wire the
Tauri commands onto it with an app.emit-backed CacheEventSink and delete the
old src-tauri/src/offline_cache/ module.
cc
Moves the download pipeline into the crate and removes its Tauri coupling:
- event: new CacheEvent enum (Started/Progress/Completed/Processed/Failed/
UnlockStart/UnlockEnd) + CacheFormat + the CacheEventSink callback type.
- downloader: StreamFetcher + try_cmaf_offline_download + spawn_track_cache_download
now report through a &CacheEventSink instead of tauri AppHandle::emit. The
CoreBridge param is gone — both the CMAF raw download, metadata fetch, and the
legacy stream-url fallback unify on a single core qbz_qobuz::QobuzClient handle.
TLS uses the workspace rustls (same backend qbz-qobuz uses for all CDN traffic).
- playback: adds load_cmaf_bundle_with_ui_events that emits UnlockStart/UnlockEnd
through the sink (the pure load_cmaf_bundle is unchanged).
Crate compiles headless; 5 unit tests pass. Remaining for Slice 0:
OfflineCacheState + purge, then re-wire the Tauri commands with an
app.emit-backed sink and delete the old src-tauri module.
cc
Continues Slice 0. Moves the near-pure modules, re-pointing the src-tauri
client/library types to the core crates (the make-or-break for a mechanical
extraction — verified the field shapes match):
- metadata: fetch_complete_metadata now takes &qbz_qobuz::QobuzClient (was
crate::api::QobuzClient); body compiles unchanged against qbz_models.
- migration: crate::api::QobuzClient -> qbz_qobuz::QobuzClient,
crate::library::database::LibraryDatabase -> qbz_library::LibraryDatabase,
super::metadata -> crate::metadata.
- playback: the PURE load_cmaf_bundle fn (the UI-events wrapper stays out —
it becomes a CacheEventSink call in the next commit).
Deps added: qbz-models, qbz-library, tokio, reqwest, lofty. Crate compiles.
purge + OfflineCacheState + downloader (CacheEvent) remain.
cc
Slice 0 of the offline-cache port (extraction per ADR-006). Creates the new
qbz-offline-cache workspace crate and moves the tauri-free leaf modules out of
src-tauri/src/offline_cache/ verbatim:
- types: the shared serde DTOs (was inline in offline_cache/mod.rs)
- db: the SQLite cached_tracks index (super:: -> crate::types)
- secret_vault, cmaf_store, path_validator: moved verbatim
- maintenance: super:: refs re-pointed to crate::
src-tauri is untouched and keeps its own copy, so the production build stays
green; the re-point + delete happens at the end of Slice 0. Crate compiles
in isolation and all 5 unit tests pass.
Remaining for Slice 0: playback pure-fn split, metadata/migration/purge
(re-point crate::api/crate::library -> qbz_qobuz/qbz_library), the downloader
+ CacheEvent/CacheEventSink abstraction, the OfflineCacheState struct, then
re-wire the Tauri commands and delete the old module.
cc
The sidepanel left artist list is now always A-Z grouped (derive_artists
groups when group-enabled OR view-mode==sidepanel; the view toggle re-derives)
with letter headers, and an AlphaStrip on its right edge jumps the list
Flickable proportionally (same approach as the album grouping — not exact,
by design). Replaces the flat ListView.
cc
Add the grid<->sidepanel view toggle to the Artists toolbar (group select is
now grid-only, matching Tauri) and the sidepanel two-column layout: a
self-scrolling left list of SidepanelArtistRows (round thumb + name, selected
highlight) that calls select-artist, and a right column with the 5-state
machine (select-hint / loading / error / no-albums / sections). Each release
section renders its title + a flat AlbumCollectionView grid; a footer links to
the full artist page. The other-flick branch now excludes the sidepanel so the
two columns scroll independently.
cc
Wire the sidepanel backend ahead of its UI: select-artist loads the chosen
artist via artist::load_artist (reusing the /artist/page release_type
classifier per the user's decision) and apply_selected_artist maps its
release sections into selected-artist-sections (DiscoverSections) + total;
a FavoriteArtistAlbum artwork target fills the section covers. The
artists-set-view handler flips grid<->sidepanel (persisted as artists_view)
and clears the selection on the way back to grid. card_to_item is now
pub(crate) for reuse.
cc
Add the optional A-Z group toggle to the Artists grid (Group: Off/A-Z
QbzSelect, default Off, persisted via favorites_prefs artists_group).
derive_artists now buckets the filtered set into FavArtistSection letter
groups (# first, then A-Z) and builds the alpha-jump model; the grouped grid
renders per-letter headers + sub-grids and an AlphaStrip on the right that
jumps the Flickable proportionally (mirrors the Albums alpha grouping).
Also renames the favorites view header title "Favorites" -> "Library" to
match Qobuz.
cc
The per-view action+toolbar row is a HorizontalLayout whose sized children
are conditional per tab. When they are absent its auto vertical-stretch
becomes 1, so it split the leftover height with the scrolling content. The
Flickable-based tabs (artists/playlists/labels) have a small content
preferred height, leaving slack the row grabbed -> a giant empty sub-header.
Tracks/Albums looked fine only because their content fills. Pin the row to
vertical-stretch: 0 so the content always absorbs the slack.
cc
Add a Shuffle CircleAction to the Artists row-2 action cluster that opens a
random artist from the visible set (random ARTIST, not a random album —
matches Tauri). Adds favorites::random_visible_artist + the artists-shuffle
handler routing through invoke_open_artist.
cc
Point the artists grid at artists-visible (the search-filtered model) and
add the "No artists match your search" empty state. Add the Artists row-2
toolbar branch with the shared ExpandableSearch wired to artists-search +
search-artists -> derive_artists.
cc
Drop the albums_count line from the artist card (Tauri #169: Qobuz's count
includes compilations/tributes and is misleadingly high) — removed from
ArtistCard, map_artist, FavoriteArtistItem and FavArtistCard. Shrink the card
image to a 120px circle to match Tauri. Add derive_artists (name-substring
search filter → artists-visible/artists-shown), seeded on load; grouping +
the alpha strip layer on next.
cc
Add the FavoritesState fields for the Artists port: the grid toolbar
(artists-search / artists-view-mode grid|sidepanel / artists-group-enabled),
the search-filtered + A-Z-grouped models (artists-visible / artists-grouped
via the new FavArtistSection struct / artists-alpha / artists-shown), and
the sidepanel selection state (selected-artist-id/name, loading/error,
selected-artist-sections as DiscoverSections + total). Add the FavoritesActions
callbacks (search-artists, artists-set-view, artists-set-group, artists-shuffle,
select-artist). Controller + view follow.
cc
Add the user-requested grid/list switch to the Playlists toolbar
(parameterized the shared ViewToggle so albums + playlists both drive it)
and a new PlaylistListRow (collage thumb + name + owner·tracks + play +
overflow menu: play/play-next/queue/favorite), bound to playlists-view-mode.
The view mode persists via favorites_prefs (playlists_view). Tauri ships
grid only — the list view is a labeled Slint-better addition.
cc
Wire the playlist card media-actions: play / play-next / queue fetch the
playlist's tracks (core get_playlist) and route to playback play/enqueue;
share copies the Qobuz web-player playlist URL to the clipboard; favorite
toggles the LOCAL QBZ favorite (Library un-favorites in place and drops the
row, Following adds to Library — per user decision). Wire the sub-tab switch
and the name|owner search to derive_playlists. Add share::qobuz_playlist_url
and favorites::remove_playlist_row.
cc
Swap the passive single-cover FavPlaylistCard tile for the shared
discover/PlaylistCard (collage + hover overlay play/fav/more + context
menu) over playlists-visible, and add the Library/Following underline
sub-tab selector (no pills, ADR-008) above the grid with per-sub-tab and
search-empty states. Extend the shared PlaylistCard with a body-opens
prop so a favorites card-body click opens the playlist while the play
button plays (discover/search keep body=play, unchanged). Add the
Playlists search box to the row-2 toolbar. Remove the dead
FavoritePlaylistItem struct + playlists field + FavPlaylistCard.
cc
Replace the single merged get_user_playlists load with the two real
sources mirroring Tauri: Following = playlists the user follows but does
not own (get_user_playlists filtered owner.id != current_user_id);
Library = locally-favorited ids (SQLite get_favorite_playlist_ids) in
favorited order, intersected with the fetched set and falling back to a
single get_playlist for ids not present. Both map to the rich
SearchPlaylistItem (4-cover collage) via search::map_playlist/playlist_item,
replacing the single-cover PlaylistCard. derive_playlists picks the active
sub-tab source and applies the name|owner search filter; the 4-cover
artwork pipeline (FavPlaylistCover{following,index,slot}) fills the collage
and propagates into the search-filtered visible model. The tab badge now
counts the local Library set. Adds library_db::current_user_id.
cc
Add the FavoritesState fields for the Playlists port: two SearchPlaylistItem
sources (playlists-favorites = local Library, playlists-following = Qobuz
followed) + playlists-visible (current sub-tab after search), the transient
sub-tab/search/view-mode strings, and the following-count badge. Add the
FavoritesActions callbacks (playlists-set-sub-tab, search-playlists,
playlists-set-view, playlist-action). Wiring + controller follow.
cc
Add an ExpandableSearch to the Labels toolbar (mirrors tracks/albums),
wired to labels-search + the new derive_labels controller fn, which
forks a name-filtered clone of the loaded labels and shares the full
model when the query is empty (artwork stays live). apply_favorites now
derives the visible set on load.
cc
Replace the generic disc-icon row list (FavLabelRow) with FavLabelCard:
a 140px circular cover in an auto-fill grid, mirroring FavArtistCard and
the artists-grid layout. No-image labels show a disc-3 placeholder on an
indigo->violet gradient (1:1 with Tauri's label card). The grid renders
labels-visible and adds the "No labels match your search" state.
cc
The favorites-labels wire item (LegacyLabelDto) carries an `image`
field that FavLabel silently dropped, so label cards had no artwork.
Parse it (bare string or the object form, via label::extract_label_image),
plumb it onto LabelCard + FavoriteLabelItem, and issue a FavoriteLabel
artwork job + apply arm mirroring favorite artists. Align the count line
to "{n} albums" (matches Tauri and the sibling artist card) and seed
labels-visible for the upcoming search filter.
cc
In sm mode the open list options rendered at the standard body size while
the collapsed control shows 12px, so the dropdown looked oddly larger.
Use the same 12px for the option rows when sm (standard size unchanged).
cc
- QbzSelect sm now matches the favorites genre button: no outline, 6px
corners (was Radius.sm 8px), 12px label (was body). Standard size
(Settings) unchanged.
- ToggleButton snaps its icon to whole pixels (the HeaderBar/QueueSidebar
treatment) so the grid/list glyphs render crisp instead of blurry.
- AlbumCollectionView grouped sections now follow the view mode: in list
mode each group renders AlbumListRow rows (column header once on top)
instead of always a card grid, so group + list no longer conflict.
cc
The artwork pipeline wrote a decoded cover into the SOURCE model
(FavoritesState.albums / .tracks) by index, but the views render the
derived albums-visible / albums-grouped / tracks-visible models, which
are independent clones whenever a sort/group/search is active. So a
late-arriving cover never reached the rendered card (grey until a
re-derive). Add favorites::set_album_artwork / set_track_artwork to also
propagate the cover by id into the rendered model(s); apply_artwork calls
them for FavoriteAlbum / FavoriteTrack. The disk cache was already in use;
this was purely the render target.
cc
Add a Bootstrap-style `sm` flag to the shared toolbar primitives
(QbzSelect, ExpandableSearch, ToggleButton) — default stays the standard
size (Settings keeps it), and the favorites toolbar opts in (genre /
group / sort / search / grid-list now ~30px). FavGenreButton + the
favorites grid/list ViewToggle shrink to match. Also vertically center
the MultiSelectBar action buttons (only the count was centered before).
cc
Add an AlphaStrip overlaid on the right edge when grouping
alphabetically: tracks "Group: Name" (jumps the virtualized ListView to
the first row of a letter — accurate, uniform 50px rows) and albums
"Group: A-Z" (jumps the grid Flickable proportionally by section ordinal
— approximate, always lands the section in view, per the agreed
ArtistView-style behavior). derive_tracks/derive_albums build the
per-letter jump model (tracks-alpha / albums-alpha).
cc
Drop the redundant "N tracks" / "N albums" toolbar count (the tab-menu
badge is now the permanent counter) and split the content area: the
Tracks tab renders in a virtualized, self-scrolling ListView +
ListScrollbar (the PlaylistView pattern, now possible thanks to the
fixed-chrome restructure), while Albums/Artists/Playlists/Labels keep a
Flickable. Loading/error states center in the content area. Group headers
+ all per-row behavior are preserved inside the ListView items.
cc
Favorites tab badges previously showed a count only after visiting each
tab (the per-tab load set its own total). Add favorites::load_counts —
cheap limit=1 probes per type + the playlist count — applied to
FavoritesState once at startup, so all five badges in the new tab menu
are populated before the user opens a tab. A tab's own load still
refreshes its count.
cc
Rework all five favorites views into a fixed chrome block over a single
scrolling content area (prerequisite for list virtualization):
- Row 1: back/forward + "Favorites" title inline (left), the tab menu as
one floating group (right) — new FavTabMenu.
- Row 2: per-view action buttons (Play/Shuffle/select) float left, the
per-view toolbar (count + search + genre + group + sort + grid/list)
floats right. Lifted out of each tab's content into the fixed row.
- The lists/grids now scroll in a self-contained area below the divider
(own Flickable + ListScrollbar), instead of the whole page scrolling.
Behavior (search/sort/group/genre/multi-select/headers) unchanged.
cc
When grouping the favorites tracks by album or artist, render a header
row each time the group key changes (computed inline from the previous
visible row, no extra model field), and hide the redundant album column
in album-group mode (mirrors Tauri). Name grouping keeps its alphabetical
reorder. Completes the group-by UX without a structural list rewrite.
cc
Extend the favorites tracks bulk bar beyond Remove: Play next and Add to
queue build the queue from the already-loaded Track objects (new
playback::enqueue_tracks, no re-fetch; play-next inserts in order), and
Add to playlist opens the picker for the whole selection. The picker
gains track-ids for bulk add (open_multi); on_pick adds every id via the
existing add_tracks_to_playlist. Make-offline stays out (no Slint
offline-cache path yet).
cc
New favorites_prefs module stores albums view/sort/group + tracks group
in <data-dir>/qbz/favorites_ui.json (search stays transient). Loaded into
FavoritesState on each favorites navigation (before the data applies),
saved on every toolbar change; apply_favorites now re-derives on load so
the restored sort/group take effect (also fixes a latent case where a set
group mode wasn't applied after a tab reload). Album view toggle routes
through an albums-set-view action to persist it.
cc
Make genre_filter.rs selection per-context (HashMap keyed by "discover" /
"favorites" + a current context the popup edits), so favorites filters
independently from Discover. Discover keeps its server-side path
unchanged (reads filter_ids("discover") explicitly; its genre buttons set
the "discover" context on open). Favorites get a genre button in both
toolbars that opens the shared popup in the "favorites" context, plus the
popup overlay; derive_tracks / derive_albums filter client-side by the
selected genre NAMES (selected_names expands parents to descendants).
on_toggle / on_clear branch on the current context to re-derive favorites
instead of re-fetching Discover. AlbumSummary gains an optional `genre`
(favorites track album objects carry it; TrackItem can't, so a track
id->genre map backs the tracks filter). Persisted per-context (legacy
flat selection migrates to "discover").
cc
navigate_favorites stores the load error in FavoritesState.load-error
(cleared on each load attempt); the view shows a "Couldn't load
favorites" + detail + Retry block when set, and the tracks/albums tabs
are gated so the error replaces them instead of showing alongside an
empty state. Retry re-navigates the current tab.
cc
Add a group QbzSelect to the Tracks toolbar. derive_tracks reorders the
visible rows by the group key (album+title, artist+album+title, or
title) so a group's tracks sit together. Visible group-header rows are
deferred until the list is virtualized (the list is a plain for today);
this is the group-ordering half, matching how Tauri's tracks tab uses
grouping as the ordering mechanism.
cc
Add a group QbzSelect to the Albums toolbar. derive_albums buckets the
filtered set by artist name or by the title's first letter (# for
non-alphabetic), sections ordered alphabetically, rendered through the
shared AlbumCollectionView grouped path. albums-shown carries the
post-filter count so the count line + no-match state stay correct in
grouped mode.
cc
AlbumCardItem gains is-favorite (filled accent heart on the card +
"Remove from favorites" in the list-row menu) — favorites albums set it
true so the heart shows its state, which it never did before.
Un-favoriting from the favorites list (track heart or album card heart)
now removes the item: the row/card fades out (new `removing` field drives
a 250ms opacity animation on TrackRow / AlbumCard / AlbumListRow), the
favorite is removed on the server (+ fav_cache for tracks), and the row
is dropped from both the full and visible models after the fade. All
TrackItem / AlbumCardItem initializers carry the new fields.
cc
Add an expandable search (title / artist), a sort QbzSelect
(Default / Title A-Z / Title Z-A / Artist A-Z) and a Shuffle header
action (play a random visible album) to the Albums tab. albums-visible
derives from the full albums set via derive_albums (shared model on the
no-filter/default fast path, filtered+sorted clone otherwise — the
label.rs::derive_releases pattern), and the count reflects the filter.
cc
Replace the bare favorites albums grid with the shared
AlbumCollectionView plus a grid/list segmented toggle and a count line —
adding the list view that was missing (the AlbumListRow extras now fill
via the shared album_map). Album card actions (play / queue / favorite /
go-to) route through a new album-action callback to the album
media-action arms; the artist link still opens the artist.
cc
Add a select toggle to the Tracks header that enters multi-select mode
(per-row checkboxes via the shared TrackRow) and shows the shared
MultiSelectBar with Select all / Remove from favorites / Clear. Selection
lives on the rendered tracks-visible rows (mirrors playlist.rs); the
toggle-select router arm and recount gain a Favorites case. Bulk remove
loops remove_favorite, syncs fav_cache, then reloads the tab. Play next /
queue / add-to-playlist / make-offline are deferred (each needs a backend
path: batch enqueue / multi-id picker / offline cache).
cc
The multi-select checkbox circle was 22px and read as oversized. Drop it
to 13px (border-radius 6.5px) with an 8px check; the existing
(parent - self)/2 centering keeps it centered in the play/number cell.
cc
Retain the loaded favorite tracks as a play-ready Vec<Track> and add
Play all + Shuffle CircleAction buttons by the title (Tracks tab only),
wired to playback::play_tracks. Mirrors the Mix/Playlist transport
(those expose exactly these two; no batch play-next path exists yet).
cc
Add an expandable search over the loaded favorite tracks (title /
artist / album) plus a results-count line. tracks-visible (what the list
renders) derives from the full tracks set: an empty query shares the
model so artwork stays live, a query forks a filtered clone carrying the
already-decoded artwork (the LabelState albums/visible pattern). Adds a
no-match empty state.
cc
The Tracks row routed every media-action through track-action, which
forces kind="track"; an album-name click arrived as ("track", album-id,
"open") and was handled as an artist open, so only the artist link
worked. Route album/artist "open" through the wired open-album /
open-artist actions; everything else still goes to track-action.
cc
load_favorites looped a single 500-item fetch; users with larger
libraries silently lost everything past the first page. Page through
get_favorites (offset += page len) until a short page or offset >= total,
capped at 10k (mirrors Tauri's fetchAllFavorites). Applies to tracks,
albums, artists and labels.
cc
map_track now carries the album title (was dropped, blanked as ""), and
the Favorites tracks rows enable show-album + show-favorite so the album
column and the un-favorite heart render — matching the Tauri favorites
tracks row. Offline-cache download button stays off until that path is
wired.
cc
Extract PlaylistView's inline bulk action bar into a shared
MultiSelectBar primitive driven by a [BulkAction] model (id / label /
danger / needs-selection) emitting action(id). PlaylistView now declares
its two actions (Select all, Remove) as data; the Favorites tracks tab
will reuse the same bar with its larger action set instead of copying
it. Same visual (count label + bordered/hover buttons, ADR-008).
cc
DiscoverBrowseView and LabelReleasesView each carried their own album
grid column math, flat-list (AlbumListHeader + AlbumListRow loop), and
LabelReleasesView additionally the group-by-section rendering. Extract
all of it into a shared AlbumCollectionView (with an inner AlbumGrid),
driven by albums / grouped / is-grouped / view-mode / show-source props.
Both views now mount the one renderer; behavior is preserved (the empty
length>0 guards mirror the previous per-block guards). Readies the same
renderer for the Favorites albums tab.
cc
LabelReleasesView and DiscoverBrowseView each carried a verbatim copy of
the 34px segmented/toolbar toggle. Move it to primitives/ToggleButton and
import it in both. No visual change — same surface, one definition.
cc
Move map_album (V2 nested-first with flat fallback), tier/tier_hires,
release-type classification, to_item and sort_album_items out of
label.rs into a shared album_map module. favorites.rs now maps albums
through it too, so favorite album cards gain the AlbumListRow extras
(release_type / quality_detail / track_count / plain_year) and the
hires-without-bit-depth tier fallback — one implementation to maintain
ahead of the Favorites album list/grid toolbar port.
cc
/label/getAlbums returns the V2 nested album shape (DiscographyAlbumDto:
artists[], audio_info, dates, track_count) — confirmed against the Android
decompiled sources — but it was deserialized into the flat Album model,
which dropped artist, quality and release date (Unknown Artist in
group-by-artist, no quality badge, no date, empty list-row columns).
qbz-models Album: capture the nested V2 fields (audio_info, dates,
track_count, release_type) as optional serde(default) — backward
compatible with the flat-shape endpoints.
label.rs map_album: read nested-first (quality from audio_info, date from
dates, count from track_count, artist from artists[] main-artist, explicit
release_type) with flat fallback. Fixes the releases grid + list columns,
the landing Releases carousel, and Critics' Picks.
Wire ("album","favorite") -> add_favorite + toast (works on every album
card via the shared component).
LabelReleases load-more now driven by has_more (the endpoint caps each
page well below the catalog, so total was an unreliable gate).
cc
CHECKPOINT — this batch is NOT yet acceptable; substantial feedback is pending. Committed so the work is preserved for rework next session, after a max-effort Tauri code review. Areas: TrackRow favorite toggle/state + clickable artist/album (TrackItem gains is-favorite/artist-id/album-id + fav_cache, populated across all builders); PlaylistView header buttons + cover-art hover overlay; LabelView full AlbumCard + grid/list toggle; vertical scrollbar moved to the right gutter across ~12 views.
cc
Marks every non-functional Appearance row (Theme, Language, Font, UI Scale, notifications, window-title, player views, system tray, immersive) with the SettingRow 'unsupported' red label. Only the wired 'Show playlist cover collages' and the 'Detected' status row stay normal. Red = not actually connected, so what still needs wiring is visible at a glance.
cc
slint-dev.sh runs qbz-slint via 'cargo +nightly run --release -Z threads', splitting the serial compile of the Slint-generated module across cores while producing an identical optimized release binary (no cranelift). RUSTFLAGS re-lists the AES-NI features from .cargo/config.toml since RUSTFLAGS overrides rather than merges it.
cc
Add a SettingRow 'unsupported' flag that renders the label red. Mark the Title Bar and Window Controls sections: that Tauri custom-window-decoration paradigm does not carry over to Slint's TitleBar/Plasma chrome.
cc
Playlist Manager: list view renders compact rows with total play time; folder grid cards no longer clip the playlist count (content-driven height); list view shows compact folder chips; the move-to-folder menu gains a folder search and a 4-row scroll cap. Search: the most-popular track hero is resized to the artist-card height with the quality shown as centered text instead of a clipped badge.
cc
Mirrors the play-cell click on the row body: double-clicking a track plays it, or toggles play/pause when it is already the current track. Applies to every TrackRow (album, artist, search, playlist, mix).
cc
Sidebar:
- Fix "Recursion detected" panic from focusing the playlist search input in init (defer via a single-shot Timer).
- Right-click context menus: playlist (Edit, Hide from sidebar, Move to folder with folder search + Move to root) and folder (Edit, Hide from sidebar), built on ContextMenu/ContextMenuItem; remove the unreachable hover "..." button.
- Filter hidden playlists/folders out of the list; add folders::set_folder_hidden.
- Folder list in the move-to-folder menu scrolls (cap 5 rows); close-on-click-outside so the folder-search input no longer dismisses the menu.
- Sort-menu Refresh now uses a dedicated refresh-cw icon.
Toast: in-app ToastKind/ToastState + Toast overlay mounted at the app root, crate::toast (show/hide, per-kind auto-hide via a reusable Timer, cross-thread *_weak helpers); surface play album/artist load failures.
SearchView: title inline with NavButtons + searchType filters floated right; tab strip on its own row; Most-popular hero for a track uses the new SearchTrackHero, sized to the artist card.
Playlist Manager: list view renders compact PmListRow rows instead of cards; grid/folder card fonts tightened to match Tauri; folder card centered with hover-reveal edit; search input narrowed to 70%.
cc
Port the Playlist Manager: header/toolbar (search/filter/sort/folder-flat
toggle/view-mode/new-folder/counter), FOLDERS card grid (accent icon, count,
edit), PLAYLISTS grid/list/tree, folder CRUD (FolderEditModal), move-to-
folder, per-card actions, and arrow-button reorder (custom sort). Reached
from the sidebar "Manage playlists" item; backend 100% reused via
QbzCore/library.db (no new commands). Pointer-event drag-and-drop and
gradient folder colors are deferred follow-ups.
This commit also carries the cross-cutting wiring the session's features all
share — state.slint globals/structs, NavEntry variants, main.rs dispatch +
action wiring, artwork targets, app.slint exports, AppShell mounts — so HEAD
is the verified-green tree (cargo check + release build clean).
cc
Add an Appearance sub-section to the Slint settings, mirroring Tauri's:
theme + auto-theme, font/UI-scale/language, startup page, sidebar playlist
collage (wired to the sidebar flag), notifications/toasts/tray, title bar +
window controls, immersive + miniplayer. Controls reuse QbzToggle/QbzSelect
and the AudioSettings/PlaybackSettings layout. Most controls are visual
stubs (AppearanceState + TODO callbacks) pending backend wiring.
cc
PLAYLISTS header: smaller title, recursive search (hides title), create
modal at 1:1 with Tauri (name/description/folder/public), sort menu, and a
collapse chevron. Folders: accent-tinted icon, open/close chevron as the
primary affordance (no more purposeless "..."), count shown on hover only.
Playlists render a 2x2 micro-collage of track covers (opt-out) instead of
the glyph, sourced from the existing playlist images (no extra fetch).
cc
Per-module discover See-All pages (new releases, press accolades, ideal
discography, qobuzissimes, albums of the week, most streamed) reachable
from each carousel's "View all" link: a paginated grid OR list view with
client-side search, genre filter, and a fixed header. Adds a reusable
AlbumListRow (columns #/ITEM/TYPE/SOURCE/QUALITY/TRACKS/YEAR, SOURCE
hideable, reuses QualityBadgeFull) with play-on-hover + even/odd zebra.
Recently Played albums now carry genre + localized release date + quality
(captured at play time, no extra fetch). Shared home::map_album /
card_to_item reused; grid-only AlbumCardItem literals get ..Default.
cc
Album-card hover overlay now shows the full release date as "MMM D, YYYY"
(e.g. "Nov 6, 2025") instead of the year alone. New dates::release_label
helper keeps the month/day/year order fixed but localizes the month via
chrono format_localized (the app ships 5 languages; current_locale() is the
single hook to thread the real UI language once Slint wires translations —
English for now). Applied to the Discover and Favorites album mappings.
Also fixes Favorites > Albums cards, which hardcoded an empty genre and a
year-only label: they now read album.genre and the full date, matching the
Discover cards 1:1. The data was already in the getUserFavorites response
(qbz-models Album parses genre + release_date_original) — no backend change.
cc
Each favorites tab is now its own history entry (NavEntry::Favorites{tab},
mirroring Discover) recorded from both the header nav and the in-view tab
bar, so back/forward moves between tabs instead of exiting Favorites. Add
the back/forward NavButtons at the same position as every other page
(11px top padding + 22px gap) — FavoritesView was the only view missing
it — and shrink the "Favorites" heading to Typography.section (the album
name size in AlbumView).
cc
The clickable-artist change bound the artist TouchArea width to the artist
text's preferred-width, which clamped the whole stretch:1 title column to
the (often short) artist name — truncating titles to a few letters and
shoving the quality badge inward instead of letting it float right. Drop
the explicit width; the inner Text fills the column (width: 100%) and elides.
cc
Port #470's UI behavior: add an "Auto" entry at the top of the backend
dropdown that, on select, resolves to the best available backend (PipeWire
if present, else System) and persists that concrete value. The rebuilt
snapshot then moves the dropdown onto the resolved backend, so backend_type
is never left null/Auto. Concrete backends keep mapping by their shifted
index; System wiring comes from available_backends() (merged backend half).
cc
The catch-up merge staged the pre-release lock (--theirs) before cargo
regenerated it, so the committed lock dropped qbz-slint's GUI dependency
tree (slint / accesskit / winit / skia / ab_glyph, ...). Commit the
regenerated lock so the checked-in slint-mvp lock matches the workspace.
cc