Purchases is now exercised against an account with real purchases, so
the one-time "built in blind mode" notice no longer describes the
feature. Remove the modal, its controller state, the bridge invokable,
the persisted pref and the three strings from the seven catalogs that
carried them.
cc
A property named `on<Upper>` is filed as a signal handler as soon as a
sibling member `<upper>` exists: its initializer never binds, it sits at
the type default, and nothing warns. `PurchasesView.qml` declared
`onAlbums` next to `albums`, so the Albums tab bound `[]` once and never
re-evaluated, which blanked Purchases for every account with real
purchases. Reproduced standalone with the `qml` runtime: the identical
expression under another name reads `true`.
Rename it to `albumsTab` and the four other `on<Upper>` properties
(`collectionsTab`, `surfaceTint`, `accentFloor`, `litColor`) so the rule
can be exact, and add `qml_on_prefixed_property_audit.py` to the four
QML audit sites (qt-run, cargo-test, test-crates, qt-gate-windows).
cc
Preserve local playback when it is authoritative, make renderer conflicts explicit and persist the selected policy. Filter unsupported queue rows, restore portable Qobuz identities after disconnect, and fence takeover commands to avoid stale queue and playback loops.
cx
The Qt 6.9 SafeArea padding neutralizer was gated to macOS in eada3bd12,
which was correct THEN — the Windows ExpandedClientAreaHint arm did not
exist yet. 415cba57d added it without widening the gate, so on Windows
the caption-height safe-area margin auto-padded the contentItem down and
the strip rendered with the window's #1a1a1a background: a dark grey bar
above the QBZ header, pushing the drawn window controls down with it.
The Loader stays imperative, so Linux/Qt 6.8 still never instantiates it.
cc
The cinematic split card's shared shrink-to-fit typeScale is retired: a
long title now word-wraps at its nominal size instead of scaling the
whole metadata block down, and the album/artist lines elide instead of
dictating a reduction. The bottom-right song card's badge also drops the
overAmbient chip — that card only mounts over dark surfaces and the dark
backdrop read as a bigger badge; the themed look was already right.
cc
Qt keeps the window SIZE across hide()/show(), but the remap is a fresh
mapping and KWin's placement policy re-placed it at the horizontal centre
of the screen. The persisted geometry cannot help (it carries only
width/height + flags), so the position is captured at hideToTray and
re-applied around showFromTray — seeded before the show and re-asserted
once 50ms after the map, since a placement WM can overwrite the hint on
the map itself. Windowed frames only: maximized/fullscreen restore via
their latches and an iconified window's x/y are not trustworthy. The
miniplayer enter/exit pair rides the same verbs and inherits the fix.
cc
Playlists can be fully local or mixed, so hiding the row on local albums
was wrong — the resolution logic already existed end to end. The quick
view drops its !localAlbum gates (album and track menus) and a new
'playlist' arm hands the overlay's row snapshot to the picker in LOCAL
MODE, fired before close so the generation guard cannot refuse it. The
local album cards and list rows reuse the existing bulk 'album' scope
(group key -> rows -> picker). My QBZ's heterogeneous cells stay out via
an explicit opt-in, since their local ids are container items the bulk
seam cannot resolve.
cc
The album ⋯ menus on AlbumView's header and the quick-view overlay
already offered Add to playlist, but the AlbumCard menu (Home, Search,
Artist, Label, feeds) never did. The card only carries the album id, so a
new QbzPlaylistPicker.openForAlbum fetches the album off-thread (the
add_to_mixtape pattern), collects its streamable tracks' catalog ids and
seeds the existing picker. Catalog-gated like the mixtape row.
cc
The ambient legibility scrim was a fixed BLACK layer at ambientDim — right
for dark themes, but over a LIGHT theme it pushed the field to mid
luminance, the worst ground for dark text, and the grey secondary/muted
ramp drowned. Light themes now veil with their own near-white surfaceMain
a notch stronger, with the in-shader (mode 1) and in-atmosphere (mode 2)
darkening turned off there; dark themes keep the exact previous look.
QbzTheme additionally steps textSecondary/textMuted toward textPrimary
while ambient is on under a light theme, so the weak ramp keeps a
contrast floor everywhere without call-site changes.
cc
The artist/label/musician links kept the theme accent, which has no
contrast guarantee over the ambient field — over ambient they now use the
AmbientAccent album-palette color. The panel header also reads at a
distance, so its title/album/artist step up to 24/15/20px there; the
desktop modal keeps 16/legal/16.
cc
The window-controls capsule only offered fullscreen and exit; on
platforms without native traffic lights there was no way to minimize or
maximize from immersive. Non-macOS expands to four slots (minus,
square/copy for maximize/restore, fullscreen, X) with the same
expansion-bool gating; macOS keeps the two-slot capsule since the native
controls already cover the rest.
cc
The card's waveform, seek fill, elapsed time and Now Playing indicator
now share the AmbientAccent album-palette color (the local complement
derivation is gone). The Now Playing indicator and the quality badge grow
1.5x (EqualizerBars gains a barScale knob), and the metadata stacks three
lines - title, album, artist - with the shared type scale measuring all
three. The lyrics variant dims its karaoke base like the other immersive
lyric surfaces.
cc
New theme/AmbientAccent helper: reads ambientPrimary (the album's own
dominant hue) and lifts its lightness into a legible band. The shell's
ambientAccent is the +180-degree complement, which is how a deep-blue
cover ended up with orange chrome; accents now stay in the album palette.
ImmersiveTrackMeta tints its Now Playing indicator with it instead of the
theme accent (no contrast guarantee over ambient), opts its badge into
overAmbient and shadows its text rows for light covers.
cc
Immersive surfaces sit directly on the ambient field, where theme tokens
have no contrast guarantee — a light cover washed the split Track Info
panel and the quality badges out completely. QualityBadgeFull,
TrackInfoBody, InfoMetaCell and InfoCreditCell gain an opt-in overAmbient
mode: fixed light text with the restrained native shadow the lyrics
surfaces already use, and the badge swaps the theme surface for its own
translucent dark scrim covering only the chip. Defaults stay off, so the
desktop modal and every themed mount are untouched. The immersive Track
Info panel and song card opt in.
cc
LyricsLineRow gains inactiveColor (default theme.textSecondary, so the
desktop lyrics panel is unchanged) and LyricsLinesView forwards it. The
immersive lyric surfaces pass 35% white: their active overlay is solid
white and the theme's textSecondary base was barely dimmer, so the timed
karaoke fill read as static text.
cc
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
Codename becomes 'Rebuild Q (You Can (Not) Redo)'. The contributor list
gains the ten people whose work shipped since the last sweep — merged
PRs (b0bbywan, Ronjar, eldios, herder, PhilipVinc, Mazipani) and the
house rule for adopted work without a merge (pbaart's Dutch locale,
stshow's DLNA strict-renderer handling, RayneGit's Wayland clipboard
feature flag, LuckyTheCoder's Liquid Glass icon from issue #712) — and
the README list is now identical to the modal's.
New Sponsors section: public GitHub sponsors (linked chips) then Ko-fi
supporters by their public display names, in a fixed three-row strip
that scrolls itself, closed by the translated anonymous-sponsors line.
The panel grows to 920x860 and the modal now eats the wheel on scrim
and panel.
cc
The WindowsDisclaimerModal mechanism on the purchases pref seam: a
purchases_disclaimer_seen key seeded on entry, a disclaimerVisible field
in the list document, and an OK that persists the dismissal for good. The
overlay uses the DiscoverConfigModal wheel-lock shape; the scrim does not
dismiss, so a stray click can never acknowledge it. Body ships as msgids
in all locales with the Star Wars line kept in English everywhere.
cc
CortRow gains an albumId — the Qobuz album id for catalog tracks, the
album group key for local ones (both are what crate::open_album routes),
empty elsewhere. The track menu gates its new entry on the field, the
action funnels through cortinillaMenuAction like its siblings, and the
new msgid ships in the seven non-English locales.
cc
QML TextInput has no system edit menu; a RightButton overlay opens a
CardMenu with Cut / Copy / Paste / Select all. Menu cut/paste are
programmatic edits that never emit textEdited, so the live-query hook is
factored out and re-run after them. New scissors/clipboard glyphs baked
across the six tints; Cut and Paste msgids added to the seven non-English
locales.
cc
Drop the Slint-inherited 4.5s/30s idle auto-close (and its hover-latch
machinery): while the header search field keeps focus the dropdown stays
open, like a combobox. What closes it is explicit — the click-outside
scrims (which now also hand the keyboard to the shell root, the same
duck-walk Escape uses), Escape, activating a row, or a page change. One
timer fewer, per the repaint-pulse doctrine.
cc
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
Add to mixtape and Make available offline (label swaps to Refresh offline
copy on a fully cached aggregate), gated on catalogAffordances — the
catalog-id guarantee both by-id invokables need — and refused for pulled
albums, matching the PopularTrackRow refusals.
cc
The section-level menu offered Play all next and Add all to queue with no
block-tail option; enqueue_track_list_mode already carries the "later"
arm, so only the entry was missing. New msgid translated in the seven
non-English locales.
cc
AlbumView's album menu gains the row and stops mapping its append action
to mode "later" (that aliasing only worked while the backend collapsed
the two). AlbumQuickView drops the local-only gate on Play later now that
catalog albums own a real block-tail insert.
cc
QualityBadge has always owned a hover tooltip, but every AlbumCard host
passed only the tier, so the tip had nothing to say. The card gains a
qualityDetail input feeding the badge's label, and all fifteen mounts
now pass their model's detail (HomeCard rides its qualityLabel twin).
Un-hydrated Plex/remote rows surface their bare filetype — the known
honest fallback; an empty detail still shows no tooltip at all. The
Scene panel's own card already wired this and is untouched.
cc
The PopularTrackRow fork rendered a lone tier word (HI-RES/CD) in a
plain Text while the document has carried qualityDetail all along; it
now mounts the same bare QualityBadgeFull as the shared TrackRow's
quality cell — tier label over the exact bit-depth/rate line. Audit:
every other popular/track list already goes through rows/TrackRow.qml,
so this fork was the only divergent quality cell.
cc
The picker and its duplicate sub-modal eat the wheel on scrim and card
(the DiscoverConfigModal contract) so the page behind no longer
scrolls. The card widens to 735px, and Already in moves from an in-flow
horizontal strip to a right-hand column that exists only when there are
containments to show — the playlist dropdown overlays the main column
only, so the answer stays readable while the list is open.
cc
The shared drag gains a LOCAL payload arm: a Local Library / Explorer
row enters via dragStartLocal carrying its ROW ID, resolved to
source-aware refs at drop time — a library row id can never be read as
a catalog id, the same invariant the picker's Payload enum enforces.
TrackRow's localDragSource reuses every existing guardrail (horizontal
arming, select-mode/dead-row/playBlocked gates, preventStealing,
cancel teardown) without touching catalogRow's meaning. Drops route
through the picker's own add matrix: refs onto a local playlist or the
Qobuz sidecar, and Qobuz ids onto a local playlist (previously a
documented POC gap); queue drops land at the exact slot through the
same insert path as Qobuz drags. Native-catalog tracks rows keep the
gesture off until their identity can ride it.
cc
The dropdown pins the session's last successful target above the
results (clock glyph, de-duplicated by Rust, retained under a search
only when it matches). The body gains the collapsible Already in strip:
one card per writable playlist already holding carried tracks, with an
N-of-M badge on partial multi-track containment; clicking a card
selects that playlist so completing a partial add is one click plus the
duplicate-confirm's only-new answer. The strip renders only when the
index can back the claim — complete with matches, or updating under an
explicit caveat — and an empty answer from an incomplete index renders
nothing. Four new strings in all eight locales.
cc
The Library Explorer caret now says what it does on hover (Show/Hide
filter panels, all eight locales). While the panels are collapsed with
filters applied, a single-line strip of removable chips stays visible
next to the caret (top of the results area for the left/right rail
layouts), so hiding the panels never hides what is filtering the
results. The chip is the empty-state delegate extracted into a shared
component; removal reuses clearFilterChip verbatim.
cc
Add capability-aware enablement, exact control selection, diagnostics, live volume synchronization, and translated unsupported-state feedback to the Qt settings surface.
cx
A double-click on a clickPlays row fired playRequested three times
(two releases plus doubleClicked) and restarted the track three times
within 230 ms. Debounce the release and skip the doubleClicked arm on
clickPlays rows.
cc
Settings > Integrations > PRIVACY: a Listening history toggle (default
ON; the state lives in the per-user listen_meta, so it follows the
account) and a confirmed Clear (DELETE + VACUUM). Strings in all eight
catalogs; README names where the data lives.
ScrobblerSettings::history_read_allowed(service) = *_active(): the
scrobbling switch now governs reading a service's history for
Recommendations as well as sending to it, and its Settings text says
so. recommendations_qt uses it instead of *_is_authed().
cc
Add bounded native library models, complete grouping rails and scan progress. Bring source-aware metadata editing, media information and collection offline preflight to Qt, and repair inherited Plex album artwork without overriding healthy tokens.
cx
focusSearch() force-focuses the search input and nothing in HeaderBar ever
released it: the Escape arm only fired while the cortinilla was open, and
even then it dismissed the dropdown and left focus in the field. Since the
central dispatcher refuses every binding while a text input holds focus
(AppShell.qml:133-139), the field was a keyboard dead end whose only way out
was a mouse click.
Escape now always dismisses the dropdown if open, accepts, and hands focus
to the shell root via the same isQbzShellRoot duck-walk the modals use.
Clearing focus alone would not do: a null activeFocusItem passes the gate
but leaves AppShell's Keys handler receiving nothing at all.
Reachable from Ctrl+f since the port; the Vim keymap's `/` puts it one
keystroke from the reflex that expects Escape to work, which is how it
surfaced.
The preset swaps the BASE shortcut table underneath the user's overrides
instead of rewriting them, so Default <-> Vim is lossless. Vim binds single
modifier-free keys: n/p, h/l, Shift+J/K, /, and Ctrl+o / Ctrl+i for the jump
list; rows with no vim idiom keep their defaults.
j and k are deliberately left UNBOUND. They belong to a per-list cursor,
which QBZ has no notion of yet, and a global action bound there now would
have to be taken back the day one lands. A test holds them free.
Where an override has already claimed a shortcut the new base wants, that
action goes unbound rather than double-binding a combo that
action_for_shortcut would otherwise resolve by BTreeMap id order.
Seven new tests, including a gate that fails if any preset ever binds one
shortcut to two actions.
Eight Playback actions the table never had, on Spotify's desktop defaults:
volume up/down (Ctrl+Up/Down), mute (M), shuffle (Ctrl+S), repeat (Ctrl+R),
like (Alt+Shift+B), and a surface-independent seek (Ctrl+Shift+Left/Right).
Each reuses an existing transport seam, and the immersive bare-arrow seek
rows are untouched. Volume honours the player bars' volLocked predicate, so
the hotkey cannot move the slider on an ALSA-Direct hw path where the engine
documents the write as a no-op. Like rides a QML seam, as nav.search does,
because the track id is a QbzPlayer property rather than part of the Rust
now-playing model.
Two new tests; labels added to all eight catalogs.
A startup modal carrying the release notes' "Windows -- yes, but read this
first" section verbatim: Windows is not a supported QBZ platform, the build is
as-is, and the port is open for adoption.
Persisted as two fields, hidden plus the acknowledged version, written in ONE
transaction and read from ONE parsed document. Two separate whole-document
read-modify-writes could leave a torn pair after a crash or a failed rename,
and either half alone re-opens the modal -- the user would tick the box and see
it again. ui_prefs.json is shared with the shipping Slint app, so the discipline
lives in settings_qt with the rest of that file's writers.
A version rather than a bool alone is what makes "reset it on every update"
need no reset step: a build whose version differs simply does not match. A
reinstall of the SAME version does not bring it back, which is accepted -- the
prefs file outlives the installer, and the MSI is where that would be cleared.
Focus is taken in Component.onCompleted, not onVisibleChanged. Item.visible
defaults to true and the Loader only creates this when it should already be
shown, so there is no false->true transition to observe: the handler would
never have run, Escape would never have reached it, and ordinary hotkeys would
have kept working behind the modal. Focus is handed back to the shell root on
destruction, because Qt strands activeFocus on an item being destroyed and that
kills AppShell's key dispatcher until the next click -- the failure
QbzConfirmModal already documents.
Mounted through a Loader gated on isWindows. `visible: false` still constructs
the whole subtree, and Linux and macOS have no reason to build a panel, a
Flickable and fourteen delegates for a modal they can never show.
The body is NOT translated, deliberately: it is the author's statement of
policy and the wording is the point. The chrome around it goes through tr.
cc