Hardens the renderer auto-detect so users never need the manual
options (the anti-videogame-settings goal).
Ladder (phase 1): a failed start now walks one rung down instead of
jumping to GL — wgpu (default adapter) -> wgpu (opposite
PowerPreference) -> GL -> software. The alt rung exists because the
#542 family is an ADAPTER mixup, not a wgpu failure; its survival is
version-stamped at disarm time (renderer_wgpu_alt) so later starts arm
it directly. GL-rung failures continue to software through the
non-auto branch (the persisted 'gl' carries the 'auto-gl' sentinel
value; a user-picked gl arms the literal 'gl' — unambiguous).
Auto-degradations are version-keyed (renderer_auto_degraded): a new
build re-probes auto once, the ladder re-degrades within a start if
the stack is still broken; manual picks clear both markers.
Sentinel disarm reworked: was a fixed 5s timer that lost the race
against late startup crashes (#558's swapchain error loops forever);
now first real input / close request / clean exit, with a 30s
no-touch fallback. The 5s timer keeps only its DPR-persist duty.
User-picked 'software' no longer arms (it cannot fail to start).
Watchdog (phase 2, #555 diagnosis aid): background thread probes the
event loop's cross-thread dispatch latency every 2s (30s startup
grace, clamped against suspend, 10 consecutive >250ms probes flag
once). Detection only — never switches tiers; surfaces as the 'UI
Loop Latency' Diagnostics row + bundle line so field reports carry a
number. The #555 lag is renderer-independent, so this measures the
layer where it actually lives.
One msgid x7 (the degradation toast is now rung-generic).
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
Per-element custom theme editor (port of Tauri's swatch editor,
extended): first-class Custom theme seedable from any active theme,
12 base tokens deriving the full coherent palette, in-app ColorPicker
primitive, JSON persistence, live apply, 7 locales.
cc
Selecting Custom for the first time seeded the editor from the OLED
default; snapshot the live palette instead (the theme the user was
just looking at) — customizing what you see is the point of the
editor. Subsequent selections keep loading the persisted base.
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
Add crates/qbz-ui/ui/primitives/ColorPicker.slint: a self-contained inline
HSV picker (no PopupWindow, so no z-order/positioning fight). A saturation/
value square (pure-hue base + white->transparent + transparent->black
overlays, draggable crosshair), a 7-stop hue strip with a draggable marker,
a live preview swatch, and an editable #rrggbb HEX field.
The picker owns its hue/sat/val so dragging s or v to an edge never loses the
hue; it seeds from `value` on mount and on external changes, guarded by a
`dragging` flag so the round-tripped color the parent feeds back mid-drag does
not fight the crosshair. HSV->RGB uses Colors.hsv; RGB->HSV and int->hex are
self-contained pure functions (Slint has no local bindings, so intermediates
are derived helper properties). Hex parsing is delegated to the parent via
`hex-committed` (Slint cannot parse a hex string). Emits `changed(color)` live
on every drag frame. Validated with the slintcheck stub.
cc
Append a "Custom" entry to the theme dropdown, right after "Auto (dynamic)"
(theme::CUSTOM_SLUG/custom_index, mirroring the AUTO_SLUG pattern). New
crate::custom_theme persists the CustomThemeBase to
<data_dir>/qbz/custom_theme.json (load/save mirror ui_prefs) and derives it
via qbz_theme::theme_from_base.
Startup: when the persisted theme slug is "custom", seed the editor swatches
from custom_theme.json and apply the derived palette (load_or_seed seeds the
OLED default the first time). The dropdown "theme" arm gains a Custom branch
with the same live-apply path. AppearanceState grows theme-is-custom, the 11
custom-* swatch colors, custom-is-dark, custom-open-token and the
custom-set-token / custom-seed-from-current / custom-toggle-dark callbacks
(handlers wired in a later commit).
cc
Add crates/qbz-theme/src/custom.rs: the extended port of the Tauri "edit
theme colors" feature. A CustomThemeBase of 12 hand-picked semantic tokens
(surfaces, text, accent, status hues, border, favorite + is_dark polarity),
stored as opaque #rrggbb hex strings, derives the full ThemeColors contract.
Unlike Tauri (8 raw vars, no derivation, over the auto-theme only), editing a
base token now cascades: theme_from_base derives the accent triplet
(+0.10/-0.10 lightness, contrast-picked accent_text), the danger/warning/
success families (0.1/0.3 tints, polarity hover), muted/disabled text tiers,
border_strong, the translucent edges and the polarity alpha ramp.
Reuses the existing math instead of duplicating it: generator::{tint,
pick_btn_text_for_accent_set, ensure_text_contrast_target} are now pub(crate);
PaletteColor::shift_lightness and alpha_ramp are shared as-is. base_from_theme
is the inverse seed ("Start from current theme") and round-trips exactly for
any theme this module authored. Adds Rgba::to_hex. 9 unit tests (roundtrip,
polarity, determinism, accent-text legibility, malformed-hex fallback, JSON).
cc
The random-album / random-artist / random-playlist / random-label
buttons all exist (1:1 with 1.x) but rendered as bare shuffle icons —
1.x had title tooltips, so users couldn't tell them apart from a
tracks-shuffle and reported the feature as removed (#554). CircleAction
already supports the shared tooltip overlay; set it on all seven
Library header actions. 7 msgids translated in all 7 locales.
Fixes#554
cc
Review follow-ups: extract_palette decoded the full image before the
100x100 downsample with no limits — a decompression-bomb PNG expands
to tens of GB, and this box hard-freezes on OOM; decode is now bounded
at 12k x 12k / 512 MB (any real wallpaper fits). The generating
indicator is now set through upgrade_in_event_loop so the select_image
path (which runs on a tokio thread, where a plain upgrade() returns
None) shows it too. Startup generation stays synchronous by design:
with the decode bounded it costs milliseconds and the first paint
comes up in the generated palette instead of flashing the default.
cc
Vendor i-slint-renderer-skia 1.16.1 with a second-chance typeface
workaround: skia bug 310510989 makes new_from_data fail for TTC faces
at index > 0 on macOS, and for AppleSDGothicNeo.ttc — which packs
every weight at index > 0 — the upstream read/write-fonts rebuild is
ALSO rejected by CoreText, so draw_glyph_run silently dropped every
Korean glyph run at non-regular weights (Japanese survived because
Hiragino ships one file per weight, always index 0). Resolve the same
face through CoreText by family name (typographic name preferred,
localized names resolve too) + OS/2 weight; the match points at the
same installed file so parley's glyph ids stay valid, enforced by cmap
spot checks that reject a mismatching face rather than draw wrong
glyphs. Probe-verified on the Mac Mini: matched face returns identical
glyph ids for U+C0AC across w500/600/700.
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
Integrate the dynamic "Auto (dynamic)" theme into the bin without disturbing
the static-theme path:
- ui_prefs: add auto_theme_source ("system"|"wallpaper"|"image") and
auto_theme_image_path, with serde defaults and index<->key helpers.
- theme.rs: extract push_colors() (shared by static + auto paths), append the
"Auto (dynamic)" dropdown entry, and add AUTO_SLUG / auto_index /
is_auto_index / selected_index_for_slug so the "auto" slug maps to the
appended entry (it has no static ThemeId).
- auto_theme.rs (new controller): source_from_prefs, startup apply (OLED
fallback + toast on failure), off-thread regenerate, rfd image picker, and
source switching — all pushing the palette back on the event loop.
- main.rs: auto-aware startup seeding/application, an "auto"/"auto-theme-source"
arm in on_appearance_select, and a new on_appearance_action handler for the
image picker + Regenerate button.
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
The current-row equalizer animated via animation-tick() on the strong
(wgpu) tier — every step is a full-window repaint, so a playlist view
with the playing row on screen sat at ~4.5% CPU on a 165 Hz monitor
for a three-bar affordance. The bars now always use the shared ~8fps
coarse clock (already the accepted look on the GL/software tiers) and
the AppShell timer additionally runs while audio plays; when the
playing row is not on screen nothing reads the property, so the tick
dirties no binding and costs nothing.
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
take_stream_error_message is a SharedState method, not a Player one —
the toast commit called it on the Arc<Player> and the release build
caught it (the bin crate can't be cheaply cargo-checked locally).
cc
register_audio inserted every cast track's complete bytes into the
entries map and NOTHING ever removed them — not stop, not disconnect;
only app shutdown dropped the server. A DLNA session therefore grew by
one full track per auto-advance (issue #550: 50 MB at launch to
4.5 GB, never released). Three changes:
- register_audio/register_file evict every other entry — only the
track being cast is servable anyway.
- stop_if_cast and disconnect clear the entries outright, so stopping
actually releases the buffers.
- MediaSource::Data is Arc-backed: the per-request entry clone no
longer copies the whole track (it did, on every GET/HEAD/range
request), and an in-flight response keeps its own Arc so eviction
can never truncate an ongoing range request.
Fixes#550
cc
A bare DLNA STOPPED is ambiguous — strict renderers hiccup mid-track —
so end-of-track now requires the max observed position (RelTime resets
to 0 on STOP; the instantaneous value is useless) to reach within 5 s
of the duration. Two escape valves keep the guard from ever blocking a
genuine end, which review flagged as the regression risk: renderers
whose RelTime never moves (position signal unreliable — common, and
duration nearly always resolves via the catalog fallback, so the
`duration <= 0` hatch alone never fires) skip the guard entirely, and
a STOPPED persisting 4 consecutive polls advances anyway (~4 s late
beats a queue stuck forever). Premature stops are logged with a
counter and recover on PLAYING.
Based on the end-of-track guard contributed in PR #547, reworked per
review findings.
cc
Three device-side gaps that kept strict renderers (Signalyst HQPlayer 6)
from working:
- Services were matched by the hard-coded ":1" URN, missing renderers
that only announce AVTransport/RenderingControl :2/:3. Match the
service name at any UPnP version (unit-tested; recursion over
embedded devices preserved via services_iter).
- DIDL protocolInfo advertised "*" as its 4th field; strict renderers
cross-check it against the response headers. Advertise the same
DLNA.ORG_OP/FLAGS the media server sends.
- HQPlayer clears its transport at natural end-of-track, so a bare Play
(manual button or late auto-advance) faulted 702 "No contents" or
no-op'd. play() now pre-checks an idle transport (STOPPED/
NO_MEDIA_PRESENT — PAUSED untouched so resume keeps position) and
re-asserts the last SetAVTransportURI first, plus a bounded retry net
(6 attempts, linear backoff) for raw-status 701/702 settle races.
Based on the DLNA hardening contributed in PR #547; retry-net comment
corrected against rupnp 3.0's actual error mapping (non-200 becomes
HttpErrorCode before any SOAP parse — the UPnPError arm only covers
fault-inside-200).
cc
Strict DLNA renderers (e.g. Signalyst HQPlayer 6) HEAD-probe the media
URL before playing and reject chunked bodies: tiny_http auto-selected
Transfer-Encoding: chunked (dropping Content-Length) past its 32 KiB
threshold, so those renderers played the buffered seconds and went
STOPPED. Answer HEAD with 200 + Content-Type/Accept-Ranges/DLNA
headers, pin Identity encoding with an explicit Content-Length on all
media responses (200/206/HEAD), and advertise transferMode/
contentFeatures on GET too. Range handling is unchanged. Neutral for
renderers that never HEAD-probe and ignore the extra headers.
Based on the DLNA hardening contributed in PR #547.
cc
The nav tab already reads Library, but the surrounding communication
kept the old Qobuz term: the Home/Discovery section "Favorite Albums",
the Add/Remove-favorites menu entries on every card/row surface, the
playlist context-menu "Favorite" verb, and the Favorites view's
empty/error states. Align all of it with the Library terminology
(9 msgids renamed, translated in all 7 locales). Routes, ids and the
genre-filter context string are untouched — this is copy only.
cc
arboard ties the clipboard offer to the LAST live Clipboard object:
dropping it destroys the X11 selection window (contents survive only
when a clipboard manager accepts the handoff — KDE ships one, stock
GNOME/XFCE/Cinnamon do not) and ends the Wayland offer likewise. The
create-per-copy pattern therefore worked on KDE smokes and silently
lost the text everywhere else — the HiFi wizard copy buttons and every
other copy surface (#514). Keep a single lazily created instance in a
static; on set_text failure the instance is dropped so the next copy
reconnects. Verified against arboard 3.6.1's Drop impl and with a
live repro (offer now persists with the instance held).
cc
The file was an untracked local script predating this session; a
directory-level git add pulled it into the #549 commit unintentionally.
Back to untracked, file kept on disk.
cc
Users on CPUs without AES-NI (Nehalem i7-860, pre-2010 Intel Macs) or
without SSSE3 (AMD Phenom II) crashed at launch with SIGILL. The
global `-C target-feature=+aes,+ssse3` was kept after the #353/#343
x86-64-v3 incident on the belief that the aes crate's cpufeatures
runtime check would protect CPUs without AES-NI — but a compile-time
target feature makes cpufeatures compile its check to a constant, so
the AES-NI intrinsics ran unconditionally (credentials AES-GCM decrypt
at startup), and +ssse3 let LLVM autovectorize anywhere, breaking
Phenom II. Measured on the qbz-cmaf hot path (Ctr128BE<Aes128>
apply_keystream, 256 MB, release): 2145 MB/s compiled-in vs 2162 MB/s
runtime-dispatched, identical output — the flags bought nothing on
capable CPUs. Removed from .cargo/config.toml, all dev/build scripts
and the release/build workflows; old CPUs now get the software AES
fallback instead of a crash.
Fixes#549
cc
The player has recorded a user-readable message on stream-open failure
since the Tauri era, but the drain lived in Tauri's polling loop and
was never ported to the Slint poll loop — record_stream_error had ZERO
consumers, so ALSA Direct/exclusive failures showed as a bar frozen at
0:00 with no explanation (#508, #534, #500). Drain the message each
tick and toast it. When running inside Flatpak/Snap and the failure is
ALSA-shaped, explain that direct hw: access is blocked by the sandbox
and point at PipeWire/System Default instead of showing the raw error.
2 new msgids translated in all 7 locales.
cc
The clock.force-rate reset only ran in the audio thread's Stop, pause
suspend and ReleaseDevice handlers — none of the quit paths (custom
titlebar close, WM close, tray Quit) go through them, so quitting mid
playback with DAC passthrough left the PipeWire graph pinned at the
last track's sample rate for every other app until PipeWire restarted.
Run the same self-gating resume_suspended_sink + reset_pipewire_clock
pair once after the event loop exits — a single choke point covering
all three quit paths. No-op when QBZ didn't force anything (and inside
Flatpak, which could never force the clock in the first place).
Fixes#521
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
On layouts where the apostrophe is a dead key (us(alt-intl):
dead_acute), winit composes via xkb and delivers the committed text in
event.text, but Slint's winit backend maps logical_key first — so
' + space inserted a plain space and the apostrophe could never be
typed into any text field (search included). Detect commits whose
composed text differs from what Slint would synthesize (second key =
Character with different text, Named(Space), or Key::Dead) in the
winit event hook, dispatch the composed text as a synthetic Slint key
event and swallow the raw one. Gated on a focused text input so hotkey
behavior outside fields is unchanged; control characters excluded so
Ctrl-shortcuts keep working.
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
Review follow-ups to the chevron z-order fix: disable the arrow
TouchArea on leaf rows (an enabled TouchArea consumes the click even
with a no-op handler, leaving a 20px selection dead-zone) and include
arrow hover in the row highlight so it doesn't flicker off over the
chevron.
cc
The startup sentinel only guarded explicit Settings overrides: an
auto-detected wgpu start that crashed pre-paint re-detected wgpu on the
next launch — an unrecoverable crash loop (#542 hit exactly this). Arm
the sentinel when auto-detect picks wgpu; if it survives to the next
start, persist the GL (compatibility) renderer instead of re-detecting,
and explain with a toast. Linux-only by design (macOS has no GL tier).
Also add the .po entries for the existing revert toast, which was
missing from all catalogs, plus the new degrade toast (7 locales).
cc
PowerPreference::LowPower makes wgpu pick the integrated GPU. On a hybrid
discrete+integrated DESKTOP the monitor usually hangs off the discrete
card and the display-less iGPU cannot present the window surface, so
Surface::configure panics with "Invalid surface" at startup. Capture the
GPU topology during the adapter probe (or on demand for forced-wgpu
paths) and default hybrid machines without a system battery to
HighPerformance; laptops (system battery present) keep LowPower — their
panel is wired to the iGPU and it saves power. WGPU_POWER_PREF still
always wins. Peripheral batteries (scope=Device) are excluded from the
laptop check.
Fixes#542
cc
winit sends no xdg_toplevel.set_app_id unless the window is created with
an explicit name, and derives WM_CLASS from the binary name ("qbz") on
X11 — so compositors could not associate the window with
com.blitzfc.qbz.desktop: blank dock icon, no running indicator, and the
pinned launcher spawned a second instance. Set with_name on both the
Wayland and X11 attribute extensions in the window-attributes hook; it
runs for the main window and the miniplayer, so both group under the
same icon. StartupWMClass=com.blitzfc.qbz already matches in every
shipped .desktop file.
Fixes#544
cc
2.0 dropped the v1.x "added" sort. Restore it with the same semantics:
a positional proxy over the playlist's natural insertion order (the API
returns tracks oldest-first; no added-at timestamp is deserialized),
starting newest-first on first selection, re-click toggles direction.
New msgid translated in all 7 locales.
Fixes#546
cc
The full-row selection TouchArea was declared after the row layout, so it
sat on top of the chevron's TouchArea and swallowed every click — sub-genres
could never be expanded. Declare the row TouchArea first (bottom of the
z-order) so the arrow's TouchArea wins inside the chevron region, matching
the stopPropagation isolation the Svelte version had.
Fixes#545
cc
Without RESTRICT=mirror Portage walks every official Gentoo mirror
(guaranteed 404s — the distfiles only exist on GitHub Releases) before
falling back to SRC_URI. The source template already had it.
cc
The root snapcraft.yaml (source build, kept for Snap Store remote
builders) shadowed the staged snap/snapcraft.yaml dump recipe, so the
v2.0.0 snap job recompiled the entire workspace inside LXD and the
container OOM'd. Remove the root recipes from the CI working copy
before invoking snapcraft.
cc
The v2.0.0 tag run was cancelled at exactly 30 minutes with snapcraft's
LXD build still running. The job does no compilation, but container
bootstrap plus platform snap downloads on public runners routinely
exceed the old budget.
cc
Verified empirically against the pre-flight artifact (objdump shows
GLIBC_2.39 as the highest versioned symbol — the ubuntu-24.04 runner's
libc). Fedora needs 40+ accordingly; the Ubuntu/Debian guidance was
already right.
cc
Pre-flight caught it: the deb/rpm step failed with the literal
'Glob failed: ${QBZ_BINARY}: no matching files'. The workflow already
places the stripped binary at ./qbz, so the manifest now uses the
literal path.
cc
threads=2/cgu256/opt3 under the widened cap margin — completed with a
5.7 GB global floor and zero kill signals while the desktop ran. Same
flags across runs keeps the runner's target cache warm.
cc
Root-caused with an ftrace signal trap: earlyoom (RAM-only trigger,
10% of 31 GB) SIGTERM'd the qbz_ui rustc while the cgroup sat under
its cap — the 3.5 GB margin ignored that the desktop grows during an
hour-long build, and global MemAvailable cratered to 3.1 GB. Anchor
memory.high at avail minus 9 GB so the build throttles into swap
long before the global floor approaches the trigger.
cc
The uncapped opt-3/threads-2 recipe dies to earlyoom whenever the
desktop holds RAM (its trigger is RAM-only, so free swap cannot save
an uncapped build). Switch to the capped threads-1/opt-2 recipe,
proven end-to-end under an 8G/10G cgroup cap with the desktop open —
slower, but it finishes regardless of box load.
cc
Recent macOS (Sequoia+) dropped the right-click Open bypass for
un-notarized apps, so the section now walks both unlock paths: the
Privacy & Security 'Open Anyway' route and the xattr quarantine
removal command, with a plain explanation of what each does.
cc
Documents the Slint memory wall properly: the per-knob guard table,
a CI-proven low-RAM recipe for machines under 32 GB, the auto-tiering
scripts/slint-run.sh (FAST/SAFE/MIN) and the two development loops
(release-perf slint-dev.sh vs fast-debug slint-dev-fast.sh, plus the
macOS counterpart). Fixes the toolchain claim (scripts need nightly
for -Z threads; plain stable cargo build works), adds macOS build
prerequisites, and records the DSD direct-mode seek/volume limitation
under known issues.
cc
The fullscreen toggle only exists in the immersive header cluster, so
exiting immersive while fullscreen stranded the user with no UI way
out (Tauri always dropped fullscreen on immersive exit). Immersive
closes from several .slint sites with no Rust callback, so a light
UI-thread timer edge-detects the open-to-closed transition and exits
fullscreen, reading the real window state so WM-entered fullscreen is
covered too.
cc
Repo policy: public workflows fire on release tags only. A push to
main is a release merge whose tag immediately fires the real release
builds, so the auto-build on main was pure double spend.
cc
Restores the ALSA layout binds in both snap recipes (bit-perfect hw:
access inside the snap), lets the build cache save on main as well as
slint-mvp, installs the desktop file as com.blitzfc.qbz.desktop so the
AppStream launchable and the MPRIS DesktopEntry resolve in deb/rpm,
clears the stale boot-restore error when a new browser sign-in starts,
and adds the X11 dlopen libraries to the flake's runtime set. The
flake version/rev/srcHash bump remains a tag-time step (documented in
the release checklist, which is rewritten for the 2.0 Slint flow).
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
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
Conversion rides the existing play_streaming path as an exact-size
88.2 kHz/24-bit WAV. DoP and native DSD (Phases 2-3) get a dedicated
direct engine: additive S32/DSD_U32 ALSA stream constructors and a
verbatim i32 write path (no f32, no gain), a writer thread that feeds
0x69 DSD silence on pause and pads stream teardown, and a source queue
for gapless DSD. AudioSettings gains dsd_mode (convert|dop|native,
default convert). Delivery resolves per track: user mode + ALSA direct
backend + stereo + rate support, everything else converts. Seek is
ignored in direct modes and the pause suspend path is gated off there.
cc
Scanner accepts .dsf/.dff; new AudioFormat::Dsd; metadata extraction
via qbz-dsd (embedded ID3, artwork, bit_depth=1 with the DSD bit rate
as sample_rate so badges derive DSD64/128/256). Ephemeral folder open
now sorts tracks by album/disc/track/title and re-keys ids so the
displayed order matches the play-queue order (was readdir order).
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
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
Zero missing msgids in all six locales (was 27 es / 36 de-fr-pt / 50
ru-ja): renderer tiers, shortcut modals, link resolver, Purchases, the
album-blacklist batch (de/fr/pt/ru/ja) and the recommendations + JACK
batch (ru/ja); plus es Collections leak fixes. msgfmt-validated.
cc
A standalone repro mirroring the exact chain (if-mounted view, Flickable,
layouts, if/for components, nested slot) measured absolute-position as
exact window coordinates at any scroll position in Slint 1.16.1 — the
double-count quirk is ListView-only and this chain has none. The anchor
math is correct as written; no code change.
cc
Idle timer at the Cortinilla root (a Timer inside the if subtree cannot
be restart()ed — 1.16 compiler panic): paused while the pointer is over
the panel (ancestor TouchArea hover covers the row/button children),
restarted by typing and arrow-selection moves, hover mirror reset on
open/close.
cc
The search inputs' changed has-focus handlers re-invoked live() whenever
focus returned with text present, and Slint hands focus back without user
intent (focus reassignment when the focused element unmounts on a page
change, window re-activation) — the dropdown popped open on navigation.
Gate the refresh on the dropdown being already open; typing still opens
it through edited. Also release the header search focus and reset the
typing guard on every view change, so single-key hotkeys work again after
typing a search, and a guard left stuck true by an unmounted input is
cleared.
cc
The single-key hotkey guard (UiFocusState.text-input-focused) was missing
on the three editable TextEdit description fields (create/edit playlist,
My QBZ edit) and on the read-only inputs (QConnect dev modal, log viewer
URL), so Space/q/? hotkeys fired while typing or selecting there.
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
Flatpak manifest builds the crates/ workspace with rust-stable+llvm20
(node SDK and the intltool/libdbusmenu/ayatana chain removed). Root
snapcraft.yaml becomes a from-source Slint recipe (core24, rustup,
MIN memory tier) mirroring packaging/snap-slint runtime sections.
AUR/arch/gentoo swap webkit/gtk/appindicator runtime deps for the
measured Slint set and the source builders compile crates/ directly.
flake.nix: cargoRoot crates, no npm hooks, dlopen runtime libs wrapped.
cc
release-linux, release-linux-aarch64, release-macos and release-flatpak
now build the crates/ workspace (Tauri npm builds removed) with the
proven build-slint recipe: 18 GB extra swap, mold, pinned nightly, MIN
memory tier. Build-once-package-many: nfpm deb/rpm, minimal-AppDir
AppImage, and the AUR/Gentoo/APT tarball with its name and layout
contract preserved. macOS assembles a QBZ.app skeleton, ad-hoc signs
it, and packs a dmg with hdiutil.
Cross-cutting: prerelease detection extended to -rc/-beta/-alpha/-pre,
tag-on-main ancestry guard (manual dispatch skips it), fail-fast
version assert against crates/Cargo.toml, unstripped binary archived
per tag. Tauri-updater bridge: AppImages and macOS .app.tar.gz are
signed with the existing Tauri minisign key so 1.2.x installs
auto-update into v2 via the unchanged release-updater-manifest.yml.
cc
Icons and qbz.desktop move to packaging/ so packagers stop depending
on the retired src-tauri tree. New nfpm.yaml builds deb/rpm from the
prebuilt binary: deps are the measured ldd set (t64 alternatives on
deb, soname requires on rpm), display/GPU stack as recommends.
cc
The release binary carried 173 MB of symbol tables (.strtab 151 MB +
.symtab 22 MB, no .debug_* sections) inflated by Slint's generated
generic names: measured 400 MB -> 218 MB (-45%). Panic backtraces
become address-only; the release workflows archive the unstripped
binary as an artifact per tag for offline symbolication.
cc
The screen sat inert while the system-browser OAuth ran: no waiting or
authenticating feedback, and failures were log-only. New LoginState
global (phase + error) driven by an on_phase callback in
auth::login_via_system_browser (both call sites: login + recovery).
The blue button now reads "Sign in with your browser", disables while
an attempt is in flight, and the screen shows continue-in-browser text
(with a no-focus hint), a Cancel link that aborts the pending flow, an
authenticating state, and a sign-in-failed box. The redundant
"Use your system browser instead" link and its AppCommand are removed.
Five new msgids translated in all seven locale 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
On weak GPUs every animation frame is a full-window femtovg repaint, so
continuously-animated loading indicators dominate transient CPU (opening
the search cortinilla spiked to ~40% on a 2014 Mac Mini). New
ShellState.reduce-motion (seeded true on non-wgpu tiers, like the shader
gate) switches those animations from display-rate animation-tick to a
shared ~8fps coarse clock advanced by ONE timer in AppShell — same phase
math, 60 -> 8 repaints per second:
- LoadingSpinner / LoadingDots (all mount sites inherit)
- Equalizer bars (TrackPlayCell + immersive track info)
- Fetch/download spinners (TrackPlayCell x2, TrackRow, offline manager)
- Both search-cortinilla skeleton pulses (the reported spike)
- Home skeleton: the 900ms opacity fade collapses to a snap (its phase
timer becomes the only driver, ~1fps while mounted)
Also logs the window scale factor at startup (circle-AA investigation:
femtovg's fringe AA halves at fractional scale factors, dpi=ceil(scale)).
cc
Covers and card elements now scale with the viewport instead of the
library: the shared AlbumGrid gains opt-in windowing (row band = visible
rows +/- one viewport, computed in .slint, row-granular change events),
cards outside the band render as same-footprint placeholders (scroll
geometry unchanged), and the Rust side dispatches artwork only for the
band, evicting covers outside a retention span back to the placeholder.
- New id-keyed ArtworkTarget::LocalAlbumById (replaces the index-keyed
LocalAlbumCard): immune to derive re-sorts between dispatch and apply;
reloads still drop stale covers via the generation guard. The enum
loses Copy (String variant).
- In-flight dedupe set freed on apply AND on fetch failure, cleared on
generation bumps.
- Flat list view and grouped mode keep the pre-windowing full dispatch
(they don't fire window events); switching grid->list re-dispatches
covers the window may have evicted.
- Windowing is off by default in the shared grid: Discover, Labels and
Favorites are untouched (phase 2).
A 2,000-album library drops from ~540 MB of resident cover RGBA to a
viewport-sized working set, and scrolling repaints ~40 mounted cards
instead of the whole collection.
cc
The immersive shader scenes (Plasma/Tunnel/Aurora/Spectral Ribbon/Line
Bed) render through the wgpu underlay, which does not exist on the
femtovg-GL and software tiers - picking one there showed a black view.
Gate the five picker rows and the g-cycle behind a new
ImmersiveState.shader-scenes-available flag seeded from the renderer
selection; FOCUS views and splits (Slint-drawn) stay available.
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