Commit Graph
100 Commits
Author SHA1 Message Date
vicrodh 0c159c141c feat(qt): For You gains Qobuz Mixes pages, Artist Spotlight and Radios
Ports the three sections the tab was missing, against the Slint
implementation rather than invented: the section rails, the cards, and the
mix landing page with its navigation seam so back/forward behave.

The mixes reuse the shared crates for their requests -- the Slint app
already makes them, so nothing new talks to Qobuz here.

Also carries the qt-run.sh portability work needed for the Mac mini:
`free`, `nproc`, `pgrep` and `timeout` all differ or are simply absent on
BSD/macOS. Two of those bit during the first Mac build. `pgrep` exits 1
when it matches nothing -- the normal case -- and under `pipefail` that
killed the script before a single line of output, which reads exactly like
a build that did nothing. And `timeout` does not exist on macOS at all, so
the smoke produced a one-line log over which the grep cheerfully reported
"0 QML complaints": a test that never ran, rendering as a pass. The smoke
now fails outright on a log that short.

cc
2026-07-29 16:26:11 -06:00
vicrodh a3b89107fc fix(qt): table headers line up, the mini rail is centred, tooltips have a background
Three of the owner's visual reports, one theme: geometry that was derived
in two places and drifted.

The track table laid its header out from the LABELLED columns only, so
the heart and the overflow menu took no space there and every label sat
right of the column it names. Header and rows now come from one shared
set of widths, including the responsive arms -- if the header hid columns
on a different condition than the row, the drift would come back only on
narrow windows.

The collapsed rail goes back to Slint's 64px. It had been 50 by owner
request, and the measurement showed why that was the actual defect: the
rail centred its row at 25 while the header's own sidebar toggle, sitting
directly above it, centres at 30. Symmetric padding inside a 50px rail
still reads as misaligned because the eye compares the glyphs to the
header control, not to the rail; chasing it from inside bought the
alignment at the price of a visibly asymmetric hover pill. At 64 both
references land 2px apart and the padding is symmetric again. The row
stays a literal 34 rather than being derived from the width -- deriving
it is what coupled the two tokens and would have silently grown every row
to 48.

Tooltips: the port had none, while Slint has a shell-level overlay. This
adds the overlay rather than a Popup per call site.

cc
2026-07-29 16:25:57 -06:00
vicrodh 6b5226acf9 fix(qt): covers are actually downscaled, and non-square art keeps its ratio
The owner's clue was the whole diagnosis: the same image looked fine at
220px in the hover preview and bad at 150px in a card. So not the source,
and not the URL variant either -- every Qt producer picks exactly what
Slint picks, `large` (600px) into a 200px cell, 3x headroom.

`artwork_qt` was producing the right derivative and `RoundedImage` was
drawing the untouched 600px original next to it. `Context2D.drawImage`
does not filter, so every card was a nearest-neighbour downscale: measured
on a card cropped out of the running binary, RMSE 0.000 against a
point-sampled reference. The preview looks right because it is a plain
Image with smooth+mipmap and never touches this path.

Three defects were latent behind that dead derivative: `thumbnail(w,h)`
does not preserve aspect (46 square covers on disk written as 32x28),
`to_rgb8` matted transparent logos onto black, and the file was JPEG q75 --
more damage than the resample it was meant to cure.

Qobuz playlist art is 800x380, and both the header and the card were
stretching it to fill a square. It now keeps its ratio, and the uncovered
area takes a gradient derived from the image itself, reusing the ambient
colour pipeline rather than starting a second one.

cc
2026-07-29 16:25:42 -06:00
vicrodh d5bf4398ac fix(qt): Library sub-tab switch no longer segfaults, and the pane keeps its corners
Three identical faults in dmesg over three days, all
`segfault at 8 ... libQt6QmlModels.so.6.11.1[420c5]`. `nm -DC` puts that
offset in `QQmlDelegateModel::model()`; the QVariant return means sret,
so %rsi is `this` and address 8 is a null pointer plus the member offset.
`QQuickItemView::model()` reads its d-pointer, checks a QPointer guard and
then calls straight through without testing the value.

The re-entry is `onContentYChanged`, not `onModelChanged` as first
assumed: `setModel()` emits it from inside the window where the old
delegate model is already gone. Switching to Labels re-derives the model
AND clamps contentY to a shorter list, which is the emit -- hence the
intermittency, since at contentY 0 nothing is emitted and nothing
crashes. Reproduced in plain qml6 with no app and no login, and bisected:
readback in onContentYChanged dies, in onModelChanged survives 401 swaps,
cached array survives everything.

The fix is to stop asking the view for its own model. A tree-wide audit
found `.model` is the only member that is unsafe this way in 6.11.1 --
count, contentHeight, indexAt and itemAtIndex all null-check -- and the
three LocalLibrary tabs already cached their arrays, which is why this
stayed a Library-only bug.

Also: `clip: true` is a RECTANGULAR scissor in Qt Quick and does not
follow `radius`, so the album/artist header band painted over the content
pane's rounded corners. Slint clips to the rounded shape on the same
element, which is why the identical structure is correct there. The mask
sits OUTSIDE the frame rather than at a high z, because ADR-009 pins
in-pane modals at z >= 3000 with no ceiling and a z fix would be a
standing arms race.

cc
2026-07-29 16:25:26 -06:00
vicrodh f3f0acc192 fix(qt): favorites and pins tell the truth on every surface
The reported symptom was a 404 un-favoriting an artist. The cause was one
word: the shared client builds its query key as `format!("{}_ids", ty)`,
so it wants the SINGULAR noun, and every Qt call site passed the plural.
`artists_ids=` is not a parameter Qobuz knows, so /favorite/delete
answered 404 -- and /favorite/create had been failing just as silently in
the other direction. The trap is that the SAME identifier takes the
plural for /favorite/getUserFavorites, which is why the migration read
right and was wrong; label reads were the mirror image, sending the
singular where that endpoint wants plural and 400ing on every page open,
so a label could never be unfollowed.

Playlists were worse than wrong -- they went to /favorite/create at all.
That endpoint has no playlist_ids; Slint routes them to library.db and
says so in a comment. Rerouted.

Fixing the write then exposed the display: the toggle read a favourites
cache that only filled after the Library view had been opened once, so on
a fresh launch a favourited album drew an EMPTY heart and the click
REMOVED it. A backend fixed without its display is worse than neither,
because the two now disagree. So every producer stamps its real state at
document-build time from the same authority the write uses, and there is
one settle point every heart reconciles through.

Pinning gets the same treatment plus the fan-out it was missing, and both
now patch badges by signal instead of republishing a document -- a
republish hands `model:` a new array, which resets scroll and tears down
the delegate model.

cc
2026-07-29 16:25:12 -06:00
vicrodh 82b88ee8e0 chore(qt): add scripts/qt-run.sh
The Qt-track sibling of slint-run.sh, and deliberately not a copy of it.

No memory tiering and no cgroup cap: qbz-qt does not depend on qbz-ui, so
the 20-30 GB compilation unit never enters this graph. That is the point
of the track.

No RUSTFLAGS and no +nightly either. This worktree's target dir was built
on stable with rustflags=[], and RUSTFLAGS is a fingerprint input, so
exporting mold would buy a from-scratch rebuild of the workspace to save
seconds on a two-minute incremental. MOLD=1 opts in and warns first.

The two QML audits run before the build, because cargo check sees nothing
of the QML: a missing component or a bridge member that does not exist
compiles clean and throws when the view opens. They take a second.

SMOKE=1 runs the track's offscreen gate; a guard refuses to start while
another cargo/rustc is alive, since one Slint build in a sibling worktree
is enough to freeze the box.

