Commit Graph
100 Commits
Author SHA1 Message Date
vicrodh bc44fe5e0c feat(purchases): add pagination + per-type totals service + controller load shells
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
2026-06-24 04:16:14 -06:00
vicrodh 26b6480ff1 feat(purchases): add purchase HTTP + getFileUrl + CDN fetch
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
2026-06-24 02:08:08 -06:00
vicrodh aceaf9b039 feat(purchases): add purchase wire models + lenient deserializers
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
2026-06-24 01:48:34 -06:00
vicrodh 502b0dae2b i18n: apply bundled translation after window init + reseed option arrays live
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
2026-06-24 00:04:32 -06:00
vicrodh 24da9f1b74 i18n: translate full catalog to es/de/fr/pt (100% coverage)
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
2026-06-23 21:45:05 -06:00
vicrodh 25f9a15f90 i18n: register dynamic msgid literals via mark() and fix double-translation
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
2026-06-23 21:31:42 -06:00
vicrodh 294f01d8bb i18n: re-extract fresh Slint pot (capture option arrays + Task-10 strings)
cc
2026-06-23 21:25:24 -06:00
vicrodh 630663fc4a i18n: extract Rust msgids and re-seed catalogs
cc
2026-06-23 15:15:04 -06:00
vicrodh e167936cc1 i18n(rust): misc strings
cc
2026-06-23 15:11:43 -06:00
vicrodh ed6bda2108 i18n(rust): integrations + dac wizard strings
cc
2026-06-23 15:11:38 -06:00
vicrodh 150ca7f2fc i18n(rust): playback/offline strings
cc
2026-06-23 14:55:18 -06:00
vicrodh f26e1a6626 i18n(rust): artist/album/label strings
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
2026-06-23 14:47:57 -06:00
vicrodh b2facfbd24 i18n(rust): discover/home/search strings
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
2026-06-23 14:47:50 -06:00
vicrodh 66aa469aa5 i18n(rust): my-qbz strings
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
2026-06-23 14:32:38 -06:00
vicrodh f14c513099 i18n(rust): local library strings
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
2026-06-23 14:32:31 -06:00
vicrodh 937b80c4c7 i18n: translate Rust-seeded 'No folder' option
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
2026-06-23 14:13:48 -06:00
vicrodh 171b175b4f i18n: wrap hardcoded Slint labels and plural concatenations
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
2026-06-23 14:13:40 -06:00
vicrodh 8243f2a856 i18n: language selector switches translations live + persists
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
2026-06-23 14:02:04 -06:00
vicrodh 69d85f1932 i18n: init language at startup + persist + locale-aware dates
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
2026-06-23 14:01:57 -06:00
vicrodh 86b22d7325 i18n: extract Slint msgids and seed es/de/fr/pt from Tauri JSON
cc
2026-06-23 13:52:34 -06:00
vicrodh 7142435211 i18n: honor LC_ALL precedence in resolve_auto
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
2026-06-23 13:49:10 -06:00
vicrodh 57244ac0c1 feat(qbz-i18n): gettext .po catalog, plural rules, and t/tn/t_args/tf API
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
2026-06-23 13:45:58 -06:00
vicrodh f92c95ee21 build(qbz-i18n): scaffold pure-Rust i18n crate
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
2026-06-23 13:45:29 -06:00
vicrodh 28d7ada650 i18n: bundle Slint translations via compile_with_config (no gettext C dep)
cc
2026-06-23 13:41:46 -06:00
vicrodh 404526f118 i18n: scaffold empty po catalogs (en/es/de/fr/pt)
cc
2026-06-23 13:41:42 -06:00
vicrodh dd32f573b4 feat(slint): persist session across restarts (startup page, window geometry, playback)
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
2026-06-23 12:09:34 -06:00
vicrodh 181b85c359 feat(slint): My QBZ rename-only with the fixed monochrome icon
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
2026-06-22 23:57:23 -06:00
vicrodh 2c9104e1e3 feat(slint): persist sidebar state, volume and nav placement
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
2026-06-22 23:57:08 -06:00
vicrodh be8c90bb80 feat(slint): disable the Track info trigger for non-Qobuz tracks
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
2026-06-22 23:55:38 -06:00
vicrodh 8bc7026a01 fix(slint): right-align the Output device controls
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
2026-06-22 23:55:24 -06:00
vicrodh fbbd54679c chore(slint): remove the Match system window chrome appearance setting
Drop the unported "Match system window chrome" row from Settings >
Appearance entirely (UI only; the AppearanceState flag + handler stay
inert for now).

cc
2026-06-22 22:56:05 -06:00
vicrodh ef38e7a04e feat(slint): port Discord Rich Presence (opt-in) from Tauri
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
2026-06-22 22:56:05 -06:00
vicrodh e7c969549c feat(slint): wire System Notifications + Mini player default view, drop obsolete Appearance rows
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
2026-06-22 21:36:13 -06:00
vicrodh 907054711f fix(slint): install the rustls CryptoProvider at startup for QConnect
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
2026-06-22 21:35:56 -06:00
vicrodh ca46e3c478 feat(slint): offline playlists in folders + sidebar cover collage
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
2026-06-22 21:35:45 -06:00
vicrodh 8401258326 fix(slint): reflect now-playing and shuffle on the queue sidebar
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
2026-06-22 21:35:20 -06:00
vicrodh 88f1967d8a fix(slint): single click on button primitives (FocusScope stole the first press)
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
2026-06-22 21:34:59 -06:00
vicrodh 21942c60aa fix(slint): show album art for every row in the immersive Queue
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
2026-06-22 21:34:59 -06:00
vicrodh a515eb5529 fix(slint): wrap long credit lists in Album Info + Track Info modals
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
2026-06-22 11:01:19 -06:00
vicrodh 091d023529 polish(slint): album sidebar external-link brand icons + wider column
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
2026-06-22 11:01:11 -06:00
vicrodh 74880009f6 feat(slint): AlbumView polish — booklet reader, carousels, external links
- 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
2026-06-22 01:27:12 -06:00
vicrodh 391cbb86b9 feat(slint): ArtistView overhaul — server-driven buckets, dedicated discography, sort/load-more, Magazine, image fixes
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
2026-06-21 21:42:22 -06:00
vicrodh 9d6b3533bc feat(slint): add Large now-playing-bar mode (NPB mode B)
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
2026-06-21 14:53:33 -06:00
vicrodh b1ac402cf7 feat(slint): extend cortinilla with Local Library + Plex + per-source artwork
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
2026-06-21 11:19:00 -06:00
vicrodh 85b02417be feat(slint): immersive views polish — 50/50 split, no glass, sharp art, default-view
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
2026-06-20 19:45:16 -06:00
vicrodh d0deeb7044 merge: integrate Tauri->Slint theme migration + a11y redesign
33 themes via Rust-driven qbz-theme registry, theme persistence + OLED
default, 6 redesigned accessibility themes (WCAG AAA, High Contrast x2,
Okabe-Ito colorblind) with contrast unit tests, and the P4
redundant-encoding pass (focus rings, HC-gated borders, tokenized status
colors). Branch feature/slint-themes-a11y.

