Extends Play later (end of the manual block) from track rows to every
surface that already offered Play next / Add to queue:
- playback.rs: enqueue_album_later, enqueue_playlist_later (mixed
sidecar interleave), enqueue_artist_top_later, enqueue_tracks_later,
enqueue_local_tracks_later — all NATURAL order (each add_track_later
appends, no reverse), QConnect controller routing via
play_later_batch_on_peer_if_active, and post-add queue sync.
- local_playlist::enqueue_later_by_id (source-aware rows, offline-only
stamp preserved), myqbz_play RowMode::PlayLater + BulkEnqueueMode.
- 19 dispatch arms in main.rs (album/playlist/artist/label/mix/
favorites/local-library/album-page/disc/myqbz dialects).
- 12 context menus + 10 bulk bars in .slint: list-plus icon between
Play next and Add to queue, popup heights bumped.
The immersive tap-action dropdown is deliberately untouched (it is a
configurable single-tap behavior setting, not a context menu).
km
Replaces the trash can (Favorites bulk bars, tracks + albums) and the
plain heart (Local Library bulk bar) with Lucide heart-crack — trash
reads as file deletion, not as un-favorite.
NOTE: touches .slint, so the next build recompiles qbz-ui (long path).
km
- Local Library bulk un-favorite was O(selected x visible) on the UI
thread and froze on big libraries (owner: 'se ciclo'). Now: one model
pass (hearts hollow instantly), store mutations on a blocking thread,
'Removing…' info toast up front and a success toast at the end.
- Favorites > Tracks bulk remove: 150 ms courtesy delay between Qobuz
API calls (no burst) + the same progress toasts.
km
- #661: a pause issued during the load window fell into the toggle's
resume branch and got swallowed — the track started anyway and the
sleep inhibitor stayed held. stop() now cancels the in-flight play
(PENDING_PLAY_ID), so nothing plays and no inhibitor is acquired.
- Accent ring on the playing row: 1px -> 1.5px border (femtovg 1px
stroked circles alias; CircleAction's header rings use 1.5px).
- Menu position hints (#N) removed from the track context menu (owner).
- Icon mapping across menus: list-start = Play next, list-end = Add to
queue, list-plus = Play later; new list-start.svg +
list-indent-increase.svg (queue manual-block section marker).
- Local Library > Tracks multi-select gains bulk Remove from favorites
(local-favorites store, live heart flip, toast).
km
- Steam PR1/PR3/PR4 wiring: apply_gamescope_quirks at the top of main(),
tray + miniplayer suppression, gilrs spawn, UiFocusState OSK hook.
- #558 watchdog arm at window-ready; #654 monitor-DPR geometry clamp;
local-heart routing in the favorite arms; play-later media-action arm;
sort-flip handlers with direction (#657/Library All pattern); gilrs +
wayland-data-control deps (#514).
km
- Sidebar + playlist manager sorts gain both directions with a caret on
the active row (re-select flips); 'recent' keeps the API order
(newest-first — the old reverse was the #657 bug).
- Import now inserts the new playlist optimistically and bounded-retries
until the API lists it (read-after-write lag forced a manual refresh).
- Play later: tracks land at the end of the manual block (after every
play-next/later, before the source resumes); Play next and Add to
queue are untouched. The queue sidebar shows the section divider
(Next in queue / Next up) with drag-math compensation, and the context
menu shows the landing position of each queue action.
km
- #558: a backend can be alive yet wedged (zero presented frames forever
on legacy stacks). Vendored femtovg counts presented frames; a watchdog
8s after window-ready re-arms the renderer sentinel and restarts, so
the existing ladder degrades wgpu -> alt adapter -> GL -> software on
its own instead of stranding the user on a blank window.
- #655: 'Add only new tracks' stays enabled with all-dupes (adds zero
and closes) instead of disappearing as the primary action.
km
- PR1: gamescope detection at boot — X11 backend, scale 1, kiosk
fullscreen; tray + miniplayer suppressed (click-offset fix).
- PR2: full keyboard navigation for the kiosk (3 zones, focus ring,
scroll-into-view; typing never navigates).
- PR3: native gamepad via gilrs — D-pad/stick/A/B to synthetic Slint
keys with hysteresis, repeat and debounce; B suppresses Escape while
a text field is focused (closes the Steam OSK).
- PR4: Steam OSK via the steam://open/keyboard deeplink on text focus.
- PR5: scripts/qbz-add-to-steam (stdlib-only, 19/19 unit tests) with
artwork assets + pure-gamepad controller layout.
Spec: qbz-nix-docs/steam-deck/STEAM_DECK_GAME_MODE_PLAN.md
km
- Appearance > Play indicator animation (default OFF): static play glyph
in an accent ring on the playing row, pause on hover, accent edge pill;
the 8fps eq-bars clock no longer runs while playing with the pref off
(CPU complaint on some machines).
- Appearance > Invert swipe navigation direction for the two-finger
touchpad back/forward gesture (#653 follow-up).
km
- Viewport windowing for the All grid + list (AlbumGrid-class sampler);
thousands of items no longer mount eagerly (owner's 2200-item library
was unscrollable, even typing in search lagged).
- Artwork is id-keyed (kind:id) + generation-guarded + band eviction,
ending wrong-cover races and per-keystroke image churn; covers persist
on the full model across derives.
- show-local defaults ON (persisted) with the filter toggle kept in the
view toolbar; Settings gains the scope selector (hearted items vs the
whole local library, loaded with Local-Library query parity).
- Cards respect favorite/followed state; hearts of local/Plex items route
to the local-favorites store instead of the Qobuz API.
km
The published Flathub manifest has shipped --device=all since 2026-05-30
(flathub/com.blitzfc.qbz@11d38546), so /dev/snd IS exposed in the sandbox;
the device-override row added earlier on this branch was diagnosed against
the stale pre-repack local manifest clone and would have users grant what
they already have. The likely #652 mechanism is the ReserveDevice1 name
(qbz-audio device_reservation): PipeWire holds the DAC and Flatpak can't
own org.freedesktop.ReserveDevice1.* (Flathub linter blocks it), which is
exactly what the pre-existing Sandbox row fixes. Drop the device=all row
and make that row's note name the #652 symptom explicitly.
km
The Flathub manifest grants wayland/x11/pulseaudio/dri only, so /dev/snd
is not visible inside the sandbox: opening an ALSA hw:X,0 output fails
and exclusive mode silently falls back to PipeWire (other apps keep
mixing through the DAC). Per owner decision the manifest stays as-is
(Flathub reviewer friction); instead the Sandbox settings page gains a
third Flatpak row with the manual one-time grant
`flatpak override --user --device=all com.blitzfc.qbz`, following the
existing copyable-command-row pattern, and notes the required restart.
km
Extend the AppShell bottom-layer TouchArea (the one already catching the
mouse back/forward buttons) with a scroll-event handler that accumulates
horizontal touchpad deltas and fires one NavState.request-back/forward
per gesture — the same callbacks the header nav buttons drive, so entry
swap, scroll restore and nav flags all follow the existing path.
Delivery is front-to-back, so a horizontal carousel Flickable consumes
horizontal deltas before they reach this layer (carousels keep scrolling
and never trigger navigation); a vertical-only page scroller rejects
horizontal-only deltas as an orthogonal direction, letting them fall
through to the gesture handler. The handler always returns reject, so
scroll routing is untouched everywhere. One navigation per gesture via
an idle-timeout/direction-flip reset and a fired latch; 70px threshold;
fingers-right = back / fingers-left = forward under libinput natural
scrolling (commented in code).
km
Owner 2026-07-23: drop the mobile-parity circle — square, no border, no
shadow; bg lights up on hover, matching the Discover configurator gear
(GearButton in HomeView.slint). Glyph swapped to the owner-picked
translate-svgrepo icon (translate.svg; the Lucide languages.svg added
earlier on this branch is removed). Accent tint remains the ON state.
km
New msgids hand-added to all 8 qbz-ui catalogs: the tab description, the
empty-state heading + hint, and the dismiss/undo toasts. "Not
interested" and "Recommendations" reuse the pre-existing msgids
(PlaylistView / Discover). en msgstr = msgid; es/de/fr/pt/ru/ja/nl
translated. msgfmt --check clean on all 8.
km
Manage the reco-scoped Not-interested dismissals: a third tab in
Settings > Blacklist lists dismissed artists (search-filtered like the
other axes, count badge, empty + no-results states) with a per-row undo
that removes the entry from the reco_dismiss store — the artist becomes
eligible for the Recommendations rails again on their next paint. Row
click reuses the existing artist-select navigation. No bulk Clear-All on
this tab (undo is per-row); the modal stays scoped to the blacklist axes.
km
ArtistGridCard's context menu gains a 4th item (Not interested,
thumbs-down icon; popup height bumped for the extra row) that emits
("artist", id, "not-interested") through the existing media-action
chain. The new central arm persists the dismissal to the reco_dismiss
store and, when the artist is in a reco rail, removes the card live and
backfills the freed slot from the rail's retained overflow (already
Qobuz-validated — no network; the pop re-checks live exclusions and
cross-rail visibility). Dismissed from a non-reco surface, the display
name is resolved via get_artist before persisting, so the management
list and toast still read well.
km
New msgids: Show/Hide lyrics translation (toggle tooltips), Translation
language + Auto (account language) (flyout row), and the fail-soft toast
"Translation is not available right now. Please try again later.".
Register-matched per catalog (de/nl informal, fr/pt formal); en follows
its msgstr = msgid convention. msgfmt --check clean on all eight.
km
New QbzSelect row (Auto (account language) + en es fr de it pt nl ja ru,
the owner-approved list) after the Size row; language names are native
literals, the same convention as the Appearance dropdown. The options
array lives on LyricsState (reseeded from Rust on language change) and
the choice persists per-user in lyrics_prefs.json as translation_language
("auto" or an ISO 639-1 code), sanitized on load (unknown -> auto) and
round-tripped through apply_to_ui/persist_from_ui like the other prefs.
Flyout popup height grows by the new row (and the comment now counts the
lite-fill row it had missed).
km
Circular button anchored bottom-right of the lyrics body, overlaying it —
mobile parity (owner decision 2026-07-22: floating, NOT in the header,
NOT in the flyout). Mounted only while LyricsState.translation-available,
so a doc without translations renders the sidebar exactly as before.
Off = dim, on = accent; tooltips via the shared TooltipState channel.
The glyph is Lucide's languages icon, matching the bundled icon set.
km
LyricsLineItem gains translation-text (empty when the doc has none) and
LyricsState the translation props (translation-available, show-translation,
toggle-translation callback) per spec section C. LyricsLinesView renders
the translation under its original line while show-translation is on —
smaller (~72%) and dimmer, static text that follows the line; the karaoke
fill stays keyed on the original timings/text. Secondary surfaces share
the model, so the field flows through untouched.
km
Play-count album history store (album_play_history, WAL per ADR-002) hooked
into record_recent, Most Played Albums carousel on Home + For You (default
off), View-all page with search + per-card play count, i18n in all 8
locales.
Conflicts resolved against pre-release: kept the Slint-era Pinned section
alongside the new MostPlayedAlbums everywhere (DiscoverySectionId, prefs,
render kinds, nav, ContentView, HomeActions); AlbumCardItem carries both
is_pinned and plays (populated via card_to_item); .po files union-merged,
msgfmt clean; discover_prefs tests updated (home 15/8 ON, forYou 14/13 ON)
— 10/10 green. Release build green.
km
The 2px .slint literal proved invisible (owner ask 2026-07-21, iteration
2026-07-22). The extra clearance is now set from Rust at startup
(ShellState.npb-small-extra, default 6px, macOS-only, QBZ_NPB_SMALL_EXTRA
env override) so tuning costs a ~2-minute qbz-bin rebuild instead of a
~2 h qbz-ui one on the 8 GB Mac. Linux stays pixel-identical (extra = 0).
km
Spec 2026-07-19-local-album-grouping-mode (B2). A compilation album (the
Saint Seiya case: 10 distinct track artists) now shows the single artist
line plus a '+N more artists' chevron that expands into the full
track-artist list, each entry clickable into the LocalLibrary Artists tab
(name-based open-artist). Distinct raw track artists in first-appearance
order, computed in apply_album_version into LocalAlbumState.all-artists.
Expansion is view-local and resets on album change; single-artist albums
never see the chevron. Track rows already showed their own artist
(verified, no change needed).
2 msgids ('+{} more artists', 'Show less') x 8 locales; slintcheck green.
km
Spec 2026-07-19-local-album-grouping-mode (A2-A5 + i18n). The Albums tab
gains a third header dropdown ('Albums by folder' / 'Albums by metadata')
and Settings > Local Library a matching 'Album grouping' row — both write
the same pref: LocalLibraryState.albums-id-mode, persisted in
locallibrary_ui.json (default 'folder': compilations and box sets stay
whole; the metadata split stays opt-in for #411's use case).
The group key changes with the mode, so switching reloads the Albums set
(reload_albums) and invalidates the Artists tab (its album cache groups
the same way — a folder-mode compilation cross-lists under every artist
in all_artists, the owner's 'appears in all artists' ask). Threaded
through spawn_albums_load, seed_counts, the Plex quality-hydration
aggregate lookup, and the artists feed; myqbz_builder pins Metadata
(artist-scoped candidates, not the Albums view).
5 msgids x 8 locales (msgfmt clean); slintcheck green.
km
LabelView parity with the revamped Qobuz web label page (semantics
verified live against play.qobuz.com via Playwright):
- Header: Follow + Shuffle (new play_label_top_shuffled, shuffle-then-
delegate to play_tracks_ctx) + overflow menu (Play next / Add to queue /
Share -> play.qobuz.com/label/<id>; no Song.link equivalent for labels).
No header Play by owner decision (the Popular Tracks play does the same).
- The Popular Tracks section '...' button was dead (fired an unhandled
'top-tracks-menu' action): now a popup with Play next / Add to queue
over all cached popular tracks. Section buttons wrapped so the 44px
primary and 32px secondaries share one vertical centre.
- Catalog/library toggle: the ArtistPage SegmentedTabBar verbatim, floated
right of the header cluster (visible only with library items; mirrors
the web's A/B-rolled 'Del catalogo / En la biblioteca'). Library tab
renders added tracks + albums on the label, backed by the new
library_by_label session index (AlbumCard.label_id / TrackCard.label_id,
seeded from the SAME two favorites fetches as library_by_artist).
- More Labels carousel now renders the Library LabelCard (new label-cards
mode in ArtistCarousel) instead of the legacy generic card.
- Artists carousel images: /label/page top_artists often carry no parseable
image, leaving placeholders forever. Restored the Tauri fallback wave:
custom artwork first, then v2/artist/get, fed through the stable
LabelArtist artwork targets.
i18n: zero new msgids (all reused across the 8 locales). slintcheck green.
km
With the custom chrome (titlebar_transparent + fullsize_content_view)
AppKit parks the native traffic lights at the stock ~28pt titlebar height,
visibly above the 42px header controls. macos_chrome.rs measures the close
button's centre in window coordinates and shifts the shared button
container by the delta to the 21pt header centre (custom chrome only,
one-shot after show; known gap: AppKit re-lays the titlebar on fullscreen
roundtrips). Deps: raw-window-handle direct + slint raw-window-handle-06 +
objc2-app-kit NSWindow feature, all macOS-target only.
NPB-Small on macOS gains 2px of bar height (ShellState.npb-small-height,
44px, growing upward from the bottom-anchored bar) so CJK/descender-heavy
meta lines stop clipping at the window's bottom edge. PlayerBarSmall pins
the song card to its old 39px top-anchored geometry: the art keeps its
exact top-relative position and the 2px land as bottom clearance. Linux
geometry is pixel-identical to before. slintcheck stub green.
km
The What's New / release-notes modal renders the GitHub release body through a
custom block model that stripped inline markup and had no link support, so a
markdown link showed as literal `[text](url)`. Now:
- inline links `[text](url)` collapse to just their text (clean attributions),
- a whole-line link becomes a KIND_LINK block rendered as a clickable accent
link opened in the browser (a new WhatsNewActions.open-url reusing open::that,
mirroring about.rs).
Adds a `url` field to WhatsNewBlock plus a small markdown-link parser. The 2.0.2
notes use it for the daemon-wiki link. Built + smoked at release (window ready,
ALSA 192kHz bit-perfect, playback OK).
cc
The bubble had a hardcoded 24px height, so the #638 Source/Output/cause tooltip clipped to its middle line. Height now follows the content: multi-line fits, a one-line tooltip stays compact.
cc
The no-art (number) variant drew a hardcoded #ffffff glyph with no backing, so it vanished on light themes (white-on-white). It now sits on a themed circular backing (polarity-baked alpha fill/border + shadow) and tints with Theme.text-primary; the row hover uses the themed alpha ramp too. Zebra left as-is to avoid a dark-theme shift. ADR-008 compliant (icon-only circle, not a pill).
cc
The delivered-first badge, amber downgrade arrow and Source/Output/cause tooltip only lived in AudioStamp (Small/Large/kiosk). The default New mode - and Classic - mount SongCard, whose badge was bound to the catalog max only, so a CD-capped stream showed '24-bit / 96 kHz' with no arrow. SongCard now consumes the same NowPlayingState delivered fields; no new strings (reuses AudioStamp's msgids).
cc
The per-renderer cap dropdown now uses the plain Settings product names (Hi-Res / CD Quality / MP3, looked up by key so they can't drift) instead of the misleading verbose 'CD - 16-bit / 44.1 kHz' form - a CD-tier track can be 48 kHz; the tier is a format_id, not a fixed rate. Removed the '...no 48 kHz option' caption. quality_label_from_track detail is now bit-depth-first to match every other badge.
cc
The cause-3 line "Limited by the quality cap for this device" only ever
fires while casting, yet it renders in the main window's badge tooltip
where its sibling cause-2 line uses "device" for the local DAC — same
word, two different devices. Interpolate the connected renderer's name
(CastState.device-name), the form the spec's 2.5 copy sketch specifies.
All 8 catalogs updated in place; the picker's "capped for this device"
suffix is unchanged (the device name sits directly above it).
cc
The four msgids fix 1 left en-only — the streaming-quality-setting cause
line, the no-higher-quality cause line, and the offline/cached over-cap
picker suffixes — now carry real translations in es/de/fr/pt/ru/ja/nl,
inserted in the same tail-block order as en.po. Register and vocabulary
match each catalog's existing quality-cap block (es tu/neutral, de Sie,
fr vous, pt-PT, ru formal, nl je) and reuse the catalogs' established
terms (Calidad de streaming, Titel, piste, faixa, качество потока,
ストリーミング音質, nummer). Informative tone throughout — a downgrade
the user configured is stated as fact, never as an error.
With this, every user-visible string introduced by the four #638 fixes
exists in all eight bundled catalogs (verified against the full
pre-release..HEAD diff, 16 msgids x 8 locales).
cc
CastState gains the fix-4 cap-row surface (device-cap-available/-key/
-options/-index + global-quality-label) and CastActions the
set-device-quality-cap(cap-key, index) callback - property/callback
additions to existing globals only, no app.slint export change. The
connected panel gets one label-left / QbzSelect-trailing row per the
flyout alignment rule (explicit menu-width, popup wider for the long
option labels, no pills), visible only when the renderer has a stable
identity, plus the muted caption that is the literal answer to #638:
Qobuz sells four discrete tiers, there is no 48 kHz option.
The quality-line suffix property now also names the cap at work
(cause 3, 'capped for this device') when no over-cap disclosure takes
precedence. Copy discipline throughout: the cap is the user's own
quality/bandwidth choice - never an implied device incapability
(renderers accept ~anything and downsample internally).
i18n: 8 new msgids in ALL EIGHT locales (en identity-translated),
including the cause-3 tooltip line AudioStamp pre-wired in fix 1.
Unit style matched per catalog (ru Cyrillic units, nl decimal comma).
Validated with the slintcheck stub (full tree + all catalogs).
cc
Settings > Audio (#638 fix 3, spec C.6): the "Limit quality to device"
toggle's description now states what the wired setting really does — it
caps the REQUESTED tier, local playback only, never casting. The
conditional "Maximum sample rate" Hz dropdown is replaced by a read-only
"Detected device limit" line ("192 kHz · Hi-Res+") fed by the probe: a Hz
control was the documented root-cause trap (it offered rates the app
cannot request — Qobuz sells four tiers, and no 48 kHz tier exists), and
with real detection there is nothing left to pick. When the probe fell
back to the assumed common rate set, a plain informative caveat below the
row discloses that the cap still applies but may not match the hardware
(owner Decision B: no warning icon, no error styling).
Removes the dead dropdown plumbing from the desktop bridge only:
MAX_SAMPLE_RATES, the SettingsState sample-rates/sample-rate-index
properties, the snapshot fields/derivation/pushes, the "sample-rate"
select arm (and with it a dead Apply::Reinit device teardown for a value
nothing read), and the table test. Deliberately KEPT per the spec's
hazard analysis: every qbz-audio SQLite column/migration/setter/getter
(positional 23-column SELECT), the qbzd CLI/TUI surfaces, the settings-
bundle entries, and all existing .po msgids (msgmerge will mark obsolete
ones; "No limit" is reused elsewhere) — grep hits for the old identifiers
under crates/qbzd/ are expected and correct.
3 new msgids in all 8 locales. Slint tree + catalogs validated with the
slintcheck stub.
cc
Wires limit_quality_to_device to a real consumer on the LOCAL output path
(#638 fix 3, spec Phase C). New crate::device_cap module caches the DAC's
detected rate ceiling mapped to a Qobuz tier (spec C.3: >96k -> Hi-Res+ =
no effective cap; 96/88.2k -> Hi-Res; <=48k -> CD 16/44.1, bit depth
included since no 48 kHz tier exists). Detection is the proven read-only
query_dac_capabilities probe; it runs in spawn_blocking on EXPLICIT
triggers only (startup, the Settings toggle, output-device change, backend
change, reset-to-defaults) — never on the playback hot path or poll tick.
A stale cap after a hot-unplug until the next device change is the
accepted trade, same class as the HiFi wizard.
playback::local_playback_quality() returns (tier, QualityLimit): the
preference clamped by the cap, with the request-time cause for the badge
tooltip (tie reports LocalDeviceCap — the more specific constraint,
mirroring the spec's cast tie rule). All LOCAL request sites switch to it:
play_track_resolved, prefetch (fetch + throttle estimate), the gapless
pre-fetch, and the now-playing badge seed (which can now name the device
as the downgrade cause, msgid x8 locales).
PRECEDENCE (owner decision): the device actually playing governs.
playback_quality() stays the PURE preference and the cast path keeps
calling it — cast_service is untouched and the play_audible cast branch
returns before any local resolve, so the local DAC cap is structurally
unreachable from a cast. The two settings stay independent; nothing is
auto-synced. No downsampling anywhere: the cap only changes which tier is
REQUESTED; cached/offline bytes still play as-is.
Settings plumbing: SettingsState gains device-cap-summary/-detected
(property additions to an existing global — no app.slint export change);
build_snapshot fills them from the cache; refresh_device_cap re-probes and
re-pushes on the explicit triggers.
Protected-crate note: no protected file is touched — qbz-audio is only
READ through the dac_capabilities probe; qbz-player, playback_engine.rs,
alsa_direct.rs, alsa_backend.rs and the backends are untouched; no
sample-rate handling changes.
cc
Fix-1 cast half (spec Phase 1). During cast there is no PlaybackEvent —
the renderer is playing, not our engine — so the delivered quality is now
MEASURED from the bytes about to be served: register_qobuz probes the FLAC
STREAMINFO (shared qbz-models prober) before register_audio moves them,
which reports the truth regardless of which tier (network / L1/L2 cache /
offline store) produced the bytes.
- CastPicker line: measured label+detail via the shared formatter
("Hi-Res FLAC", "24-bit / 96 kHz"); quality_label_from_track demoted to
the fallback for non-FLAC/local casts (F20 closed).
- The now-playing badge stays live during cast: cast_track publishes the
85e11d28 effective properties plus the fix-1 cause/effective-tier through
the SAME shared helpers the local poll uses (stream_downgraded /
classify_limit_cause / delivered_tier_str — no forked arithmetic). The
cause is the cast request-time resolution: preference today, renderer cap
when fix 4 lands; NEVER the local DAC cap (precedence rule).
- Over-cap disclosure: origin != network AND measured tier > requested tier
-> logged at info and surfaced on the picker line as "offline copy — cap
not applied" / "cached copy — cap not applied" (Decision C: local bytes
go out as-is, never resampled, never re-fetched — disclosed, not hidden).
Three new CastState properties (quality-limit-cause, quality-over-cap,
quality-origin), reset on disconnect; properties on existing globals only,
no app.slint export change.
- remote_stream.rs now calls the shared prober, keeping its own CD-shaped
defaults at the call site — behavior byte-identical (F17/F21).
- Two new msgids land in en.po; remaining locales come with the i18n pass.
Validated with the slintcheck stub (full .slint tree + 8 .po catalogs).
cc
Phase A of the #638 fixes (owner decision, reverses the deliberate #590
catalog-max choice back to Tauri behavior): while the delivered stream is
below the track's catalog max, the AudioStamp main line shows the DELIVERED
tier/detail, the amber arrow stays on, and the tooltip gains a third line
naming WHY — Source/Output plus the cause. A downgrade caused by the user's
own setting is the app doing what it was told, so the strings are
informative, never alarming.
Mechanics:
- REQUESTED_QUALITY_ID / REQUESTED_CAUSE atomics seeded once per track
change beside TRACK_MAX_* (one ui_prefs load per change, never per 450ms
poll tick), gated to Qobuz sources — local/Plex/ephemeral are not
governed by the streaming preference and never get a cause line.
- Display-time classification (classify_limit_cause): requested Hi-Res+ or
delivered below the requested tier's promise -> catalog availability;
promise met -> the request-time cause (the user's preference today; the
device/renderer caps of fixes 3/4 reuse the same discriminants).
- The #590 downgrade arithmetic (0.9 rate-family guard + full DSD
exemption) is extracted UNCHANGED into stream_downgraded so the fix-1
cast half reuses it instead of forking it.
- Two new NowPlayingState properties (quality-limit-cause int,
quality-effective-tier string) on the existing global — no app.slint
export change — reset on track change, mirrored to the miniplayer.
- Causes 2 (local device cap) and 3 (renderer cap) are pre-wired in the
tooltip chain but unreachable until fixes 3/4 land; their .po entries
ship with those fixes. The two reachable msgids land in en.po here; the
remaining locales are populated by the i18n pass.
Validated with the slintcheck stub (full .slint tree + 8 .po catalogs).
cc
Tauri parity (the sections did not survive the Slint port): a Settings
section shown only on sandboxed installs holding the one-time permission
commands with copy-to-clipboard buttons. Flatpak: exclusive-DAC D-Bus
reservation override (the user-side fix for #641's exclusive-mode failure
— the Flathub manifest cannot carry the wildcard own-name past its linter,
so the user grants it locally) and NAS/external-drive filesystem access.
Snap: the audio interface connections (qbz-player:alsa/pulseaudio/
pipewire/mpris) plus optional removable-media. New SandboxState global
seeded from qbz_app::diagnostics install_method; 14 msgids across all 8
bundled locales.
km
The card overlay's context menu used a fixed x:26/y:56 anchor, which put
the menu's vertical MIDDLE on the cursor (menu is 196x208, the button
sits at ~(152,142) of the artwork box). Natural context-menu behavior is
top-left at the click point. The OverlayButton clicked callback carries
no pointer coordinates, so anchor at the more-button's center (the
cursor proxy), converted into the artwork box's coordinate space via
absolute positions (artwork container gets an id for that).
km
Popup windows are always inlined by the Slint compiler, so every
`track-menu := TrackContextMenu` site — TrackRow in ~15 repeaters plus
SearchTrackHero — generated its own full copy: 18 structs, ~22k lines of
the 1.9M-line generated module, for one logical menu.
Now a single TrackContextMenu lives in AppShell, fed by the new
TrackMenuState global. Openers (TrackRow right-click / "..." gutter,
SearchTrackHero more button) write every gate input and bump
`open-token`; the shell instance watches the token and shows at the
pre-computed window coordinates (same scroll-safe absolute-position math
as the drag ghost). Gate expressions and dispatch ported 1:1 from the
former per-site instances — local-album view routes play/play-track +
track-menu-action, everything else hits the central media-action handler.
Menu closes on item click and on navigation.
QueueSidebar's queue menu and TrackCard's card menu are different item
sets and stay local (single instances, no multiplication).
Spec: qbz-nix-docs/context-menu-unification/2026-07-17-single-context-menu-spec.md
km
Replace the bespoke SceneArtistCard in ArtistsByLocationView with the
app-wide ArtistGridCard (follow / play / pin / context menu, homologated
with the other artist carousels). The scene model rides the shared
SlimItem (LocationArtistItem deleted): genres land in the subtitle — the
card's second row under the name, as requested. Follow and pin states
seed from the disk-backed caches at map time; mark_artist_followed now
walks the scene model too, so toggles from any surface flip the chips
live. ArtworkTarget::LocationArtist writeback updated for the SlimItem
artwork field; media-action routed through AppShell. Cards at 200x246
(AlbumCard parity).
km