- 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
Aligns the README with the wiki and website: QBZ exists to give Linux users the
Qobuz client they deserve, and is explicitly not a web wrapper and not a
download tool. Keeps the technical native-pipeline explanation below it.
qbzd pulls qbz-audio whose jack-sys build script requires jack.pc; the
desktop build job already installs libjack-jackd2-dev, the new qbzd jobs
did not.
cc
Promote the headless daemon to its own section with a screenshot and the
wiki link, add a Kiosk Mode section for Raspberry screens and handheld
consoles, remove the mermaid architecture diagram and the star-history
chart.
cc
Split the intro highlights into separate paragraphs, restore the em-dash
bullet pattern in the Casting entry, add the missing section separator
before What Won't Be Developed.
cc
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
The copied track and album links now use the canonical open.qobuz.com
form. Artist, label and playlist links are unchanged.
The builders also feed the Odesli (Song.link/Album.link) URL fallback;
that fallback never resolves Qobuz URLs anyway (every qobuz.com form
answers 400 could_not_resolve_entity, verified in-tree 2026-07-16) and
the working path stays ISRC/UPC -> Deezer, so the resolution machinery
is unaffected. open.qobuz.com is already accepted inbound by
qbz-qobuz's link_resolver.
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
Owner decision 2026-07-23: enabling translation applies only to the song
it was toggled on, in that playback — not a session/album-wide switch.
Every track change now resets the translation session and fetches
original-only (was: track-change requests rode the resolved target so
the translation followed across tracks). enable_translation/disable and
the fail-soft toast paths are unchanged.
km
Static-review miss (qbz crate can't be check-built without the 30 GB
qbz-ui unit): as_weak() is a ComponentHandle trait method, so the trait
must be in scope. Found by the owner smoke build (E0599, qbz/src/
deep_link.rs:153) — the sole error in the crate.
km
qbz:// resolves nowhere in the link resolver, so the registration
advertised a scheme the app never handles. Keep
x-scheme-handler/qobuzapp (handled by the resolver) in all 7 desktop
files.
km
A second launch carrying a deep link exited after a bare Present(),
taking the URL with it — focus worked, navigation didn't. Add
OpenUrl(url) to the SingleInstance D-Bus interface: the primary stashes
the URL, presents itself, and dispatches through the same pending/drain
path as a cold-start link (stays pending while no shell is up).
acquire_or_raise calls OpenUrl when argv carried a link and falls back
to bare Present() when the method is missing on an older primary
(version skew), then to MPRIS Raise as before; no-link launches keep
plain Present().
km
xdg-open https://open.qobuz.com/album/<id> raised/focused QBZ but never
navigated: main() never read argv, so the desktop files' Exec=qbz %u URL
was silently dropped (Tauri parity gap — the old app scanned argv on
cold start and in its single-instance callback).
New deep_link module: capture_argv() at the top of main() stashes the
first Qobuz-link argv entry (legacy is_qobuz_link prefixes) BEFORE the
single-instance guard runs — a second launch must read its own argv
before deciding to raise+exit. enter_shell binds a shell context
(runtime, window weak, tokio handle, image cache; cleared on logout) and
drains the pending URL at its very end, after the startup-page/view
restore, so the restore can't re-root over the deep link. Session is
active and the AppWindow exists there, so no readiness sleep hack.
Dispatch is the existing Ctrl+L machinery (link_resolver::resolve ->
apply_resolved_link); failures surface as toasts. At the login screen or
in offline mode the URL stays pending for the next successful online
enter_shell.
km
position() + index instead of an iter_mut().find() borrow held across a
match arm — no NLL subtlety, and the lifecycle test passes against the
verbatim file (scratch-crate run, since the qbz crate itself is not
built here).
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
The two Recommended-Artist rails showed artists the user already follows
or blacklisted, and the same artist could appear in both rails at once.
Builders now retain the full validated pool (rotated, not truncated at
DISPLAY_CAP); the pools ride the results blob as before, so old blobs
simply have no overflow until they expire. A new pure compose step in
qbz-external-reco (unit-tested: exclusion, within/cross-rail dedup with
common-wins ordering, visible/overflow split order) runs at the single
paint choke point in external_reco.rs — covering BOTH the fresh build
and the cached-blob paint. Exclusions fold followed (fav_cache) +
blacklisted + reco-dismissed ids, each source independently fail-open.
The past-cap survivors are retained as overflow for the Not-interested
live backfill (no extra network).
km
Reco-scoped dismissal (NOT the app-wide blacklist): a dismissed artist
only leaves the Recommendations rails and stays visible everywhere else.
One small JSON store per user (the light discovery_dismiss precedent)
bound at session activation next to fav_cache (login / restore /
offline) and dropped on logout. Fail-open: unbound or corrupt file reads
empty and mutations no-op. Unit-tested lifecycle (roundtrip, idempotent
dismiss with snapshot backfill, re-bind persistence, undo, junk/unknown
format tolerance).
km
slim_from_artist hardcoded following: false, so reco artist cards never
reflected the current follow state at build time (the pinned carousel
already seeded from fav_cache). Seed from fav_cache::is_artist_favorite
and extend the shell-entry favorite-artists warm walk to the three
ExternalRecoState artist models, in place (no model swap, no flicker).
Live toggles were already propagated by search::mark_artist_followed.
km
Live wire (verified 2026-07-22, track 266725027 with language=es): when a
translation is requested and available, lyrics_url points at the TRANSLATED
document (<id>-<lang>.json) carrying ONLY 'translation' — no 'original' —
and 'translation_requested' is absent. get_lyrics rejected such documents
('without original content -> no lyrics'), so every translation toggle
ended in the unavailable toast. Now get_lyrics fetches the original-only
document in a second round trip and merges both into one bilingual doc
(merge_translation_into); requested-but-unavailable still 403s the CDN
URL -> typed miss (fail soft).
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
The commit push now carries translation-available (doc translation_langs
non-empty) and per-line translation-text, aligned 1:1 with the rendered
lines (the core filters translation lines like the originals; a missing
entry degrades to empty). Toggle ON resolves the target (explicit setting,
else Auto: account language_code -> UI locale) and refetches the current
track with LyricsRequest.language set — show-translation flips only when
the response actually embeds a translation; any gap toasts the Android-
parity error string and reverts, never putting the lyrics view in an
error state. Toggle OFF is pure UI (no network). While ON, track-change
requests ride the resolved target so the translation follows across
tracks; prefetch stays original-only. A doc that already carries the
target's translation flips locally without a refetch. The miniplayer
mirrors show-translation so the mini lyrics surface follows the sidebar.
km
Read-only accessor over the session lock — the lyrics translation "Auto"
resolution reads it through the shared client handle. None pre-login and
for pre-v10 persisted sessions.
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
SharedClientProviders implements the new language-aware LyricsProviders
boundary; both track-change and prefetch requests pass None (the default
original-only flow per spec section B.1) — the refetch-with-language is
triggered by the UI layer in a follow-up task. The committed LyricsDoc
now carries the translation into CURRENT_DOC automatically, so the sync
engine and the future UI toggle read the same document. qbz-core's
headless get_lyrics wrapper keeps its signature and fetches
original-only.
km
LyricsRequest carries an optional translation target that threads
through the LyricsProviders::qobuz boundary to the client; the external
fallback chain is untouched. LyricsDoc gains translation
(Option<Box<TranslatedLyrics>> with a LyricsKind dispatch mirroring the
Android v10 mapper: wsync->WordSynced, lsync->LineSynced, plain->Plain)
plus a client-derived has_translation (requested lang in
translation_langs, false when none requested). QobuzWsync stores the
translation verbatim in the existing cache JSON (serde default, no DB
migration) with its lang recorded: a synced cache hit only serves a
language request when the stored translation matches it, otherwise the
chain refetches; original-only requests keep today's behavior.
km