Buttons that rest on surfaceElevated used surfaceHover for their hover
fill: a 6% white wash meant for rows that rest transparent. Over an
elevated button it lands DARKER than the rest fill — on a modal's dark
ground the Cancel/Save/footer buttons went near-black under the pointer.
QbzTheme gains surfaceElevatedHover (a lift of surfaceElevated, derived
so every palette has it), its ambient half-alpha twin, and the
elevatedFill / elevatedHoverFill pair an ambient-aware control paints.
Every site with the old pairing (65 across modals, toolbars, selects,
circle actions, chips and cards) now hovers to the lift.
cc
Integrate the Kiosk profile hardening round: 12 dedicated Kiosk views plus
22 shared opt-in hosts, shared navigation history with Back/Forward, Albums
as the default library tab, native paged Local Library (artists/tracks),
lightweight MyQBZ grid and detail, Ephemeral lifecycle, bounded lists and
artwork sized to the physical frame, static skeletons, 64px transport bar,
Settings touch reflow and a persistent Immersive exit.
Desktop keeps its prior behavior: every shared component defaults to the
desktop path and only opts into Kiosk behavior through explicit properties
(origin-relative ScrollMemory, viewport-bounded artwork requests).
Regression coverage: kiosk navigation and artwork Node tests added to the
runtime gate, scroll-origin fixtures for fixed and grouped rows, i18n
strings for all eight locales.
Verified by 750 release tests, six QML audits, Kiosk smokes at 800x480 and
1280x800 and a Desktop A/B with seven pixel-identical captures.
cc
Restore downward stream-format downloads while keeping purchased masters on the download grant. Make Purchases playback prefer the highest complete local copy, repair duplicate copy inventory, and move copy management into a bounded modal with safe library and record actions.
cx
The purchase detail badges what was bought on every row (DSD128 on a
DSD purchase, not the catalog's 24/352.8), marks the formats already on
disk with a check in the dropdown, offers a track filter, links to the
album in the Local Library once its folder is indexed, and gives the
download the accent while Play drops to a plain control of the same
size — on this screen the download is the point.
Downloads are process-wide and survive leaving the page; the nav flyout
now lists the purchases with a download in flight under "Downloads · N"
so the user can find them again, each row reopening its detail.
cc
A downloaded purchase played nothing: Play on the purchase detail (and
every other Qobuz entry) streamed the catalog, so a DSD64 album bought
and saved to disk still came out of the CD stream with a "downgrade"
badge. Resolve the download registry at the audible step instead: the
best registered copy of the track that answers the bounded reachability
probe plays under the SAME Qobuz id (`play_id` = catalog id, so
now-playing, history, scrobbling and QConnect see the track they always
did), `.dsf`/`.dff` through the player's additive DSD ticket and PCM
through the file ticket. The gapless successor takes the same route, so
an album bought as DSD stays DSD across the track edge. A play that asks
for a start offset keeps the stream for a DSD copy (DSD cannot seek), and
anything the copy cannot do falls through to the tier walk unchanged.
Contract: qt-frontend/2026-09-01-purchases-local-playback §6, first
slice (implicit preference: the copy wins; no per-album select yet).
cc
The port of the block Slint repeats in 27 views (NavState.restore-scope /
scroll-restore / report-scroll). A .slint file cannot hand a component a
reference to a Flickable, which is why the reference inlines it; QML can, so
controls/ScrollMemory.qml exists once and 31 containers mount it.
Two divergences from the reference, both forced by the same fact — tabs are not
history entries in this port:
1. THE SCOPE IS NAMED BY THE VIEW, not derived from the entry by a scope_for
table. Slint can derive it because its entries carry the tab
(Favorites { tab }); here an entry is a bare route id, so a derived scope
would read "library" for all five Library tabs and pour the Albums grid's
offset into whatever tab mounted. Letting the reporter name its own scope
buys tab granularity for free, and the mismatch case degrades the right way:
an unmatched scope restores NOTHING instead of restoring the wrong thing.
2. THE RESTORE RE-ASSERTS instead of firing once. A Qt item view resets
contentY to 0 whenever its model is replaced, and several pages assign their
model from a binding whose inputs arrive over the network after the mount —
the album page rebuilds listCells from three deferred rails, so it hands its
ListView a new array up to four times in its first seconds and snaps back to
the top over a restore that already ran correctly. The offset is held and
re-applied until a 2.5 s deadline, or until the user takes over (a wheel, a
drag or a flick raises `moving`, and from then the position is theirs).
Diagnostics are off by default: QT_LOGGING_RULES="qbz.nav.scroll.info=true" for
the QML half, RUST_LOG=qbz=debug for the stamp/arm half.
cc
Two screens, a controller and a bridge. `QbzPurchases` publishes one JSON
document per screen plus a revision counter, so QML re-parses even when the text
is byte-identical.
- The list: two tabs with per-type counters, a server-side search on a 300 ms
debounce, and a toolbar that DIFFERS by tab — Albums gets group, sort,
direction, two filters and grid/list; Tracks gets group and the filters only.
Building the Albums toolbar on both would have invented two controls.
- The detail: a format dropdown whose selection re-scopes every downloaded mark,
the progress section and Add-to-Library together; a determinate bar counted in
TRACKS, never bytes; goodies as an album-level action counted separately.
- The album download is the frontend loop, not the Rust command the reference
registered and never called. Sequential, concurrency 1, cancellable, and a
failed track does not abort the album.
- Both prefs are read at runtime, not merely written. `show_purchases` is the
master gate and `nav_tb_purchases` only relocates the entry to the title bar;
both default false, so Purchases still ships hidden.
- Scanned local rows keep their raw `qobuz_purchase` source so the gold badge
survives in list mode as well as grid, while still classifying under the
Offline source filter. Remote feed rows deliberately stay `qobuz`: stamping
them would badge every purchase the user merely OWNS as locally downloaded.
Defects fixed after the screens were written, none of which any test could have
caught:
- `QbzPurchases.boot()` was called nowhere. Without the Qt-thread hop every
publish is a silent no-op, so both screens would have stayed permanently empty
with nothing logged. The cause was an ownership gap rather than the code:
`Main.qml` belonged to no lane because the plan listed the files each lane
CREATES rather than the files it TOUCHES, and "belongs to nobody" is
indistinguishable from "done" until something runs.
- A second download of the same album nested inside the first. The store rewrites
its destination to the album subfolder after the first successful track so that
Add-to-Library adds the album rather than the whole music root, and the download
planner read that value first — producing `.../Album [q]/Artist/Album [q]/...`,
one level deeper each time. The picked ROOT and the resolved ALBUM FOLDER are
now separate fields with names that cannot be confused, with a regression test.
- A tab counter could stick at zero forever with its list plainly full: the two
ids calls each swallow their error, and the reference's backfill from the list
response was missing.
- Cancelling made the whole progress block vanish instead of showing the
cancelled state, whose msgid was already translated in all eight catalogs.
- Add-to-Library re-walked the entire library when the folder id it needed was
already in hand.
- A live language switch and a logout left the previous state behind.
Deliberate divergences from the contract, each recorded with its reason: the page
header is a plain `fontSection` title with no icon tile, because nothing else in
this application announces itself over a large coloured square; purchase download
status travels inside the album document rather than on the shared cache channel,
which is keyed by track id alone and would let the two features overwrite each
other on the same row.
Gate: release build clean, QML audits 278 files / 37 bridges, `cargo test -p
qbz-qt` 398 passing, offscreen smoke green with zero QML complaints.
Not verified, and cannot be from here: every populated screen. This account owns
nothing, so only the empty states have ever rendered.
cc