cc

# Conflicts:
#	crates/qbz-slint/src/main.rs
#	crates/qbz-slint/src/ui_prefs.rs
2026-06-20 17:47:06 -06:00
vicrodh 22fb4162f7 feat(slint): search cortinilla — main-header + immersive
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
2026-06-20 17:38:02 -06:00
vicrodh 8f90c234b6 feat(a11y): tokenize DiscoverConfigModal warning icon to Theme.warning
cc
2026-06-20 16:02:12 -06:00
vicrodh 80e4d5da4e fix(slint-a11y): destructive button hover darkens solid danger, not the tint
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
2026-06-20 15:54:28 -06:00
vicrodh 269cae3db8 feat(a11y): tokenize inline disabled-warning banner to Theme.warning
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
2026-06-20 15:52:22 -06:00
vicrodh 0512b97d2c feat(slint-a11y): use focus-ring token for ExpandableSearch focused border
Switch the open field's focused border from Theme.accent to the dedicated
Theme.focus-ring token, reusing the existing input.has-focus signal. Stays
accent-equal in most palettes but reads distinctly in the a11y themes, so
keyboard focus is consistent with every other primitive's ring.

cc
2026-06-20 15:51:04 -06:00
vicrodh b4aa514653 feat(slint-a11y): add keyboard focus rings to shared interactive primitives
Give SecondaryButton, ToggleButton, CircleAction, QbzCheckbox, QbzToggle
and QbzSelect a keyboard FocusScope (Enter/Space trigger the same action
as a click) plus a non-layout focus-ring overlay: a sibling Rectangle
with a transparent-default 2px border that turns Theme.focus-ring only
when the scope has keyboard focus, with its radius matched to the control
(circle for CircleAction) so focus never reflows the layout.

The existing TouchArea stays the pointer source on every control; the
FocusScope only adds key activation and focusability. Universal: the ring
is a per-theme token and only appears on keyboard focus, so it stays
visually neutral at rest across all 33 themes (WCAG 2.4.7 focus-visible).

cc
2026-06-20 15:50:58 -06:00
vicrodh 9e9135e3d6 feat(a11y): favorited heart uses dedicated favorite token in TrackRow
Swap the filled-heart tint from the generic Theme.accent to Theme.favorite
so the favorited fill keeps a stable >=3:1 contrast against the row in
every theme and does not flatten against accent in High-Contrast. The
heart-filled/heart SVG swap remains the primary WCAG 1.4.1 shape cue.

