Commit Graph
100 Commits
Author SHA1 Message Date
vicrodh 00a44e123f feat(qconnect): startup behavior setting — remember state / on / off
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
2026-07-07 17:37:30 -06:00
Victor RH 65d4f92880 Merge pull request #563 from eldios/bugfix/external/flake-nix-missing-libjack2
fix(nix): add libjack2 to buildInputs
2026-07-07 17:06:06 -06:00
vicrodh b88905d1c1 Merge feature/dutch-locale into pre-release
cc
2026-07-07 16:14:24 -06:00
vicrodh 836478ed90 feat(i18n): Dutch locale — 8th bundled language
Ports PR #561 by @pbaart into the live gettext system (the PR
translated the retired Svelte catalog): full nl.po (1481 msgids, 13
plurals, msgfmt-clean, zero untranslated), 262 strings taken directly
from the contribution via the legacy-English bridge and the rest
translated using the contributor's terminology as the style guide
(nummer/afspeellijst/wachtrij/bibliotheek, informal je). qbz-i18n
registration (2-form plural, resolve_auto nl_NL->nl, 21/21 tests) and
the language dropdown at index 8 (Nederlands) across ui_prefs,
reseed_i18n_labels and state.slint — same shape as the ru/ja commits.

Dutch translation credit: pbaart (PR #561).

cc
2026-07-07 16:14:24 -06:00
vicrodh 33460f9f9b fix(ui): log viewer footer = Copy all + Upload; advanced stays behind [+]
Owner-corrected UX: the compact view keeps exactly TWO buttons — the
screen must serve fast. Upload moves from the advanced row to the
always-visible footer (with its uploaded-URL row un-gated so the link
shows in compact view too); level/search/refresh/auto-tail, the
diagnostics-bundle copy, open-file and clear stay behind [+]. Reverts
the open-expanded default from fe43c698.

cc
2026-07-07 14:50:32 -06:00
vicrodh d0fadbc906 fix(playlist): mixed-playlist Plex rows render on raw connectivity, not mode
E13 gated the Plex sidecar rows of a mixed Qobuz playlist's offline
detail on mode == InducedOffline. A logged-out session (#553 start
offline) reports RealOffline WITH connectivity Up, so a Plex+Qobuz
playlist rendered EMPTY: Plex hidden by the mode check, the Qobuz
block snapshot-bound. Whether the Plex server is reachable depends on
raw connectivity, not on why the session is offline — gate on that.
Field log: paste.rs/d0d7f (mode=RealOffline, connectivity Up,
offline_session=true).

cc
2026-07-07 13:38:10 -06:00
vicrodh fe43c698d0 fix(ui): log viewer opens expanded — upload/copy actions visible by default
The compact-by-default state hid the Upload and diagnostics-bundle
buttons behind the [+] toggle, defeating the point of the new Share
logs entry (users looking to share logs saw only Copy all). The [-]
still collapses. The upload already ships the full diagnostics report
+ the last 200 redacted lines — no content change needed.

cc
2026-07-07 13:21:04 -06:00
vicrodh eca07b146c fix(ui): grouped sections back to full mounts — abs-position binding recurses
Field-hit on LL Albums grouped (alpha): deriving each section grid's
content-offset from an absolute-position DELTA binding panics with
'Recursion detected' (i-slint-core properties.rs:583). Evaluating the
target-row chain through absolute-position can land inside the parent
layout solver's own pass, and the grid's height/columns read back
self.width — re-entry. The Timer-seed guard only covered init-time
reads, not binding-evaluation context. Grouped windowing needs
Rust-computed per-section offsets (backlog); the flat grids — the big
win — keep their windowing unchanged.

cc
2026-07-07 12:21:54 -06:00
vicrodh dc284bb272 fix(playback): declare the boxed Send future in auto_skip_unavailable's signature
The async recursion (play_audible -> auto_skip_unavailable ->
after_track_change -> play_audible) made the future's Send-ness
self-referential: with inferred impl-Future types rustc hits a query
cycle ('cannot satisfy ...: Send') at the Box coercion. Returning the
concrete Pin<Box<dyn Future + Send>> from the signature — the shape
async_recursion expands to — cuts the cycle; the inner Box::pin at the
recursion point is gone.

cc
2026-07-07 12:06:32 -06:00
vicrodh 39304dbef5 Merge bugfix/hyprland-wgpu-surface-timeout into pre-release
cc
2026-07-07 09:36:08 -06:00
vicrodh 6a920f6f2a Merge feature/renderer-autodetect-hardening into pre-release
cc

# Conflicts:
#	crates/qbz-ui/translations/de/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/en/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/es/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/fr/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/ja/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/pt/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/ru/LC_MESSAGES/qbz-ui.po
#	crates/qbz/src/main.rs
2026-07-07 09:14:48 -06:00
vicrodh 4c93f85aa3 Merge fix/log-noise-filter into pre-release
cc
2026-07-07 09:14:23 -06:00
vicrodh 7bf79378d2 Merge feature/guest-local-mode into pre-release
cc

# Conflicts:
#	crates/qbz-ui/translations/de/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/en/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/es/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/fr/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/ja/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/pt/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/ru/LC_MESSAGES/qbz-ui.po
2026-07-07 09:14:23 -06:00
vicrodh 6a46e1e818 Merge bugfix/556-favorites-lenient-parse into pre-release
cc
2026-07-07 09:14:23 -06:00
vicrodh cd4082909e Merge feature/ll-tracks-sorting into pre-release
cc
2026-07-07 09:14:23 -06:00
vicrodh ba45e1f216 Merge bugfix/single-instance-559 into pre-release
cc
2026-07-07 09:14:23 -06:00
vicrodh 7575b81623 Merge feature/log-uploader-visible into pre-release
cc

# Conflicts:
#	crates/qbz-ui/translations/de/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/es/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/fr/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/ja/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/pt/LC_MESSAGES/qbz-ui.po
#	crates/qbz-ui/translations/ru/LC_MESSAGES/qbz-ui.po
2026-07-07 09:14:09 -06:00
vicrodh c1efa24034 Merge fix/gentoo-overlay-hardening into pre-release
cc
2026-07-07 09:13:47 -06:00
vicrodh c6c331553b Merge bugfix/skip-unavailable-track into pre-release
cc
2026-07-07 09:13:47 -06:00
vicrodh 722dda025e Merge feature/viewport-scroll-plumbing into pre-release
cc
2026-07-07 09:13:34 -06:00
vicrodh 7d3fc2a3aa feat(renderer): autonomous degradation ladder + UI loop watchdog
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
2026-07-07 08:00:31 -06:00
vicrodh 11467fce2a fix(player): volume tick log at debug, not info
A slider drag delivers dozens of SetVolume commands per second and
each logged an info line from the audio thread — 758 of 1025 lines in
a #555 field log. Log-level change only; the audio path is untouched.

cc
2026-07-07 06:48:23 -06:00
vicrodh 8581d39585 fix(log): demote zbus/tracing D-Bus dumps out of the default filter
zbus 5 logs every dispatched/read D-Bus message as a multi-KB Debug
dump at INFO (through the tracing-log bridge, which also emits
tracing::span events). With any MPRIS applet polling GetAll this
flooded the file sink within ~1s of startup and drowned everything
else — both #555 field logs confirmed it (one diagnostics upload
covered exactly one second of runtime). Filtered records now cost
nothing (log! checks the level before formatting). RUST_LOG still
replaces the whole default, so full zbus tracing stays available.

