The editor gains a Folder row — the same folders the sidebar and the
manager file playlists into, "No folder" for the root — seeded from the
playlist's current folder and applied on Save through the manager's own
move verb, so an existing playlist no longer needs the sidebar's row menu
to be filed (#785). "Delete playlist" joins the sidebar row menu (own and
local playlists; sidebar entries now carry isOwner) and the playlist card
menu (own playlists), behind a shell-level "Delete playlist?" confirmation
that runs the same delete the editor's button runs and leaves the detail
page only when it was the deleted one (#776). Eight catalogs updated.
cc
Restore tray-aware close and quit confirmation, circular label covers, and missing label artist portraits. Publish artwork progressively, preload two grid rows, and skip redundant motion reports while preserving data refreshes.
Include regression tests and isolated artwork benchmarks. Verified Linux build, QML checks, and scrolling smoke tests.
cx
Add to mixtape opens the MyQBZ picker with the call-site payload idiom
(trackCount omitted: the card contract has no member count and the store
persists the value); Make available offline runs the shared playlist
cache preflight. The card only mounts on Qobuz playlist rows, so neither
entry needs a gate.
cc
Accumulated qt-frontend work from the 2026-08-11/12 sessions (cc),
uncommitted until now: the app-wide dynamic background in both modes
(AmbientField with GPU/Canvas arms, AppShell layering, ambient palette
pipeline), the wave-1 clip/batch sweep across the tree (alpha batches
449 -> 327, reproducible), the rounded-cover and icon-tint arms, and
scripts/qt-batches.sh (batches + presents/s sampler).
The GPU behaviour of these pieces was finalized in the follow-up pulse
commits; see qbz-nix-docs/qt-frontend/2026-08-11-scenegraph-batches/
GPU-COST-INVESTIGATION.md for the full campaign.
km
Un-following a Qobuz playlist left it in the sidebar and in My Library.
toggle_follow called unsubscribe_playlist and settled the ownership snapshot
and stopped there, so the row only left at the next full refetch. Both
surfaces are patched directly now, in both directions: following from a card
buys the metadata with one get_playlist rather than re-reading
getUserPlaylists, which lags the write. The un-follow collapses a
hearted-and-followed playlist to the one row the next load will actually
produce instead of dropping it, because the local heart is a library.db flag
that survives the unsubscribe — removing every row made it vanish and come
back, which is the original complaint wearing the other shoe.
The Plex mark on album cards was a hard-drive glyph tinted accent — a blue
hard drive — and the three Qobuz kinds drew no badge at all. The badge now
ports discover/AlbumCard.slint:326-354 exactly: 24x24 at radius 4 pinned
bottom-right with a 6px inset, plex-logo untinted at 16, qobuz-logo at 18
(22 for a download), the purchase arm gold on its amber chip, hard-drive
white at 14 for local only. The brand marks are plain Images: routing them
through QbzIcon floods every pixel with the tint and flattens a logo to a
silhouette. SourceIcon moves to controls/ since it is no longer local-only,
and every other surface drawing a hard-drive was audited against its .slint.
The local album detail loses the two nav chevrons above the art. They are a
Tauri-era leftover from before the app had global navigation.
My QBZ used a bespoke card in the grid. MyQbzDetailCard is deleted and the
standard cards/AlbumCard is mounted with its overlay. localMode bundled four
separate things — action routing, the heart, the pin and Block this album —
so a multi-source container had to switch all four off together; it is split,
and the catalog affordances now follow the ITEM. On a Qobuz cell the heart
works through the shared favourite seam; on a track, playlist, local or Plex
cell it is absent rather than dead. Row covers carry two rungs: the list keeps
_50, the grid gets _600. Not the _230 that would merely cover a 200px card —
the complaint was that the cover reads softer than a normal AlbumCard, and a
normal AlbumCard is fed image.large, which IS _600. _200 is not a rung the
CDN serves at all (it answers 404).
The My QBZ grid sets reuseItems, and AlbumCard assigns to its own isFavorite
and isPinned when toggled, which destroys the binding that fed them. A
recycled delegate therefore showed the previous row's heart and the click
added the new album while the glyph said remove. Restored with the
Qt.binding idiom this port already uses on TrackCard, PlaylistCard and
TrackRow.
The details view no longer force-opens every row on entry, and the per-row
open state persists.
Library > Artists showed placeholders for nearly every portrait. The side
panel never reported its artwork window — the one caller of
libraryArtworkWindow is LibraryView.reportWindow, and the panel had no
report call at all, so no key ever reached the resolver and artMap stayed
empty for its rows. The rail now reports through that same path, with the
section-header sentinel reportWindow already skips. Note the reference has
no portrait fallback either: an artist Qobuz serves no image for is a
placeholder on both sides.
cc
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
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
**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
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
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
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
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