Slice 3/13. New qbz-offline-cache/src/purchases_service.rs (pagination glue,
get_purchase_total two-call per-type-totals primitive, byte-for-byte
filter_purchase_response). New qbz-slint/src/purchases.rs controller load shells
(get_downloaded_track_ids -> HashSet, load_purchases_metadata with TWO separate
ids-totals calls, load_purchases_by_tab with total backfill + error mapping).
Replicates Tauri's 'Failed to fetch ... purchases' wrapper so network errors map
to purchases.loadFailed 1:1 (TDD-caught). 10 unit tests green. Lazy/cache/force
gating deferred to UI slices 8/9 by design.
cc
Slice 2/13. New impl QobuzClient block in qbz-qobuz/src/purchases.rs ported
1:1 from src-tauri api/client.rs: typed/ids purchase listing (page_limit 500,
two-call per-type totals quirk preserved), get_track_file_url_by_format
(RPC-signed, intent=stream, empty-url -> TrackUnavailable), and download_audio
(http1_only, NO total timeout). Adds 2 endpoint consts; bumps
http()/secret()/authenticated_headers() to pub(crate) for the sibling module.
Offline-gated via self.http()?. Drops .use_native_tls() (crates/ workspace is
rustls-only; matches cmaf.rs build_cdn_client) to keep the standalone build green.
cc
Slice 1/13 of the Purchases Tauri->Slint port. Adds PurchaseResponse,
PurchaseIdsResponse, PurchaseAlbum, PurchaseTrack (no version field) and
PurchaseFormatOption to qbz-models, plus the 5 lenient deserializers ported
verbatim from src-tauri api/models.rs. Derives Default on SearchResultsPage
for the empty-page fallback. 4 round-trip tests, cargo test green.
cc
select_bundled_translation must run after AppWindow::new() (the component's
global translation context does not exist before then), so startup now paints
in the persisted language instead of falling back to English. Adds
reseed_i18n_labels() which seeds the 14 AppearanceState option arrays from
qbz_i18n::t() — these were @tr() property defaults, which are not reactive to
translation changes — called at startup and on live language switch so the
settings <select> options translate and update without restart.
cc
Fills all previously-untranslated msgstr (UI strings absent from the Tauri
JSON seed) via a translation pass, then hand-fills the remainder. Neutral
Latin-American Spanish; standard professional register for de/fr/pt; brand/
technical tokens (FLAC, kHz, Plex, etc.) kept literal; {} placeholders and
gettext plural forms preserved. All four catalogs pass msgfmt -c.
cc
Adds qbz_i18n::mark() (identity, extraction-only) and applies it at the
definition sites of literals later translated through t(variable):
release-type classifier, discover section labels, artist release-section
titles + month names, label jump tabs, and audio settings dropdown tables.
Fixes the discover_prefs double-t() (label_for now returns the marked
literal, translated once at the consumer).
cc
Wrap user-facing Rust-built strings in qbz_i18n helpers across the
Artist/Album/Label surfaces: artist release-section titles + jump-tab
labels (routing kept on the English keys, only displayed labels
translated), MusicBrainz month-name/date formatting, album track-count
subtitle + "From the same artist"/"Listening suggestions" carousels,
label carousels + jump tabs + group-by "Unknown", and the shared
album_map release-type labels (Album/EP/Single/Live/Compilation).
cc
Wrap user-facing Rust-built strings in qbz_i18n helpers across the
Discover/Home/Search surfaces: discover section titles, configurator
labels (discover_prefs::label_for), For You carousel titles, mix
subtitles + duration, suggestions track-count + Song Radio, search
section headers + count subtitles, and the Lossless quality tooltip.
Reuses shared msgids ({} track/{} tracks, {} album/{} albums, section
names) so the catalog stays deduped.
cc
Wrap user-facing eyebrow labels (MIXTAPE/COLLECTION/ARTIST), album/item
counts, add/edit/builder/cover toasts, dialog titles + image filters,
play/queue toasts, and the default discography name in the My QBZ
controllers with qbz_i18n t/t_args/tf helpers. Keep the My QBZ brand
itself untranslated via positional interpolation.
cc
Wrap user-facing toasts, dialog titles/descriptions, durations,
track/folder counts, scan/cleanup status, and grouping labels in the
Local Library controllers (local_library, local_library_settings,
local_playlist, tag_editor) with qbz_i18n t/t_args/tf helpers.
cc
The folder-options dropdown is re-seeded from Rust at runtime in both
create-playlist builder sites, so the state.slint default is dead. Wrap
the seed-site 'No folder' literal in qbz_i18n::t so it resolves through
the shared gettext catalog.
cc
Wrap remaining user-facing literals in @tr across discover (RADIO,
TOP TRACKS), DiscographyBuilder (release-type/order-by labels), the
OfflineManager hero, ExpandableSearch placeholder default, and the
appearance option arrays in state.slint (displayed as-is, no Rust
re-seed). Replace string concatenation with positional @tr plus
ngettext plurals for the DiscographyBuilder footer and OfflineManager
track counts so word order is locale-stable. Language endonyms and the
macOS/Adwaita/quality-tier brand names stay literal.
cc
Wire the Appearance language selector (appearance-select "language"): map the
dropdown index to a language key via the verified order (0 Auto, 1 English,
2 Espanol, 3 Francais, 4 Deutsch, 5 Portugues), persist the raw user choice,
resolve "auto", then set_language() + select_bundled_translation() to switch
all @tr bindings live with no restart. Seed the selector index from the
persisted key at startup.
cc
Add a persisted `language` UiPrefs field (default "auto") with index<->key
helpers matching the existing appearance-prefs pattern. At startup, before
the AppWindow is created, resolve "auto" via qbz_i18n::resolve_auto(), call
qbz_i18n::set_language() and slint::select_bundled_translation() so the first
paint uses the right translations. Map dates::current_locale() from the active
runtime language so month tokens localize with the rest of the UI.
cc
POSIX locale precedence is LC_ALL > LC_MESSAGES > LANG. resolve_auto
previously only checked LC_MESSAGES and LANG; add LC_ALL as the
highest-priority env var, skipping it when empty. Extend the
resolve_auto test (env-mutation serialized via the existing Mutex).
cc
Implement the frontend-agnostic translation layer test-first:
- plural.rs: PluralRule parsing nplurals + the two expressions our
locales use ((n != 1), (n > 1)); index(n) selects the form, with a
safe English default for anything unrecognized.
- po.rs: Catalog::parse handles the header entry, msgid/msgid_plural/
msgstr/msgstr[N], multi-line continuations, and \n \t \" \\
escapes; empty msgstr means no translation (get returns None so
callers fall back to English). Reads Plural-Forms from the header.
- lib.rs: 5 catalogs embedded via include_str! and lazily parsed
(OnceLock); AtomicU8 language state; set_language/current_language/
resolve_auto (LC_MESSAGES then LANG -> 2-letter prefix); t/tn/t_args/
tf with left-to-right {} substitution and English fallback.
17 unit tests, all green via cargo test -p qbz-i18n. Content tests use
inline fixtures since the bundled .po files are header-only for now.
cc
Add new no-dependency qbz-i18n crate to the workspace members.
Frontend-agnostic translation catalog per ADR-006 (reusable by
Slint/TUI/headless; no slint or tauri deps).
cc
Three restart-persistence features built and tested together (they share
ui_prefs + the enter_shell restore sequence, so kept as one change):
- Startup page: persist + restore the last SAFE top-level view via the
canonical apply_entry dispatcher (loads data, not a blank view). Wired to
Settings > Appearance > "Where you left off"; ui_prefs startup_page/last_view.
- Window geometry: persist logical size + outer position and restore them at
launch, clamped to the current monitor so a smaller/disconnected display can
never open an oversized or off-screen window (winit Resized/Moved hook).
- Playback session: new session_persist module wiring qbz-app's SessionStore.
Saves queue + current track + volume + shuffle/repeat + position on
track-change / pause / ~5s tick / exit (incl. hide-to-tray and tray Quit);
restores the queue PAUSED at launch. Position resume on first play threads
the player's existing start_position_secs (no protected-audio change beyond
passing that param). Gated on the persist_session / resume_playback_position
playback prefs; opened per-user on login + boot-restore + offline entry.
Known issue: the seek bar/timer do not visibly show the resume position until
play is pressed (the audio resume itself works); marked in code for follow-up.
cc
Drop the custom-icon picker (owner decision): My QBZ now always uses
QBZ's monochrome (tray) glyph — in the Settings preview, the sidebar row,
and the header section nav (incl. the icons-only compact nav when the
sidebar is fully closed). Only the name stays editable; the reset control
is a square glyph button that clears the name to the default. The
icon-change backend (pick-icon / custom-icon) stays but is unsurfaced.
cc
Restore across restarts: the three-state sidebar (open/mini/closed) and
the section-nav placement (sidebar vs header) seed from ui_prefs before
the shell renders and persist via new ShellState callbacks; the player
volume persists on slider release only (a new QbzSlider `released`
callback + NowPlayingState.persist-volume — no ui_prefs write per drag
tick) and is reapplied to the player on session entry.
cc
Track info is a Qobuz-only metadata page, but it fired for local / Plex /
ephemeral playing tracks too. Add NowPlayingState.source (populated from
the QueueTrack: None coerces to "qobuz" for catalog tracks, "local" for
local files) and gate the trigger on source == "qobuz" — in the SongCard
TouchAreas (pointer cursor + click both no-op otherwise) and the PlayerBar
menu handlers.
cc
The refresh button + device select sat in a center-aligned row, so they
floated with a right-hand gap once the window widened out of responsive
mode instead of landing flush on the same right edge as the single-control
rows. Switch the row to alignment: end.
cc
Drop the unported "Match system window chrome" row from Settings >
Appearance entirely (UI only; the AppearanceState flag + handler stay
inert for now).
cc
Frontend-agnostic core in qbz-integrations::discord (DiscordRpc +
NowListening) — the Tauri backend minus the tauri::command/State
wrappers: lazy IPC connect, Listening activity (details/state/assets +
"Get QBZ" button + timestamps), and the opt-in Flatpak socket bridge.
qbz-slint discord_rpc controller: process-global client, init AFTER the
session is active (PR #477 persistence fix — both online and offline
entry, never at early boot), push on the track-change and play/pause
edges, clear on logout. Opt-in toggle in Settings > Integrations
(DiscordState, persisted in ui_prefs.discord_rpc_enabled) + a Flatpak
socket-access note. The opt-in is per-machine (ui_prefs) rather than
Tauri's per-user.
cc
System Notifications now persists (ui_prefs.system_notifications) and
gates the desktop notification via a poll-thread atomic, seeded at
startup and flipped live by the toggle (scrobbling stays independent).
Mini player default view persists to mini_default_view and seeds its
select index at startup; the miniplayer already reads the key on open.
Both lose the red "unsupported" flag. Also removed 13 unsupported
Appearance rows that will not be ported: Font, the title-bar navigation
group (Search/Navigation in title bar + per-section toggles) and
navigation buttons position, window controls style/size/color preset,
and a duplicate immersive default view.
cc
The binary compiles both rustls providers (aws-lc-rs via rust_cast, ring
via reqwest), so rustls cannot auto-select one and panics on the first
TLS connection. It was only installed lazily on the cast paths, so
joining a Qobuz Connect session without ever casting panicked the
WebSocket worker thread. Install it once in main() before any TLS;
idempotent, so the cast paths' calls stay no-ops.
cc
Local playlists were excluded from both sidebar folders and the cover
collage. Folders: new local_playlists.folder_id column (+ migration) on
the shared playlist_folders, move_to_folder/clear_folder helpers, the
move-to-folder context menu enabled for local rows, optimistic cache
move, and grouping under folders in the rebuild + collapsed-folder
flyout. Collage: resolve up to four covers from a local playlist's own
tracks with no network (local file artwork, Plex thumbs, cached-Qobuz
covers), populated in the async load and routed through the local/Plex
art loader (file/Plex paths would miss the HTTP loader).
cc
The sidebar Queue reads the same core pointer as the now-playing bar,
but several paths refreshed the bar without the sidebar, so the panel
showed a stale NOW PLAYING track and the NPB shuffle button looked dead
(the reordered up-next was never re-pushed). Pair refresh_sidebar with
the now-playing refresh in toggle_shuffle, the local-tracks inline
shuffle, the gapless seamless-change (regardless of moved), and the Plex
quality patch. The shuffle engine itself was already correct.
cc
Slint hit-tests sibling children in reverse declaration order, so the
keyboard FocusScope declared after the pointer TouchArea was visited
first and, with the default focus-on-click, consumed the first Pressed
just to grab focus — aborting traversal before the TouchArea, so every
button needed two clicks. Set focus-on-click: false on the FocusScope in
QbzPrimaryButton, SecondaryButton, QbzCheckbox, QbzToggle, ToggleButton
and QbzSelect; the press now reaches the TouchArea on the first click
(Tab/Enter/Space activation unaffected).
cc
The immersive Queue up-next reuses the coverflow model, whose art was
windowed to ±4 around the current track, so only now-playing + the next
four rows decoded a cover. Build art jobs for the whole list and widen
the decode window when the immersive Queue panel is open (still lazy per
row); the panel fires QueueState.panel-opened on mount so the covers
repaint when it opens.
cc
Album Info expanded-track credits: the roles line now stretches and word-wraps
instead of running off the right edge of the modal.
Track Info credits: render two independent fixed-width columns (even-index ->
left, odd-index -> right) so long name lists wrap and the right column stays
aligned on every row, with no height-coupled row gaps; names stay clickable,
one per line. Mirror the wrap in the immersive Track Info panel.
cc
Widen the album label/awards/external-links sidebar 160->200px. Replace the
generic external-link chips with inline brand SVG icons (Last.fm / Discogs /
MusicBrainz) rendered in their native colors (MusicBrainz split half purple /
half orange), no visible label + a hover tooltip. Uppercase the "EXTERNAL
LINKS" heading to match LABEL / AWARDS.
cc
- Booklet: new frontend-agnostic qbz-booklet crate (MuPDF render to PNG;
the Tauri Cargo stanza + device_rgb/alpha=false render workaround ported
verbatim). In-app reader modal (paginate/zoom/rotate/download) driven by
BookletState/BookletActions; header booklet button now opens it.
- "From the same artist" carousel (/artist/getReleasesGrid) and "Listening
suggestions" carousel (/album/suggest), reusing the Discover Carousel,
appended below the track list with their own artwork pipeline targets.
- External Links sidebar block (Last.fm / Discogs / MusicBrainz) built from
artist + title, opening in the system browser.
cc
Bucketing now trusts the server release_type: drop the foreign-artist filter
that hid band/guest releases (e.g. Vicky Psarakis' Albums were credited to
Sicksense), render every non-empty bucket, hide 'Purchase Only' (download),
and move 'Other' to the end, collapsed by default. Drop the misleading
per-section item counts.
New 'Latest release' highlight (last_release) and 'Appears On' track section
(tracks_appears_on, previously mislabeled as Compilations).
Per-section persisted 5-mode sort (artist_prefs) + per-section load-more
capped at 4 pages, reusing get_releases_grid. New dedicated discography page
(ArtistReleasesView + artist_releases controller) with sort + infinite scroll,
reached via each section's 'See discography' link.
Magazine/Stories sidebar tab (artist/story) with 1:1 thumbnails and a
Network|Magazine tab switcher; hide the empty similar-artists block to remove
the 'no similar' over populated-discovery incoherence. Header Share action.
Per-release award ribbons.
Fix custom artist AND album images: decode + apply immediately on upload and
resolve the local override on navigation, so a custom image now shows for
artists/albums with no Qobuz artwork.
Data layer: type last_release as PageArtistRelease, add /artist/story endpoint
+ core/client methods + ArtistStory* models, export ArtistReleases* globals.
cc
Fourth NPB layout: a cover + spectrum dock pinned flush to the bottom-left
of the open sidebar (forming an L), with the bottom bar reusing the New
layout (window-centred transport, full-width inset seekbar, right cluster)
minus the cover. Gated on the open sidebar (npb-mode 3 && sidebar-state 0);
cycling the sidebar out of the open state reverts live to New while keeping
the persisted choice.
- SidebarNowPlayingDock: AppShell root overlay, cover flush to the window
bottom-left, compact spectrum band (Bars 28-mirrored / Waveform / Energy)
above it, single top-right eye toggle to show/hide it. FFT reuses
VisualizerState; when hidden the sidebar list reclaims the space.
- Extract AudioStamp into a shared exported component (consumed by the Small
and Large bars); in Large it sits at a constant gap left of the right
cluster, out of the left song card.
- Persist the visualizer on/off and the spectrum mode in ui_prefs; restore
at startup and prime the FFT tap after visualizer::install().
- Reduce the horizontal volume slider width 10% across all NPB modes.
cc
Add local albums/artists/tracks sections to the main header cortinilla
(below the Qobuz results) and local albums to the immersive cortinilla,
derived by grouping the on-device track rows. Merge the Plex cache into
the local search so Plex-only content surfaces (db.search is
local_tracks-only). Route each cover by source via spawn_search_loads
(Qobuz http / Plex /library PlexThumb / local file). Widen the local
section caps when offline or signed out. Rename the section lore from
"on this device" to "on Local Library".
Also clear the search input after a cortinilla element is activated
(row click / View more / Enter) so lingering text can no longer
re-invoke the dropdown when focus returns to the field; replicated in
the immersive search field via a dedicated raw-text property.
cc
Split layout is a true 50/50 (explicit half-widths; the horizontal-stretch weights skewed it because the left's intrinsic content out-measured the right). Removed the glass backing plates from the split panel and the song-data metadata block — both sit directly on the atmosphere now.
Cover art no longer upscales blurry on large windows: the now-playing large cover decodes at 1000px (was 300, the hover-preview size) so the full Qobuz source resolution comes through, and the split / Static / Album-Reactive panels cap the art at its native size so it grows sharply instead of stretching.
New Settings > Appearance 'Immersive default view': 'Remember last' restores the last view, or pin Album Reactive / Static / Coverflow / Spectrum / Lyrics / Queue. Persisted via ui_prefs + ImmersiveActions opened/view-changed hooks.
cc
Intelligent Search (phases 3-4): a frontend-agnostic SearchService in qbz-app (Capa A result cache + Capa B interaction ranking) behind a live header dropdown with skeleton loading, stale-while-revalidate, 220ms debounce, keyboard nav + scroll-into-view, per-section caps, a URL-match artwork guard, and an opt-out Settings toggle. Typing previews in the dropdown; Enter goes to Search > All; local-library results show in the dropdown only.
Immersive search: an in-immersive glass cortinilla (Albums/Artists/Playlists) that switches playback without navigating — a Settings action select (disabled/replace-queue/play-next/add-to-queue), the HUD stays up while searching and never hides while the cursor rests on the chrome, twinned top buttons, and a clear button with always-centered field text.
cc
Theme.danger-hover is a 15-25% translucent tint sized for danger-bg
surfaces, so using it as the destructive QbzPrimaryButton fill made the
button ~80% transparent (standard themes) or pale-pink (a11y themes) on
hover. Use Theme.danger.darker(0.15) instead — an opaque, richer red in
every palette, matching the existing danger-button hover idiom in
BlacklistManagerView.
cc
The BlacklistManagerView disabled-filter banner hardcoded the Dark-theme
warning hex (#fbbf24 / #fbbf241a / #fbbf244d) for fill, border, icon tint
and label. These resolve to no other cluster (item #6's named-file list
excludes this view; item #7 owns the WarningBanner.slint primitive, a
different file), so without this swap accessible themes (wcag-dark,
high-contrast) would render the wrong amber here, defeating P4's
'accessible themes take effect everywhere' goal. The hex equal the Dark
warning family exactly, so the swap is identity-preserving on the default
theme.
cc