cc
2026-07-07 06:48:23 -06:00
vicrodh a4dce34b28 feat(ui): login disclaimer line for the guest offline mode
One sentence under the subscription notice so the always-visible
'Start offline' link is self-explanatory: QBZ works as an offline
player without a Qobuz account, with no access to the Qobuz library.
One msgid x7 locales.

cc
2026-07-06 21:59:39 -06:00
vicrodh b6e56e3b47 feat(session): guest local-only profile — offline mode without a prior login
The 'Start offline' link was gated on has-previous-session and the
shell backstop refused to open without a recorded user, so a user who
never logged in (e.g. a purchases-only Qobuz account, #553) could not
use the app at all. The link is now always visible and the offline
shell falls back to the GUEST profile — user 0, the Tauri-era fallback
this port used to refuse — building a real per-user store set (local
library, mixtapes, prefs) under users/0. AppRuntime::activate_offline
already resolved user 0 and never persists it as the last-user marker,
so the guest never masquerades as a real session.

First real login takes possession: AppRuntime::activate renames the
users/0 data+cache dirs to the account's id when that account has no
profile on this machine yet (existing profiles always win — the guest
dirs stay parked, no merge). Best-effort, login never breaks on a
failed rename.

cc
2026-07-06 21:47:00 -06:00
vicrodh bcd4574225 fix(favorites): parse favorites items leniently instead of all-or-nothing
serde_json::from_value::<Vec<T>>(...).unwrap_or_default() nuked the
ENTIRE tab when one favorite among thousands failed to fit the model
(a delisted or odd-shaped item): the count badge kept the separately
parsed total while the list rendered empty — 'Tracks 6125' over 'No
favorite tracks yet' (#556, 6125-track library on macOS; a 101-track
library never hit the poison item). Items now deserialize one by one;
misfits are skipped with a warn carrying the item id and error, plus a
summary line, so the reporter's log will name the exact offender.
Applies to all four get_favorites tabs (tracks/albums/artists/labels).

