Commit Graph
100 Commits
Author SHA1 Message Date
vicrodh c8ef2a1b1d refactor(slint): split qbz-slint into qbz-ui lib + qbz bin; reclaim qbz binary
Slint cannot split the .slint compilation across crates, but a Slint LIBRARY
crate exposes its generated Rust types to dependent crates (verified with a
minimal proof). So:

- NEW crate `qbz-ui` (lib): owns the whole .slint tree + build.rs + bundled
  translations; `slint::include_modules!()` + the generated types (AppWindow,
  MiniPlayerWindow, all globals/structs) are re-exported. Translation domain is
  now `qbz-ui` (the .po files renamed accordingly).
- `qbz-slint` (bin) -> renamed to `qbz` (package + `[[bin]] name = "qbz"`),
  reclaiming the binary name from the retired Tauri app. It depends on `qbz-ui`
  and does `pub use qbz_ui::*;` so existing `crate::AppWindow` / `crate::*State`
  references resolve unchanged.

This splits the single ~20 GB rustc into two crates (UI/codegen vs the 75k LOC
of logic) — the foundation for lower build peak, incremental caching, and the
upcoming per-feature crate extraction.

Also fixed for the move: qbz-i18n bundled-.po include paths, the font/WGSL
`include_bytes!`/`include_str!` paths (-> ../../qbz-ui/ui/), workspace members,
dev scripts (`-p qbz`), and the CI workflows. `cargo check -p qbz` is green.

cc
2026-06-25 17:51:09 -06:00
vicrodh b9eb300516 ci: self-hosted runner workflow for qbz-slint (real artifact path)
The GitHub-hosted private runner is 2 vCPU/8 GB and cannot build qbz-slint
(died at 1h42m). The dev box CAN, so a self-hosted runner (qbz-box) gives a
reliable CI artifact. Manual trigger; uncapped threads=2 (box-proven);
persistent work dir = incremental after first build.

cc
2026-06-25 13:32:06 -06:00
vicrodh a4371f294b ci: add mupdf-sys system libs (harfbuzz, jbig2dec, openjp2, jpeg, zlib, gumbo)
mupdf-sys (via qbz-booklet) links the system third-party libs; the build
got 21 min in and failed on a missing harfbuzz. Add the full mupdf system-lib
set so it links cleanly.

cc
2026-06-25 10:31:25 -06:00
vicrodh 2f58778abd ci: fix swap setup in slint build
The runner already ships an active /swapfile, so fallocate on it fails with
"Text file busy". Add the extra swap on a fresh path (/swap_extra) and guard
the whole swap block so a hiccup never fails the job — memory was never the
blocker (the prior run died on disk, not OOM).

cc
2026-06-25 10:08:17 -06:00
vicrodh 0b466474ef ci: fix runner disk exhaustion in slint build
ubuntu-24.04 has no separate large /mnt disk — it's all on the 72 GB root
(~16 GB free), so the 12 GB swapfile + cargo target overflowed it (protobuf
failed with ENOSPC, not OOM — the build was memory-fine). Free ~25-30 GB of
unused preinstalled toolchains, drop swap to 8 GB, and keep the target on the
default path.

cc
2026-06-25 10:04:54 -06:00
vicrodh 5806b0c057 ci: build qbz-slint on GitHub Actions (linux)
Build-only workflow on ubuntu-24.04 with a 12 GB swapfile and the
lowest-memory rustc settings (serial frontend, codegen-units 256,
opt-level 2, one rustc at a time) so a stock 16 GB runner can compile the
Slint app. Triggers on push to slint-mvp and via workflow_dispatch.

cc
2026-06-25 09:43:50 -06:00
vicrodh 337062f316 feat(slint): port keyboard shortcuts + Open Qobuz Link from Tauri
Adds the full keybindings system (26 actions, 5 categories, customizable
and persisted) plus the Ctrl+L "Open Qobuz Link" cross-platform resolver
to the Slint frontend, matching the Tauri implementation 1:1.

- New shared crate qbz-music-link: de-Tauri'd cross-platform link
  resolver (Qobuz/Spotify/Apple/Tidal/Deezer/song.link via Odesli) behind
  a QobuzSearchBridge trait. src-tauri left untouched.
- src/keybindings.rs: action model, winit shortcut grammar, conflict
  detection, ui_prefs persistence, main-window + miniplayer dispatch.
- src/link_resolver.rs: controller + bridge over QbzCore.
- New globals + 3 modals (read-only cheatsheet, customize editor with key
  capture, link resolver), hamburger menu entries, and a UiFocusState
  text-input guard wired across the app's TextInputs.

cc
2026-06-25 09:43:50 -06:00
vicrodh 14e800ea1f chore(scripts): make slint-run.sh memory-aware to skip the OOM wall
The fixed threads=16/opt=3 release build SIGTERM'd at codegen whenever the
desktop held ~8-11 GB (30 GB box, no hibernation, hard-freezes on OOM). Now
scales rustc frontend threads + codegen-units + opt-level to MemAvailable
(FAST >=26G / SAFE 14-26G / MIN <14G), runs under cargo-capped with the cap
matched to free RAM, and adds FAST/THREADS/CODEGEN_UNITS/OPT/CAPPED/NORUN env
overrides. Verified: full release build completed at SAFE tier (threads=2
cgu=256 opt=3, capped ~19.7G) in 55m without freezing.

