Theme system as data: qbz_theme registry (36 themes + auto/custom)
resolved from the shared ui_prefs slug into a themeJson document;
QbzTheme rebound to it so the whole app repaints live (OLED->Light
probe-verified). Full subnav (8 sections + Share logs footer);
Appearance 1:1 (theme dropdown with dark/light filter, ambient/titlebar
rows in their real home, typography, library visuals, window chrome,
tray via tray_settings.db, renderer); Integrations 1:1 (MusicBrainz,
Last.fm 2-step browser connect + authed state, ListenBrainz token
validate, Discord toggle -> live DiscordRpc.set_enabled). Offline/Local
Library/Blacklist/Developer render honest placeholders.
km
Port of the spec's palette derivation (16x16 downscale, coverage-
weighted 24-bin hue histogram, genuine secondary cluster or deeper
same-hue, +180 accent, B/W defaults) driven by the now-playing artwork
on track change. QPainter AmbientField with the WGSL scene's exact
four blob orbits, dim scrim, 30fps cap paused when inactive; chrome
surfaces translucent per the AppShell alpha tokens (content frame
surface-main @ 0.22 frosted). Same shared app_background pref (owner
has it 'ambient'), app-menu toggle applying live, QBZ_BG_* env knobs.
4 new unit tests (14/14 total).
km
settings_qt.rs controller over the shared stores (AudioSettingsState,
PlaybackPreferencesState, ui_prefs.json, qconnect_settings.db) with the
upstream cascades (dac-passthrough->skip-sink-switch, streaming-only->
gapless, backend switch resets routing) and live apply through public
player APIs (reload_settings/reinit_device/clear_audio_cache). ALSA
device regrouping (Defaults/Bit-perfect/Plugin/Other + BP badges),
searchable QbzSelect, QbzToggle/QbzSlider/SettingRow replicas, exact
row order and gating. Persistence probe verified (gapless +
streaming-quality round-trip, original values restored).
km
Queue panel 1:1: History tab (click replays per queue.rs semantics),
#442 'Next in queue'/'Next up' section markers from manual_next_count,
40-row pagination + live search + page clamp, row actions (play,
remove, remove-all-after, heart), press-drag reorder via move_track,
clear queue, plural-aware footer count, exact empty states. New Lyrics
panel: qbz-lyrics service + per-user cache wired, Qobuz-first chain
with LRCLIB fallback, synced line view with line-level highlight +
centered auto-follow + click-to-seek, exact status bodies. Both panels
stack Feishin-style in the right column; NPB lyrics button wired.
km
Six live tabs (All/Tracks/Albums/Artists/Playlists/Labels) with real
count badges, paged favorites/playlists/purchases loaders, library_all
merge semantics, heart routing (Qobuz API vs local store —
LocalFavoritesService now initialized at session entry), sort popup
with direction carets, grid/list toggle with natively windowed
GridView/ListView delegates, id-keyed artwork via signal with hand-
rolled eviction. Perf marks on the owner's account: 344 items, fetch
wall 2.7-3.4s (97% network, sequential), map+merge ~11ms, JSON
serialize 10ms, QML parse 7ms (~20us/item => ~140ms at the 2200-item
reference scale), RSS +29MB after publish.
km
Brings the windowed Library All backend (qbz-library) and the latest
Slint reference implementation into the POC base. POC branch only;
pre-release untouched.
km
QbzBridge QML singleton (12 properties, 8 invokables) driven by tokio
tasks via CxxQtThread. Port of auth.rs (browser OAuth loopback listener
with qbzd-style nonce, 180s timeout, cancel via JoinHandle abort,
scoped offline-gate lift for cold bundle init, D1 token-keep on network
failure / clear on explicit rejection, D4 subscription grace marks).
OfflineModeEngine + ConnectivityActor started at boot with live watch
forwarding (connectivity/captive portal/recovery banner/has-previous-
session). Boot: splash -> silent token restore -> shell | login.
Start-offline enters the guest profile. LoginScreen.qml is a faithful
port with exact msgids routed through qbz-i18n. 5 unit tests green.
km
The crate `ring` was transitively pulled due to the feature `rustls-tls` being
selected for `reqwest`. In order to ensure `ring` is not pulled,
`rustls-tls-webpki-roots-no-provider` must be used instead.
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.
- 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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