cc
2026-07-06 21:12:42 -06:00
vicrodh 763b5ae81f feat(ui): always-visible Share logs entry in the settings sub-nav
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
2026-07-06 20:57:00 -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 e379aa65f6 feat(library): sort control for the Tracks tab (SQL-side)
The Tracks tab is server-paginated (200 rows/page), so the new 8-option
sort dropdown (Default / Artist / Title / Year / Date added, mirroring
the Albums toolbar control) rides the SQL query through a validated
ORDER BY allowlist in search_with_filter_page (same pattern as the
albums page query), re-queries page 1 on change (gen-bumped, stale
in-flight pages drop), and persists in locallibrary_prefs. Every sort
key ends in id so LIMIT/OFFSET pagination is deterministic across
ties. The once-only Plex page-1 merge re-sorts client-side with a
comparator matching the SQL semantics. Group modes keep their
client-side visual reorder on top, unchanged. Zero new msgids — all
labels already exist.

cc
2026-07-06 20:56:02 -06:00
vicrodh 1d16d84468 ci(gentoo): require the amd64 dist in the overlay Manifest, retry downloads
A Manifest published without its amd64 DIST entry bricks emerge for
every amd64 user, which is worse than failing the job; a 3x30s retry
absorbs release-asset propagation lag right after the tag. Also
documents the #557 root cause at the top: the overlay must never be
edited by hand — an ebuild change without Manifest regeneration is a
guaranteed digest mismatch; template changes are republished by
re-dispatching this workflow.

cc
2026-07-06 20:30:49 -06:00
vicrodh 41d93e5e5f fix(playback): auto-skip unavailable tracks instead of parking on them
Tauri regression: the Svelte playbackService caught TrackUnavailable play
errors and called autoSkipToNext (bounded at 5 consecutive skips, issue
Tauri#467); the Slint port only logged and cleared the spinner, so the
queue cursor parked on the dead track and playback stopped.

play_audible's error arm now detects TERMINAL unavailability (substring
contract over the flattened ApiError Display: TrackUnavailable /
NonStreamable / NoQualityAvailable — transient errors keep NOT skipping,
the client already retries those), toasts, honors stop-after-this-song
on the failed track, and advances through the real machinery
(advance_to_playable + after_track_change, async recursion boxed).
Superseded fetches (user already picked another track) never skip.
Counter resets when audio actually starts. One new msgid x6 locales.

cc
2026-07-06 19:55:49 -06:00
vicrodh 54b8dc4708 perf(ui): element windowing for the search albums grid
Ports the band + hysteresis scheme into SearchResultsView's bespoke
AlbumGrid (no window-changed — search artwork is dispatched per loaded
page onto the directly-rendered model) and wires the Albums tab
instance to the view's Flickable.

