The AppShell hotkey seam duck-types selectAll, exitMultiSelectMode and
multiSelectOn on the mounted view. The offline manager exposed none of
them, so Ctrl+A did nothing there although the Slint ticked every track,
and MixView lacked multiSelectOn, so Escape never left its select mode. The
seam's comment, which still listed Artist, Playlist, Mix and Label as views
without multi-select, now names the views that implement it.
cc
The manager's checkboxes toggled one track at a time. They now follow the
shared rule over the track rows as drawn, album headers excluded, and a
Shift or Ctrl click on a track row selects instead of starting playback; a
plain click still plays. The Slint had this surface on its range table
(SURFACE_OFFLINE).
cc
Settings > Offline loses its "Clear cache" row: it ran the same
QbzOffline.clearAll() as the trash glyph in the Offline Cache Manager's
stats bar, so the whole cache had two entry points and one misleading text
("your downloaded albums are kept" — what is kept are the purchased
downloads in the music folder). The glyph now carries a hover tooltip (the
manager mounts the shared QbzTooltip overlay; the limit and folder glyphs
get one too) and its confirmation says exactly what goes — the live track
count and size — and what stays. The Tauri-era string leaves the eight
catalogs; the new ones enter them.
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
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
The last missing Settings surface: the OFFLINE CACHE group (Open manager /
Open folder / Clear all) and the manager view behind it — stats bar with
the GB limit, sort and failed-only toolbar, A-Z artist rail, album+track
rows with per-row re-download and remove. The engine was already here; what
was missing were four actions (remove_album, redownload_track, open_folder,
clear_all), the rollup and the view.
Two bugs the owner's smoke exposed, both older than this view:
ALBUM IDENTITY WAS BEING LOST ON EVERY ALBUM DOWNLOAD. A Track inside an
/album/get payload carries no nested album object, and `track_cache_info`
read exactly that, so `cache_album` wrote `album = NULL, album_id = NULL`
for every track it queued. Measured on a real index: 34 of 46 rows, three
complete albums. Not cosmetic — `album_id` is the key `remove_album`,
`redownload_album` and the downloaded-album registry look rows up by, so
those albums could not be removed, refreshed or recognised as downloaded.
The album document now stamps id and title onto every track that lacks its
own. The reference has the identical bug.
AND THE ROLLUP MIXED ARTISTS. Every row without an album_id collapsed into
one "__singles__" bucket titled from the first track and credited to its
artist, so three albums by three artists rendered as ONE album header while
two of the artists vanished from the rail. The key is a ladder now: album
id, else (artist, title), else a per-ARTIST singles bucket — two artists
can no longer meet in one group even with the broken data. Rows already on
disk recover their title from library.db, which does know it, so an
existing cache reads correctly without re-downloading.
Deltas from the reference, both because Qt is not Slint: covers are paths
(slint::Image is not Send, which is why it pre-decodes; Qt has no such
constraint) and selection is QML-local, this port's convention, which keeps
the document a pure function of the DB so a finishing download cannot fight
the user's checkboxes.
Clear-all confirms first — the reference fires straight from the glyph, and
undoing this one means re-downloading the whole cache.
cc