Commit Graph
2386 Commits
Author SHA1 Message Date
vicrodh ad4d48eeef feat(qt): one-time blind-mode disclaimer on Purchases
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
2026-08-31 11:35:45 -06:00
vicrodh 5e6daf0e81 feat(qt): Open containing album on cortinilla track rows
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
2026-08-31 11:26:17 -06:00
vicrodh 7f702f118d feat(qt): edit context menu on the header search input
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
2026-08-31 11:26:12 -06:00
vicrodh 712a0d1903 feat(qt): focus-driven cortinilla lifetime
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
2026-08-31 11:25:57 -06:00
vicrodh de4c4b487f feat(qt): Create Artist Collection on the artist card menu
Same Discography Builder seam ArtistView's overflow uses; QbzDisco.open
takes the artist id, so no open view is required.

cc
2026-08-31 11:12:58 -06:00
vicrodh d3dc801bc1 feat(qt): mixtape and offline actions on the playlist card menu
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
2026-08-31 11:12:58 -06:00
vicrodh 3b413f4b81 feat(qt): mixtape and offline actions on the album card menu
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
2026-08-31 11:12:58 -06:00
vicrodh 6cdbd12bd0 feat(qt): Play all later in the Popular Tracks group menu
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
2026-08-31 11:09:53 -06:00
vicrodh c2d5a30bfa feat(qt): Play later in the album overflow and quick-view menus
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
2026-08-31 11:09:48 -06:00
vicrodh f51a6e4e81 feat(qt): real block-tail arm for album enqueue
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
2026-08-31 11:09:48 -06:00
mazipani 658524c404 fix(cast): accept X.509 v1 device certificates in TLS handshake
Connecting to Cast devices failed with:
Connection error: invalid peer certificate: Other(OtherError(UnsupportedCertVersion))

Many Cast devices (notably AV receivers with Chromecast built-in) serve a
self-signed X.509 version 1 certificate on port 8009. rust_cast 0.21's
NoCertificateVerification verifier accepts any certificate in
verify_server_cert, but its signature checks delegate to rustls helpers
that parse the certificate with webpki, which only supports version 3
certificates. The handshake therefore aborts for v1 device certificates;
this affects every rustls-based rust_cast release (0.19+) and only devices
happening to serve v3 certificates can connect.

Vendors rust_cast 0.21.0 under crates/vendor with a minimal patch: the
verifier's verify_tls12_signature/verify_tls13_signature now assert
validity, matching the verifier's no-validation contract and the rustls
danger verifier examples. Wired up via [patch.crates-io]; see
crates/vendor/rust_cast/QBZ_PATCH.md for the full analysis and how to drop
the patch once upstream releases the fix.

Verified against a real device serving a v1 certificate (TLS 1.2,
ECDHE-RSA-CHACHA20-POLY1305): handshake, receiver channel and heartbeat
now establish; unpatched fails at the first channel write. cargo
check/test -p qbz-cast pass (13 tests).

Refs: rustls/rustls#1298, rustls/webpki#29, #439
2026-08-31 14:50:01 +02:00
vicrodh bb1b1a5648 merge: library favorites freshness and reco cleanup
cc
2026-08-31 03:35:43 -06:00
vicrodh 4c4647bdf4 merge: add-to-playlist redesign — membership index, picker, local drag
cc
2026-08-31 03:35:35 -06:00
vicrodh c37768459a merge: library explorer collapse tooltip and filter strip
cc

# Conflicts:
#	crates/qbz-i18n/translations/de/LC_MESSAGES/qbz-ui.po
#	crates/qbz-i18n/translations/en/LC_MESSAGES/qbz-ui.po
#	crates/qbz-i18n/translations/es/LC_MESSAGES/qbz-ui.po
#	crates/qbz-i18n/translations/fr/LC_MESSAGES/qbz-ui.po
#	crates/qbz-i18n/translations/ja/LC_MESSAGES/qbz-ui.po
#	crates/qbz-i18n/translations/nl/LC_MESSAGES/qbz-ui.po
#	crates/qbz-i18n/translations/pt/LC_MESSAGES/qbz-ui.po
#	crates/qbz-i18n/translations/ru/LC_MESSAGES/qbz-ui.po
2026-08-31 03:35:35 -06:00
vicrodh 0c874404b6 fix(qt): explain blocked QConnect logout
Keep fail-closed teardown semantics while telling the user how to recover when logout cannot proceed safely.

cx
2026-08-31 03:26:43 -06:00
vicrodh a9046df218 fix(qconnect-lan): harden discovery compatibility
Advertise every eligible IPv4 interface and tolerate future handoff fields while retaining canonical-field validation.

cx
2026-08-31 03:26:24 -06:00
vicrodh 2e55f01e26 refactor(qconnect): share delegation lifecycle
Move authority fences, preflight, rejoin watchdogs, LAN projection, and physical runtime teardown into qconnect-app for both Qt and qbzd.

cx
2026-08-31 03:26:01 -06:00
vicrodh de289bbf46 fix(qconnect): align buffering state reports
Preserve playing intent while buffering and emit ready only at initial decode or stall recovery across both adapters.