cc
2026-07-06 19:55:18 -06:00
vicrodh 0a05d9e4f7 perf(ui): window grouped sections; post-layout band seed via Timer
Grouped section grids now receive the windowing inputs when the surface
is windowed; each section's content-offset is the absolute-position
delta to the collection root — scroll-invariant (both live in the same
Flickable content) and exact in plain Flickable/for chains. Section
grids do not forward window-changed: grouped artwork stays full
dispatch, so windowing there is mount-only.

The band seed moves from init to a one-shot Timer a tick after mount:
evaluating the target bindings from init would pull absolute-position
while layout is still evaluating (the documented recursion panic).
Until it fires, the default [0,0] band keeps at most one row mounted
for a frame.

cc
2026-07-06 19:55:18 -06:00
vicrodh 059d34cda9 perf(ui): element windowing for Discover/Label/Artist/Award grids
Flips windowed on the four AlbumCollectionView consumers with their
scroll wiring and documented content-offset approximations. Mount-only:
artwork stays per-page full dispatch (index-keyed, append-only models),
so covers are already on the model when a card mounts. Load-more
triggers untouched (grid height formula is windowing-invariant).

cc
2026-07-06 19:55:07 -06:00
vicrodh 19ac438097 perf(ui): horizontal element windowing in the shared Carousel
Column band from viewport-x (visible plus one viewport of margin each
side) with the same contract hysteresis as AlbumGrid; off-band cards
are same-footprint placeholder tiles. Element-count only, always on —
section loaders dispatch artwork for the whole strip up front, so a
card mounting later picks its cover from the model instantly. Covers
every Home/ForYou/Discover/Search strip through one component.

cc
2026-07-06 19:55:07 -06:00
vicrodh bf6336cc78 perf(ui): full viewport windowing for Favorites albums
Mirrors the LL albums pattern 1:1: artwork migrates from index-keyed
FavoriteAlbum{index} to id-keyed FavoriteAlbumById{id,gen} (re-sort
immune), a FAV_ALBUMS_GEN token guards stale applies, the band from the
windowed grid routes through a thread-local BandDispatcher (180ms
leading+trailing), covers far outside the window evict back to the
placeholder, and list/grouped modes keep full dispatch. The load path
no longer dispatches every favorite cover up front — the first window
report pulls only what is near the viewport.

cc
2026-07-06 19:55:07 -06:00
vicrodh e4ca7f1a61 perf(ui): contract hysteresis on the AlbumGrid mounted band
Split the row band into pure target props and a stored mounted band.
Expansion still follows the target immediately (cards mount before
reaching the visible area); contraction lags by one row, so a few-px
oscillation across a row boundary no longer mounts/unmounts a whole
row of AlbumCards per direction change. window-changed now fires only
when the mounted band actually moves. Non-windowed consumers
(Discover/Favorites/Labels/grouped) are behavior-identical.

cc
2026-07-06 14:22:38 -06:00
vicrodh cd1b22bfac perf(ui): throttle albums window dispatch with gen-guarded trailing edge
Band reports from the windowed LL Albums grid now route through a
leading+trailing throttle (new viewport::BandDispatcher, 180ms) instead
of dispatching artwork on every row-boundary crossing during a fling.
The scheduled pass captures ALBUMS_GEN and drops itself if a reload
superseded it — the reload's own derive dispatch is authoritative.

