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
Thirteen commits: a real block-tail arm for album enqueue and Play later
across every queue-capable menu; mixtape/offline/collection actions on the
album, playlist and artist cards; a focus-driven cortinilla with an edit
context menu on the search input and Open containing album on track rows;
the one-time Purchases disclaimer; the 2.1.0 version bump with the About
refresh (codename, contributor sweep, sponsors) and a wheel-lock sweep
across the plain-Item modal overlays.
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
Workspace version + lock, and the metainfo gains its 2.1.0 release entry
(the release workflow re-stamps version and date at tag time). flake.nix
and the Flathub manifest are post-tag and stay untouched; the snap-qt
recipe stamps its version from the tag.
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
enqueue_album collapsed "later" into a plain append, so the album-level
"Play later" rows did exactly what "Add to queue" does. Route the mode
verbatim to the peer arm (which always had a later batch path) and feed
add_track_later locally, matching enqueue_track_list_mode and the local
enqueue_rows funnel.
cc
The Library feed loaded behind a pure once-per-session latch: a
favorite added anywhere mid-session updated every glyph cache but the
grid membership only changed on restart — or a language switch, the
only other reload_library caller. The latch is now a staleness flag:
every mutation through the fav_cache funnel (album/track/artist/label
hearts, playlist hearts) resets it, and the next Library visit
refetches. Deliberately not a live republish (the toggle_pin doctrine);
in-view bulk actions already reload directly. The reporter's
survives-restart half remains unexplained by code and needs his data.
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 membership hydrator looped forever on one playlist (smoke
2026-08-30): playlist/get and getUserPlaylists do not report comparable
updated_at stamps, so the revision evidence never settled, and the
driver had no forward-progress guard. replace_tracks now copies the
HEADER's revision into the membership evidence, staleness requires
comparable evidence on both sides (count is the fallback when either is
missing), and a playlist successfully written in a round is never
fetched again within it — after the initial index build, only the
playlists the authoritative list reports changed re-sync.
Library Explorer rows never opened the picker: the bulk resolver did
not know the explorer's own row caches and dropped every merged
Plex/remote row. It reads them now, and Jellyfin/Subsonic tracks are
first-class playlist citizens: source-prefixed refs (the Plex pattern),
a playlist_remote_tracks sidecar for Qobuz playlists, source-qualified
rows in local playlists, read resolution through the media cache, and
containment arms in the membership index.
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
playlist_index_qt walks the library's hydration queue one
playlist/get?extra=track_ids fetch at a time (300ms pace, two attempts
per playlist per round, nothing written on failure), poked by every
recorded authoritative list. The sidebar and playlist-manager loads now
record ownership-aware authoritative generations (skipped while the
session user is unknown, and never from a failed fetch). The picker doc
gains lastUsed — session-local, set only by a confirmed successful add
or create, validated against the live row list — plus alreadyIn rows
with distinct-ref counts and the complete/updating/unavailable
tri-state (offline degrades updating to unavailable). Followed
playlists are no longer offered as add targets. Successful add/remove/
create/delete mutations move the snapshot synchronously; local-mode
refs resolve to typed membership refs at the index boundary.
Verified: cargo test -p qbz-library (158) and -p qbz-qt (615) green.
cc
An offline copy (qobuz_download row) attaches to Qobuz playlists as a
library-row sidecar but to local playlists as its catalog id; the
Library ref now carries the optional catalog id so containment looks
both ways. Also adds mark_inactive for the delete-playlist hook.
cc
Playlist and PlaylistWithTrackIds deserialize the API updated_at stamp
(absent on some list shapes, hence optional), and the core facade
exposes the lightweight playlist/get?extra=track_ids fetch the
membership hydrator drives.
cc
Split the snapshot's single timestamp into honest freshness columns
(header_updated_at vs membership_synced_at, plus membership_count and
remote revision evidence), add authoritative-list generations with a
two-generation retirement grace, persist ownership explicitly, and add
incremental add/remove/create producers so successful mutations move
the snapshot synchronously. New playlist_membership module answers the
inverse question — which writable playlists already hold these tracks —
over the snapshot, both sidecar tables and local playlists, through a
typed PlaylistTrackRef instead of the plex:/id string mangling. All
membership tables gain reverse-direction indexes; migrations are
probe-then-ALTER with a legacy backfill test.
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
gmediarender ignored a SetAVTransportURI issued while it sat in
PAUSED_PLAYBACK (2026-08-30 smoke: a Plex row did not replace the Qobuz
item until Next), so a loaded renderer is stopped before the new URI.
Server-streamed (Plex/Jellyfin/Subsonic) tracks now also feed the
visualizer: a second, sequential download of the item into a progressive
buffer (LAN traffic, capped at 400 MB) gives the shadow decoder the bytes
the Range proxy never holds; cancelled with the track like the Qobuz one.
cc
The FFT/scope surfaces are fed by the local engine's writer thread, which
is stopped while a renderer plays, so every visualizer animation froze
the moment a cast started. cast_viz decodes the same bytes the renderer
is served (progressive download buffer, cached track or local file)
silently on its own thread and pushes them into the VisualizerTap at
real-time pace, re-anchored to the renderer's reported position on each
poll (re-seek past 0.75 s of drift, paused with the renderer). Proxied
server-streamed tracks have no local bytes and stay dark.
cc
InMemorySource::from_shared decodes an Arc<Vec<u8>> without copying and
MediaServer::register_audio_shared serves the same Arc, so the cast media
server and the cast visualizer's shadow decoder can sit on one copy of a
100 MB track.
cc
Four findings from the 2026-08-30 casting smoke, each with its cause:
- A Local Library click while casting played the OLD queue's current
track on the renderer: that funnel stages its queue after the audible
step, and the gate routed "the current track". route_play_remote_track
hands the renderer the clicked track itself.
- Plex/Jellyfin/Subsonic rows were refused ("serves it over the network").
The media server now proxies the source url with the ticket's request
headers (exact size from the probe), closing the TODO the Slint
service carried.
- A cold Qobuz track waited for the whole download before the renderer
heard anything (5.5 s for 97 MB; every frontend did this). Uncached
tracks are served progressively from the player's download buffer;
cached ones keep the instant byte path. The download is cancelled when
the track changes, on disconnect and on shutdown.
- Closing the app could leave the renderer playing: the poll held the
service lock across renderer round trips and the 2 s bounded shutdown
timed out before sending Stop. The poll handle now lives outside that
lock and is aborted first. Chromecast: a toggle with nothing loaded
casts the current track instead of asking an empty receiver to play,
and IDLE counts as "ended" only after the track was seen PLAYING (an
idle receiver app auto-advanced the whole queue).
cc
Player::open_external_stream starts the CMAF download and returns a
shared buffer (exact final FLAC size known from the segment table) once
the first segments are in, instead of after the whole file. The finished
download is cached like a played stream. Core exposes it as
open_external_stream_resolved.
cc
MediaServer gains a RangeSource entry whose bytes may still be arriving
(known final size, blocking reads) and serves every request on its own
thread so a slow media body no longer stalls the HEAD probe or a seek's
second Range request. Chromecast: play/pause/stop never launch the
Default Media Receiver (an app launched with nothing loaded sat IDLE and
read as "track ended"); disconnect stops the receiver app instead of
leaving it on screen with the last item.
cc
Carry hardware-volume capability, selection, diagnostics, and status through the daemon API, CLI, and TUI while preserving fail-closed behavior on unsupported devices.
cx
Add capability-aware enablement, exact control selection, diagnostics, live volume synchronization, and translated unsupported-state feedback to the Qt settings surface.
cx
Probe exact mixer element identities, retain stable device routes, and fail closed for ambiguous or switch-only controls. Synchronize player volume through ALSA events without changing direct PCM configuration.
cx
`SleepInhibitor::acquire` runs on every Playing state update and retried
the `Inhibit` call each time it failed. On a headless daemon with no
logind session polkit answers `InteractiveAuthorizationRequired` forever,
which produced one WARN every ~2 s (369 lines in 11 min on the Pi smoke,
2026-08-29). Classify the failure: a policy denial is logged once at info
and never retried for this process; a transient error backs off 5 min.
Playback was never affected.
cc
The daemon only prefetched the next track after a gapped advance, so every
edge after a seamless hand-off (and every QConnect/API play) reached the
10 s gapless window cold and had to materialize the whole next file in
time. Whether it made it was file size vs bandwidth (#699 follow-up on a
1 GB Pi: gapless "sometimes, varies by album"; alternating pattern).
Add a `WarmSuccessor` driver action fired once per current track id on
the first tick that sees it, for every entry point, run as a driver-owned
task that never blocks the control loop; drop the awaited prefetch from
`advance_and_play`. For a successor that is still not cached at arm time,
append its initial CMAF buffer as an incremental stream
(`queue_gapless_streaming`, the desktop's hand-off) and keep the
whole-file byte fetch only as fallback.
Tests: 2 new driver tests (warm once per track for any entry, seamless
edge warms the new successor); qbz-app 269 and qbzd 298 pass.
cc
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
The cast gate lived only in `play_queue_track`; every other entry point
(single track, album, artist, track lists, playlists, MyQBZ, the Local
Library funnel) only checked the QConnect peer, so clicking a track while
casting started the local backend beside the renderer (DLNA played one
track, the host another). `route_play_remote` (cast, then peer, then
local) is now the single gate, called from all 12 entries and from
`play_audible`; `route_play_to_peer` is private. A tripwire warning in
`play_resolved_offline_aware` names any future entry that skips it.
cc
`get_media_position` returned `NotConnected` whenever no media app had
been launched yet, and the 1 Hz poll folded that into "no answer", so a
connect made while paused hit LOST_POLL_MAX and disconnected after
exactly 5 s (reproduced three times on the 2026-08-29 smoke). Add a
typed `CastError::NoMediaSession`, and while idle prove liveness with
the receiver-level `get_status` request instead of the media session.
Also coalesce cast volume: a slider drag now hands the newest level to
one sender task every 120 ms instead of one Cast/SOAP round trip per
pixel.
cc
rusqlite 0.40 removed ToSql/FromSql for u64 (SQLite integers are i64):
every u64 column now reads/writes through an explicit i64 cast. It also
rejects a multi-statement execute() instead of silently running only the
first statement — which is what catalog.rs's editions/logical_albums
prune had been doing all along; it is an execute_batch now, so the
logical_albums prune finally runs.
cc
Decided 2026-08-29: the arm64 Qt build only works on ubuntu-24.04-arm
(aqt's arm64 host tools segfault on 22.04-arm), so the arm64 desktop
app requires glibc 2.39; qbzd keeps building on 22.04-arm at 2.35 so a
trimmed Pi OS bookworm image stays a valid target. The floor gate asserts
per arch instead of warning; README states both floors.
cc
Qt 6.9's macdeployqt leaves @rpath/Qt*.framework references resolved
through LC_RPATH @executable_path/../Frameworks; the old assert only
accepted a literal @executable_path and failed a correct bundle. Reject
absolute paths, require the rpath, and let the offscreen smoke prove it.
cc
Both 6.8.3 and 6.9.3 arm64 qmake die with SIGSEGV on ubuntu-22.04-arm
with no missing libraries. Try the 24.04-arm image; the glibc floor gate
warns instead of failing for aarch64 until the floor is decided.
cc
The QMAKE handoff exposed the real failure: aqt's linux_gcc_arm64 6.8.3
qmake dies with SIGSEGV on ubuntu-22.04-arm, which cxx-qt-build reported
as QtMissing. The arm line pins 6.9.3 (QT_VERSION_LINUX_ARM64 overrides);
x86_64 keeps the 6.8 LTS floor. file/ldd diagnostics run before the
qmake query so the next such failure names itself.
cc
aarch64 runner: cxx-qt-build reported QtMissing with a correct
QT_ROOT_DIR — set QMAKE/CMAKE_PREFIX_PATH explicitly (as macOS/Windows
already do) and print the qmake version before the build.
macOS: StreamType::output_sample_rate (macOS-only) matched Rodio only;
Direct is intentionally not cfg-gated, so the macOS build failed with
E0004. A direct sink answers with its opened rate.
cc