cc
2026-07-29 10:31:05 -06:00
vicrodh 89c54cd5bd feat(qt): restore and persist the main window geometry
The reported symptom was titles eliding early in every bar mode. Root
cause was not the breakpoints: Main.qml pinned 1280x800 on every launch
while Slint restores window_width from the shared ui_prefs.json (1716 on
the owner's profile). 1280 sits one tier below the 1366 breakpoint, so
Classic's song card capped at 446px instead of reaching its 560px cap --
the ~114px of early elision.

Restoring alone would have fixed that. Saving is the half that makes the
feature real, and it is where the risk lives, so it is built to Slint's
semantics rather than invented: floating-size-only keys, the app minimum
as a gate, the >0.5px dirty check, maximized only ever re-applied.

Two things Qt makes harder than winit and both bit:

Visibility is ONE enum where Minimized replaces Maximized, while winit
keeps them orthogonal and still reports maximized while iconified. So
the flag is latched from the last non-transient visibility. The first
attempt read that through a derived property inside the handler for the
same signal that dirties it, which is one transition stale -- it passed
grep, qmllint and rustfmt, and was caught only by driving real
transitions under qml6. It is a function now; a function has no cached
value to be stale.

And the writes share a file with the shipping Slint app. A truncating
fs::write plus a json!({}) fallback on parse failure meant either side
could flatten the other's document: Slint reading inside the truncate
window falls back to UiPrefs::default() and writes those defaults back.
Every writer in the crate now goes through one atomic read-modify-write
(tmp + rename), and a file that is present but unparsable aborts the
save instead of being rebuilt from nothing.

The Slint side keeps its own non-atomic write, so the cross-process race
is narrowed, not closed.

cc
2026-07-29 10:30:56 -06:00
vicrodh 81c43a114e fix(qt): glyph colours follow the theme, on light palettes and on accent fills
Two defects with one cause: a tint name that means a frozen hex.

"primary" is the legacy alias of literal #ffffff, not Theme.text-primary,
so every control passing it drew a white glyph. On the eleven light
themes that is white-on-light -- the header back/forward chevrons, the
sidebar toggle and the carousel arrows the owner marked. They now pass
the theme-following names, which icon_tint_qt bakes at runtime.

The stale `hslLightness > 0.5 ? "primary" : "black"` workarounds that
predate the runtime tinter collapse into those names, and the qrc floor
underneath them becomes polarity-aware: dir_for() returns "" whenever
the cache dir is unavailable, a bake fails or the theme document has not
been published, and that path used to resolve textPrimary straight back
to the white bake -- reproducing the bug it was fixing, app-wide, in the
degraded state.

On accent fills the fix is a deviation from Slint, approved by the owner
after measurement. A fixed white glyph on Theme.accent falls under 2.6:1
on 16 of the 35 shipped palettes, including high-contrast at 1.70:1 and
wcag-dark at 1.82:1 -- the two themes whose reason to exist is contrast.
Slint hardcodes #ffffff there (CircleAction.slint:72 and three others),
so the bug is upstream. accentText alone is not the answer either: it is
picked for the theme's text ramp and gives 2.56:1 on Rose Pine Dawn,
worse than the black it replaces. So QbzTheme picks the best-contrast
tint by real sRGB luminance against the live accent, and the labels
beside those glyphs use its colour twin -- BrowseGenreButton and
LibraryView were drawing a white glyph next to an accentText label
inside the same pill.

Rounds out the two parity items the same files owe: QbzNavButton gains
the ambient arm from Carousel.slint (wired at the six carousel sites,
not left dead), and QbzCircleAction rounds its glyph to whole pixels the
way CircleAction.slint does.

cc
2026-07-29 10:30:43 -06:00
vicrodh 8af04c5272 fix(qt): NPB Small mounts the shared song card
The Small bar hand-rolled a reduced copy of the song card inline: album
art plus a two-line title/artist block. All three features the owner
reported missing live in SongCard.qml and only there -- the "layers"
playback-context glyph with open-context(), the album name in the meta
row, and the hover area that publishes the floating art preview. The
overlay itself was already mounted shell-level with no bar-mode gate; it
simply never had a writer while Small was loaded.

PlayerBarSmall.slint mounts the shared SongCard with text-center, so
this is the port catching up rather than a redesign: art-size 37,
art-border 3px surface-card, show-badges false, title 13px, art-text-gap
9. The 18px meta row fits the 39px controls row with ~1px clearance, and
the loading scrim on the cover comes along with it, as it does in Slint.

Also folds the responsive tier arms into shared helpers so the four bar
modes cannot drift apart on the 1366/1920 breakpoints.

cc
2026-07-29 10:30:27 -06:00
vicrodh 8b26cda6f6 feat(qt): floating album-art preview on now-playing cover hover
Port of ArtPreviewOverlay.slint: hovering the small cover floats a 220px
preview above it. Missing in every bar mode except Large.

Non-interactive and mounted last in AppShell, both for the reason the
.slint records: the original used a PopupWindow and it stole the hover
from the trigger, so the preview flickered open and shut. An overlay that
cannot receive events cannot do that, and clicks pass through.

The anchor rides QbzShell like the drag ghost does -- trigger and overlay
are in different files with no shared id.

cc
2026-07-29 00:56:12 -06:00
vicrodh 87b18fa7f7 feat(qt): wire local-network casting end to end
The two cast buttons in the now-playing bars rendered and did nothing: no
service, no bridge, no picker. This ports crates/qbz/src/cast_service.rs
onto the shared qbz-cast crate (discovery, both device families and the
media server are the crate's, not reimplemented) and hangs a QbzCast
singleton off it.

Discovery is armed only by openPicker() and stopped by closePicker(), so a
user who never opens the picker sends no mDNS/SSDP traffic. Connecting
stops the local backend through its public seam and re-casts the current
track at the streaming preference clamped by the per-renderer cap.

Playback is routed at four seams in playback_qt (new track, toggle, seek,
volume) and the local poll yields while casting — without that the two
polls fight over the bar every second. Logout and exit tear the connection
down, or the renderer keeps playing after the app is gone.

Deliberate gaps, each named in the module header: no offline-cache tier,
no QConnect suspend/restore, no Plex casting (the Slint carries the same
TODO), and lyrics do not follow a remote position yet.

cc
2026-07-29 00:56:07 -06:00
vicrodh c56fab3c4e fix(qbz-qt): covers were never actually resized, plus the adversarial parity round
**The scaled-cover fix from the last round produced nothing.** The seam ran —
462 requests in one offscreen smoke — and wrote zero files, so the owner was
right that nothing improved. The artwork cache stores files as `.img`, and
`image::open` picks its decoder from the EXTENSION, so every call failed
silently. Sniffing the content instead yields 101 derivatives on the same run.
The header atmosphere had the identical bug and had therefore never produced an
image either, which is why the flat fallback was all anyone ever saw.

Reported as fixed last round without checking that it ran. Verified by effect
this time, not by reading the diff.

The rest came through an analyse / refute / implement / verify pipeline, and the
adversarial passes earned their keep:

- The sidebar mini rail: icons sat left of centre because their slot kept its
  fixed 24px width while the rail collapsed, so the hit target was rectangular
  and off-axis. The rail narrows, rows are square, and the mini state gains the
  folder flyout and tooltips it never had. The reviewer caught a comment
  claiming the width change was a one-token revert when it is two — corrected
  rather than left to mislead the next reader.
- Playback context now travels from every producer, not the handful that
  happened to stamp it.
- Collapsible per-tab search in Library, and label artwork.
- Every View-all destination Home offers, plus the label page and its releases.
- Collapsing the sidebar drops a Large bar back to New, live, without
  overwriting the user's persisted preference.

cc
2026-07-29 00:06:34 -06:00
vicrodh 2a5b5ac16e feat(qbz-qt): theme-following icons, the real header atmosphere, sharp covers, playback context
**Icons follow the theme.** The bakes are frozen dark-palette hexes, so on light
themes secondary, muted and accent were simply wrong, and accent was wrong on
any theme whose accent is not #4285f4. Slint recolours at render time, which
this path cannot do — so the tint is generated once per colour into the cache
and loaded through the same Image+svg path the bakes already use. No effect, no
shader, so it cannot hit the "renders nothing on software" failure.

The sweep found the part the diagnosis had missed: "primary" was two meanings
welded together — max-contrast-on-theme-surface, and literal white for glyphs on
hosts that are dark under every theme. Remapping it globally would have painted
dark glyphs on dark scrims in fourteen places. The vocabulary is split instead,
with "primary" left as an alias so nothing changes until it is migrated
deliberately.

**The album header gradient was Slint's fallback, not its design.** Slint paints
a blurred artwork field and only falls back to the flat two-band gradient when
that image is unavailable; the port shipped the fallback. The field needs no
shader — it is a 128x128 image, so the pipeline ports verbatim: the cover down
to 8x8, back up to 128, blur, colour adjust, vignette. The 8x8 step is the look,
not an optimization; everything after it works from eight pixels of colour,
which is why it reads as atmosphere rather than as a blurred cover. It comes out
of the same decode that already produced the flat tint.

**Covers were point-sampled.** Canvas drawImage does not filter when it scales,
measured at RMSE 0.000 against a nearest-neighbour reference — a 600px cover in
a 200px cell throws away eight of every nine pixels, and the error is aliasing,
not softness. Rust now produces a derivative at the drawn device size so the
draw is 1:1 and there is no resample at all.

**The now-playing context stack.** It had no Qt counterpart: the model carried
context_kind and context_id, some builders stamped them, and nothing ever read
them back. The album path only appeared to work by coincidence, because the
glyph was hardcoded to open the album.

Plus: Discover's missing rails with real play history, Recommendations on the
integrations, queue drag reorder, and the playlist detail collage.

cc
2026-07-28 21:12:31 -06:00
vicrodh a1c771c39e fix(qbz-qt): row buttons, cover triggers, light-theme contrast, flyouts, genre filter
**Track-row buttons did nothing.** Declaration order is z-order in QML, and the
row-wide MouseArea added for right-click was declared last — so it covered the
heart, cloud and menu buttons and swallowed their presses.
propagateComposedEvents cannot rescue that: a press is not a composed event,
which is why only the row's own double-click still worked.

**Covers appeared only after a scroll.** Three compounding defects, not one. The
window report fired on contentY and on model change, both of which happen while
the tab body is still hidden behind its loading flag, so the report that
mattered hit the not-visible early return and was discarded with nothing to
re-fire it. The debounce reset on every change, so a burst of mount-time reports
kept pushing its trailing edge out. And a single pending slot meant that when
two surfaces are mounted at once — the artist rail and that artist's grid — the
second overwrote the first, so one pane was never requested at all. Eviction had
the same shape: each report deleted everything outside its own array, so the two
panes deleted each other's covers.

**Latest release had no cover** because the artist page's collector skipped it,
along with Appears On and the playlists strip.

**Light themes.** The mechanism is Slint's, from 2.0.2 and issue #638: the
on-surface arm on circle actions, header-light deciding the header foreground
only when a dark backdrop is actually painted, and the circular backing behind
the track-row play glyph. Ported as specified rather than approximated. Two
icons rendered in their SVG's own colour because they pointed at the raw asset
instead of a baked tint — a sweep found the rest of that class.

**Navigation flyouts** exist, one component serving both the sidebar and the
header nav modes.

**Filter by genre and the Discover sort now work**, everywhere the control is
drawn. The genre ids were already a parameter on the discover fetch that the
port always passed empty.

cc
2026-07-28 19:55:33 -06:00
vicrodh 9ab3d7db96 fix(qbz-qt): local albums route to the local view, and the detail-page parity round
**The empty album view.** Opening a local album from Library "All" landed on a
blank page. The mixed feed hands Qobuz and local albums to the SAME card, and
the card always called the catalog open — so a local id, which is a group key
or a filesystem path, went to /album/get and came back 404. It was in the
owner's log before this round as `get_album(/mnt/nas/…) returned 404`; what
changed is that clearing the stale document turned "shows the previous album"
into "shows nothing", which is how it surfaced. Routing now happens in
open_album itself, using the predicate the feed already uses to route
favourites, so every call site is covered at once instead of card by card.

**Header atmosphere.** Both detail pages get Slint's two-band gradient, tinted
from THIS page's artwork through the ported header_tint — not the now-playing
ambient triad, which is a different subject with a different algorithm.

**Artist page.** The catalog/library toggle existed but was broken: a
hand-rolled copy of the shared tab bar whose delegate walked the wrong parent
chain, so it threw on every evaluation and never took its active colours. It
now mounts the shared control. Added the JUMP TO label, the tab-strip search
with the client-side filter it drives, and the year under release-section
albums. The Network panel ran the full window height and never auto-opened; it
is bounded below the tab strip now and opens unless the content is constrained,
as Slint does. The action circles were the wrong size with the wrong glyph
colour and no overlay arm — which would have left them dark-on-dark once the
gradient landed.

**Quality in track rows.** The column rendered a bare tier; it carries the
badge now, in the one shared row component every surface mounts. playlist_qt
was the only producer not emitting the bit-depth line.

**Skeletons hand over instead of disappearing.** They cleared when the PATH
arrived, but the image still had to load and rasterize after that, so the
placeholder vanished and left an empty cell. They now wait for the image to be
ready.

**Local covers.** Most local covers ARE already the scanner's thumbnail, but
re-hashing their path produced a different filename, so each one was decoded,
resized to its own size and re-encoded into a duplicate before being shown.
That was most of the wait.

cc
2026-07-28 19:01:25 -06:00
vicrodh f3a33245b2 feat(qbz-qt): staged loading, skeletons everywhere, lyrics, gestures, richer menus
**Deferred external rows.** The album page awaited the Last.fm row before
publishing anything, so the tracklist waited on an external service plus
MusicBrainz. It publishes in stages now — header and tracks first, then each
Qobuz rail as it lands, then Last.fm — with every patch merged under a
generation counter so a late reply for album A cannot paint onto album B. The
artist page was already staged this way; its views were the part still showing
bare "Loading…" text.

**Skeletons as the standard.** One control, new variants rather than new
components, mounted across the views. Its second job is the one the owner
named: cleanup. A cover that never resolves settles the placeholder once and
reveals the card's own empty state instead of shimmering forever — local albums
with no embedded art produce no artwork entry at all, so a placeholder gated
only on "not arrived yet" would never stop.

**Local artwork streams.** Fifteen seconds to first cover on a 1274-album grid
was one batch emitted after the whole window resolved. Each cover emits as it
resolves now, visible band before prefetch margin.

**Lyrics migrated.** Smooth sync instead of a 1 Hz tick, word-level karaoke
where the document carries it, the surrounding-line dimming, the controls
flyout and the translation layer the v10 backend already supported. Exactly one
row subscribes to the 30 Hz output; the timer drops to 250ms when paused or
hidden.

**Navigation by mouse and touchpad.** Side buttons and a two-finger horizontal
swipe, matching #653 — including the part that matters, which is that a swipe
over a carousel scrolls the carousel. The layer is declared first in the shell
so everything else gets first refusal.

**Context menus.** Brought to the Slint inventory, Play later included, with
right-click opening the same menu as the button everywhere. Entries whose seam
does not exist are hidden rather than shipped inert.

Two controls that rendered and no-opped are now honestly disabled: the My QBZ
nav row, which routes to a view the shell has no case for, and three sidebar
menu entries with no backing call.

The sidebar caret was never a condition bug. The row is a positioner and the
name cell's width ignored the count label, so a folder WITH content pushed the
chevron past the clip — which is why it vanished exactly when it mattered.

cc
2026-07-28 18:12:31 -06:00
vicrodh 2b31d3866c fix(qbz-qt): clear a detail view's document when the next one opens
Opening album B after album A showed A's contents until B's fetch landed. The
open path raised the loading flag but left the previous document in place, so
the view had stale data to render and dutifully rendered it — and the wait grew
when the album fetch gained the Last.fm row.

Album, artist, playlist and the local album view now clear in the SAME hop that
raises the flag. That is the half of loading state the port was missing: a
placeholder over stale content is worse than one over nothing, because the user
cannot tell it is stale.

cc
2026-07-28 17:40:50 -06:00
vicrodh 83999fc552 feat(qbz-qt): rebuild Settings 1:1 against Slint
The pane was written fast and had drifted both ways: four whole sections were
a placeholder reading "not ported to the Qt frontend yet", and what was there
carried rows and copy Slint does not have — an invented custom-theme info row,
made-up "Restart to apply" descriptions, dividers Integrations never had, and
two buttons forked inline instead of using the shared control.

Every section is real now, in Slint's order and grouping, with Slint's labels
and control types: Audio, Playback, Appearance, Offline, Local Library,
Blacklist, Integrations, Developer, and the sandbox section that only appears
on a Flatpak or Snap install. Local Library brings the folder table and the
Plex panel — 856 lines of Slint with no counterpart here until now — wired to
the Plex store that landed earlier this round. Its PIN auth flow is not ported,
so the manual server-url and token path is what this exposes.

The rule the round was run under: a row whose key is not in settings_qt's
dispatch renders and does nothing, so it is either wired or not shipped.
"Share logs" was exactly that — present since the port began, inert — and now
opens the on-disk log.

The directory is per-section files under a thin composition root rather than
one growing file, and the sub-nav's bottom spacer stops being arithmetic over a
hardcoded row count, which broke the moment anyone added a section.

cc
2026-07-28 17:21:44 -06:00
vicrodh 75ea846768 fix(qbz-qt): the app menu carries actions again, not settings
Intelligent search, Ambient background and Use system title bar were sitting in
the hamburger menu. They are settings, not app-menu actions, and all three
already have their real rows in Settings > Appearance — which is where Slint
keeps them — so the menu was duplicating them in the wrong place.

Documentation is added; it is the one Slint row that needed no surface the port
lacks, just the external-link invokable QbzShell now has. Still missing against
Slint, each waiting on a surface that does not exist here yet: Open Music Link,
Keyboard Shortcuts, Report an Issue, What's New, About QBZ. They are named in
the file header rather than shipped as rows that do nothing.

build.rs now watches qml/ for changes. Cargo reruns a build script only when a
source file changes, so dropping the new icon into qml/assets/ produced a
binary whose qrc did not contain it — the file was on disk and the app logged
"Cannot open" at runtime. That was going to bite every future asset.

cc
2026-07-28 16:57:07 -06:00
vicrodh d76f694781 feat(qbz-qt): close the two declared debts on the album and artist pages
The Last.fm similar-albums row exists now. The recommendation building was
already frontend-agnostic in qbz-external-reco, so this only adapts QbzCore to
its catalog trait and publishes the result; the row seeds on the album's artist
and excludes what the two Qobuz rows already show, including ids the
pre-resolution dedup misses. Resolved rows cache per album for thirty days in
the same store the Discover tab uses, so re-opening an album makes no Last.fm
or Qobuz calls, and only a non-empty result is cached — a transient failure
must not hide the row until restart. With Last.fm not connected it resolves to
nothing and makes no network call, so the rail simply never appears.

Relationship rows navigate. They carry a name rather than a catalog id, so the
click resolves through MusicBrainz first and only a CONFIRMED match opens an
artist — landing the user on a same-name musician is worse than a row that does
nothing. The role travels with the name so a match in another role is not
mistaken for this one.

cc
2026-07-28 16:45:45 -06:00
vicrodh 960715c4ac feat(qbz-qt): skeleton placeholders for the heavy views
Library and Home showed grey squares that filled in unevenly, which reads as
broken rather than as loading. One shared QbzSkeleton — a card shape and a row
shape — now stands in while data is on its way, and each placeholder clears as
its own item arrives rather than all at once, so loading reads as progressive.

The animation is the part that could have undone the point of this track, so it
is capped: a bounded number of placeholders breathe, and the timer stops when
the view is not visible or the window is minimized. Not on focus loss — a
tiling desktop keeps windows visible and unfocused, which is the rule already
established for the ambient field.

cc
2026-07-28 16:23:49 -06:00
vicrodh 49ab5f5b9d feat(qbz-qt): fill the Album and Artist pages from the integrations
The two pages were missing most of what Slint shows. With MusicBrainz, Last.fm
and ListenBrainz wired this round, the sections that depend on them can exist:
the artist biography and network, relationship rows, listener figures and the
credit blocks the Slint pages carry.

Nothing is faked. A section whose data an integration cannot supply is absent
rather than an empty frame, and a section fed by an integration the user has
not connected degrades to absent without a network call — integrations stay
strictly opt-in. The relationship rows are non-navigable for now because
resolving a musician name to a catalog artist needs a bridge invokable that is
not there yet; an informational row is better than a dead click.

cc
2026-07-28 16:23:36 -06:00
vicrodh e0fcf7a089 fix(qbz-qt): local context menus, collapsing search fields, toolbar density
Four things the owner hit in Local Library.

The context menu did nothing there — local rows and cards now mount the shared
menu the rest of the app uses, with the action set Slint gives local and Plex
items. Anything with no seam in this port (add-to-playlist, add-to-mixtape, the
tag editor, local favourites) is hidden or visibly disabled rather than a row
that silently does nothing.

The per-view search fields did not open and collapse the way the header's does.
That behaviour moved into the shared QbzLineEdit, so the Local toolbar, the
queue and the tree rail all get it from one place instead of four copies. The
queue's field also escaped its 300px column; it is width-bound now.

Filter, sorting and grouping become icon-only buttons with an explanatory
tooltip, which is what the owner asked for and what the crowding called for.

Found on the way, and affecting Qobuz cards too: the album card's ⋯ menu had no
"Play later" at all, and its "Add to queue" row dispatched play-later — the row
lied about what it did. Both fixed, with Slint's icon set for the three.

cc
2026-07-28 16:23:23 -06:00
vicrodh c63889b487 feat(qbz-qt): Track Info in the now-playing bars, and PlayerBar split up
The info button on both bars did nothing. The modal is ported from Slint, and
the document behind it — title, album, artist, duration, quality, ISRC, label,
copyright and the grouped credits — comes from a new track_info_qt module.
Credit parsing and its role localization were already frontend-agnostic in
qbz-qobuz, so this only fetches and shapes; it does not re-implement them. A
local or Plex id is rejected before the request rather than sent to the Qobuz
catalog API, which is how a local album path ended up as a 404 in the owner's log.

PlayerBar was 708 lines and still carried an inline SongCard, transport cluster
and favourite toggle; those are their own files now, so the bar leaves this
change smaller than it arrived.

The volume controls in both bars are wired to the lock from the previous commit.

cc
2026-07-28 16:23:08 -06:00
vicrodh c8a5b03aee fix(qbz-qt): live output LEDs, closer stamp, and a volume lock on ALSA direct
The LEDs only changed when the user navigated. They were published from the
settings snapshot, so they refreshed when that document republished rather
than when playback started — which is exactly when the backend and mode are
decided, a moment after "now playing" in the log. They publish on the stream
edge now, without polling and without republishing settings.

The stamp itself was close but not 1:1, so it went back against
shell/AudioStamp.slint, primitives/QualityBadgeFull.slint and shell/SongCard.slint
line by line; the divergences in sizes, weights, spacing and the inactive LED
colour are corrected.

Volume is locked while the bit-perfect ALSA direct path is engaged. Software
volume there is meaningless and applying it would not be bit-perfect, so the
control now reads as inert rather than silently doing nothing. Slint has the
same gap and no lock to copy, so this is new: the condition comes from the
output state already published, and no audio behaviour changed — only the UI
reflects it.

cc
2026-07-28 16:22:48 -06:00
vicrodh 4690c842b0 fix(qbz-qt): resolve artwork by source instead of GET-ing everything
Local and Plex covers never reached the now-playing bar or the queue.
download_missing HTTP-GET every url it was handed, so two whole classes died
before leaving the process: a file:// thumbnail that is already on disk cannot
be turned into a request at all, and a Plex path like /library/metadata/49856/
thumb/… has no scheme. Both surfaced as "builder error" in the log.

There is one url taxonomy now — http(s), local file, bare absolute path, Plex
relative, unconfigured Plex, empty — and every consumer routes through it.
Plex paths are tokenized by the same builder the Local Library grid already
used, so the grid, the queue and the bar can no longer drift, and they share
one cache entry per cover because the transcode size is process-wide.

Also the reason a revisit showed grey squares filling in unevenly: the
"already on disk" path was never synchronous. Every visible card called the
cache service once per window pass on the GUI thread, and that call is a WRITE
— it stamps last_accessed on the shared SQLite connection behind the same
mutex the sixteen downloaders hold. Dozens of write transactions queued behind
the download pool, so covers landed in lumps. A resolved-path memo makes a
repeat lookup a read lock and one stat, and entries drop when the file is gone
so it cannot serve a dead path.

Two smaller ones found on the way: a non-2xx body was being stored AS the
cover, which cached a grey square permanently, and file urls were not
percent-encoded, so an album folder with # or ? in its name silently resolved
to nothing.

cc
2026-07-28 16:22:35 -06:00
vicrodh b580fdafad fix(qbz-qt): rasterize every Canvas on the GUI thread
The owner hit a segfault after clicking around. Cooperative and Threaded
rasterize on the scene-graph RENDER thread, and RoundedImage is instantiated in
25 files — every card, row and queue row — so list recycling creates and
destroys hundreds of these while async artwork callbacks schedule repaints on
them. The render thread can be mid-draw on a pixmap whose Canvas is already
gone. The ambient field makes it worse by repainting 30 times a second for the
whole window lifetime on that same thread.

Immediate keeps paint and destruction on one thread. renderTarget: Image
already meant CPU raster, so nothing looks different.

Not yet confirmed on hardware: the decisive check is running the previous
binary with QSG_RENDER_LOOP=basic, which gives Cooperative no separate thread
to race with. If that survives the repro and the default loop does not, this is
the cause.

cc
2026-07-28 16:22:19 -06:00
vicrodh 31b1484c01 feat(qbz-qt): Local Library complete — five tabs, routed album page, Plex
The view had roughly a folder tree and an album grid inside one 1,847-line
file. Slint's is 2,671 lines across five tabs, and the local album detail is a
routed page of its own. This closes that gap and splits the result: the view
becomes a composition root over qml/views/local/ modules, and
local_library_qt.rs becomes a narrow facade over local_* modules by concern.

Plex is wired for real. The port hardcoded the cache path to None, so the union
was local-only and Plex thumbnails were skipped. The settings store binds per
user at session activation, the album and track union includes Plex, thumbnails
resolve through the artwork path, rows carry their source badge, and the manual
re-sync is on the toolbar behind the availability gate — as in Slint.

Tabs: Albums with its quality/format/source filter popup, Tracks paginated,
Folders in both flat and two-pane tree modes, Artists as an A-Z master/detail.
Every list is windowed from the start, artwork id-keyed and evicted, because
16k tracks is exactly where the Slint build freezes.

The tree grows a real select mode with its bulk bar, the ad-hoc "ephemeral
folder" pane plays music that was never indexed, and the album page gets its
version picker — a local album with two physical copies now switches between
them without a database round-trip.

Two agents built the QML and the Rust halves in parallel and the QML called
thirty QbzLocal members that did not exist. QML resolves lazily, so each would
have been a control that renders and does nothing; a static cross-reference
caught them before a build ever ran, and they are implemented here.

Deliberately logged seams, not half-paths: add-to-playlist, add-to-mixtape and
the tag editor have no picker or modal in this port yet, and local favourites
are not wired, so those actions log rather than route a local id into the Qobuz
API. Plex playback downloads the part instead of range-streaming it, which is
Slint's own documented fallback.

cc
2026-07-28 15:25:00 -06:00
vicrodh ade156a72a refactor(qbz-qt): split QbzAlbum and QbzArtist out of the God-object
Two singletons rather than one merged object, per the owner: each maps to the
view it backs. Registering them and deleting the duplicates from bridge.rs is
one change, because wiring one without removing the other compiles fine and
leaves two independent states with the view frozen and nothing in the log.

view_param_id goes with them. It had no consumer in QML, verified, and none in
Rust once the two opens stopped writing it — so it is deleted rather than
carried into a new bridge for symmetry's sake.

bridge.rs is down to three domains: settings, search and playlist.

cc
2026-07-28 15:24:38 -06:00
vicrodh e8a821dd9e feat(qbz-qt): the quality stamp, ported from Slint per bar mode
The port had one hand-made badge taken from the archived Tauri tree. Slint has
three distinct components and each bar mounts a specific one, so the wrong tree
had cost the stamp its LEDs entirely — they exist only in Slint, derived from
the output backend and mode, and no amount of polishing the Svelte port would
have produced them.

QualityBadge becomes Slint's icon-only mark with its hover tooltip, for cards
and rows. QualityBadgeFull is the icon plus stacked tier and detail. AudioStamp
is the inline two-row form that carries the LEDs: backend blue when engaged,
mode green, purple for a cast session, amber with the renderer name for a peer
Qobuz Connect. New and Classic mount the SongCard stamp column, Large and Small
the inline stamp, matching the .slint mount sites.

The downgrade case follows Slint too: row one reports what is actually being
delivered with an amber arrow, and the catalog maximum moves into the tooltip
next to the reason it was limited.

PlayerBar's LEDs stop reading the settings document, which never knew whether a
backend was engaged, and read the now-playing output state instead — where
Slint reads them.

cc
2026-07-28 15:24:18 -06:00
vicrodh 942f408fe1 feat(qbz-qt): publish the now-playing quality and output state
The bar had two quality fields and no way to say what was actually being
delivered, so the stamp had nothing to render and the two output LEDs did not
exist at all.

quality_state.rs is a verbatim port of the Slint downgrade arithmetic — the 0.9
rate-family guard with its DSD exemption, the limit-cause classification and
the delivered-tier resolution — kept pure and unit-tested, with the per-track
catalog maximum seeded once on the track edge rather than per tick.
output_labels.rs ports the backend/mode label derivation from
qbz/src/settings.rs; it only reads AudioSettings and never touches the audio path.

Fourteen properties join QbzPlayer: the delivered-vs-catalog block, the two
LEDs with their active flags, and the cast/remote quartet. They refresh from
where they actually change — the labels follow the settings, the downgrade
follows the stream, and set_effective_stream dedupes so a steady stream costs
no Qt-thread hop.

Removes publish_with_cache, a near-copy of publish that was the reason the
cache overlay and the quality fields could disagree. There is one publisher now.

cc
2026-07-28 15:23:59 -06:00
vicrodh 4b34b70f70 perf(qbz-qt): interpolate the spectrum band instead of raising the FFT rate
The band moved at the producer's 30 Hz. Raising TARGET_FPS would have been the
obvious knob, but it lives in qbz-audio and is shared with Slint, and the
amplitudes are already EMA-smoothed in Rust to roughly 6 Hz — a faster producer
would have bought CPU and no smoothness.

The published frames are now replayed across the render frames that follow
them, which is what Slint's dock gets from its per-bar 90ms animate. A late
publish degrades to a hold instead of a jump, and the motion runs at the
display's rate rather than the data's.

Splits the strip out of the dock into SpectrumBand, so the dock file owns only
the gating and the layout arithmetic.

cc
2026-07-28 15:23:44 -06:00
vicrodh 5c631000b7 refactor(qbz-qt): split QbzLibrary out of the God-object
library_bridge.rs has sat in the tree unreferenced since the phase-23 work
stopped: no mod declaration, not in build.rs, zero call sites, and duplicating
members that were still live in bridge.rs. It could not have been wired as it
stood — its module was named qbz_library, which is ambiguous with the
qbz-library crate this binary depends on, so it never compiled. The module is
now qbz_library_bridge; the QML type name comes from the QObject, so QML is
unaffected.

Registering it and deleting the duplicates had to happen together. Wiring it
alone would have compiled cleanly and produced two independent Library states —
QML reading one object while Rust published to the other — with the view simply
frozen and nothing in the log.

Moved with it: four properties, five invokables, three signals, the seven
library-only ui() hops in main.rs, and thirty-four QML references. The
Connections blocks in LibraryView, Sidebar, QueuePanel, AlbumView and
PlaylistCollage retarget wholesale because every handler in them is a library
signal. ArtistView's does not: it mixes onLibraryArtworkReady with
onReleaseSectionReady, so it becomes two blocks — retargeting it whole would
have orphaned the releases pager, and QML resolves handlers lazily enough that
the discography would just have stopped loading with nothing in the log.

bridge.rs is down to 389 lines and five domains.

cc
2026-07-28 14:07:40 -06:00
vicrodh 0799d59e01 feat(qbz-qt): Local Library — folder tree, windowed album grid, detail
The sidebar has had a "Local Library" row since the shell was ported and
nothing behind it: the nav entry did not even navigate. This builds the view.

QbzLocal is its own domain bridge (the phase-23 pattern, one #[cxx_qt::bridge]
per domain) over local_library_qt, which consumes qbz-library for the scanning,
the folder-vs-tag album identity and the queries rather than reimplementing any
of it. The view carries the folder tree, the album grid, album detail, the
track tabs, sorting and search, plus the scanning and empty states.

Windowed from the start, not as a later pass: real local libraries reach 16k
tracks and that is exactly the surface where the Slint port freezes. Artwork
is id-keyed and evicted with the window.

Playback is source-aware — a local track plays from disk through the player's
play_data seam, because the Qobuz tier-walk needs a client and fails on every
local advance. TrackRow gains an opt-out for its drag source: a local row's id
is a local database row, and the sidebar's drop handler forwards whatever it
receives to `playlist add-tracks` as a Qobuz catalog id, so dragging one onto a
playlist would have silently added an unrelated track.

Not included, and tracked as follow-ups: Plex union and thumbnails, multi-select
and the bulk bars, ephemeral folders, the tag editor, A-Z jump strips, the
group-by dropdown, the tree's resize handle, per-tab scroll restore, and local
favourites on rows. Album detail renders inside the view rather than as its own
routed page.

cc
2026-07-28 13:49:15 -06:00
vicrodh cd1f4486b7 fix(qbz-qt): drive the FFT drain by signal instead of a second clock
The band animated like slow motion. The producer runs its own 30fps clock and
the drain slept a fixed 33ms after each publish, so two independent clocks beat
against each other and the drain intermittently missed a frame.

The sink now unparks the drain right after latching a frame of the stream the
visible mode consumes, and the drain parks again once it has published: exactly
one publish per produced frame, no aliasing. It is also cheaper than the poll
it replaces — zero idle wakeups instead of thirty a second — and an unpark that
arrives before the park is remembered, so the latch/park race cannot drop a
wake. Frames for the other two modes are still latched (not signalled) so
switching mode has data on the first frame.

cc
2026-07-28 13:48:48 -06:00
vicrodh 1916d8d794 feat(qbz-qt): wire every integration to its real backend
The Integrations pane rendered rows that did nothing: Last.fm, ListenBrainz,
MusicBrainz and Discord all had prefs but no runtime behind them, and Discord
never pushed a presence at all.

Each row now drives the qbz-integrations APIs for real — connect/disconnect
state, the account label once connected, and the per-service toggles — and the
runtime starts on shell entry, applying the persisted opt-ins and the scrobble
queue's flush watcher. Scrobbles arm on the de-duped track edge (firing from
the republish path would re-arm the timer on every poll tick) and the Discord
presence refreshes on both the track and the play/pause edges, then clears on
logout so a signed-out session stops advertising what was playing.

All of it stays strictly opt-in: nothing here does anything until the user
connects the service.

cc
2026-07-28 13:48:35 -06:00
vicrodh f44f03e414 feat(qbz-qt): now-playing bar buttons 1:1 with the Tauri original
Both bars are brought to the reference's control set, order, geometry and
hover treatment, and the bar's quality readout now mounts QualityBadge in its
compact mode instead of a hand-drawn stamp.

settings_qt publishes `normalization` in the settings document. It was already
settable through settingsBool but never read back, so both bars had to shadow
the toggle in local state and would drift from the real value after any change
made elsewhere.

cc
2026-07-28 13:48:16 -06:00
vicrodh 10461bb49c fix(qbz-qt): playlist artwork — own graphic for Qobuz, collage for the rest
Two bugs with one root: the card bound the playlist's `images*` lists, which
are MEMBER-ALBUM covers, not the playlist's own artwork. So a Qobuz editorial
playlist showed one of its albums' sleeves, and a user playlist — which has no
`images*` at all — showed nothing.

Qobuz playlists carry their graphic in `image_rectangle`, which the model did
not deserialize; the field is added (additive, `serde(default)`, and Playlist
is only ever built through serde) and the card prefers it, contain-fitted so
the artwork is not cropped. Playlists without one fall back to the member-cover
mosaic, reusing the collage the sidebar already draws rather than a second
implementation. Both the All-tab grid and its list mode are covered — the 44px
list thumb binds the same map and would otherwise have gone blank.

cc
2026-07-28 13:48:05 -06:00
vicrodh 9618bfe137 feat(qbz-qt): give LabelCard its artwork and a hover action
The card rendered neither an image nor any hover affordance. Root cause of the
missing artwork: the logo was gated on `item.imageUrl` while its source came
from the host-resolved artwork map, so any mount that resolves covers into a
side map without republishing the item drew nothing. Artwork is now driven by
the resolved source alone and `imageUrl` only chooses the placeholder arm.

The container is round, per the owner, matching ArtistCard rather than the
square art the Slint reference uses; the contain-fit box is inset so a wide
wordmark is not clipped by the circle, keeping the reference's "label logos
are never cropped" rule. The hover row carries a single disc: labels are
opened, not played, and the Slint reference offers exactly one label action.

cc
2026-07-28 13:47:49 -06:00
vicrodh 6e81b1ac78 fix(qbz-qt): forward the overlay button's click, and use it in AlbumCard
CardOverlayButton declared `signal clicked()` with no argument, but three
cards write `onClicked: function (mouse) { menu.openAtCursor(btn, mouse.x,
mouse.y) }` against it — so `mouse` was undefined and the context menus on
TrackCard, PlaylistCard and ArtistCard threw instead of opening. The signal
now carries the event; handlers that ignore it are unaffected.

AlbumCard drew its three hover discs as raw Rectangle + QbzIcon + MouseArea
blocks instead of the shared control every other card uses, which is how its
geometry drifted from theirs in the first place. It now mounts
CardOverlayButton with identical behaviour. One deliberate change rides along:
the favorited heart goes from the red `favorite` tint to the accent tint,
which is what discover/AlbumCard.slint actually specifies — the red was the
drift, not the parity.

cc
2026-07-28 13:47:39 -06:00
vicrodh 2875063626 fix(qbz-qt): one centred hover-action row for every card family
All four card families hand-rolled the same overlay row and all four were
misaligned, each differently. They centred the group with a manual spacer
Item computed as (cardWidth - 44 - 2*36 - 2*12) / 2 — the leftover once you
account for the primary disc, the two ghost discs and the gaps between them.

AlbumCard set spacing: 12 AND kept the spacer, so the spacer itself took a
12px gap and the group sat 12px right of centre. ArtistCard, PlaylistCard and
TrackCard never set spacing at all, so the gaps the formula had already
subtracted never materialised: the discs touched each other and the group sat
12px left of centre. ArtistCard also measured against 190px while its art is
200 wide.

The arithmetic was never needed. CardOverlayRow centres a Row on its parent,
which computes its own width from whatever children it has — so a card that
hides a button (artists without the follow action) stays centred for free,
where the old formula needed a conditional term to keep up.

cc
2026-07-28 13:10:41 -06:00
vicrodh 71c2356f7b feat(qbz-qt): port the quality badge 1:1 from the Tauri original
The port had reinvented this badge: an icon-only replica with no MP3 tier, no
full or compact form, and none of the playback states. QualityBadge.svelte and
QualityBadgeStatic.svelte in the archived Tauri tree are the reference, and
every number here comes from their CSS.

Four modes, the union of both originals: full (the 136x36 pill with tier row
and bit-rate), compact (the narrow-bar "24/96" form from issue #303), bare
(full's content without the pill chrome, for toolbars) and iconOnly (the
Hi-Res brand mark alone, or a framed 30px square for CD/MP3 so both line up in
a grid). Tier resolution follows the original's order, MP3 first so it can
never come out as CD.

The four playback states come with it: system resampling, ALSA plughw
fallback, deliberate quality adjustment and confirmed bit-perfect, each with
its own border hue and indicator glyph, plus the rich hover tooltip carrying
source, output, the reason and the fix. Rows that only advertise stored
quality leave the flags false, which is exactly what QualityBadgeStatic was.

QualityMini becomes an alias over iconOnly mode rather than a second drawing
of the same thing, so the six existing call sites keep working while there is
one badge again.

Brings the assets the port was missing: cd.svg, mp3.svg and the LINE Seed JP
face the badge is set in. Their tint is baked to #b3b3b3 because Tauri applied
it with a CSS filter and QML has none on the software path. Adds theme.alphaTier(pct)
so the badge names the alpha ramp the design specifies instead of a raw index.

build.rs now collects the whole asset tree recursively — the hand-written root
list meant a new asset compiled clean and failed at runtime with "Cannot open".

18/18 tests; offscreen smoke clean.

cc
2026-07-28 12:49:50 -06:00
vicrodh aabde3be63 fix(qbz-qt): unresolvable QML imports, focus-based freezing, and dock render cost
Three views instantiated components the phase-23 directory move had left
unimportable: PlaylistView and SearchView use TrackRow (now in rows/) and
SectionRail uses AlbumCard (now in cards/). QML resolves lazily, so these
survived cargo check, the test suite and the offscreen smoke — they only
failed when the user opened a playlist. Adds a repo-wide audit of every
instantiated component against the importing file's visible directories; the
three above were all of them.

The ambient field and the new dock band froze on lost focus. That is wrong on
any tiling or mosaic desktop, where windows stay fully visible while
unfocused — the field would stop while the user is looking at it. Both now
gate on the window not being minimized or hidden, which is the real "nobody
can see this". Freezing on not-visible stays.

Dock band render cost, in descending order of impact: the drain publishes
only the stream the active mode consumes (Bars mode was marshalling a
512-float waveform QList 30 times a second for nothing); each mode marshals
its QList into JS once per tick instead of once per delegate; one shared
Gradient replaces 28 per-delegate instances; and the per-bar Behaviors are
gone since qbz-audio already smooths and clamps the amplitudes upstream.

18/18 tests; offscreen smoke clean; component-resolution audit clean.

cc
2026-07-28 12:26:40 -06:00
vicrodh 70bd337d16 feat(qbz-qt): Large NPB dock — sidebar space reservation + FFT spectrum band
The Large dock was a bare cover overlay: the sidebar knew nothing about it,
so the playlist rows kept running to the sidebar's bottom edge and sat
underneath the album art. Sidebar.slint reserves the in-sidebar portion of
the dock; the port had dropped that.

Reserve it the same way, from a single source of truth. QbzShell gains
largeDockHeight (shell_bridge.rs owns the arithmetic: 9 top + band + gap +
208 art + 4 bottom), the sidebar subtracts largeDockHeight - npbLargeHeight
from the playlist list, and AppShell pins the overlay at height -
largeDockHeight. The 112px bar height becomes a theme token so the shell and
the reservation cannot drift apart.

The band the cover was missing needs the FFT tap, so wire it: the runtime is
built with_visualizer, viz_qt.rs runs the frontend-agnostic producer from
qbz-audio and drains it at 30fps into a new QbzViz domain bridge as typed
QList<f32>. Bars (28 mirrored columns over the 14 active bins), Waveform (48
downsampled L columns) and Energy (5 bands) render as Slint does, the cover's
eye button toggles the band, clicking it cycles the mode, and both persist to
ui_prefs.

Cost is zero when unused: the tap starts disabled, the eye toggle gates
capture, the drain thread parks while off, and the transport is mirrored so a
paused player parks the producer instead of re-analyzing a stale buffer. The
QML binds bar HEIGHTS against static-model Repeaters — the lists are replaced
per frame, so a model binding would rebuild every delegate 30 times a second.

Adds the eye/eye-off icon pair to the seven baked tint variants.

18/18 tests; offscreen smoke clean (0 QML errors, home published).

cc
2026-07-28 12:05:37 -06:00
vicrodh 9a4fec6b70 refactor(qbz-qt): phase 23 part 1 — QML module layout + first five domain bridges
Two fronts of the modularization, both gated green before this commit.

QML leaves the flat qml/ directory for seven modules: cards, controls,
rows, settings, shell, theme, views. Only Main.qml and LoginScreen.qml
stay at the root. build.rs matches the tree exactly (55 files), relative
imports and asset paths were re-anchored for the new depth.

The QbzBridge God-object starts breaking up: QbzSession, QbzShell,
QbzPlayer, QbzQueue and QbzHome are now their own #[cxx_qt::bridge]
singletons, each with its own OnceLock<CxxQtThread> and ui() hop, booted
in order from Main.qml. Bridge files stay directly under src/ because
cxx-qt-build only accepts rust_files from one directory (QTBUG-93443).

bridge.rs keeps 5.5 domains (library, album/artist, settings,
integrations, search, playlist) and dies once they are split out.

cc
2026-07-28 11:08:20 -06:00
vicrodh 580b72084f refactor(qbz-qt): phase 22 — full QML duplication sweep beyond cards
TrackRow.qml unified from 4 copies (context arms, net -716 LOC with
strictly more behavior); QbzSectionHeader, QbzTabBar, QbzNavButton,
QbzIconButton, QbzCircleAction, QbzEmptyState, QbzOfflinePlaceholder
consolidated; sort dropdowns onto QbzContextMenu; QualityMini routed
everywhere. Deliberately separate per the Slint architecture: QueueRow,
FeedListRow, underline tab strips, ReleaseSection header. Restored the
offline 'Open Settings' action the POC had stubbed.

km
2026-07-28 08:42:54 -06:00
vicrodh a19e2dac60 refactor(qbz-qt): phase 21 — one shared card component per family
Consolidated to the Slint architecture (one component per family with
context arms, mounted by every surface): ArtistCard, PlaylistCard,
TrackCard, LabelCard, SlimCard promoted to shared files; 7 duplicates
deleted across HomeView/LibraryView/SearchView (net -1038/+93).
Migration made several surfaces MORE Slint-true (ArtistGridCard
200x246 on all rails/heroes, real overlays+pin badges on Home/Pinned/
Search cards). SearchTrackHero kept distinct — it IS a distinct Slint
component. CardMenu/CardOverlayButton/QualityMini promoted shared.

km
2026-07-28 07:42:08 -06:00
vicrodh 4dc7034e03 chore(qbz-qt): drop dead app_background_index helper
km
2026-07-28 06:51:49 -06:00
vicrodh 245eaa4d45 feat(qbz-qt): phase 20 — live i18n across the 8 bundled locales
tr() gained a revision parameter (trRev qproperty bumps on switch) so
all ~465 QML call sites re-evaluate — the Qt equivalent of Slint's
reseed after select_bundled_translation(). Rust-built documents
(home/library/settings option labels, plural tf() strings, active
album/artist docs via record-free republish paths) re-publish on
switch. Settings > Appearance language selector applies live (Auto +
8 locales, native names, shared ui_prefs.language key); boot honors
the persisted pref (was unconditional resolve_auto). Verified es/ru/ja/
de mid-run with zero tofu (fontconfig covers CJK/Cyrillic); restart
persistence verified; owner's en pref restored.

km
2026-07-28 06:44:10 -06:00
vicrodh ea644f5e05 feat(qbz-qt): phase 19 — full Settings subnav, Appearance + Integrations, live theming
Theme system as data: qbz_theme registry (36 themes + auto/custom)
resolved from the shared ui_prefs slug into a themeJson document;
QbzTheme rebound to it so the whole app repaints live (OLED->Light
probe-verified). Full subnav (8 sections + Share logs footer);
Appearance 1:1 (theme dropdown with dark/light filter, ambient/titlebar
rows in their real home, typography, library visuals, window chrome,
tray via tray_settings.db, renderer); Integrations 1:1 (MusicBrainz,
Last.fm 2-step browser connect + authed state, ListenBrainz token
validate, Discord toggle -> live DiscordRpc.set_enabled). Offline/Local
Library/Blacklist/Developer render honest placeholders.

km
2026-07-28 06:13:04 -06:00
vicrodh c59c883ac1 feat(qbz-qt): phase 18 — all four NPB modes + ViewMode flyout
PlayerBar.qml covers New/Classic/Large (full-width seekbar with times,
responsive 3-column layout, glass Classic SongCard variant, right
cluster), NowPlayingBar seam Loader picks per mode; Small unchanged.
ViewMode flyout wired (New/Classic/Small/Large live, Miniplayer/
Immersive/Kiosk inert replicas), mode persisted to the shared
ui_prefs npb_mode key, Large forces the sidebar open and docks the
208px cover (SidebarNowPlayingDock equivalent); AppShell bar height
mode-aware (42/112). Empty and playing states verified per mode;
user's persisted small pref restored after probes.

km
2026-07-28 05:02:11 -06:00
vicrodh 8c1ff34205 feat(qbz-qt): phase 17 — PlaylistView 1:1 + drag & drop as Slint
Full playlist surface: header with owner/foreign gating, play/shuffle,
favorite/follow optimistic, copy-to-library, pin, rename, delete,
per-row remove for owners, search filter, full sort set, custom-order
drag reorder persisted per user. Sidebar and every playlist card
navigate; card overlay/menu actions un-stubbed. Drag & drop per the
Slint gesture: row-body press-drag (>6px) from album/playlist/search/
library rows, window-top ghost pill, sidebar playlist rows drop-hot
highlight, drop -> add_tracks_to_playlist + reload reconcile; in-list
release reorders. Round-trip proven end-to-end on a throwaway playlist
(create -> drag -> drop -> refetch shows track -> delete).

km
2026-07-28 03:39:35 -06:00
vicrodh 22e65e939b feat(qbz-qt): phase 16 — card overlays everywhere + window-centered modals
Overlay opacity bindings referenced a nonexistent parent chain — all
card overlays evaluated invisible. Fixed per family: LibTrackCard
(scrim + fav/play/more row, full Slint menu), LibArtistCard rewrite
(circle-clipped scrim, play + more; play_artist with Popular-tracks
first and studio-discography fallback), LibPlaylistCard overlay fixed,
LibLabelCard verified overlay-less like the Slint. Modals: shared
AppShell-level layer (full-window scrim, window-centered panel)
replacing the view-local content-frame-centered ones.

km
2026-07-28 02:26:24 -06:00
vicrodh 9eb46d5e25 feat(qbz-qt): phase 15 — search 1:1 (cortinilla + full page with filters)
Intelligent search engine reused from qbz_app (SWR cache, interaction
recording, rank_within, top_for_query; app-menu kill switch on the
shared pref). Header field fully live: 220ms debounce, keyboard
semantics (arrows/Enter/Esc), clear, idle-close. Cortinilla as a plain
overlay (pointer stays on the field — Slint's exact reason): top
result, spec-ordered sections with View more, skeleton, row activation
dispatches (album/artist views, track play). Search page: 5 searchType
filter radios (Main Artist/Performer/Composer/Label/Release Name),
5 scope tabs, Most-popular hero, carousels, per-type grids/lists with
paged Load more. Perf: cortinilla 245ms cached / 1.1s cold. Fixed an
artwork attach bug the screenshots exposed (url/path slot mixup).
18/18 tests.

km
2026-07-28 00:49:53 -06:00
vicrodh 9f86a40f52 feat(qbz-qt): phase 14 — app-wide ambient background (modo Cider)
Port of the spec's palette derivation (16x16 downscale, coverage-
weighted 24-bin hue histogram, genuine secondary cluster or deeper
same-hue, +180 accent, B/W defaults) driven by the now-playing artwork
on track change. QPainter AmbientField with the WGSL scene's exact
four blob orbits, dim scrim, 30fps cap paused when inactive; chrome
surfaces translucent per the AppShell alpha tokens (content frame
surface-main @ 0.22 frosted). Same shared app_background pref (owner
has it 'ambient'), app-menu toggle applying live, QBZ_BG_* env knobs.
4 new unit tests (14/14 total).

km
2026-07-27 23:37:13 -06:00
vicrodh cc71337293 feat(qbz-qt): phase 13 — correct card families in Library + Discover tabs
Library audit: the phase-7 conversion never actually mounted the shared
AlbumCard — ALL kinds used the invented feed card. Now per-kind 1:1:
LibTrackCard (discover/TrackCard replica with wired next/later/queue
enqueue), shared AlbumCard for albums, LibArtistCard (ArtistGridCard
circle), LibPlaylistCard (collage, follow-aware), LibLabelCard
(gradient + contain-fit logo), list rows rewritten to the real
FavoritesView inline row, Tracks tab on a TrackRow replica. Discover:
Editor's Picks and For You tabs rendered from the same discover-index
fetch, prefs-ordered per DiscoverPrefsStore (incl. mostStreamed#album
variant, Qobuz Mixes gradient tiles, Pinned in the user's own position);
Recommendations tab gated on the user's pref with an explicit engine
placeholder. No refetch on tab switches.

km
2026-07-27 22:35:21 -06:00
vicrodh 71d41b927c fix(qbz-qt): phase 12 — card rounding, menu anchoring, opaque window, titlebar toggle
Rounding: QML clip is rectangular (radius+clip does NOT round-clip) —
new shared RoundedImage (software Canvas raster clip, GL-independent)
applied at 16 artwork sites incl. true circular artist crops. Menus:
new QbzContextMenu (cursor-anchored / below-trigger, window-clamped)
converted at 7 sites, replicating TrackRow.slint positioning. Window:
main window is OPAQUE in Slint (transparency is miniplayer-only;
rounded corners are KWin's) — dropped the translucent frameless
misread, squared app corners, opaque root. Titlebar: wired the EXISTING
shared ui_prefs use_system_title_bar key (app-menu checkable item,
restart semantics like Slint).

km
2026-07-27 21:43:53 -06:00
vicrodh da36d86ccc feat(qbz-qt): phase 11 — final polish vs owner's Slint reference shots
Fixed an ALSA enumeration zip bug that silently dropped the last device
(the stored DAC now shows with its BP badge). Home rails now follow the
persisted DiscoverPrefsStore (order + disabled sections) like the Slint
shell; Pinned rail built from pinned_items.db (albums/artists/playlists,
ArtistGridCard circle); View-all header links on endpoint rails; pin
badge hover-gated like the Slint overlay buttons. Restore path now
binds the pinned + local-favorites stores (was login-only). Settings
panels verified row-for-row against the 3440px references.

km
2026-07-27 20:01:02 -06:00
vicrodh d57cec974f feat(qbz-qt): phase 10 — Settings > Audio and Playback 1:1
settings_qt.rs controller over the shared stores (AudioSettingsState,
PlaybackPreferencesState, ui_prefs.json, qconnect_settings.db) with the
upstream cascades (dac-passthrough->skip-sink-switch, streaming-only->
gapless, backend switch resets routing) and live apply through public
player APIs (reload_settings/reinit_device/clear_audio_cache). ALSA
device regrouping (Defaults/Bit-perfect/Plugin/Other + BP badges),
searchable QbzSelect, QbzToggle/QbzSlider/SettingRow replicas, exact
row order and gating. Persistence probe verified (gapless +
streaming-quality round-trip, original values restored).

km
2026-07-27 19:12:06 -06:00
vicrodh b10ff98620 feat(qbz-qt): phase 9 — Queue homologation + Lyrics side panel
Queue panel 1:1: History tab (click replays per queue.rs semantics),
#442 'Next in queue'/'Next up' section markers from manual_next_count,
40-row pagination + live search + page clamp, row actions (play,
remove, remove-all-after, heart), press-drag reorder via move_track,
clear queue, plural-aware footer count, exact empty states. New Lyrics
panel: qbz-lyrics service + per-user cache wired, Qobuz-first chain
with LRCLIB fallback, synced line view with line-level highlight +
centered auto-follow + click-to-seek, exact status bodies. Both panels
stack Feishin-style in the right column; NPB lyrics button wired.

km
2026-07-27 18:22:22 -06:00
vicrodh 86de977f3c feat(qbz-qt): phase 8 — AlbumView and ArtistView 1:1
AlbumPageView replica: 224px cover, credits, meta, description modal,
action row, quality badge + track search, exact tracklist (playing-row
pill, disc/work headers, live hearts, row menus), label/awards
sidebar, same-artist + suggestions carousels. ArtistPageView replica:
circular portrait, bio modal, From catalog/In library toggle (both
arms live), JUMP TO tabs with jump-scroll, Popular Tracks with Load
more, server-bucketed release sections paged per section, Appears On,
Network sidebar (labels, similar artists). Card clicks navigate
everywhere via the nav stack; new playback paths (play from track,
shuffled album, track lists). Owner data live: album 654ms, artist
670ms loads.

km
2026-07-27 17:49:08 -06:00
vicrodh 4d196223cd feat(qbz-qt): phase 7 — shared AlbumCard, rounded frameless window, sidebar playlist tree
One AlbumCard.qml replicating the Slint shared card (pin badge with
PinnedItemsService wired, heart, hover play overlay, exact context
menu with Play next/Add to queue wired) used by BOTH Home rails and
the Library All grid. Frameless translucent window with 12px rounded
corners, replicated WindowControls, header drag, 8 resize grips.
Sidebar playlist tree: 56 playlists / 12 folders live, expand state,
5-option sort with direction caret, recursive search filter, collage
covers, create-playlist wired. Fixed a std-Mutex self-deadlock in the
sidebar loader (two lock temporaries in one log statement).

km
2026-07-27 16:45:44 -06:00
vicrodh c06297c915 feat(qbz-qt): phase 5.5 — visual polish pass audited against the Slint sources
Screen-by-screen audit with before/after screenshots: rounded
surface-main content panel with 8px bezel (the Slack-style frame),
real splash card, QbzPrimaryButton/QbzCheckbox/QbzTextLink replicas,
accent arc spinner replacing BusyIndicator, custom thin auto-hide
scrollbars (Home + Library grid/list), carousel edge fades, toolbar
pill offset, QbzSlider/SeekBar metrics, alpha-format re-audit of all
8-digit hex literals. Pure look — zero functionality changes.

km
2026-07-27 15:19:24 -06:00
vicrodh 1b81b289d1 feat(qbz-qt): phase 5 — Library view 1:1 with All grid/list and perf marks
Six live tabs (All/Tracks/Albums/Artists/Playlists/Labels) with real
count badges, paged favorites/playlists/purchases loaders, library_all
merge semantics, heart routing (Qobuz API vs local store —
LocalFavoritesService now initialized at session entry), sort popup
with direction carets, grid/list toggle with natively windowed
GridView/ListView delegates, id-keyed artwork via signal with hand-
rolled eviction. Perf marks on the owner's account: 344 items, fetch
wall 2.7-3.4s (97% network, sequential), map+merge ~11ms, JSON
serialize 10ms, QML parse 7ms (~20us/item => ~140ms at the 2200-item
reference scale), RSS +29MB after publish.

km
2026-07-27 14:57:29 -06:00
vicrodh b405755c1a merge: pull smoke-2026-07-25 backend for the Library All POC phase
Brings the windowed Library All backend (qbz-library) and the latest
Slint reference implementation into the POC base. POC branch only;
pre-release untouched.

km
2026-07-27 14:05:08 -06:00
vicrodh 80dcd85a91 feat(qbz-qt): phase 4 — live playback, queue, and transport via QbzCore
Album-card click plays the album: get_album -> QueueTrack vec ->
set_queue -> play_track_resolved (tier walk stays in the core). 1 Hz
poll publishes position/meta/quality into the NowPlaying model; end-of-
track edge auto-advances with meta refresh (traced live: 24-bit/48kHz
CMAF stream, position advancing, track 2/3 transitions). Transport
invokables rewired to real core calls; queue panel shows NOW PLAYING +
numbered UP NEXT from get_queue_state. Public QbzCore API proved
sufficient — protected audio/player crates untouched. Named gaps:
no mute API, no playback event stream, auto-advance/prefetch semantics
remain frontend glue (~150 lines every UI must port).

km
2026-07-27 14:04:58 -06:00
vicrodh 95df643a25 feat(qbz-qt): phase 3 — Discover > Home with live Qobuz data and artwork
Pure mapping port of home.rs/foryou.rs (quality tiers, award ribbons,
slim grids) into Slint-free rows; discover index + favorites +
release-watch + top artists fetched concurrently (12 sections, ~170
cards live-verified). Artwork via the shared qbz-cache ImageCacheService
(semaphore-16 downloads, file:// rows, native QML decode). HomeView.qml:
tab pill, five rail kinds, shimmer skeleton, error+Retry, offline
placeholder — exact msgids throughout. Sections travel as one JSON
document (cxx-qt-lib cannot nest QVariant containers — documented).

km
2026-07-27 13:36:01 -06:00
vicrodh 744bce3819 feat(qbz-qt): phase 2 — app shell with 1:1 Slint visual parity
HeaderBar (42px, sidebar cycle, back/forward, centered search replica,
offline badge + flyout, app menu), three-state Sidebar (open 240 / mini
64 / closed 0, 160ms animation, nav rows with exact msgids), NowPlaying
bar SMALL (3px three-color seekbar, art+title+time left, transport
center, quality/volume/queue-toggle right, exact empty state), Queue
panel (300px animated column, Queue/History tabs, exact empty states).
Nav history stack + NowPlayingModel with empty-state defaults behind
new bridge properties/invokables (transport log-and-noop until
playback lands). Icons as pre-baked tinted SVG variants (ColorOverlay
is broken on the software path); Inter fonts bundled; full dark theme
tokens ported from the Slint foundation files.

km
2026-07-27 12:56:38 -06:00
vicrodh df7eb3588e feat(qbz-qt): phase 1 — login screen with full offline guardrails
QbzBridge QML singleton (12 properties, 8 invokables) driven by tokio
tasks via CxxQtThread. Port of auth.rs (browser OAuth loopback listener
with qbzd-style nonce, 180s timeout, cancel via JoinHandle abort,
scoped offline-gate lift for cold bundle init, D1 token-keep on network
failure / clear on explicit rejection, D4 subscription grace marks).
OfflineModeEngine + ConnectivityActor started at boot with live watch
forwarding (connectivity/captive portal/recovery banner/has-previous-
session). Boot: splash -> silent token restore -> shell | login.
Start-offline enters the guest profile. LoginScreen.qml is a faithful
port with exact msgids routed through qbz-i18n. 5 unit tests green.

km
2026-07-27 12:09:31 -06:00
vicrodh 469f83781d feat(qbz-qt): phase 0 — crate skeleton + cxx-qt toolchain gate
New qbz-qt bin crate (cxx-qt 0.7 + QML) as the only default workspace
member in this worktree, so plain cargo build produces just the Qt
binary. Gate verified against system Qt 6.11.1 (codegen, moc,
qmltyperegistrar, qrc load, offscreen run all green).

km
2026-07-27 11:36:18 -06:00
vicrodh ae519279f6 fix(cast): error-tolerant DLNA discovery loop + per-device logging (#667)
The window loop broke on the FIRST device whose description fetch/parse
failed, silently dropping every other responder in that window — and
every subsequent window, forever, with zero log lines. One malformed
UPnP device on the LAN (router, TV, printer) could starve the whole
DLNA list. Now each error is logged and skipped, and every discovered
renderer is logged at info level (name / ip / capability flags), so the
next field report is self-diagnosing.

km
2026-07-27 09:57:57 -06:00
vicrodh a69491a49e feat(player): re-wire the low-memory profile into qbz/qbzd (#660)
The system_capabilities profiler (Normal vs LowMemory, built for #331)
lost all its consumers in the Tauri removal; 2.0.2 shipped it as dead
code and qbzd got OOM-killed (exit 137) on 1 GB Pi hosts at track
transitions. Phase A wiring:

- Frontends read memory_profile() at startup and push it down via the
  new player memory_tuning (initial-buffer cap, L1 budget, low-memory
  gate) before the Player is created.
- L1 audio cache sized from the profile (400 MB Normal / 50 MB
  LowMemory; was hardcoded 400 MB).
- Oversized tracks (>L1/4 = 12.5 MB on LowMemory) skip the promotion
  clone and the parallel CMAF RAM accumulator entirely; the finished
  track is streamed to the L2 disk cache in 1 MB chunks straight from
  the playback buffer — no second in-RAM copy. Gapless degrades to a
  small gap for those tracks (a gap beats an OOM kill).
- Stream buffer preallocates total_size up front (try_reserve_exact,
  1 GiB cap, dynamic fallback) — no more ~8 doubling reallocs + memcpy
  per track (weak-CPU relief).
- New memory-pressure watchdog (15 s): critical pressure evicts the
  L1 (L2 disk kept) and halts prefetch; low pressure pauses HiRes
  prefetch (downgrades to Lossless). Spawned by both frontends; qbzd
  aborts+joins it for the #521 shutdown ordering.
- Prefetch honors the profile (depth cap, HiRes->Lossless downgrade).

Normal-profile behavior is byte-identical (every behavioral change is
gated on the LowMemory class; profile values equal the old hardcodes).

Phase B (follow-ups): tempfile spill of the live stream buffer for
giant tracks, POLLERR flood rate-limit, desktop kick_prefetch halt
check, L2-read on the play_track cache-hit path.

km
2026-07-26 23:56:39 -06:00
vicrodh 96acc4386a test(qbz-app): install the rustls CryptoProvider in shell tests
Follow-up to #663: with reqwest no longer pulling a rustls provider,
QbzCore::new (MusicBrainzClient) panicked "No provider set" in 6
shell tests. The test ctor now calls ensure_crypto_provider(), exactly
like the real binaries do at startup.

km
2026-07-26 23:56:38 -06:00
vicrodh ad6c98ff7d feat(queue): Play later everywhere — every context menu and bulk bar (#442)
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
2026-07-26 21:08:12 -06:00
vicrodh 09380d4634 Merge branch 'pre-release' into chore/internal/smoke-2026-07-25 2026-07-26 16:45:03 -06:00
Victor RH fe2be63f0f Merge pull request #663 from fengalin/fix/external/qbzd-add-tls-provider
fix(qbzd): add tls provider
2026-07-26 16:42:35 -06:00
vicrodh 6ca5813fb1 feat(mpris): expose xesam:url with the public Qobuz track link (#658)
playerctl scripts/widgets can now identify the exact song:
https://open.qobuz.com/track/<id> on every Qobuz catalog row (offline
qobuz_download keeps the real id, so it qualifies too). Local/Plex rows
omit the field — QueueTrack carries no file path to map to file:// and
a synthetic local id would mint a bogus Qobuz link. Wired in both the
app (refresh_now_playing_meta) and qbzd (track_meta).

km
2026-07-26 16:38:38 -06:00
vicrodh def93528bc fix(audio): open ALSA mixer on the card ctl id, not the PCM alias (#659)
set_hardware_volume attached the mixer to the stream's raw PCM id
(iec958:/hdmi:/front:/DEV-qualified hw:) — snd_mixer_attach needs the
per-card ctl device, so hardware volume silently no-oped on plugin-alias
devices (HiFiBerry Digi as iec958:CARD=sndrpihifiberry,DEV=0). Derive
hw:CARD=<name> / hw:<n> first. With this, qbzd's 'Hardware volume'
toggle reaches the DAC's mixer control (e.g. 'Digital') as intended;
with the toggle off, ALSA Direct stays fixed-volume by design
(bit-perfect, no software gain).

km
2026-07-26 16:32:30 -06:00
vicrodh 4d2ddec66e fix(ui): heart-crack icon for Remove from Library/favorites actions
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
2026-07-26 16:26:58 -06:00
vicrodh 64fe89cf52 feat(qconnect): play-later positional insert on peer + queue sync on add
Controller mode: play-later now routes to the peer's manual-block tail
(insert_after steered via a new ControllerManualBlock tracker that
confirms echo positions) instead of degrading to play-next; play-next
inserts also feed the tracker. Single + batch variants.

Local renderer mode: every queue ADD (enqueue/play-next/play-later,
album/playlist/artist/favorites/local batches) now pushes the queue to
the session immediately via sync_local_queue_if_changed, so controllers
see new items at their position instead of waiting for the next track
transition. Adds containing local/plex rows skip the push (all-or-
nothing admission would refuse the whole queue anyway).

km
2026-07-26 15:45:00 -06:00
vicrodh 449f2470f6 fix(ui): mixtape row menu play-next/add-to-queue icons (list-start/list-end)
km
2026-07-26 15:45:00 -06:00
vicrodh 3f1a7d074a fix(build): move the bulk-unfav toast closure (E0373)
km
2026-07-26 15:00:50 -06:00
vicrodh 5dc4d553dc fix(favorites): bulk un-favorite without the UI freeze, with feedback
- 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
2026-07-26 14:59:29 -06:00
vicrodh 8c1750a78f fix(ux+audio): #661 pause-during-load, anti-alias ring, menu icons, bulk unfav
- #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
2026-07-26 13:22:53 -06:00
vicrodh 51b5f3bec7 fix(build): compile errors from the round
- with_db closures return the raw Result (not Option) in the all-local
  feed loader.
- build_tree takes sort_asc (its own scope, not rebuild's).
- reconcile_sidebar_after_import captures first/single before the
  closures move ids.

km
2026-07-25 20:45:03 -06:00
vicrodh 83b0ada9b9 Merge branch 'bugfix/internal/local-library-tag-fallback' into chore/internal/smoke-2026-07-25
# Conflicts:
#	crates/qbz-library/src/database.rs
2026-07-25 12:07:56 -06:00
vicrodh cb1ae7db6f i18n: strings for the round (8 locales)
Library-All scope selector + settings rows, play-indicator/swipe
toggles, sidebar/manager sort carets, Play later + queue sections +
menu hints.

km
2026-07-25 12:05:56 -06:00
vicrodh efa486ba4f feat(packaging+log): standalone qbzd packages, IP masking in logs
- nfpm-qbzd.yaml (deb/rpm, libasound-only dep) + release workflows build
  the standalone qbzd packages per arch; dual dispatch to the future
  qbz-rpm repo (continue-on-error until token scope lands).
- Gentoo overlay templates: qbzd (source build) + qbzd-bin (tarball
  repack) with systemd user unit + completions.
- Log redaction layer 3: IPv4 -> *.*.*.<last>, IPv6 -> *:*:<last>, with
  version/timestamp false-positive guards (qbz-log 10/10 tests).

km
2026-07-25 12:05:56 -06:00
vicrodh a8919cbadf feat(app): the round's wiring — gamescope, watchdog, geometry, hearts
- 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
2026-07-25 12:05:56 -06:00
vicrodh 953ba39e77 feat(playlists+queue): sort directions (#657), import refresh, Play later (#442)
- 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
2026-07-25 12:05:27 -06:00
vicrodh d99419f22d fix(renderer+ux): frame-liveness watchdog (#558), dup-add button (#655)
- #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
2026-07-25 12:05:27 -06:00
vicrodh f1eeaf9126 feat(steam-deck): game mode support (plan PR1-PR5)
- 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
2026-07-25 12:04:58 -06:00
vicrodh 55caada4c4 feat(ui): play-indicator toggle, swipe inversion, accent-ring row mark
- 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
2026-07-25 12:04:57 -06:00
vicrodh 54c2779eb5 feat(library-all): windowed feed, show-local default + scope, local hearts
- 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
2026-07-25 12:04:22 -06:00
vicrodh 6fff4aae31 docs(readme): note QBZ has no built-in EQ/DSP by design (use EasyEffects/JACK)
cc
2026-07-24 01:14:19 -06:00
vicrodh 70aca326fd docs(readme): add the "what QBZ is and isn't" statement to Why QBZ
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.
2026-07-23 23:42:48 -06:00
vicrodh e7aaeb33f9 chore: bump flake to 2.0.2
cc
2026-07-23 16:05:37 -06:00
vicrodh 93ae499f38 ci(release): install jack headers for the build-qbzd jobs
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
2026-07-23 14:00:23 -06:00
vicrodh 0328a38b2f docs(readme): count Dutch among the bundled languages
cc
2026-07-23 13:09:47 -06:00
vicrodh d8f43cc4a3 docs(readme): qbzd and kiosk sections, drop diagram and star history
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
2026-07-23 13:07:06 -06:00