cc
2026-06-24 21:01:32 -06:00
vicrodh 60dead32e4 fix(purchases): remove unused get_album_download_format_id accessor
The public getter was redundant: format-scoped completion gating reads the
download format_id internally via track_status_scoped (s.album_format_id), so the
accessor was never wired and only produced a dead_code warning. No behavior
change. cargo check -p qbz-slint exit-0 in 2m14s; the port code is now
warning-free (the remaining following_state/cached warnings are pre-existing,
unrelated to purchases).

cc
2026-06-24 19:42:07 -06:00
vicrodh ce963c8b9f fix(purchases): persist filter prefs + translate 4 toasts + explicit popup z-index
Closes the final-audit gaps for full 1:1 + standards compliance:
- Persist the 4 remaining purchase UI prefs (hide-unavailable, hide-downloaded,
  quality-filter, region-notice-seen) per-machine in ui_prefs.json + startup seed,
  so filter selections survive restart and the region notice does not reappear
  (matches Tauri's persisted prefs).
- Translate the 4 controller toast/error strings into es/de/fr/pt (were
  English-only; honors the 5-locale rule). Mexican/neutral Spanish.
- Explicit z:3000/3001 on the format-picker backdrop/popup + filter panel
  (ADR-009 >= 3000; previously relied on declaration order).
cargo check -p qbz-slint exit-0.

cc
2026-06-24 18:38:11 -06:00
vicrodh 9fbfcb5582 feat(purchases): i18n catalogs + gold purchase badge (Slint)
Slice 13/13 (final). Reconciles the download-progress interpolation in
PurchaseDetailView to proper Slint @tr placeholders (Downloading {}/{},
Download cancelled ({}/{}); tracksCount already placeholder). Fills the 4 non-en
.po catalogs (es/de/fr/pt) with 29 new purchases msgids each, in lock-step, no
duplicates, FR plural untouched, en catalog not modified. Adds the gold
qobuz_purchase source badge (24x24 chip on AlbumCard + inline gold-tint glyphs on
AlbumListRow/TrackRow/LocalAlbumView) gold #eab308. group.*/view.* not renamed.
Build: .po re-bundle + Slint macro expansion compile clean.

cc
2026-06-24 18:22:23 -06:00
vicrodh 45be8763d7 feat(purchases): add Settings toggles + nav placement truth table (default OFF)
Slice 12/13. Two persisted UI prefs (show_purchases, nav_tb_purchases, both
default false) in UiPrefs; Show-Purchases toggle drives the show-purchases flag
the S11 nav gates on; 'Purchases in title bar' toggle (nav_tb_purchases, disabled
without a custom titlebar). Implements the source-of-truth section 5.8 placement
truth table: sidebar entry shown iff S && !(T && TB), header entry iff S && T && TB
(closing the S11 per-feature-gating gap) so flipping the toggle moves the Purchases
entry between sidebar and header 1:1 with Tauri. on_appearance_bool persistence +
startup reseed. cargo check -p qbz-slint reached exit-0. Per-machine prefs
(consistent with other Slint appearance prefs).

cc
2026-06-24 17:41:43 -06:00
vicrodh 965c898b10 feat(purchases): wire navigation + view persistence (Slint)
Slice 11/13. NavEntry::PurchaseDetail(String) + full routing (scope_for/
apply_entry, history recording + update_nav_flags on purchase-album clicks and
the Purchases route), session-restore-safe purchases (id-gated purchase-album),
conditional-mount guard + A.3 reactive reload (back/forward between two purchase
albums re-fetches). Sidebar ShoppingBag row + HeaderBar tabs (full+compact),
gated on show-purchases && !offline, active for both purchases + purchase-album.
nav.rs round-trip history test. Type-check clean (exit-0 not reachable on this
box; SIGTERM at metadata = memory limit).

cc
2026-06-24 17:12:58 -06:00
vicrodh eab178c1e3 feat(purchases): add build_purchase_album detail mapper (service)
Restores the purchase-album detail mapper that the Slice-9 detail load path
(qbz-slint purchases.rs:2270) already references but which was left uncommitted
in the working tree (staged-list miss when committing S9). build_purchase_album
maps a fetched Album + purchase listing + downloaded ids + format map into the
detail payload (per-track + album downloaded flags, synthesized format options,
page counts). 5 unit tests; 30 offline-cache purchases tests green.

cc
2026-06-24 17:12:58 -06:00
vicrodh 18827eaab4 feat(purchases): wire play routing + now-playing active-track (Slint)
Slice 10/13. A.2: per-track row play in both purchases views routes to
playback::play_track_now (single track, NO replacePlaybackQueue/setPlaybackContext)
instead of media_action play_track_in_context. A.1: Play-album confirmed on the
regular album fetch+queue path. New mirror_now_playing_to_purchases feeds both
purchases globals' active-track-id + playback-active from the now-playing refresh
(meta apply + poll tick). Folded Slice-9 review fixes: detail + list track titles
trimmed (formatTrackTitle parity), dead PurchaseDetailState.artwork-url removed.
Fixed E0308 (track_id u64). Front-end type-check clean (zero diagnostics).

cc
2026-06-24 16:14:41 -06:00
vicrodh be87fe3377 feat(purchases): add PurchaseDetailView UI (detail + download screen, Slint)
Slice 9/13. New ui/purchases/PurchaseDetailView.slint cloning AlbumPageView
header (224x224, unavailable TriangleAlert overlay, LONG-month purchasedOn,
quality/stats), CircleAction Play-album + Download-All, format dropdown,
count-based progress (NOT format-scoped, A.4), Add-to-Library block, disc-grouped
track table with 4-state hover download, getTrackStatus format-scoped suppression,
bare track-row quality (A.6). Wires PurchaseDetailState/PurchaseActions globals
(state.slint + app.slint bridge), AppShell purchase-detail mount, controller
load_purchase_album + apply_detail, detail-cover artwork target. 28 tests green.

Note: per-track/Play-album routing (A.1/A.2) + now-playing active-track wiring
land in Slice 10; nav history + reactive-reload in Slice 11.

cc
2026-06-24 14:02:15 -06:00
vicrodh 06dd52e5d4 feat(purchases): add PurchasesView list UI + nav mount (Slint)
Slice 8/13. New ui/purchases/PurchasesView.slint (list surface: Albums/Tracks
tabs+counts, debounced search, group/sort/filter, grid/list toggle, format-picker
popup, quality radios, unavailable overlay, bare track-row quality A.6) reusing
SegmentedTabBar/ExpandableSearch/QbzSelect/AlbumCollectionView/TrackRow. Wires
PurchasesState/PurchasesActions globals (state.slint + app.slint export bridge),
AppShell mount on ContentView.purchases, NavEntry::Purchases, artwork target,
controller apply/derive/actions in purchases.rs, shopping-bag/download icons.
28 unit tests green.

1:1 fix: download-track fetch error shows downloadFailed toast (not noFormats);
get_album_formats returns Result to distinguish fetch failure from empty list.

cc
2026-06-24 09:53:39 -06:00
vicrodh 44aa4e1462 feat(purchases): add download state machine + actions (controller)
Slice 7/13. qbz-slint/src/purchases.rs: ports purchaseDownloadStore as a Send
OnceLock<Mutex<>> controller singleton (AlbumDownloadState keyed by albumId,
flattened allTrackStatuses, per-album abortFlags). Actions: start/execute album
download (sequential loop, abort-check, destination rewrite after first success),
start_track_download (merges into existing state, A.7), cancel, Add-to-Library,
folder picker, skip-if-remote.

1:1 fidelity fixes surfaced during review (verified vs Svelte source):
- Album loop marks complete THEN best-effort registry write (swallow failure)
  like executeAlbumDownload .catch(()=>{}); only the single-track path propagates
  a registry error to failed (B.1). Adds download_purchase_track_file_only to
  qbz-offline-cache (download-only) beside the bundled download_purchase_track.
- apply_cancellation cancels only tracks with NO status entry (verbatim Svelte).

cargo check + 20 qbz-slint / 25 offline-cache purchases tests green.

cc
2026-06-24 06:23:04 -06:00
vicrodh 76d0fd6b57 feat(purchases): wire registry reads + qualityDir in controller
Slice 6/13. qbz-slint/src/purchases.rs: get_downloaded_purchase_formats
(HashMap track_id -> Vec<format_id>, the format_map) and quality_dir(label)
(/-> - global replace + trim, section 7.5), via library_db::with_db. Unit test
for quality_dir. Dead-code until slices 7-9 wire the call sites.

cc
2026-06-24 05:01:41 -06:00
vicrodh 9e676441e4 feat(purchases): add single-track download primitive
Slice 5/13. qbz-offline-cache/src/purchases_service.rs: download_purchase_track
(get_track -> getFileUrl(format) -> download_audio -> .part write -> rename ->
registry), target_path (sanitized {dest}/{artist}/{album [quality]}/{NN-title.ext}),
purchase_extension (keys off RESPONSE format/mime). Blind-test quirks 1:1:
records REQUESTED format_id while ext derives from RESPONSE (B.2), no collision
preflight / silent overwrite (B.3), restrictions ignored (B.5), file->rename->
registry ordering with orphaned-file-on-registry-failure (B.1). Tests green.

cc
2026-06-24 04:37:05 -06:00
vicrodh b87378349e feat(purchases): add format synthesis + download-flag annotation
Slice 4/13. qbz-offline-cache/src/purchases_service.rs: synth_formats(&Album)
ports v2_purchases_get_formats (ids 27/7/6/5, exact labels, highest-first order,
hires + sr>96 conditions, default index 0); apply_download_flags ports
v2_apply_purchase_download_flags (per-track downloaded + downloaded_format_ids;
per-album all-nested-ids-in-dlIds with empty->false page-mode gotcha). Frontend
overrides backend downloaded. Unit tests green.

cc
2026-06-24 04:25:03 -06:00
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