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
Add shared playback memory profiles, bounded disk-backed playback and storage
selection for desktop and daemon. Preserve gapless during streaming-only playback
and provide explicit cache eviction with playback confirmation.
Stop watcher read events and unchanged scans from rebuilding the local catalog.
Restore playlist/sidebar updates, folder availability feedback and player layout.
Add appearance presets and a radio-based cache settings panel with translated
trade-offs, plus consistent clipboard menus across visible Qt text editors.
Validate with Rust regressions, QML interaction tests, isolated release startup
and idle-library checks; extend the shared local and CI gates.
cx
Plain-Item overlays get no modal grab, so a MouseArea that swallows
presses still lets the wheel scroll the page underneath (the rule and
its wording live in DiscoverConfigModal.qml). Every scrim and panel
swallow across the modal set now accepts the wheel; the modal Popups
(TrackInfo, CastPicker, AlbumInfo, QconnectDev, LocalMediaInfo) already
had the Overlay grab, and CoverLightbox — a non-modal Popup — gets its
own.
cc
Implements qbz-nix-docs/deferred-2.0.3/playlist-importer-expansion-design.md,
whose §11 the owner ratified 2026-07-31. PlaylistSource is an enum, not a trait
(ADR-007): the set is closed, there is no plugin story, and every arm resolves
to the same ImportPlaylist the URL scrapers already produce — so the matcher,
the 2000-track split, the sink and the summary never learn a second kind of
source exists.
import_prepared_playlist takes the playlist BY VALUE and
import_public_playlist becomes a shim. That kills the double scrape the Tauri
re-fetch-on-execute left behind, and it is not optional for the new sources:
the rfd bytes of a picked file are gone by execute time and the dialog cannot
silently reopen. It also closes a real race — open() resets the session, so a
modal reopened between the Import click and the task's read handed a detached
task None.
FILES read the track list ONLY. The paths inside are never opened, copied or
added to the Local Library, and the modal says so in a persistent row rather
than a tooltip. Content sniffing beats the extension, with an HLS guard because
.m3u8 is also the stream-manifest extension, and a utf8-then-Windows-1252
fallback because .m3u and .pls predate UTF-8 and "Björk" has to stay matchable.
JSON locates the track list BY SHAPE. Depth is a DoS bound, not a locator: the
array sits at 0, 1, 2 or 3 levels depending on the export, and a hard two-level
rule drops an ordinary GraphQL envelope. Container demotion keeps a discography
from importing album names as track titles. Walls at 16 MiB, depth 8, 2M nodes
and 10k tracks.
SERVICES are public reads with no authentication; a connected account only
prefills the field. No qbz-integrations edge — its ListenBrainz client returns
tracks with no duration and drops the playlist title, both verified in its
source, and a direct GET /1/playlist/<mbid> reads all three in one request.
A Last.fm STATION IS A RADIO, and ?page=N is not pagination. Measured against a
live profile: pages 1 and 2 overlapped and batch sizes varied (20, 20, 36). The
first cut fetched 20 pages, never hit its empty-page exit, and returned 469 rows
holding 198 distinct tracks. It now dedupes as it draws, stops after two dry
draws, and stops at MAX_STATION_TRACKS = 200 — the number shown in "Found N
tracks" has to be one we can deliver, and a station is dynamic enough that a
bigger single import buys little (owner ruling: no false expectations of
"traeré casi 500" when we bring half). A failed draw no longer fails an import
that already has rows; Last.fm rate-limits a burst of these.
The summary reports THREE DISJOINT NUMBERS that sum to the total. skipped is
now counted from the matches that found nothing, not inferred as
total - matched, which folded duplicates into failures: that same import told
the user 271 tracks were skipped when 255 of them had matched perfectly well
and been deduplicated before the add.
23 new strings across the seven translated locales; en is the sparse fallback
catalogue and needs none.
cc
Ports the whole playlist-organisation domain, which had no counterpart in
this frontend, plus the Library work the owner asked not to defer.
Playlist Manager. New controller (playlist_manager_qt, _rows, _ops) over the
already-ported folders_qt repository, which until now had zero consumers.
Grid, list and folder-tree modes; sort, filter and search; folder cards and
chips; drag reorder and up/down arrows. Reached from the sidebar's "..."
menu, whose New folder / Manage playlists / Import rows had been rendering
dimmed and inert behind a comment saying the seam did not exist.
Folders. QbzFolderEdit plus the create/edit modals and PmFolderIcon: preset
or custom-image icons, colour, hidden flag, delete. Deleting a folder nulls
its LOCAL members' folder_id explicitly, because the app keeps the
foreign_keys pragma off, so the schema's ON DELETE SET NULL never fires for
them.
QbzPlaylistEdit. One shared rename/description/delete editor replacing the
inline popup in PlaylistView. Three corrections the old path had wrong: the
description is carried through a rename instead of being written as NULL,
a FOLLOWED playlist routes to unsubscribe (playlist/delete returns 200 and
silently no-ops on a playlist you do not own), and the post-delete back
navigation is conditional on actually being on that playlist's page, so
deleting from the manager no longer navigates you off it.
Importer. Spotify / Apple Music / Tidal / Deezer, over the shared
qbz-playlist-import crate.
Picker local mode. The reference's four-case matrix — target local or Qobuz,
payload local refs or Qobuz ids. Local playlists list unconditionally and
Qobuz ones only when online. Local refs onto a Qobuz playlist write the
library.db sidecar at next_playlist_sidecar_position(pid, qobuz_count); the
0-based enumerate the reference used before collided slots and lost rows.
The carried payload can no longer be parsed as a catalog id when it is a
library row id. Unblocks the local bulk add-to-playlist.
Library. A-Z jump strip, Albums list mode, Artists side panel, per-tab
persistence and multi-select. Preferences land in the same
<data-dir>/qbz/favorites_ui.json the shipping build writes, so switching
between the two frontends keeps them. LibraryView is split into
views/library/ on the way past 1800 lines.
Local library. Group-by now persists; playback queues the order on screen
rather than the SQL page order; switching album-identity mode invalidates
the Artists tab; artist matching folds diacritics and compares normalized
credit parts, so "Air" stops listing "Airbourne" and "Beyonce" stops being
two artists.
Offline. activate_offline never persisted a last-user marker, so every
per-user path resolved to None and all local reads came back empty; a
session that started offline saw no local playlists at all. The sidebar's
load-once latch was function-local and therefore unreachable from the logout
reset, so a second login never rebuilt the tree. Both are session-scoped
now, and the sidebar Refresh row runs the local-safe verb instead of being
dimmed with no way to recover.
Also fixes, found while verifying rather than reported: artwork derivatives
were written straight to their final path, so a QML Image resolving one
mid-encode opened a truncated png and Qt cached that failure against the URL
for the rest of the session; Library's track group separators referenced a
Component declared in another delegate's scope and could never render; the
genre filter was drawn on Tracks and Albums but applied only on All; an
All-feed row called a player method that does not exist; and the sidebar's
drop-hot highlight used Slint's #RRGGBBAA literals in Qt's #AARRGGBB slot.
17 new msgids across all 8 locales.
cc