cc
2026-06-20 15:50:27 -06:00
vicrodh 24f64e687e feat(a11y): active-row shape cue + tokenized drop target in Sidebar
The active playlist/folder row and a hover row both paint surface-hover,
so the only active signal was a subtle text-color shift. Add a 3px accent
left-edge bar (non-layout overlay, no reflow) as a redundant WCAG 1.4.1
shape cue. Move the hardcoded green drop-target fill/border (#22c55e26 /
#22c55e66) to Theme.success-bg / success-border so the affordance is
themed across all 33 palettes.

cc
2026-06-20 15:50:10 -06:00
vicrodh 247ee2556f feat(a11y): tokenize muted quality-badge text/tints to theme tokens
Replace the fixed mid-grey #b0b0b0 / #999999 on the CD/MP3/lossless
marks, the tier label, the detail line, and the SongCard LED labels
with Theme.text-muted (and text-secondary for the primary tier label).
A fixed grey vanished on light/HC backgrounds; tying it to the theme
keeps the badge legible everywhere. The Hi-Res brand mark is a
non-tinted Image and is left untouched (stays branded).

cc
2026-06-20 15:49:51 -06:00
vicrodh c5aec4e1ac feat(a11y): tokenize destructive-action colors to Theme.danger family
Replace hardcoded danger reds (#ef4444 / #dc2626 / alpha variants) with
the Theme danger tokens so destructive affordances follow every theme,
including the high-contrast and colorblind palettes (WCAG 1.4.1, P4 #10).

- ContextMenuItem: danger row icon tint + label -> Theme.danger.
- MultiSelectBar: danger button hover bg -> Theme.danger-hover,
  border -> Theme.danger-border, icon tint -> Theme.danger.
- BlacklistManagerView: per-row remove + Clear-All control + confirm
  modal danger button -> Theme.danger / danger-bg. The solid-filled
  confirm button uses Theme.danger.darker(0.12) for hover (mirroring
  how accent-pressed is derived) instead of Theme.danger-hover, which
  is a translucent ghost-surface tint and would wash out a filled
  button (and is lighter in some a11y palettes).

These danger surfaces already pair an icon + verb label with color, so
color is reinforcement (not the sole signal); no HC-gated borders added
here to avoid touching shared button geometry.

cc
2026-06-20 15:49:49 -06:00
vicrodh 09cb284b0f feat(a11y): tokenize WarningBanner tone/bg/border to semantic tokens
Replace the hardcoded variant hex with the themed families:
error -> danger/danger-bg/danger-border, success -> success(+bg/border),
warning(default) -> warning(+bg/border). The "info" variant has no
dedicated token, so it maps to Theme.accent with the same 10% bg / 30%
border alphas the literals used -- a small hue shift in some themes
(behavior change, not a pure 1:1), but accent is the themed
informational color. The per-variant glyph already provides the
non-color cue (WCAG 1.4.1).

cc
2026-06-20 15:49:43 -06:00
vicrodh 2ef299caa0 feat(a11y): tokenize Toast icon colors to semantic tokens
Replace the hardcoded per-kind icon hex (#4CAF50/#ff6b6b/#FFA726) with
Theme.success/danger/warning so the toast tint is themeable; buffering
and info keep Theme.accent. The per-kind icon already carries the
non-color cue (WCAG 1.4.1) -- this only guarantees contrast in the a11y
palettes while staying visually neutral in the 31 standard themes.

cc
2026-06-20 15:49:37 -06:00
vicrodh f72e70bfcb feat(a11y): redundant cues + token badges on SegmentedTabBar
Active segment gains a 2px accent underline (non-color shape cue, WCAG
1.4.1) drawn as a non-layout overlay so it never reflows the row, plus an
HC-only strong border gated on Theme.is-high-contrast. Count-badge fills
move from fixed white washes to Theme.alpha-15/alpha-8 so they follow
theme polarity.

cc
2026-06-20 15:49:25 -06:00
vicrodh dd5f798042 feat(a11y): HC-gated border on PlayButton white disc
Add Theme.is-high-contrast-gated 1px border-strong border to the PlayButton
filled white disc so it stays delineated against a light HC background. The
disc colour stays 1:1 with Tauri (#ffffff); only an HC outline is added, so
the 31 normal themes and the non-circle glyph variant are untouched.

cc
2026-06-20 15:49:18 -06:00
vicrodh faf72f94f4 feat(a11y): HC-gated border on SeekBar hover thumb
Add Theme.is-high-contrast-gated 1px border-strong border to the seek-bar
hover thumb so it is delineated against the accent progress line under a
flattened HC palette. The 3px rail itself gets no outline (a 1px top+bottom
border would swallow the thin line); the thumb border carries the
redundant-encoding for the seek bar. Zero visual change in non-HC themes.

cc
2026-06-20 15:49:11 -06:00
vicrodh bd7086df06 feat(a11y): HC-gated track outline + thumb borders on volume sliders
Add Theme.is-high-contrast-gated 1px border-strong outlines to the track
and thumb of QbzSlider and QbzVerticalSlider so the rail stays visible and
the thumb is delineated against the filled track under a flattened HC
palette. Borders draw inside Slint's box so thumb travel math is unchanged.
Zero visual change in the 31 non-HC themes.

cc
2026-06-20 15:49:06 -06:00
vicrodh fbacd3ca0f feat(a11y): add Theme.is-high-contrast flag to gate HC-only borders
P4 foundation for the redundant-encoding accessibility pass. Adds a single
flag that High-Contrast-only affordances (1px control borders, slider-thumb
borders) read so they never leak into the polished normal themes, mirroring
the existing is-system wiring.

- qbz-theme: `pub fn is_high_contrast(id)` at the crate root next to
  `is_light` (true for HighContrast + HighContrastLight), with a unit test
  `high_contrast_flag_only_for_hc_themes` asserting exactly the two HC themes.
- qbz-slint theme.slint: `in-out property <bool> is-high-contrast: false;`
  beside is-system; re-exported via semantic-colors.slint so all ~145
  components get `Theme.is-high-contrast` with no per-file import change.
  Embedded the canonical HC border gate and the non-layout focus-ring
  snippet as a comment for the cluster agents to copy verbatim.
- qbz-slint theme.rs: apply_theme sets it via
  `qbz_theme::is_high_contrast(id)`.

cc
2026-06-20 15:47:28 -06:00
vicrodh 48eefa3c5b feat(qbz-theme): add 6 redesigned accessibility themes + WCAG/APCA contrast tests (P3)
Materialize the final verified Part B palettes for wcag-light, wcag-dark,
high-contrast (dark), high-contrast-light, and colorblind. Each is a fully
populated ThemeColors row with solid (opaque) status surfaces/borders (not
alpha tints) so accessible contrast is guaranteed independent of backdrop.
Includes the success + focus-ring families per token; high-contrast-light
applies the adversarial warning correction (#735c00 -> #5e4b00, AA-only ->
AAA on white). colorblind uses the exact Okabe-Ito values with body-size
success routed to success-hover on the lightest tier (C.6).

Mark accessibility themes as implemented and add WCAG/APCA + CVD unit tests:
- wcag-* assert AAA (>=7:1) text/accent/status + APCA body |Lc|>=75
- high-contrast/-light assert >= the wcag-dark/-light text-bg ratio and
  >=3:1 non-text for border-strong/focus-ring/border-subtle
- colorblind asserts AA on all tiers + danger/warning/accent stay distinct
  under a Machado deuteranopia simulation (delta-E separation)
- all-rows test asserts every registered theme is fully populated (no zero
  color), exactly 5 accessibility themes, 33 total ThemeId rows

cc
2026-06-20 15:12:09 -06:00
vicrodh 40bd77f70f feat(qbz-theme): transcribe 24 standard themes into the registry
P2 of the theme migration. Materialize every non-accessibility theme
from src/app.css (per 01-tauri-themes-inventory.md): the core Light row
plus the 23 branded/community themes (warm, nord, dracula,
catppuccin-mocha, breeze-dark, adwaita-dark, alucard, aurora, ikari,
ayanami, iscariot, stratego, rumi, zoey, mira, rose-pine-dawn,
breeze-light, adwaita-light, duotone-snow, snow-storm, frost, langley,
kurosaki).

- No CSS cascade on the Slint side: every row is fully materialized.
  Omitted tokens (light's accent trio; breeze-dark/adwaita-dark's
  danger/warning families) are resolved against :root Dark at
  transcription time.
- Alpha polarity is luminance-derived, not the Tauri type flag: frost
  (#2e3440) and langley (#2c0a0a) are registered light but are dark
  canvases -> white alpha base; alucard (#fffbeb) is genuinely light ->
  black base. surface_hover/border_muted bases flip with polarity too.
- Status families (danger/warning) are baked as straight-alpha tints of
  the solid hue (0.1/0.3/0.2; dracula 0.15/0.4/0.25; light hover 0.15),
  matching Tauri's rgba() derivation.
- Derived (no Tauri parity) per-theme: success (project green #3fae6a on
  dark, #1f8a4c on light), focus_ring (= accent), favorite (= danger).
  These are placeholders to be polished in P4.
- is_implemented() now reports all standard themes ready; the 5
  accessibility themes remain gated for P3.

Adds a full-population smoke test over every registered theme plus
per-theme hero-value, polarity and non-standard-tint assertions.
cargo test -p qbz-theme: 33 passed.

cc
2026-06-20 15:05:03 -06:00
vicrodh 71f92e62a3 feat(qbz-slint): wire theme persistence + Rust-driven palette apply
Connect the qbz-theme registry to the running app and persist the choice
across restarts (owner hard requirement 2026-06-20).

- `src/theme.rs`: Rgba->slint::Color conversion, `to_slint` building the
  generated `ThemeColors` (55 fields, exact name parity with the Slint struct),
  `apply_theme` (set_c + set_is_system), and slug<->index<->id mapping with
  slug as the source of truth. Unit tests cover the roundtrip + fallbacks.
- `ui_prefs.rs`: new `theme: String` slug field, `#[serde(default)]` ->
  `qbz_theme::default_slug()` (OLED). Old profiles without the field restore to
  OLED. Tests assert the default.
- `main.rs`: add the missing `"theme"` arm in `on_appearance_select` (was
  falling into the debug `other =>` arm) — maps dropdown index to id, persists
  the slug, hot-switches the palette. Startup restore seeds the dropdown list
  from the registry, derives the index from the persisted slug, syncs the
  legacy `ThemeState.mode`, and applies the theme before first paint.
- `app.slint`: export `Theme`/`ThemeColors`/`ThemeState` so Rust can drive them.
- `AppearanceSettings.slint`/`state.slint`: the theme list is now Rust-seeded;
  comments updated (index no longer drives colors; slug is source of truth).

Headline acceptance: pick a theme, quit, relaunch -> same theme; fresh profile
boots into OLED Dark. The 4 existing themes are unchanged (legacy alias values
preserved in the registry).

cc
2026-06-20 14:56:08 -06:00
vicrodh 8c8871be2a feat(qbz-slint): add Rust-driven Theme global (dumb sink)
Replace the per-token `ThemeState.mode` ternary system with a single
`ThemeColors` struct (`Theme.c`) that Rust pushes from the qbz-theme registry.

- New `foundation/theme.slint`: `ThemeColors` struct (kebab-case fields in
  lockstep with `qbz_theme::ThemeColors`) + `Theme` global of thin aliases.
  Every existing token name is preserved (surface-*, text-*, accent-*,
  border-subtle/muted, favorite, card-shadow, alpha-08/10/12/18/55/65/70/75)
  so the ~1900 call sites compile verbatim; the danger/warning/success
  families, border-strong, bg-hover, focus-ring and the full 24-tier alpha
  superset are added.
- System mode preserved: `Theme.is-system` (set by Rust) falls the
  System-overridden tokens back to the std-widgets Palette exactly as the old
  `mode == 3` branch did; all other themes read `c.*`.
- `semantic-colors.slint` becomes a re-export of `Theme`/`ThemeColors` from
  `theme.slint`, so the 146 existing imports keep resolving.
- The `Theme.c` default is seeded with the Dark palette so the live `.slint`
  preview renders without a Rust feed.

cc
2026-06-20 14:51:18 -06:00
vicrodh ff5536a02e feat(qbz-theme): add frontend-agnostic theme registry crate
Introduce the `qbz-theme` crate (ADR-006): pure Rust, no Slint/Tauri/heavy
deps, hand-rolled color + contrast math. It holds the scalable theming data
model the Slint migration consumes.

- `Rgba` + WCAG relative-luminance/contrast-ratio + an APCA Lc approximation
  (secondary a11y gate for P3 tests).
- `ThemeColors`: the complete token contract (surfaces incl. bg-hover, text,
  accent, danger/warning/success families, border-subtle/muted/strong,
  focus-ring, favorite, card-shadow, 24-tier alpha superset with polarity
  baked in).
- `ThemeId` enum (all 33 themes) with stable slugs, display names (proper-noun
  data, not i18n keys), Core/Dark/Light/Accessibility categories, and a
  luminance-derived light/dark flag. `default_theme_id()` = OLED.
- Registry materializes the 4 P1 themes (Dark/OLED/Tokyo Night/System-fallback)
  from src/app.css, preserving the legacy translucent alias values so the
  existing themes stay pixel-identical; derives the new token families.
- 22 unit tests (registry fully populated, default is OLED, legacy alpha
  aliases unchanged, slug roundtrip).

cc
2026-06-20 14:49:53 -06:00
vicrodh c5cb9dbbe0 fix(slint): immersive controls toggle - whole-circle hit area, no stray exit
The collapsed cluster put the (opacity-0 but still hit-testable) exit
button on top of the circle, so it stole the hover (only a sliver of the
circle reacted) and captured clicks (every press exited immersive). Make
the circle a permanent right-edge handle with its own full-area TouchArea;
reveal the action buttons to the LEFT of it so they never overlap; gate
the buttons inactive while collapsed; drive expand from an OR of bg/circle/
button hover (no steal-flicker); and let a click on the circle pin it open
as a fallback.

cc
2026-06-19 16:01:09 -06:00
vicrodh 105bdaff70 polish(slint): immersive header - window-centered search + collapsing controls
Center the search field to the window (absolute x, independent of the
left mode trigger and right controls) like the main UI, instead of a
stretch slot that drifted off-center. Collapse the window controls to a
single circle pinned to the right edge that hover-expands LEFTWARD into a
glass capsule (right edge fixed, so it never displaces the centered
search). Reduce the active controls to Fullscreen toggle + Close (the
Close now EXITS the immersive overlay, not the app). Minimize, maximize/
restore, drag and close-app are parked as commented-out blocks for the
future qbz custom titlebar; their WindowControlActions wiring stays.

cc
2026-06-19 10:55:14 -06:00
vicrodh 9f7fec78eb polish(slint): immersive panels contrast, alignment and legibility pass
Content sat directly over the variable album atmosphere and washed out.
Add a semi-opaque dark backing behind the split-right panel (and a lighter
plate behind the split-left metadata) so any panel reads on any cover.
Track Info: replace the dark-gray modal labels with bright local variants
(labels #ffffffb3, values near-white). Queue: fix the misaligned clear
button (content-sized, 34px baseline, flush trailing edge), de-pill the
sub-tabs (underline active), brighten rows. Suggestions: hairline rims and
badge backing discs, brighter text. Header: unify the glass capsule idiom
across the mode trigger, search placeholder and controls; polish the
two-level dropdown (caps group headers, spacing). Split gutters/gaps tuned.

cc
2026-06-19 08:38:08 -06:00
vicrodh 7a84c8b566 fix(slint): make immersive window controls work via the Slint Window API
minimize/maximize/fullscreen were calling winit directly, but the Slint
winit adapter reconciles window state from Slint's own properties on every
realization (winitwindowadapter.rs ~1328 toggles winit to match
properties.is_fullscreen()), so a direct winit set_fullscreen was undone on
the next frame - fullscreen never stuck. Drive them through
slint::Window::{set_minimized,set_maximized,set_fullscreen} (api.rs), the
same properties the adapter reads, so the state holds. Drag stays on winit
(no Slint API). Same lesson as the miniplayer decorations fix.

cc
2026-06-19 08:38:08 -06:00
vicrodh e6f9c3b4cf feat(slint): immersive header overhaul - mode dropdown, search slot, 1:1 window controls, 50/50 split
Replace the immersive ModeBar/ShaderBar with a two-level dropdown (Focus
and Split groups with their children) behind a display glyph, freeing the
header for a future Album/Playlist search (an inert styled placeholder
sits in the center for now). Port the Tauri header window-controls cluster
1:1: a new WindowControlActions global drives the main window via the
winit accessor (minimize, toggle-maximize, toggle-fullscreen, drag-move)
plus exit-immersive and close-app (reusing invoke_close_app). Fix the
split layout: the columns were both stretch 1 so the panel got a sliver;
art-left is now 55% with a large responsive cover, the panel 45%.

cc
2026-06-19 08:10:23 -06:00
vicrodh 876d25ad30 feat(slint): album-complement accent for the immersive lyrics-focus line
The focus line used the cover's dominant hue (spectrum-primary), which
matched the atmosphere (built from the same cover) and blended in. Derive
a contrast accent instead: immersive::lyrics_accent_color takes the cover's
coverage-dominant hue and rotates it to the complement (+180deg), vivid and
mid-bright, with a teal fallback for B&W covers; exposed as
ImmersiveState.lyrics-accent and set on every cover decode.

cc
2026-06-19 07:42:16 -06:00
vicrodh 653a449d92 feat(slint): two milkdrop shader modes (Tunnel, Aurora) + scene picker
Extend the wgpu fragment-shader underlay from one scene to three.
shader_underlay builds a pipeline per scene (Vec, indexed by mode-1) over
the shared uniform feed; render_frame takes the active mode. Two original
clean-room WGSL scenes: tunnel.wgsl (neon polar-warped tunnel, zoom on
energy0, ring flare on transient) and aurora.wgsl (domain-warped curtains).
ImmersiveView gains a header scene picker (Off/Plasma/Tunnel/Aurora) and
the g key now cycles 0->1->2->3. WGSL is wgpu-compiled at runtime, so the
look needs a GUI smoke (cargo only checks the Rust/Slint).

cc
2026-06-19 06:58:19 -06:00
vicrodh b584c75ef9 fix(slint): drive the lyrics sync engine for immersive lyrics panels
The 30Hz lyrics engine gated solely on ShellState.lyrics-open (the
sidebar), so the immersive Lyrics panels (focus + split) never advanced:
active-index/line-progress stayed frozen and the open-time kick() no-op'd
because it bailed at the same gate before computing. Widen the gate so the
engine also ticks while an immersive lyrics panel is active (focus mode 4
or split panel 0), read directly from ImmersiveState.

cc
2026-06-19 06:51:05 -06:00
vicrodh db41e78383 feat(slint): immersive suggestions panel (split) with live artist queries
Add ImmersiveSuggestionsPanel for the split-right slot (split-panel 2):
a cards row (up to 2 artist-playlist book-collage cards + a Song-Radio
diamond-collage card) and a recommended-tracks list. New suggestions.rs
controller assembles everything in Rust (ADR-006): artist tracks_appears_on
with an artist-tracks fallback under 5, dedupe by title, drop the current
track, shuffle, take 10; cards from the first two playlists plus a track
radio. New core method QbzCore::create_smart_track_radio mirrors the smart
artist radio (spawn_blocking + block_on, seed hoisted to index 0). Does NOT
use reco_store (live Qobuz queries only). Collage tilt deferred (flat for
now); blacklist drop happens downstream in play_tracks.

cc
2026-06-19 06:43:00 -06:00
vicrodh 816da9cd47 feat(slint): immersive track info inline panel (split)
Add ImmersiveTrackInfoPanel for the split-right slot (split-panel 1):
title + album, Duration/Quality/ISRC/Label-link metadata rows, grouped
credits and copyright, reusing MetaCell/CreditCell from TrackInfoModal
(now exported). A new load-without-open path (info_modals::
load_track_info_inline via TrackInfoActions.load-inline) populates
TrackInfoState without raising the floating modal, so the inline panel
fills in place; the data refreshes on now-playing track change. Header
gains a Track Info tab in the split rail (Lyrics, Track Info, Queue).

cc
2026-06-19 06:19:53 -06:00
vicrodh 55fc6fa13f feat(slint): immersive queue panel (focus + split) with queue/history tabs
Add ImmersiveQueuePanel, one component used both as a focus full-screen
queue (mode 5) and the split-right panel (split-panel 3). Queue/History
sub-tabs bound to QueueState.tab/set-tab; NOW PLAYING card + UP NEXT list
from the flat coverflow-tracks model (upcoming index = idx -
coverflow-index - 1 -> play-coverflow-upcoming), History from the
canonical history list. Header gains a Queue tab in both rails;
QueueState.panel-opened() fires on entry. Rows reuse the queue row
shape; scrolling via Flickable + shared ListScrollbar.

cc
2026-06-19 05:54:58 -06:00
vicrodh 77068e3b75 feat(slint): immersive focus/split scaffold + lyrics panel (both variants)
Add ImmersiveState.view-mode (0 focus, 1 split) and split-panel; extend
the mode enum with Lyrics-focus (4). The immersive header gains a
focus/split toggle (the former inert panel-left slot) and a Lyrics tab
in each mode. New ImmersiveLyricsFocusPanel renders the giant centered
current line with a fadeInUp-lite entry; split mode reuses
LyricsLinesView at immersive sizing with the shared ImmersiveTrackInfo
block on the left. LyricsState.panel-opened() fires on lyrics entry so
fetch starts. Existing visualizer modes, atmosphere and the wgpu shader
underlay are gated on view-mode==0 and otherwise untouched.

cc
2026-06-19 05:43:51 -06:00
vicrodh 87116b16e2 fix(slint): match NPB Small right-button order to New/Classic
Reorder the Small now-playing bar's right control set to
Connect / Cast / Audio settings / Now-Playing view / Lyrics / Volume /
Queue, matching PlayerBar. Pure reorder; the leading AudioStamp stays.

cc
2026-06-19 02:41:17 -06:00
vicrodh 9ccfbd3845 feat(slint): port Awards pages (landing + albums listing + follow)
Port Tauri's AwardView/AwardAlbumsView to Slint: the award landing
(hero + acclaimed-releases preview grid + other-awards rail + follow
heart), the paginated albums listing with client-side search, and
clickable album-sidebar laurels that open the award page (resolving the
award id by name when /album/get omits it).

- AwardState/AwardActions globals + AwardEntry; AlbumState.awards now
  carries award ids
- award controller (src/award.rs): /award/page hero (never page.releases),
  /award/getAlbums grid, /award/explore other-awards + name->id resolver;
  follow via the shared favorite_awards cache (plural-read/singular-write
  favType, skip-if-remote, login seed)
- nav registration: NavEntry::Award/AwardAlbums, ContentView arms,
  navigate_award/navigate_award_albums, AppShell mounts, app.slint exports
- ArtworkTarget::AwardAlbum/AwardOther

cc
2026-06-19 02:41:12 -06:00
vicrodh c48c938bb9 fix(slint): drop the dated accent-tile icon from the lyrics header
The 36px accent-blue mic-vocal tile at the top of the lyrics sidebar was a
Tauri-era treatment that no longer matches the current QBZ header style.
Removed per owner request; the LYRICS overline + track meta stay.

cc
2026-06-18 22:49:11 -06:00
vicrodh ae550b76a5 feat(slint): per-visual-line karaoke highlight for lyrics
A wrapped lyric line (one logical line over two visual rows) lit BOTH rows at
once because the active line was a single word-wrap Text masked by one clip
Rectangle of full block height + scalar width. Now it progresses per visual
line — row 1 fills to 100% before row 2 starts — matching the final Tauri
behavior. NOT a Slint limitation (proven in the investigation doc).

- lyrics_measure.rs (new): swash advance pass over the active line at the same
  font (by font-index) + ppem + wght=700 + 0.2px letter-spacing the view
  renders with, greedy word-wrap (CJK per-char) -> per-visual-line segments
  with pixel widths. Embeds the same TTFs the view registers. swash is the
  shaper femtovg uses, so widths match the render.
- lyrics_sync.rs: on active-index / content-width / font / size / uppercase
  change, segment the active line and push [LyricsSegment{text, start-weight,
  width-weight}] (Tauri's width-weighted partition); pushed atomically with the
  active index. Per-surface cache keys (main + mini). line-progress unchanged.
- state.slint: LyricsSegment struct + active-segments + content-width on
  LyricsState.
- LyricsLinesView.slint: active row renders a VerticalLayout of per-segment
  rows (no-wrap), each the existing dim-base + clipped-bright-overlay with clip
  width = segwidth * clamp((line-progress - start-weight)/width-weight, 0, 1).
  Root publishes content-width. Inactive rows unchanged.
- miniplayer.rs: mini lyrics surface segmentation fan-out.

Investigation + plan: qbz-nix-docs/lyrics/2026-06-18-karaoke-highlight-investigation/.
Owner-verified working. Known tuning point: wrap-boundary precision per font tier.

cc
2026-06-18 22:49:11 -06:00
vicrodh 41973a1ea1 chore(slint): clear dead-code warnings (zero-warnings policy)
Pre-existing dead code unrelated to cast; restores the zero-warnings policy.

Deleted (abandoned refactor leftovers): ModeTextToggle (LocalLibraryView.slint,
unreferenced), section_artwork_jobs + ArtworkTarget::Section variant + its match
arm (Discover-slice leftover; live render uses DiscoverSectionAlbum),
rename_folder (orphan DB wrapper).

Marked #[allow(dead_code)] with a "reserved, pending wiring" note (ported work
from other epics, not discarded): qconnect_service set_autoplay_mode_if_remote /
autoplay_load_tracks_if_remote / stop_if_remote (QConnect autoplay/stop API);
myqbz_play skip_to_next_item / skip_to_previous_item (Mixtape item-skip, spec 40).

cargo check -p qbz-slint: 0 warnings.

cc
2026-06-18 14:50:48 -06:00
vicrodh 2e990cbf07 fix(slint): cast lyrics auto-follow + DLNA seekbar duration
- Lyrics auto-follow while casting: the local poll's cast-skip now runs BEFORE
  clear_remote_anchor() (it was wiping the anchor every 450ms), and the cast
  poll publishes its position via lyrics_sync::publish_remote_anchor each tick
  (the 30Hz engine extrapolates between ticks, like the QConnect path). Anchor
  cleared on disconnect so local lyrics resume.
- DLNA seekbar: many renderers report TrackDuration=0 in GetPositionInfo, which
  left the bar permanently full (position/0 -> clamp 1.0) with no total time.
  Fall back to the track's catalog duration when the renderer reports 0 (the
  renderer's position is still authoritative). Chromecast reports a real
  duration, so it's unaffected.

cc
2026-06-18 14:50:48 -06:00
vicrodh 2d4d4d0906 perf(cast): cache-first resolver + offline tier + cheap quality (P1+P2)
The cast was slow because the resolver always did a full CMAF download
(~10s for a 190MB hi-res FLAC) and never read the cache — unlike the fast last
Tauri build, which resolved bytes cache-first.

- P1 cache-first: Player::fetch_for_external_stream now checks L1 (in-memory) ->
  L2 (on-disk playback cache) before any download. The gapless prefetch already
  warms the next 2 tracks at UltraHiRes while casting, so next/advance/replay are
  now instant; only a cold track pays the download.
- Offline tier: CoreBridge::fetch_for_external_stream_resolved gains offline +
  sink params and tries resolve_offline_bytes (local CMAF decrypt, no network)
  before the network path — a downloaded track casts fast with no connection
  (the same local-segments+decrypt path offline playback uses). Replaces the
  hard "Qobuz cast needs a connection" gate (offline-cached tracks now work).
- P2 quality label: derived from the track's catalog metadata
  (bit_depth/sample_rate/hires) in cast_track — no extra API call, consistent
  whether bytes came from cache, offline, or network.

cargo check -p qbz-slint: clean.

cc
2026-06-18 12:13:06 -06:00
vicrodh 88cf79e1e5 fix(slint): keep the bar/queue in sync while casting
Smoke-test fixes after the first real Chromecast session:

- Local 450ms poll now skips the bar update while casting (it was pushing the
  stopped local player's position=0/playing=false/local-volume every tick,
  fighting the cast poll → seekbar flicker, wrong play icon, dead volume bar).
  The cast poll owns the bar, mirroring the existing QConnect-controller skip.
- Cast poll + connection-state push now set NowPlayingState.playing (the
  play/pause icon was stuck on play), and set_volume_if_cast pushes the slider
  position (the local set_volume that normally moves it is skipped while casting).
- next/previous no longer use a cast-only path: that advanced the renderer +
  core cursor but never refreshed the card/queue, and left the cursor desynced
  so queue-click resolved the wrong track. They now run the local
  next()/previous() flow (move cursor -> refresh card+queue -> play_audible
  casts the new track), keeping UI + renderer in sync. Auto-advance on track end
  uses the same local next() flow.

cc
2026-06-18 12:03:27 -06:00
vicrodh d525e4670a fix(cast): install rustls CryptoProvider before TLS connect
rust_cast opens a rustls TLS channel to the Chromecast. In the full binary both
the aws-lc-rs and ring providers are enabled (reqwest pulls ring, rust_cast pulls
aws-lc-rs), so rustls can't auto-select one and the worker thread panicked with
"Could not automatically determine the process-level CryptoProvider" — surfacing
as "Connection error: Thread response error".

Add qbz_cast::ensure_crypto_provider() (installs aws-lc-rs once, idempotent) and
call it at the top of ChromecastHandle::connect and DlnaConnection::connect.

cc
2026-06-18 12:02:44 -06:00
vicrodh 28fe9fa773 feat(slint): route new-track plays to cast + offline gate (Phase 5)
- play_audible now gates Cast-first (before the QConnect peer route): when a
  renderer is connected, a newly-played track is cast to it instead of starting
  the local audio backend (no double audio). Mirrors the existing QConnect
  play_on_peer_if_active chokepoint.
- Offline gate: casting a Qobuz track while offline is refused with a clear
  message (the external-stream resolver is network/CMAF only; local files still
  cast offline). Offline-cached Qobuz cast is a tracked follow-up.

Deferred (documented, non-blocking): continue-locally prompt on disconnect
(needs a small Slint modal); skipIfRemote UI gate (cast while this instance is
a QConnect remote — rare edge).

cargo check -p qbz-slint: clean.

cc
2026-06-18 10:08:51 -06:00
vicrodh 6429273d77 feat(slint): cast picker modal + cast teardown on logout/exit (Phase 4)
- CastPicker.slint: real modal (scrim + 400px card) replacing the static
  PlayerBar/PlayerBarSmall cast-menu PopupWindow stubs. Two rectangular tabs
  (Chromecast/DLNA, no pills, plain count) + per-tab device list from
  CastState.devices, connect-on-click (DLNA rows gate on can-play), connected
  banner with Disconnect + delivered-quality line (D3), scanning/empty states,
  error line. Mounted last in AppShell (on top).
- PlayerBar + PlayerBarSmall cast buttons now open the modal
  (CastState.picker-open = true; CastActions.open()) instead of the dead popup.
- Cast teardown wired: shutdown() on logout (fixes the Tauri connection + poll
  leak #33) and best-effort on app exit (renderer Stop + media server, #32).

cargo check -p qbz-slint: clean (only the 6 pre-existing warnings).

cc
2026-06-18 10:04:59 -06:00
vicrodh 3e1101ff64 feat(slint): cast service + transport gating for Chromecast/DLNA (Phase 2-3)
The Slint-side seam over the qbz-cast crate, modeled on qconnect_service.rs.

cast_service.rs (singleton):
- discovery (mDNS + SSDP) with a 2s device-refresh loop and 15s scan window;
- connect/disconnect: snapshot local playback, stop the local backend, suspend
  QConnect (remember + restore), connect, then re-cast the current track
  passing its REAL source (fixes the Tauri resume-source bug) + deferred seek;
- cast_track: source-routed (qobuz via the Phase-0 shared resolver at
  UltraHiRes; local via register_file streaming + rich MIME; plex = TODO),
  one shared lazy MediaServer, per-device source-IP URL, Chromecast auto-play
  vs DLNA two-step load->play;
- 1s position poll mirroring position/duration/elapsed to the bar + CastState,
  with the verbatim track-end heuristic (Chromecast ->IDLE, DLNA ->STOPPED/
  NO_MEDIA_PRESENT) driving auto-advance;
- delivered-quality (D3) pushed to CastState; shutdown() for logout/exit.

state.slint: CastState global + CastActions + CastDevice (tagged record with
DLNA capability flags); re-exported in app.slint.

main.rs: init + CastActions wiring (open/close/refresh/connect/disconnect) and
transport gating — play/pause/next/previous/seek/volume now route Cast >
QConnect > local.

Compiles clean (only stop_if_cast/shutdown unused pending the stop callback +
logout/exit wiring, next).

cc
2026-06-18 09:54:27 -06:00
vicrodh 8dc5f9c5c5 feat(cast): qbz-cast crate hardening for the Slint port (Phase 1)
Additive, backward-compatible enhancements to the shared cast crate (the
Tauri shims are pure re-exports, so existing callers are unaffected).

- DLNA timeouts: pause/stop/seek/set_volume now run through a shared
  run_action() with a 10s timeout (was none) so a hung renderer can't block
  the caller forever. New DlnaError::Timeout / CastError::Timeout variants.
- DLNA mute: add set_mute / get_mute (RenderingControl SetMute/GetMute,
  Master channel) — was missing alongside set_volume.
- DLNA capability detection is now version-agnostic (matches AVTransport:2/:3,
  not only :1) so newer renderers report has_av_transport correctly.
- Media server endpoint auth: served path is now /audio/<token>/<id> with a
  128-bit per-session token (RandomState-seeded, no new dep). Token lives in
  the PATH, not a query string, so DLNA renderers that strip queries still
  authenticate. Mismatch -> 403.
- register_file content-type parity: content_type_for_path now mirrors the
  rich local-cast MIME map (audio/x-ape, mp3/ogg/opus/aac, m4a/alac->audio/mp4)
  instead of the poorer table; add register_file_with_content_type so the
  caller can supply an exact MIME. Local files stream from disk (no full-RAM
  read) per decision D4.

Unit-tested: tokened-path parsing, rich MIME map, token randomness.

cc
2026-06-18 09:25:39 -06:00
vicrodh fdf2fec3bb feat(core): shared external-stream asset resolver for Cast (Phase 0)
Add a frontend-agnostic API to resolve a track's bytes + MIME + actually-
delivered quality for an external renderer (Chromecast / DLNA), so the Slint
Cast port does not need an improvised downloader (ADR-006).

- qbz-models: ExternalStreamAsset { bytes, content_type, quality, duration,
  origin }, StreamQualityInfo (kHz-normalized via from_raw, handles the
  kHz-vs-Hz endpoint ambiguity), AssetOrigin.
- qbz-qobuz/cmaf: download_full_with_quality(_progress) returns the resolved
  format_id/sampling_rate/bit_depth alongside the FLAC bytes; download_full is
  now a thin wrapper (no duplicated segment assembly).
- qbz-player: Player::fetch_for_external_stream — CMAF-first (FLAC, accurate
  quality) with legacy stream-URL fallback; never reads the local playback
  cache (casting attempts the best tier and must report the real delivered
  quality); never serves an empty content type (external_content_type helper).
- qbz-core: CoreBridge::fetch_for_external_stream_resolved wrapper.

Decision D3 (attempt UltraHiRes first + surface delivered quality) is satisfied
by passing Quality::UltraHiRes and returning StreamQualityInfo. Unit-tested:
content-type fallback by format id, sampling-rate unit normalization.

cc
2026-06-18 09:19:38 -06:00
vicrodh c974b8a973 feat(slint): miniplayer — second borderless window with five surfaces
Port the Tauri MiniPlayer to a second Slint MiniPlayerWindow (not an
AppWindow morph). Borderless via the native no-frame property (Slint's
winit adapter re-applies decorations from no-frame on every realization,
so the attributes hook alone was undone); the vendored renderer gives the
transparent rounded floating card. First QBZ surface with no system
titlebar.

State is mirror-driven with no second poll loop: the existing 450ms
playback poll and the 30Hz lyrics tick each fan out one mirror call into
the mini's per-window globals (REQ-1), gating the expensive copies
(artwork / queue / lyric lines) on track-id change. The queue surface owns
a self-contained navigable model (current + full upcoming, not the 40-row
sidebar page), and per-row play routes remote-first and bails before
mutating local (REQ-2). Transport callbacks delegate to the main window's
already-wired remote-first handlers (zero duplication).

Surfaces: micro / compact / artwork / queue / lyrics (reuses
LyricsLinesView + LyricsState). Footer with pending-seek state, transport,
volume popover, and the hover-expand controls capsule (surface tabs +
background-blur toggle + move-drag + close). Optional static
artwork-derived background-blur backdrop. Lifecycle in src/miniplayer.rs
(create-once / enter / exit / close / per-surface geometry persistence /
winit hooks). Menu entry enabled in PlayerBar + PlayerBarSmall.

cc
2026-06-17 18:35:28 -06:00
vicrodh 5fc1beb869 fix(slint): vendor femtovg-wgpu renderer for transparent window surface
Slint 1.16's femtovg-wgpu renderer configures the wgpu surface via
get_default_config() and leaves the composite alpha mode at Auto, which
resolves to an opaque surface on KWin/Wayland — a borderless transparent
window then shows a black rectangular backing behind the rounded card.
WGPUConfiguration exposes no surface/alpha hook, so the only fix is a
renderer patch.

Vendor i-slint-renderer-femtovg-1.16.1 with a preferred_alpha_mode() that
picks a supported non-opaque mode (PreMultiplied / PostMultiplied /
Inherit, guarded by the surface's advertised alpha_modes, else Auto) and
wire it into set_surface(). Pulled in via [patch.crates-io]; Slint stays
pinned =1.16.1. The main window is unaffected (it paints opaque).

cx
2026-06-17 18:35:03 -06:00
vicrodh e88de3b9ee feat(slint): auto-hide hover-reveal scrollbars (Discover/Home/Blacklist manager)
ListScrollbar gains an auto-hide mode: track and thumb fade in on list hover
or active scroll (900ms idle timer) and out otherwise. Discover and Home derive
hover from the global ShellState pointer over the flickable bounds; the
Blacklist manager swaps std ScrollView for Flickable + ListScrollbar driven by
a per-row hover counter. Resolves the hover-only scrollbar over album cards that
a TouchArea overlay would have broken.

cx
2026-06-17 11:01:41 -06:00
vicrodh e3577fc528 feat(slint): winit input hook — mouse back/forward nav + global pointer mirror
Drop to the winit backend (on_winit_window_event) to (1) route the browser
Back/Forward mouse buttons to navigation before Slint hands them to the
topmost TouchArea, so cards/sidebar rows no longer swallow them, and (2)
mirror the cursor position into ShellState (pointer-x/y/in-window) so passive
hover chrome can react without installing a TouchArea over interactive
content. This is the seam that unblocks the auto-hide scrollbars.

cx
2026-06-17 11:01:29 -06:00
vicrodh c4cf300c6b feat(slint): Settings content-filtering blacklist row (count/disabled/manage)
cc
2026-06-17 01:55:24 -06:00
vicrodh dbc62be90e feat(slint): Blacklist Manager view (list/search/toggle/remove/clear)
cc
2026-06-17 01:51:08 -06:00