Commit Graph
215 Commits
Author SHA1 Message Date
François Laignel a01097708e qbzd: install the rustls process-level CryptoProvider
The rustls crypto provider was not installed for `qbzd`, which caused the
process to panic:

> thread 'tokio-rt-worker' (15737) panicked at [...]/rustls-0.23.37/src/crypto/mod.rs:249:14:
>
> Could not automatically determine the process-level CryptoProvider from
> Rustls crate features.
> Call CryptoProvider::install_default() before this point to select a
> provider manually, or make sure exactly one of the 'aws-lc-rs' and 'ring'
> features is enabled.
> See the documentation of the CryptoProvider type for more information.

The function `ensure_crypto_provider()` was defined in the `qbz-cast` crate
which is obviously not a dependency of `qbzd`.

This commit moves `ensure_crypto_provider()` to `qbz-app`, calls it from `qbzd`
and updates `qbz` accordingly.
2026-07-26 21:12:44 +02:00
vicrodh a8919cbadf feat(app): the round's wiring — gamescope, watchdog, geometry, hearts
- Steam PR1/PR3/PR4 wiring: apply_gamescope_quirks at the top of main(),
  tray + miniplayer suppression, gilrs spawn, UiFocusState OSK hook.
- #558 watchdog arm at window-ready; #654 monitor-DPR geometry clamp;
  local-heart routing in the favorite arms; play-later media-action arm;
  sort-flip handlers with direction (#657/Library All pattern); gilrs +
  wayland-data-control deps (#514).

km
2026-07-25 12:05:56 -06:00
vicrodh 70d8dc85f2 feat(macos): center traffic lights in the header + NPB-Small +2px
With the custom chrome (titlebar_transparent + fullsize_content_view)
AppKit parks the native traffic lights at the stock ~28pt titlebar height,
visibly above the 42px header controls. macos_chrome.rs measures the close
button's centre in window coordinates and shifts the shared button
container by the delta to the 21pt header centre (custom chrome only,
one-shot after show; known gap: AppKit re-lays the titlebar on fullscreen
roundtrips). Deps: raw-window-handle direct + slint raw-window-handle-06 +
objc2-app-kit NSWindow feature, all macOS-target only.

NPB-Small on macOS gains 2px of bar height (ShellState.npb-small-height,
44px, growing upward from the bottom-anchored bar) so CJK/descender-heavy
meta lines stop clipping at the window's bottom edge. PlayerBarSmall pins
the song card to its old 39px top-anchored geometry: the art keeps its
exact top-relative position and the 2px land as bottom clearance. Linux
geometry is pixel-identical to before. slintcheck stub green.

km
2026-07-21 12:30:19 -06:00
vicrodh f9713591fe chore: bump version to 2.0.2
Workspace version 2.0.1 -> 2.0.2 (Cargo.toml + lock, workspace crates only —
no dependency re-resolution), a new metainfo <release> entry, and the release
docs: TAG_DETAILS.md (human-facing, About-modal/website format, 2.0.2 section
prepended above 2.0.1/2.0.0) + CHANGELOG.md (full detail since v2.0.1 — the
headless daemon, kiosk mode, dynamic background, cast/quality #638, audio,
rendering #617, library, themes and security work). Codename inherited on the
patch line; About-modal literal untouched.

cc
2026-07-20 19:49:09 -06:00
vicrodh bdbdf4e0a1 feat(qbzd): publish MPRIS media controls (KDE/GNOME widget + media keys)
Add a daemon MPRIS service (CONSOLE ext) via qbz-media-controls (mpris-server on
Linux, incl. the DesktopEntry that lets GNOME/KDE resolve the app icon). A KDE
Plasma media widget, a plasmoid, or hardware media keys now drive the daemon
with NO custom client code — the marquee third-party-integration surface.

Two halves in crate::mpris:
- OUTBOUND: a CoreEvent-bus subscriber pushes now-playing metadata, play/pause,
  live position, and volume into the OS controls (seeded once from live state).
- INBOUND: the qbz-media-controls callback maps MediaEvent (Play/Pause/Toggle/
  Next/Previous/Stop/Seek/SetPosition/SetVolume) onto core transport commands;
  seek/volume honor the DSD-direct guard; Next/Previous fire the full advance
  ritual off the D-Bus thread; Raise/Quit are ignored (headless).

The inbound callback holds only a Weak<AppRuntime> (upgraded per event) and the
updater task upgrades a Weak once to seed then drops it, so the integration never
pins the runtime — the #521 audio-release-before-drop(booted) ordering is
unaffected. Spawned next to the scrobbler in run(); torn down (updater abort +
handle drop) in shutdown. On by default where a session bus exists; QBZD_MPRIS in
{0,false,off,no} disables it, and spawn returns None gracefully on a headless
box with no D-Bus. 280 tests green.

cc
2026-07-19 22:24:41 -06:00
vicrodh 1638da05dc chore(deps): bump getrandom to 0.4 in qbz-cast
getrandom::getrandom() was renamed to getrandom::fill() in 0.3; adopt fill() at the cast token call site. Consolidates onto the getrandom 0.4.2 that tempfile and rand already pull in.

cc
2026-07-17 20:20:51 -06:00
vicrodh b8ee89b85c chore(deps): bump tempfile to 3.27.0
cc
2026-07-17 20:19:31 -06:00
vicrodh a1fbbafc79 feat(qbzd): ratatui setup TUI, six screens with explicit-save semantics
Implements owner requirement 2 (03-setup-tui.md): `qbzd setup`, a six-screen
raspi-config-grade configurator over the daemon's real stores — zero new
persistence. ratatui 0.30 (IV5: unifies on the lock's unicode-width 0.2.2;
0.29 forbidden). A synchronous event loop on a blocking thread; disk/HTTP only
at screen entry, `r`, save and the immediate actions, on workers with a spinner
(no I/O on keystrokes). Non-tty exits 2 with the verbatim copy; alt-screen +
panic-hook restore.

Screens: Account (T5 login engine; inline masked token paste, suspend-and-run
browser login, logout confirm), Audio (the §3.2.3 constraint matrix + 7
cascades verified 1:1 vs the desktop code, device picker re-deriving the
§3.2.2 grouping/BP-badge rules, DSD guard), Playback (ask->fallback rule,
infinite-radio read-only), QConnect (startup/name/volume_mode, live preview),
Network (whole-file qbzd.toml rewrite preserving every key), Import/Export
(pure renderer of the T12 bundle plan; interactive device re-pick; default-off
auth gate). Saves reuse T11 write_one + reload/status classification (§4.3);
dirty marker + Save/Discard/Stay. All user-facing strings centralized.

cc
2026-07-14 20:42:42 -06:00
vicrodh 0b7cd6c5e3 feat(qbz-app): settings bundle engine with importer-side classification
Export/import CLI on qbzd; requirements 4 and 5 complete with zero desktop builds.

cc
2026-07-14 19:44:01 -06:00
vicrodh 196f167873 feat(qbzd): copy QConnect glue with TODO(converge) markers and daemon trait impls
Real-volume join report and reconnect credential re-resolution are daemon-copy
fixes; desktop call sites untouched (OD2).

cc
2026-07-14 17:47:33 -06:00
vicrodh 66c9acfa86 feat(qbzd): login via one-shot nonce-bound listener, paste and token paths
Port the desktop system-browser OAuth (crates/qbz/src/auth.rs) into the daemon
CLI with three daemon-specific changes: the one-shot listener binds an ephemeral
port (never the control-API port, D6), a CSPRNG state nonce binds the callback,
and the command only validates + persists the token into the daemon root then
best-effort nudges a running daemon. Adds validate_token / login_browser /
login_paste / login_with_token_arg / nudge_reload plus logout; no email+password
surface anywhere. cli/copy.rs carries the verbatim 02 §2.2 timeout, success and
logout strings.

cc
2026-07-14 15:13:07 -06:00
vicrodh 4d0f52138a feat(qbzd): headless runtime bring-up with daemon-root stores and credential restore
Adds additive _at credential fns (crypto/format untouched), DaemonAdapter
broadcast bus, numbered boot per 01 §8.1 incl. the #521 shutdown pair.

cc
2026-07-14 13:40:49 -06:00
vicrodh 02c2dba68d feat(qbzd): scaffold slint-free qbzd crate with clap verb tree and CI gate
IV6 build peak: 795 MB max RSS (summed across the cargo-capped build's
process tree via /proc VmRSS polling; /usr/bin/time -v is not installed
on this box). aws-lc-rs in graph: 0 (single rustls provider: ring
v0.17.14 throughout).

cc
2026-07-14 12:53:09 -06:00
Afonso Jorge Ramos a0bf1328d6 fix(cast): use CSPRNG media path tokens and redact URIs in logs
Replace RandomState-based tokens with getrandom, avoid logging full
media base URLs, and redact /audio/<token>/… in DLNA info logs.
2026-07-11 23:01:05 +02:00
vicrodh 7ea9e2567b chore: bump version to 2.0.1
Workspace + lockfiles, the dead-Tauri consistency version fields, the metainfo
<release> entry, and the release docs (TAG_DETAILS.md / CHANGELOG.md).

The About-modal codename and the TAG_DETAILS h1 are left as __CODENAME__ /
__SUBTITLE__ placeholders on purpose — the owner names the release before the
tag (2.0.0 was "Rebuild 破 (You Can (Not) Advance)").

cc
2026-07-08 18:43:27 -06:00
vicrodh a2141ebe63 fix(app): single-instance guard on Linux via session-bus name (#544, #559)
Tauri parity regression: the old app shipped
tauri-plugin-single-instance; the Slint rebuild lost it, so every click
on a pinned shortcut or launcher entry spawned another full player
(reported on Hyprland in #544 and KDE in #559). The first instance now
owns the com.blitzfc.qbz session-bus name (auto-grantable inside
Flatpak — the name is the app id, no finish-args change); a second
launch asks the owner to raise its window through the already-exported
MPRIS Raise method (full name org.mpris.MediaPlayer2.com.blitzfc.qbz)
and exits. Blocking zbus 5 + async-io on purpose (the tokio feature is
forbidden graph-wide); any D-Bus failure falls through as primary so
the guard never blocks startup.

The issues' other symptoms (generic Wayland icon, forgotten window
position) trace to the missing app_id in 2.0.0 — already fixed by
dae6e7f6 (#544): compositors key both the icon and geometry-restore
rules on app_id.

cc
2026-07-06 20:56:52 -06:00
vicrodh 1a7d33d9e0 feat(ui): custom theme editor panel + controller + i18n
Wire the custom-theme editor end to end. In AppearanceSettings, when the
"Custom" theme is selected, show a "Start from current theme" button, a "Dark
theme" polarity toggle, and a grouped token grid (SURFACES / TEXT / ACCENT /
STATUS / OTHER) of 11 base tokens. Each token is a swatch that toggles an
inline ColorPicker; edits route through AppearanceState.custom-set-token
(drag) and custom-set-token-hex (hex commit).

The crate::custom_theme controller reads the base straight from the editor
swatch properties (the in-memory source of truth, no per-drag disk reads),
sets the touched field, re-derives via qbz_theme::theme_from_base, pushes the
palette live and persists. seed_from_current snapshots the live Theme global
into the base; toggle_dark flips polarity; set_token_hex parses in Rust (Slint
cannot). main.rs registers the four callbacks.

Also harden the ColorPicker reseed guard: skip decompose when the fed-back
value equals our own emitted color, so a drag ending at v=0/s=0 no longer
resets the hue. New strings added to all 7 gettext catalogs (es Mexican/
neutral); msgfmt --check clean; full .slint tree validated via the slintcheck
stub.

cc
2026-07-06 10:19:59 -06:00
vicrodh 6293098ddb Merge feature/auto-theme-slint into pre-release
Auto Theme port (system accent / wallpaper / custom image), reviewed
adversarially: port fidelity 1:1 verified, 59 qbz-theme tests green.

cc
2026-07-05 22:44:50 -06:00
vicrodh 7d46aaf200 chore: lockfile entry for the vendored i-slint-renderer-skia
cc
2026-07-05 22:37:08 -06:00
vicrodh c8d8b47e49 feat(theme): port auto-theme generation core to qbz-theme
Port the legacy Tauri src-tauri/src/auto_theme/{palette,system,generator}
logic into a new frontend-agnostic qbz-theme::auto module. The palette
extraction (k-means), WCAG contrast math, HSL shifts and DE detection are
carried over 1:1; only the final assembly changes: the output is now the
registry ThemeColors contract instead of a CSS custom-property map.

The generator mirrors registry::StdSpec::build so a generated theme
composites identically to a static one — same polarity-driven alpha ramp,
success/focus_ring/favorite/border_muted derivations, and status tints.
Per-status text tokens (--btn-danger-text/--btn-warning-text) are dropped
since ThemeColors has no field for them.

Adds `image` (0.25, minimal codec features) to qbz-theme and unit tests for
the mapping (polarity, derived families, determinism, scheme path).

cc
2026-07-05 22:25:06 -06:00
vicrodh 7401d2c3fd fix(text): always draw the first line under overflow: elide
Vendor i-slint-core 1.16.1 with a one-rule patch in sharedparley's
line culling: with overflow: elide, every line whose bottom edge
exceeded max_height was dropped — INCLUDING the first one, eliding the
text to literally nothing. CJK fallback fonts report line metrics
taller than snug fixed-height boxes (Hiragino at 14px measures ~21px
inside TrackRow's 20px title line), so on macOS every CJK track title
rendered blank while Latin titles fit; Linux survived because Noto's
metrics fit the box. Root-caused with a headless render matrix on the
Mac Mini: the verbatim TrackRow structure reproduced the blank title,
removing elide rendered it, and the patched core renders all variants.
The first line still receives the ellipsis treatment as the last
visible line.

This is the real #543 mechanism. The earlier femtovg render_glyph
retry (3a1c367d) addressed a different, hypothesized layer and stays
as harmless defense in depth.

Fixes #543

cc
2026-07-05 18:53:02 -06:00
vicrodh 6c9964481a fix(dbus): stop forcing the zbus 5 executor onto tokio
zbus features unify across the dependency graph and "tokio" wins over
"async-io": enabling it via rfd, ashpd (secret portal) and ksni's
default features flipped the shared zbus 5 executor to tokio for
accesskit_unix/atspi too — Slint's accessibility backend opens its
D-Bus connection on a plain thread and panicked at startup ("there is
no reactor running, must be called from the context of a Tokio 1.x
runtime"), killing AT-SPI support. Switch every zbus-5 consumer we
control to the async-io backend (rfd "async-std", ashpd "async-io",
ksni default-features off + "async-io"): async-io connections
self-drive from any thread and the futures still await fine inside our
tokio runtime. The zbus 4 group (keyring/secret-service/mpris-server/
qbz-audio) is a separate feature graph and stays on tokio by design.

cc
2026-07-05 10:26:18 -06:00
vicrodh 3a1c367d1a fix(text): render CJK glyphs at non-regular weights on macOS
Vendor femtovg 0.23.2 with a targeted patch: the swash raster path can
fail to scale some variable fonts at a non-default variation instance
(the CFF2-based CJK system fonts on modern macOS), and femtovg cached
those glyphs as permanently missing — so track titles and any other
text at medium/semibold/bold weight rendered blank for CJK while
regular weight (empty coords) worked. render_glyph now retries the
glyph at the font's default instance before giving up: worst case the
glyph draws at regular weight instead of not at all. Fonts and
instances that already render are untouched.

Fixes #543

cc
2026-07-05 08:23:17 -06:00
vicrodh 7252918d4d chore: bump version to 2.0.0
QBZ 2.0 "Rebuild". Workspace, Tauri manifests, npm manifests and the
Flatpak metainfo all move to 2.0.0; TAG_DETAILS for the release notes.
snapcraft.yaml uses version: git and needs no bump.

cc
2026-07-03 18:36:06 -06:00
vicrodh 1cc1b3d922 feat: add qbz-dsd crate (DSF/DFF demux, DSD to PCM, DoP, native DSD)
New frontend-agnostic crate for DSD support. In-house DSF and DFF
demuxers (up to 5.1, DST rejected with a typed error), embedded ID3
tags and artwork via the id3 crate, a verbatim BSD port of Sebastian
Gesemann's dsd2pcm table engine plus a half-band decimation chain
(uniform 88.2 kHz output, ITU stereo downmix for multichannel), DoP
frame packing per the DoP 1.1 standard (marker alternation, 0x69
silence), and native DSD_U32 packing (BE/LE) for kernel-granted
devices. 20 tests over synthesized fixtures.

cc
2026-07-03 17:56:26 -06:00
vicrodh fa3ee1e410 fix(fonts): pre-register macOS CJK fallback families in vendored i-slint-common
fontique's CoreText-based Han-script fallback fails to resolve a family on
modern macOS (the suggested PingFang is not always in the scanned family
map), so Japanese kanji rendered blank while kana worked. Vendor
i-slint-common 1.16.1 and pre-register Hiragino / PingFang / Apple SD
Gothic Neo / Arial Unicode MS in the fallback map at collection creation
(Japanese-biased default; the map takes priority over the CoreText query).
macOS-only cfg; Linux keeps its working fontconfig fallback. Type-checked
against aarch64-apple-darwin.

cc
2026-07-03 10:00:31 -06:00
vicrodh b3f607fcf0 chore: sync crates Cargo.lock
cc
2026-07-01 19:46:19 -06:00
vicrodh 99da4c381b feat(slint): pre-launch dev panel, queue controls, appearance persistence, reco cache settings
Sleep timer (queue footer): tokio monotonic deadline + 1Hz countdown that
fires core().pause() at expiry; presets + custom minutes via a popover.

Stop after this song: 4 QbzCore passthroughs + playback consume-before-advance
seam and the load-bearing gapless-prefetch gate, plus the queue marker/menu so
the Queue persists when playback halts.

qbz-log crate: TeeLogger wrapping env_logger (stderr + 5000-line ring +
~/.local/share/qbz/logs/qbz.log) with write-time secret redaction (regex +
live-secret registry) and a diagnostics bundle builder.

Dev panel logs: in-app viewer that shows content the moment it opens, advanced
controls behind a discreet [+], "Copy all" as the sole footer button, a copy
button on the upload URL, and a redacted public paste upload.

Diagnostics: qbz-app::diagnostics (extracted, src-tauri untouched) feeding a
7-section panel with the live active output device + sample rate/format (pactl)
and a complete redacted report for the upload.

Report an Issue: wires the header menu item to a 1:1 Tauri modal that opens the
GitHub bug-report template.

Appearance persistence: 6 toggles that were live-only now persist via ui_prefs
+ handler arm + startup seed (window-title-show, show-volume-steppers,
sidebar-playlist-collage, local-library-track-artwork, in-app-toasts,
theme-filter).

Recommendations cache (per-tab only): TTL select 24/36/48/72h + Refresh now in
ExternalRecoView; no change to the other Discover tabs.

Bump workspace version 0.1.0 -> 1.2.15 so app_version is correct in the
diagnostics report and the QConnect About string. i18n es/de/fr/pt for all new
strings.

cc
2026-06-29 13:19:06 -06:00
vicrodh ec98245364 feat(reco): wire the external-recommendations 4th Discover tab (Slint)
Adds "Recommendations" as a 4th Discover tab (alongside Home / Editor's Picks /
For You), rendering the qbz-external-reco engine output. NOT part of For You.

- ui: new ExternalRecoState global + ExternalRecoView (4 personalized carousels
  C1-C4 with length>0 self-hide gates + the editorial cold-start rows), 4th tab
  pill button in HomeView, mount on active-tab == "recommendations", skeleton
  gate. Export the global from app.slint.
- bin: external_reco controller — RecoCatalog over QbzCore, per-user resolution
  cache lifecycle, LocalHistory from reco_store (known artists + recent plays),
  scrobbler-username gate (is_authed), build + apply into ExternalRecoState
  (models built on the UI thread; slint::Image is !Send) + artwork jobs.
- bin: 6 ArtworkTarget variants + resolution arms; reco::recent_track_ids
  wrapper; home::select_tab + discover_prefs::push_descriptors treat the new tab
  like For You (own view, Home/Editor pushed empty); lazy-load hooks at the tab
  click + both Discover nav-restore sites.

cargo check -p qbz green. Last.fm reads still need the proxy allowedMethods
deploy (separate qbz-api-proxy repo) before they return data.

cc
2026-06-27 03:12:05 -06:00
vicrodh 408b3c8308 feat(reco): external-recommendations engine (Last.fm + ListenBrainz -> Qobuz)
New frontend-agnostic crate qbz-external-reco: blends Last.fm + ListenBrainz
recommendation signals into the Discover "External Recommendations" carousels,
validating every candidate against the Qobuz catalog (ISRC direct -> MusicBrainz
inc=isrcs bridge -> fuzzy text scorer, gated) before display. Four carousels
(C1 similar artists / C2 similar tracks you haven't heard, C3 listened-but-not-
recently, C4 deep cuts from known artists) capped at 20 with daily rotation over
a larger cache-backed pool, plus a Qobuz-editorial cold-start fallback (top
albums + artists) when no external source is connected. Round-robin blend across
sources. Resolution cache (per-user SQLite, 30d hits / 7d misses, WAL).

Supporting public read methods added to qbz-integrations:
- Last.fm: get_top_artists / get_top_tracks / get_loved_tracks /
  get_recent_tracks / get_similar_tracks (public reads via the Cloudflare proxy).
- ListenBrainz: get_cf_recommendations / get_recent_listens /
  get_metadata_recordings (204/404 -> empty, never error).
- MusicBrainz: get_recording_isrcs (the MBID -> ISRC bridge).

Engine is headless and unit-tested (matching + cache); no qbz-ui dependency.

cc
2026-06-27 02:37:32 -06:00
vicrodh 49a78c2ca0 feat(reco): per-user artist-vector store lifecycle (T7)
Open the per-user ArtistVectorStore (<user>/cache/artist_vectors.db) and install
it on the core (core.set_artist_vectors) at the three session-activation sites
(login, restore, offline entry), next to the sub-plan-1 reco init; drop it on
logout (core.clear_artist_vectors). The store is core-owned (no bin-side static),
so the suggestions engine reads/writes it through QbzCore. Adds qbz-reco as a bin
dependency.

cc
2026-06-26 21:04:23 -06:00
vicrodh 9cc44f5d1d feat(reco): wire generate_playlist_suggestions into qbz-core (T6)
Add QbzCore::generate_playlist_suggestions + a per-user artist_vectors store
field (Arc<tokio::Mutex<Option<ArtistVectorStore>>>) with async
set_artist_vectors / clear_artist_vectors. It resolves each playlist artist NAME
to a confident MusicBrainz id (reusing musicbrainz_resolve_artist; the names come
from the playlist's own Qobuz tracks so they're already canonical, making the
Tauri command's extra Qobuz-name re-fetch unnecessary), then runs the
SuggestionsEngine over the core-owned clients + vector store.

Also aligns the qbz-reco builder/engine handle types to qbz-core's reality (the
plan's by-ref pattern doesn't fit): mb_client = Arc<MusicBrainzClient> (no lock,
Send+Sync); mb_cache = Arc<std::sync::Mutex<...>> (locked only in await-free
windows); Qobuz client = Arc<RwLock<Option<QobuzClient>>> (unwrapped per access).
The vector store stays tokio::Mutex (held across awaits by the engine). No
dependency cycle (qbz-reco does not depend on qbz-core). qbz-core checks clean;
23 qbz-reco tests green.

cc
2026-06-26 20:58:37 -06:00
vicrodh 90a8240671 feat(reco): port SuggestionsEngine (direct-weight ranking + quality gates)
T5: 1:1 port of the artist_vectors SuggestionsEngine into qbz-reco (~880 lines;
structure matches the Tauri source function-for-function). generate_suggestions:
ensure each seed vector (builder.ensure_vector), gate on a non-empty combined
playlist vector, rank candidates by DIRECT summed relationship weight via
store.get_all_related_artists (NOT cosine/find_nearest, D3), then pull + dedup +
exclude tracks from Qobuz with the validate_qobuz_artist + has_incompatible_genre
gates and optional reason strings. Keeps Tauri's Arc<Mutex>/RwLock ownership
(Send-safe). DTO reconciliation: crate::api -> qbz_qobuz::QobuzClient +
qbz_models::Track (4 Qobuz calls mapped to real signatures, field-by-field).
Adds rand (0.10, workspace) + qbz-models deps. 23 qbz-reco tests green.

cc
2026-06-26 20:45:17 -06:00
vicrodh 7049a9f0d9 feat(reco): port ArtistVectorBuilder on the crate clients
T4: 1:1 port of the artist_vectors vector builder into qbz-reco. Keeps Tauri's
Arc<Mutex>/RwLock ownership of the store + MusicBrainz client/cache + Qobuz
client (NOT the plan's by-ref pattern): MusicBrainzCache holds a !Sync rusqlite
Connection, so a borrowed &cache held across the MB-API await would make the
spawned suggestions future !Send. The interleaved short-lived locking (drop the
guard before each await) is the faithful + Send-safe shape. Swaps only
crate::api::QobuzClient -> qbz_qobuz::QobuzClient (get_similar_artists ->
SearchResultsPage<Artist>); the MusicBrainz types already live in
qbz_integrations so extract_relationships ports verbatim. Drops the dead
resolve_qobuz_to_mbid (D3). 21 qbz-reco tests green.

cc
2026-06-26 20:34:25 -06:00
vicrodh 43d67335f0 feat(reco): port ArtistVectorStore (3-table SQLite, keep schema)
T3: 1:1 port of the artist_vectors vector store into qbz-reco. open_at(base)
opens <base>/cache/artist_vectors.db (WAL) with the 3-table schema kept
byte-identical (CREATE IF NOT EXISTS) for cross-frontend reuse. Ports the
in-memory artist index, get_or_create_idx, set_vector (delete-then-insert per
(artist,source) + metadata nnz), get_vector (SUM GROUP BY), has_fresh_vector,
get_artist_name, and the production ranking get_related_artists /
get_all_related_artists (SUM ORDER BY total_weight DESC). Drops the dead
find_nearest cosine path (D3) and the Tauri State/tokio wrapper (lifecycle in
the frontend/core per ADR-006). 4 store tests, 20 qbz-reco tests green.

cc
2026-06-26 20:20:49 -06:00
vicrodh 65de984988 feat(reco): scaffold qbz-reco crate + port SparseVector math
New frontend-agnostic crate qbz-reco (ADR-006) for the playlist Suggested Songs
engine (sub-plan 2). T1: 1:1 port of the Tauri artist_vectors SparseVector
(sorted parallel index/value arrays, set/get, dot merge-join, magnitude/
normalize, serde) with its full test suite (13/13 green). Drops the dead
top_k_similar helper (its only caller find_nearest is dropped per epic D3);
keeps cosine_similarity for parity + tests but not for ranking.

cc
2026-06-26 20:15:00 -06:00
vicrodh 998ab9d5c0 refactor(slint): booklet button downloads the PDF instead of the in-app MuPDF reader
Not every album ships a booklet and the in-app PDF reader was clumsy. The album
booklet button now DOWNLOADS the goody PDF (native save dialog, app HTTP client)
instead of rendering it in-app, and the MuPDF rasterizer is dropped entirely:

- crates/qbz/src/booklet.rs: reduced to set/clear URL + download_booklet (fetch
  + save dialog). Removed the reader (open/render/paging/zoom/rotate/close).
- main.rs: the ("album","booklet") action now calls download_booklet; removed the
  BookletActions reader wiring block.
- Removed the qbz-booklet crate (MuPDF) from the workspace + qbz deps; Cargo.lock
  drops mupdf/mupdf-sys. ~26 MB static MuPDF (~6 MB linked) + its heavy C build
  are gone, lightening the build (more headroom under the 16 GB rustc wall).

AlbumBookletModal.slint + the BookletState/BookletActions globals are now unused
(left in place; remove in a UI cleanup pass that recompiles qbz-ui).

cc
2026-06-26 17:20:15 -06:00
vicrodh d1437c70eb refactor(slint): extract toast into qbz-slint-common crate
New shared crate qbz-slint-common (will also host the artwork primitives via a
dependency-inversion pass). toast.rs is a clean leaf over qbz-ui: only Slint
types (AppWindow, ToastKind, ToastState) + slint core (Timer/Weak). Moved
verbatim; the only edit is the import (`crate::` -> `qbz_ui::`). Bin re-exports
via `pub use qbz_slint_common::toast;` so all 154 call sites across 21 files are
unchanged. slint dep is `default-features = false` (adversarial review: a
default-features slint here would turn on the default renderer for the whole
build via feature unification). `cargo check -p qbz` green.

cc
2026-06-25 18:15:03 -06:00
vicrodh 6f9ad4668e refactor(slint): extract strip_html + dates into qbz-text-utils crate
Both are pure leaves (no bin-module refs, no Slint coupling). Moved verbatim
into qbz-text-utils. Caught by adversarial review: dates.rs needs chrono's
`unstable-locales` feature (Locale + format_localized) and an inline
`qbz_i18n::current_language()` ref (no `use` line — the qbz_models::Track
trap). Bin re-exports via `pub use qbz_text_utils::{dates, strip_html};` so the
14 call sites are unchanged. `cargo check -p qbz` green.

cc
2026-06-25 18:13:55 -06:00
vicrodh 7678bceb3e refactor(slint): extract DAC wizard into qbz-dac-wizard crate
First per-feature crate split off the qbz binary. `dac_wizard.rs` (794 LOC)
was the cleanest candidate: zero cross-feature module deps — it only used
qbz-ui generated types (AppWindow, DacWizardState, …), qbz-audio, qbz-models
and qbz-i18n. Moved verbatim into `qbz-dac-wizard/src/lib.rs` (import
`crate::` -> `qbz_ui::`); the bin now `use qbz_dac_wizard as dac_wizard;` so
its ~20 call sites in main.rs are unchanged. `cargo check -p qbz` green.

cc
2026-06-25 17:54:10 -06:00
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 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 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 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 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 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 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 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 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 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