cx
2026-08-31 03:25:11 -06:00
vicrodh 94904f70c6 fix(qconnect): follow official renderer authority
Reproduce the server-authored shuffle order, defer renderer detach to topology, and project remote volume changes into the Qt UI.

cx
2026-08-31 03:23:49 -06:00
vicrodh aed82733ef fix(cast): serialize renderer ownership handoffs
cx
2026-08-30 22:22:57 -06:00
vicrodh 9d84f0c384 feat(qt): harden qconnect ownership lifecycle
cx
2026-08-30 22:22:48 -06:00
vicrodh c927eb9c9c feat(qbzd): harden qconnect ownership lifecycle
cx
2026-08-30 22:22:39 -06:00
vicrodh d3cf7ae53d chore(qconnect): wire LAN runtime dependencies
cx
2026-08-30 22:22:31 -06:00
vicrodh b4404f45ba fix(qconnect): enforce shared lifecycle authority
cx
2026-08-30 22:22:08 -06:00
vicrodh 0740a7368f chore(reco): drop the unused rand dependency
Declared since the identity-guardrail round, never used; verified by
grep and a clean build without it.

cc
2026-08-30 22:17:41 -06:00
vicrodh 051bdbb759 fix(qt): favorite mutations stale the Library feed (#690)
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
2026-08-30 22:17:41 -06:00
vicrodh bbe8b6a0f2 feat(qt): album card quality badges reveal the exact quality on hover
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
2026-08-30 19:55:48 -06:00
vicrodh 0c28f9e64b fix(qt): artist Popular Tracks quality cell mounts the real badge
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
2026-08-30 19:55:48 -06:00
vicrodh c650856c84 feat(qt): picker modal wheel lock, wider card, containment side column
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
2026-08-30 18:58:33 -06:00
vicrodh e9fa3fb57b feat(qt): local rows drag to the sidebar and the queue
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
2026-08-30 18:58:32 -06:00
vicrodh c6ae362b7e fix(qt): converging playlist index; explorer picker; remote rows first-class
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
2026-08-30 18:58:21 -06:00
vicrodh 4984736370 feat(qt): picker modal pins last playlist and shows containment strip
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
2026-08-30 17:23:58 -06:00
vicrodh dade0edb26 feat(qt): membership hydrator, honest picker containment, last playlist
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
2026-08-30 17:23:50 -06:00
vicrodh ee621ae28b feat(library): offline copies match both membership identities
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
2026-08-30 16:41:19 -06:00
vicrodh 44df1e53a4 feat(models,core): playlist revision evidence and track-ids fetch
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
2026-08-30 16:38:13 -06:00
vicrodh e1138756e6 feat(library): playlist membership index substrate
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
2026-08-30 16:37:25 -06:00
vicrodh 9486346130 feat(qt): explorer collapse caret tooltip and collapsed filter strip
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
2026-08-30 16:17:29 -06:00
vicrodh 74a41150e9 feat(qconnect): add transactional LAN delegation
cx
2026-08-30 15:33:23 -06:00
vicrodh f30abd12ac feat(qconnect): add official LAN wire service
cx
2026-08-30 13:50:32 -06:00
vicrodh 52c22006ae feat(qconnect): report real buffer state
cx
2026-08-30 13:34:25 -06:00
vicrodh 9493e96e05 fix(qconnect): align auth and playback wire
cx
2026-08-30 13:08:58 -06:00
vicrodh 78a177a36d fix(qt): stop a DLNA renderer before a new URI; shadow-decode proxied tracks too
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
2026-08-30 12:21:35 -06:00
vicrodh a8796ab91e feat(qt): keep the visualizer alive while casting (shadow decoder)
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
2026-08-30 11:35:57 -06:00
vicrodh d620990dad feat(player,cast): share one copy of in-memory track bytes across consumers
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
2026-08-30 11:35:57 -06:00
vicrodh 657bd9757d fix(qt): cast the clicked track, serve cold tracks progressively, proxy server-streamed sources, stop on exit
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
2026-08-30 10:50:47 -06:00
vicrodh 0451000bf0 test(cast): cover the RangeSource media entry
cc
2026-08-30 10:50:46 -06:00
vicrodh abc125bcfe feat(player): progressive external stream for cast renderers
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
2026-08-30 10:27:31 -06:00
vicrodh 976185912f fix(cast): serve arriving bytes, one thread per request, close the receiver app on disconnect
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
2026-08-30 10:27:31 -06:00
vicrodh c3556fe3ec feat(qbzd): expose ALSA hardware volume controls
Carry hardware-volume capability, selection, diagnostics, and status through the daemon API, CLI, and TUI while preserving fail-closed behavior on unsupported devices.

cx
2026-08-30 09:14:39 -06:00
vicrodh 96342e23bc feat(qt): expose ALSA hardware volume settings
Add capability-aware enablement, exact control selection, diagnostics, live volume synchronization, and translated unsupported-state feedback to the Qt settings surface.

cx
2026-08-30 09:14:29 -06:00