Add the desktop "Export settings…" surface for the qbzd daemon epic (T18):
a Developer-settings row that opens a modal with the single default-off
"Include Qobuz login and integration tokens" gate, its warning line, and a
Cancel/Export footer. Confirm() is a Rust callback (wired next). New
SettingsExportState/SettingsExportActions globals, exported from app.slint
so the Rust layer binds them, and the modal mounted in AppShell. New strings
translated across all 8 locales (en es de fr pt ru ja nl).
cc
After #606 the offline Clear-cache purges only the transient cache dirs (tracks/tracks-cmaf/artwork), never the organized downloaded albums — those are removed per-item from the offline manager. The old copy ('Remove every downloaded track. This cannot be undone.') was misleading. Relabels the row 'Clear cache', rewrites the description to say downloads are kept and point to the manager, and softens the toasts to 'Cache cleared' / 'Couldn't clear the cache'. 8 locales.
cc
Tauri let the user rename how QBZ announces itself to Qobuz Connect;
the Slint port lost the UI entirely while keeping the persistence and
the announce path (same device_name key in qconnect_settings.db, read
by connect() -> bootstrap_remote_presence). Restored 1:1: a text row
in Settings > Playback next to the QConnect startup select (LineEdit
mirroring the ListenBrainz token row, placeholder = the resolved
default 'Qbz - {hostname}'), a new settings-string callback channel
through the settings chain, and a live-service setter so a rename
takes effect on the next connection without an app restart — exactly
Tauri's semantics (no live re-announce; the row description says so).
Trim-only validation like Tauri; empty clears the override. Two new
msgids across the 8 locales.
cc
PR #569 follow-through with the owner-corrected scope: on macOS BOTH
titlebar modes work (native bar, or traffic lights integrated in the
QBZ header) — what's broken is hot-applying the toggle: the overlay
window attributes are fixed at creation, but the .slint chrome
(no-frame, header drag+inset, Purchases placement) flipped reactively,
hiding the buttons one way and overlapping them the other.
Split pref from applied state: the toggle edits use-system-title-bar
(persisted), the chrome reads a new system-title-bar-active seeded at
startup; the handler mirrors pref->active live on Linux only (behavior
byte-identical there), macOS keeps the session value + the existing
restart toast. The hide-title-bar row is macOS-hidden (that part of
the PR was right); the system-bar toggle stays.
cc
Restores the Tauri-era Qobuz Connect startup setting, now in
Settings > Playback per owner request. Reuses the shared
qconnect-app::startup module (QconnectStartupMode +
compute_effective_startup, unit-tested) and Tauri's exact
qconnect_settings.db keys (startup_mode / last_known_state), so an
upgrading user's choice survives. last_known_state is written at the
manual toggle's success points only — internal teardowns (D5 offline
force-disconnect, bootstrap failure cleanup) never clobber the
remembered state, mirroring Tauri's command layer. Startup auto-connect
runs post-enter_shell (online sessions only, once per process) through
the same connect() path as the toggle, on Tauri's bounded retry
schedule, bailing silently under offline mode instead of toasting per
retry. Select row mirrors the quality-retries row; 5 msgids x8 locales.
cc
The log uploader was reachable only through Settings > Developer >
Open log viewer, so users filing issues never found it and reports
arrived without logs. A pinned entry at the bottom of the settings
sub-nav (visible from every section) now opens the same log viewer
(copy + upload). One new msgid x6 locales.
cc
Owner feedback on the first smoke: the editor stacked 11 full-width
token rows. The tokens now render as compact inline cells (swatch with
its label underneath) grouped side by side — SURFACES/TEXT/ACCENT on
one line, STATUS/OTHER on the next — and ONE shared ColorPicker
expands below the grid for whichever token is open, so editing doesn't
reflow the swatches. The picker's external-change reseed (init +
changed-value guard) already handles switching tokens while open.
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
Repoint the existing auto-theme Settings scaffold from the "System" theme to
the new dynamic "Auto (dynamic)" option:
- state.slint: add theme-is-auto (distinct from theme-is-system, which stays
the native OS-palette follow).
- AppearanceSettings.slint: gate the Source dropdown, image picker, detected-DE
hint and Regenerate button on theme-is-auto, and drop the placeholder
`unsupported` flag now that the rows are functional.
- translations: add "Auto theme generation failed" to all 7 catalogs and give
the es "Wallpaper Sync" string a proper Mexican-neutral translation.
cc
Settings > Audio gains a 'DSD playback' dropdown (Convert to PCM /
DoP / Native DSD, ALSA-direct only, permanent warning copy since DoP
and native support cannot be detected). Local .dsf/.dff files route to
the player's DSD path with toasts on unsupported files. Quality badges
read DSD64/128/256 from the stored bit rate on every surface including
the now-playing stamp. The gapless prefetch now pre-queues LOCAL
tracks too: DSD via the DoP/native append or an in-memory converted
WAV, other local formats via their raw bytes (fixes local FLAC
gapless); CUE virtual tracks are excluded. Six new locale strings.
cc
Custom title bar (Linux opt-in, macOS overlay default): frameless window
with drawn window controls (close/maximize/minimize, left or right
placement), header drag with once-per-press latch (Wayland move-grab
swallows the release; re-arming on stale pressed state re-grabbed the
window onto the cursor), double-click maximize, edge resize, 1px
hairline. Title bar minimize is always a WM minimize now - the old
minimize-to-tray branch made it visually identical to close-to-tray.
Compact header navigation (opt-in, Appearance): with nav out of the
sidebar, the header can always use the icon-only compact nav even while
the sidebar is open. Full text tabs and compact nav stay mutually
exclusive; the playlists flyout button keeps its closed-sidebar-only
behavior. Persisted as ui_prefs.nav_header_compact.
i18n: new strings in all six locales (includes two DSD toast strings
for the in-progress DSD support, riding along in the catalogs).
cc
The six local SecondaryButton/ManageButton copies in the settings panels
and the two inline AudioSettings buttons had fixed widths sized for
English; Spanish/French/German labels overflowed the borders. Width is
now Math.max(label preferred-width + padding, old floor), so short
labels keep the 160/170/200px right-edge grid and long translations
grow leftward instead of overflowing.
cc
Webview-zoom-style presets: Extra small 0.8 / Small 0.9 / Default 1.0 /
Large 1.2 / Extra large 1.5, applied through SLINT_SCALE_FACTOR set at
the top of main() before any thread or the backend exists. The env var
overrides the compositor DPR, so the value is last observed DPR x
preset; the Default preset leaves the var unset (stock DPR handling,
including live monitor changes, stays intact). last_dpr is persisted
from the winit window (real compositor DPR) on the 5s renderer-sentinel
timer.
- const-bake guard: strip SLINT_SCALE_FACTOR in qbz-ui build.rs so the
slint compiler never bakes the factor into the generated code
- new UiScale global + scale-divided window minimums keep the physical
minimum constant across presets (XL fits a 1366x768 screen); the
three window-geometry gates in main.rs use the process-active factor
- artwork decode targets scale with the preset: sharp covers at >=1.2,
smaller decoded-cache footprint below 1.0
- Settings > Appearance "Interface size" row below Language (replaces
the commented-out percentage placeholder and its orphan properties),
restart-to-apply toast
- Typography.boost = 1.1 on Extra small only, so body text stays
readable at the 0.8 factor; Small (0.9) needs no compensation
- i18n: new strings added to all 7 locale catalogs
Known accepted limitation: slight text softness at fractional scale
factors (owner-accepted; diagnostic notes live in
qbz-nix-docs/ui-scale-presets/).
cc
Weak-GPU hardware (Raspberry Pi 4 / v3dv, Haswell / hasvk) exposes a
real Vulkan adapter, so the old binary wgpu-or-software detection handed
it the slowest possible path: femtovg-wgpu over an immature Vulkan
driver. Mesa's GL path is the fast one on that class of hardware.
- Three-tier chain: wgpu -> femtovg GL -> software. Auto-detect
classifies adapters (weak-driver markers incl. hasvk, integrated-on-arm,
tiny texture limits); adapters only reachable through wgpu's GL backend
never select the wgpu tier (the renderer masks Backends::GL out, so
detection must too). renderer-femtovg (GL) feature now compiled on
Linux.
- wgpu tier requests PowerPreference::LowPower (WGPU_POWER_PREF still
wins).
- Per-window swapchain alpha in the vendored femtovg renderer: only the
miniplayer gets a transparent (blended) swapchain; the main window
keeps an Opaque one, sparing a full-window compositor blend per frame.
- Settings > Appearance gains a Renderer row (Linux only): Auto
(default) / GPU / GPU compatibility / Software, persisted in
ui_prefs.json, applied on restart. QBZ_RENDERER env still overrides
(now also accepts gl|gles|femtovg).
- Auto-revert sentinel: a non-auto choice is armed before backend init
and disarmed after the window survives 5s; if the previous start died
before that, the setting reverts to Auto with an explanatory toast, so
a bad renderer pick can never lock the user out of Settings.
- Developer tools > Diagnostics now reports the active renderer, why it
was chosen, and the enumerated GPU adapters (panel + markdown report).
cc
The window-level hotkey handler only suppressed single-key shortcuts
(Space/q/?/1-5) when UiFocusState.text-input-focused was set. The ~24
std-widget LineEdit fields across 14 modals/settings never set it, so those
keys fired (and were swallowed) mid-typing. Each now mirrors its focus via
`changed has-focus`; import added where missing.
cc
JACK routes through the JACK graph (resamples to the graph rate), so it
can't be bit-perfect — but nothing told the user. Add a `backend-is-jack`
flag (settings.rs computes it beside alsa/pipewire at both set-sites;
state.slint property) and a WarningBanner in AudioSettings, gated on it,
pointing at ALSA-direct / PipeWire-passthrough for bit-perfect. UI-only;
backend behavior unchanged.
Blind change (no build this session) — validates on the next qbz_ui build.
cc
Phase 2 (Slint UI + wiring) for album-level blocking, plus the album-view
enhancements that share these files.
Album blacklist UI:
- "Block this album" in all three album menus: the page-header menu as a
Block/Unblock toggle (gated to Qobuz via AlbumState), and the AlbumCard +
AlbumListRow grid menus (one-way, drop-on-reload; local/Plex excluded).
Menu heights bumped for the extra item.
- Blacklist Manager gains an Artists | Albums tab bar (segmented, ADR-008);
an AlbumRow with a cover thumbnail (blind-eye fallback) + title + artist +
"Added {date}"; tab-aware controls (shared enable toggle, per-tab search /
Clear-All / count) and confirm modal. Settings row retitled "Blacklist",
status shows both counts.
- state.slint: BlacklistedAlbumItem + album-items/album-count/active-tab on
BlacklistState; set-tab/album-select/remove-album/clear-all-albums/
block-album on BlacklistActions; AlbumState.is-album-blocked.
- main.rs: ("album","block"/"unblock") arm (header, resolves display fields
from AlbumState); 5 album BlacklistActions callbacks; seed_blacklist_status
pushes album_count.
- blacklist_manager.rs: album build/push + set_tab/block_album/remove_album/
clear_all_albums; covers resolve async via a new ArtworkTarget::BlacklistAlbum
through spawn_loads.
Album-view enhancements (same files):
- E1: full credited-artist header line with localized roles, each name a
clickable artist link (Album.artists drove this; was interpreter-only).
- E3: classical work-header composer name is now a clickable artist link
(work title + composer id/name split through TrackData/TrackItem).
i18n: es (Mexican) for all new strings; de/fr/pt/ru/ja fall back to English
pending a translator pass.
cargo check -p qbz green (Slint codegen + bin).
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
- Appearance: drop the red 'unsupported' tint on Language; hide UI Scale and
the Title Bar / Window Controls / Immersive blocks (commented, not deleted).
- Window title: "Show track in window title" now works -- the OS window title
becomes "{track} - {artist} | qbz" via a reactive binding (editable template
row hidden; format is fixed).
- Plex and Scrobblers collapse carets enlarged (28/18) and moved to row level so
they are vertically centered with the master toggle.
- Hamburger: "Open Qobuz Link" -> "Open Music Link".
- Link modal: a long pasted URL no longer grows the input/modal (min-width 0 on
the clip container caps content-width propagation).
cc
MusicBrainz was hardcoded ON in the core client with no toggle, no
persistence and no startup wiring, so it could never be turned off. Add a
persisted opt-out (default ON: ui_prefs.musicbrainz_enabled +
Settings>Integrations toggle) that seeds the core client at startup and
flips it live via handle_bool.
Gate the MB-dependent surfaces when off: the artist Network/Scene sidebar
marks itself unavailable and opens on the MB-independent Magazine tab;
the playlist "Suggest songs" CTA hides and activate() early-outs; and the
musician page tolerates a disabled/failed MB resolve, falling through to
the Qobuz fallback instead of rendering blank.
cc
Surface the new Recommendations tab everywhere Discover sub-sections are listed,
and make it opt-out.
- Flyouts: the sidebar section-nav flyout and both header nav flyouts (full text
+ compact icon) now list "Recommendations" alongside Home / Editor's Picks /
For You. The Rust route handler already maps "discover-recommendations"
(strip_prefix + select_tab + ensure_loaded), so no routing change was needed.
- Opt-out: a show_recommendations flag (default on) persisted in the per-user
discover-prefs store (qbz_app), seeded into SettingsState on session start.
Settings > Integrations gains a "Show Recommendations in Discover" toggle whose
description nudges connecting Last.fm + ListenBrainz (MusicBrainz is used
automatically to match releases). Off => the tab pill and every flyout entry
disappear.
- Gating uses two mounted instances per flyout (one with the entry, one without)
because Slint can't type-infer [NavMenuEntry] from a ternary of array literals;
the HomeView pill uses a plain if.
cargo check -p qbz green.
cc
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