cc
2026-07-06 14:22:30 -06:00
vicrodh cb48e04395 Merge fix/custom-editor-inline-layout into pre-release
cc
2026-07-06 13:33:42 -06:00
vicrodh ae43b1fff6 fix(ui): inline swatch grid for the custom theme editor
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
2026-07-06 13:33:42 -06:00
vicrodh 7c938286d0 Merge feature/custom-theme-editor into pre-release
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
2026-07-06 10:23:16 -06:00
vicrodh 42b5af94cb fix(theme): first Custom selection seeds from the theme on screen
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
2026-07-06 10:23:16 -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 2001a40486 feat(ui): ColorPicker primitive
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
2026-07-06 10:10:49 -06:00
vicrodh c6a7527bbb feat(theme): Custom slug, persistence and startup wiring
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
2026-07-06 10:06:29 -06:00
vicrodh 95fed0a27c feat(theme): custom-theme base tokens + derivation in qbz-theme
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
2026-07-06 10:03:01 -06:00
vicrodh e7f4fa3b5f Merge fix/554-library-header-tooltips into pre-release
cc
2026-07-06 08:05:58 -06:00
vicrodh cd62853743 fix(library): tooltips on the Library header action buttons
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
2026-07-06 08:05:57 -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 e70d434773 fix(theme): bound auto-theme image decode, spinner via event loop
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
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 44676a9b98 Merge bugfix/korean-skia-ttc-index into pre-release
cc
2026-07-05 22:37:05 -06:00
vicrodh e5fcb9d1af fix(text): load Korean TTC faces the macOS Skia renderer refused
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
2026-07-05 22:36:48 -06:00
vicrodh 1bbc98cc8e feat(ui): auto-theme settings UI + i18n
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
2026-07-05 22:34:09 -06:00
vicrodh caabdf9327 feat(theme): auto slug, prefs and startup application + controller wiring
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
2026-07-05 22:34:09 -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 b6ec858329 Merge fix/eqbars-coarse-clock into pre-release
cc
2026-07-05 22:17:57 -06:00
vicrodh f292fc1913 fix(perf): drive the equalizer bars from the coarse clock on every tier
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
2026-07-05 22:17:57 -06:00
vicrodh b277969752 Merge bugfix/543-elide-first-line-cull into pre-release
cc
2026-07-05 18:53:02 -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 f6586d9219 fix(playback): stream-error drain lives on the player's shared state
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
2026-07-05 16:04:46 -06:00
vicrodh a183a41823 Merge feature/dlna-strict-renderers into pre-release
DLNA strict-renderer support (based on PR #547) + the #550 media
server memory fix.

cc
2026-07-05 15:08:34 -06:00
vicrodh 1261e98cc0 fix(cast): stop hoarding full-track buffers in the DLNA media server
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
2026-07-05 15:08:34 -06:00
vicrodh 9dbf3be94d fix(cast): guard DLNA STOPPED against premature ends without wedging the queue
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
2026-07-05 15:06:30 -06:00
vicrodh 3ee5d5d7e3 fix(cast): DLNA discovery, DIDL features and Play self-heal for strict renderers
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
2026-07-05 15:05:16 -06:00
vicrodh 691918e267 fix(cast): serve DLNA media strict-renderer compatible
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
2026-07-05 15:04:30 -06:00
vicrodh 8ab7bc4b37 Merge fix/library-terminology-sweep into pre-release
cc
2026-07-05 15:03:47 -06:00
vicrodh cf9d28b9e1 fix(i18n): say "Library" wherever the copy still said favorites
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
2026-07-05 15:03:47 -06:00
vicrodh 3dc07ca9f2 Merge bugfix/clipboard-lifetime into pre-release
cc
2026-07-05 14:49:51 -06:00
vicrodh 71eab7736c fix(share): keep one clipboard instance alive for the process lifetime
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
2026-07-05 14:49:51 -06:00
vicrodh 72a9f52d06 chore: untrack prune-incremental.sh (swept in by accident)
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
2026-07-05 14:18:01 -06:00
vicrodh ace60e945b Merge fix/549-x86-baseline-runtime-aes into pre-release
cc
2026-07-05 14:17:46 -06:00
vicrodh 395e537b59 fix(build): drop compile-time x86 target-features — runtime AES dispatch
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
2026-07-05 14:17:46 -06:00
vicrodh 444b21f0c1 Merge bugfix/stream-error-toast into pre-release
cc
2026-07-05 12:12:10 -06:00
vicrodh f83eadb398 fix(playback): surface stream errors as toasts, sandbox-aware for ALSA
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
2026-07-05 12:12:10 -06:00
vicrodh ceb25cd1d1 Merge bugfix/521-release-forced-clock-on-quit into pre-release
cc
2026-07-05 12:08:15 -06:00
vicrodh fd8057fda6 fix(audio): release the forced PipeWire clock on app quit
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
2026-07-05 12:08:15 -06:00
vicrodh 95408aba92 Merge bugfix/zbus5-accesskit-reactor-panic into pre-release
cc
2026-07-05 10:26:18 -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 c482ca0c0b Merge bugfix/deadkey-compose-text into pre-release
cc
2026-07-05 10:17:30 -06:00
vicrodh 1346f8fc77 fix(input): deliver dead-key compose commits to text fields
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
2026-07-05 10:17:30 -06:00
vicrodh 63837a0412 Merge bugfix/543-cjk-bold-macos into pre-release
cc
2026-07-05 08:23:17 -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 cf16fa263c Merge bugfix/545-genre-filter-subgenre-expand polish into pre-release
cc
2026-07-05 08:19:14 -06:00
vicrodh 279f67e580 fix(genre-filter): keep leaf rows fully clickable and steady row hover
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
2026-07-05 08:19:14 -06:00
vicrodh 0a2bf9a73e Merge bugfix/542-multigpu-wgpu-adapter into pre-release
cc
2026-07-05 08:11:38 -06:00
vicrodh a273088205 fix(renderer): auto-revert sentinel now covers the auto-detected wgpu tier
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
2026-07-05 08:11:04 -06:00
vicrodh 3b5192acd6 fix(renderer): default to HighPerformance on hybrid desktops
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
2026-07-05 08:09:28 -06:00
vicrodh 0a65bdfe8a Merge bugfix/544-wayland-app-id into pre-release
cc
2026-07-05 08:06:05 -06:00
vicrodh dae6e7f690 fix(window): set the Wayland app_id / X11 WM_CLASS to com.blitzfc.qbz
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
2026-07-05 08:06:05 -06:00
vicrodh 2c3636ae6e Merge bugfix/546-playlist-sort-date-added into pre-release
cc
2026-07-05 08:04:07 -06:00
vicrodh de5c50a5b3 feat(playlist): restore the "Date added" sort option
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
2026-07-05 08:04:07 -06:00
vicrodh bd2c1a6c21 Merge bugfix/545-genre-filter-subgenre-expand into pre-release
cc
2026-07-05 08:01:57 -06:00
vicrodh 30211f74c0 fix(genre-filter): let the expand chevron receive clicks in the advanced tree
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
2026-07-05 08:01:38 -06:00
vicrodh 54bb2c96a0 fix(gentoo): RESTRICT mirror+strip in the binary ebuild template
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
2026-07-04 17:37:28 -06:00
vicrodh 6bf4bb2414 fix(renderer): treat wgpu surface-acquire timeout as a skipped frame
Issue #540: under Hyprland VFR a static screen or hidden workspace
stops compositor frame callbacks, wgpu's get_current_texture() times
out, and the vendored femtovg-wgpu backend retried once then
propagated the error — killing the event loop and the app mid-
playback.

A surface-acquire timeout is transient by definition: return a
FrameSkipped sentinel instead, which the renderer treats as nothing-
to-render, and back off exponentially (250 ms to 2 s) so repeated
redraw requests do not keep blocking the UI thread on a parked
compositor. The first successful acquire resets the cooldown. A
one-time stderr notice aids field diagnosis. Also give the vendored
crate an empty [workspace] table so it can be cargo-checked
standalone.

cc
2026-07-04 17:26:04 -06:00
vicrodh 1e521a182f fix(ci): stop snapcraft resolving the root source-build recipe
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
2026-07-04 13:07:41 -06:00
vicrodh 63a3a5aa98 chore(nix): bump flake to v2.0.0
cc
2026-07-04 12:28:38 -06:00
vicrodh 3fa1334bb3 fix(ci): raise the snap packaging job timeout to 90 minutes
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
2026-07-04 10:54:10 -06:00
vicrodh 2caede7dd1 fix(about): update the release codename to Rebuild
The About modal codename is a hand-set literal and still said
1.2.15's 'Just cleaning'; it now matches the 2.0.0 TAG_DETAILS
headline.

cc
2026-07-04 08:27:45 -06:00