Commit Graph
299 Commits
Author SHA1 Message Date
vicrodh ced6e64b40 style(slint): homologate recent surfaces to the control-alignment standard
Integrations buttons rebuilt to the settings standard (160x34
SecondaryButton, body/medium/text-secondary, enabled-dim) so every
connect/finish/disconnect lands on the settings right-edge line; Offline
status green standardized to the settings token (#3fae6a, not the player
chrome green) and Check now dims while checking; the create-modal offline
hint indents onto the label grid; the favorites-rail header moves to the
canonical section-heading scale flush with the cards.

The extracted standard (SettingRow trailing controls, fixed button
widths, GroupHeader idiom, modal footers, status-line tokens) is codified
rule-per-line with citations in
qbz-nix-docs/guides/native-ui/ui-control-alignment-standard.md.

cc
2026-06-10 09:05:05 -06:00
vicrodh a5b4c9ba1c fix(slint): typed drag payload and add-to-playlist paths for local/plex rows
The drag payload was an untyped Vec<u64>, so dropping a LOCAL album row
onto a local playlist stored its library row id as a qobuz_track_id - the
D11 resolver could not resolve the fake id and hid the row (the
empty-playlist report). New DragTrack {Qobuz|LocalRow|Plex} typed per
originating view; the drop arm stores source-aware refs (qobuz_download
rows keep their real Qobuz ref, plex rows their key, local rows their
path) for both local and Qobuz playlist targets.

Add-to-playlist had collapsed to play-track in LocalAlbumView (entry
rendered, action ignored) and was an unhandled arm in the Tracks tab; both
now route to the source-aware picker, Plex rows gain the entry everywhere
(gate was excluding them), folder-detail rows route through the shared
track-action arm, and bulk arms stop skipping Plex.

D11 nuance: a local-path row missing from the library index but present on
disk renders with a filename fallback instead of hiding; only genuinely
missing files stay hidden (logged distinctly). main.rs also carries the
recovery handler hunks for the sign-in flow fix (Login screen first,
browser OAuth after, no unlocked-empty shell).

cc
2026-06-10 09:04:27 -06:00
vicrodh 952846b6a0 feat(slint): Last.fm and ListenBrainz scrobblers wired to player and settings
Re-implements the parked scrobblers-wip stash intent on the current tree
(the stash no longer applied; it remains untouched in the stash list) with
three corrections: the Last.fm offline queue uses the SHARED per-user
offline_settings.db scrobble_queue (byte-identical SQL to Tauri - queues
flush from either frontend) instead of a private table; the ListenBrainz
cache opens Tauri's per-user path so LB creds and listen_queue are truly
shared (a Tauri LB sign-in pre-seeds Slint); and the queue flush is driven
by the offline engine's online edge (plus initial flush), not login-only.

New frontend-agnostic qbz_app::settings::scrobblers store (master enable,
Last.fm session/username, LB token/username; tests) and the shared
scrobble-queue API on OfflineModeStore. Player wiring hooks the de-duped
track-change edge inside the QConnect remote gate: now-playing fires on
change (skipped offline), the scrobble arms at min(50%, 240s) with a
generation counter cancelling stale timers, all sources scrobble (Tauri
parity incl. the #360 version-enriched title). Offline or call failure
queues; Last.fm flush honors the 50-batch and 14-day cutoff.

Settings gains an Integrations section (Plex-section idiom): Last.fm
browser-auth connect/finish/disconnect, ListenBrainz token field +
validate, per-service enable toggles. Known gap documented: Last.fm creds
cannot be shared with Tauri (webview localStorage); LB creds and both
queues are shared.

cc
2026-06-10 02:52:17 -06:00
vicrodh 81587f4d17 feat(slint): playlist snapshot (names+membership) and offline favorites rail
Qobuz playlist snapshot, captured opportunistically with zero new API
traffic: every online sidebar/manager list load upserts id+name+owner+
track_count for the user's playlists; opening a playlist detail online
full-replaces its membership track ids (refusing playlists never listed,
so viewed public playlists don't land). Offline consumers: synthesized
sidebar/manager entries resolve the persisted name (killing the cold-start
'Playlist (N local)' fallback when a snapshot exists), and the mixed-
playlist offline detail now renders the snapshot tracks that are playable
(snapshot intersect cached intersect grace) from the offline-cache index -
snapshot block first in snapshot order, sidecar local/plex block after.
D11 availability widens: a playlist with zero sidecar rows but cached
snapshot tracks now shows offline. 4 repo tests.

Offline favorites rail: the Favorites placeholder gains an 'Available
offline (N)' block - disk-cached favorite ids intersected with the
offline-cache READY index and LocalLibrary qobuz_download copies, compact
rows with the offline cover chain, click plays the rail as a queue through
the cache tier. Shared OfflinePlaceholder gains an @children slot; only
the Favorites mount injects the rail.

Single commit: the two features interleave in main.rs/state wiring and
ship together to keep every commit compiling.

cc
2026-06-10 02:27:54 -06:00
vicrodh 23129cb98e feat(slint): local playlist reorder and favorite/hide organization
Reorder: repo gains reorder(from, to) with remove-then-insert semantics
and contiguity preservation (5 tests); the local detail wires the existing
per-row chevron path (shown under Default sort - the repo order IS the
editable order; the Custom sort entry hides for locals since the custom-
order sidecar is u64-keyed) with optimistic model swap and off-thread
persistence.

Organization: additive favorite/hidden columns on local_playlists
(pragma-guarded migration + 2 tests incl. pre-migration shape), manager
cards render real heart/eye state and locals participate in the
visible/hidden toolbar filter, hidden locals drop from the sidebar while
staying reachable through the manager's hidden filter. Folder membership
for locals stays out of scope (folder tables are Qobuz-keyed).

main.rs wiring for these actions rides the next commit (shared file with
the in-flight favorites-rail batch).

cc
2026-06-10 02:10:33 -06:00
vicrodh 4479052f7b fix(slint): offline badge rework, sidebar flyout drift, header MyQBZ routes
Recovery banner replaced by a tri-state header badge left of the menu
button (Offline hard amber / Offline manual accent / Logged out gray,
hidden when online and authenticated) with a flyout carrying the full
status line, a Sign in button for the logged-out state (enabled unless
connectivity is CONFIRMED down - it was born dead behind the Unknown
verdict; action fires before the popup closes) and a quick Offline-mode
toggle for the manual state (Settings shortcut dropped per owner).
Discover/Library nav entries are hidden offline instead of disabled, on
all three nav surfaces.

Sidebar flyout incremental drift root-caused by a headless harness against
slint 1.16.1: an opacity binding on SidebarNavRow's ROOT (left over from
the disabled-gating approach) forces an inlined opacity wrapper that makes
absolute-position double-count self.y - one row-pitch of drift per row,
which the in-layout banner had masked as its own bug. Binding removed;
anchors measure exact in all states. Same-class latent hazard flagged in
NavTab/CompactNavBtn (cancels today, self.x=0).

Header MyQBZ dropdown rows carried no route (documented no-op): Collections
and Mixtapes now navigate from both the full tabs and the compact rail.
The offline-edge watcher also reacts to connectivity-only changes so the
network-folder browse set refreshes in place during a logged-out session.

cc
2026-06-10 01:32:44 -06:00
vicrodh 77dfd73b6d fix(slint): network-folder visibility keyed on connectivity, playback lock on missing files
Root cause of the empty Albums tab: transposed positional booleans from
the Plex-port rewrite (a45840d1) left exclude_network_folders=true and
include_qobuz_downloads=false unconditionally on every browse query since
2026-06-07. The owner-approved model replaces it: network-folder content
hides ONLY under hard offline (link confirmed down - no link means no LAN
mounts either); a logged-out session or induced offline with the link up
shows everything (the Qobuz verdict says nothing about NFS/SMB). The
show-network-folders toggle is retired from Settings (DB column stays,
Tauri-compatible).

Playback lock instead of hiding: local tracks get an existence check at
the play seam and in the advance walk (FileMissing verdict, 'is the drive
mounted?' toasts, never on the audio thread - D-state caveat documented).

Scan-cleanup guard: tracks under a network folder whose mount is
unreachable are never treated as missing (a refresh pressed before
remounting used to hard-delete the folder's whole index). mount_info: nfs4
and smb3-style version suffixes now classify as network (its red test is
green) and the longest-mount match respects path boundaries.

cc
2026-06-10 01:32:29 -06:00
vicrodh 5fd41e8026 feat(slint): offline content filtering and LocalLibrary offline default (D11/D12)
Offline, unavailable content is filtered out rather than shown broken:
sidebar, manager and folder popups list only local playlists and mixed
playlists with local sidecar rows (100% Qobuz hidden; empty folders
skipped); a mixed playlist opens an offline-subset detail rendering only
its local sidecar rows (plex rows under induced only, read-only, same row
machinery); MyQBZ grids and details retain only available items (qobuz =
cached album/track within grace, plex = induced only, local always) and
collections with zero available items drop from the grids.

Mixed-playlist names offline: kept from the session cache on a mid-session
flip; honest 'Playlist (N local)' fallback on a cold offline start (names
are not persisted anywhere locally - noted as a future membership-snapshot
roadmap item).

D12: an offline session entry roots navigation at LocalLibrary, and an
engine watcher auto-navigates there when the mode flips to offline while a
blocked Qobuz view is open (re-filtering open MyQBZ views in place). No
navigation on the offline-to-online edge.

cc
2026-06-09 22:43:23 -06:00
vicrodh da6e9599fb feat(slint): first-class local playlists with offline-only mode (D7/D8)
New local_playlists + local_playlist_tracks schema in the shared per-user
library.db (ids 'local:<uuid>', sources qobuz/local/plex, position-compacted,
8 repo tests in qbz-library). PlaylistRef routing at the navigation seam
makes local ids unrepresentable in Qobuz playlist calls by construction
(all take u64). Local playlists appear in sidebar, manager and picker
(picker hides the Qobuz set while offline); detail view reuses the existing
playlist row machinery and renders offline; creation modal gains the
offline-only toggle (forced on while offline - creating offline always
produces a local playlist, D8) and Edit can flip it later; explicit
Upload-to-Qobuz conversion for non-offline-only locals (confirm dialog,
local rows attach via the existing sidecar, entity kept on failure).

Never-to-Qobuz guard: queues built from an offline-only playlist carry a
core-level stamp cleared by every queue replacement; the QConnect sync
skips the cloud push and play-on-peer falls back to local playback while
the stamp is set.

Deferred (logged in the slice report): plex-row attach on upload, custom
reorder for local playlists, folder/favorite/hide on local items, manager
sort interleaving, offline cover thumbs for cached qobuz rows.

cc
2026-06-09 22:17:13 -06:00
vicrodh 74142e9a44 feat(slint): app-wide offline gating (placeholder, nav, playback fast-fail)
New OfflinePlaceholder (mode-aware copy + Open Settings shortcut for induced
mode) mounted at the AppShell routing seam over the 12 Qobuz-only content
views; the matching nav rows (sidebar, header tabs, compact rail) render
disabled while offline instead of hiding (layout stability). Playback gains
the offline playable rule (local always, plex only under induced, qobuz only
when cached and within the subscription grace): explicit plays fast-fail
with an honest toast (distinct copy when the grace expired), manual and
auto advance skip unavailable tracks bounded by 5 consecutive (gapless
target structurally exempt), gapless prefetch skips unplayable successors,
and the network prefetch warmer pauses offline.

cc
2026-06-09 21:26:25 -06:00
vicrodh b4caa5a309 feat(slint): Settings Offline Mode group (tri-state status, induced toggle, network folders)
New OFFLINE MODE group above the cache group on the Offline settings page:
read-only tri-state status row (online / no connection / offline mode
enabled, with captive-portal hint) plus an honest Check now that forces an
actor re-probe; Enable Offline Mode toggle driving the shared engine (issue
#279 stream_first_track snapshot/restore included, Playback panel re-synced
after); Show Network Folder Content toggle mounted only while induced
offline is on. Deliberate deviations from Tauri per the approved spec: no
probe gate on disable, no dead cast/scrobbling toggles.

cc
2026-06-09 20:46:05 -06:00
vicrodh 38e0b4be27 feat(slint): offline status UI plumbing, login affordances, recovery banner
New OfflineState global mirrored from the shared engine by a status
forwarder task. Login screen: start-offline link gated on a previous
session existing, connectivity-down callout pointing at it (with captive
portal hint), init-error callout fed from the restore flow. Shell: D2
recovery banner (connection back during an unauthenticated offline
session) with one-click token sign-in that lifts the offline gate for the
login attempt and re-enters the full online shell.

Double-init hardening for the recovery path re-running enter_shell over a
live shell: synchronous guard on the Linux tray init (its OnceLock was set
asynchronously, leaving a duplicate-tray window) and a start guard on the
playback poll loop (a second loop would double-advance the queue).

cc
2026-06-09 20:31:07 -06:00
vicrodh 10b13be6e3 fix(myqbz): detail row ⋯ menu was unclickable — open-item TouchArea sat on top
The DetailRow had a full-row `row-ta` TouchArea (clicked -> open the item/album)
declared AFTER the cell layout. In Slint a later-declared element paints ON TOP,
so row-ta was frontmost over the whole row and captured every click -- including
the ⋯ button -- so the menu never opened and the row just opened the album. Moved
row-ta to be declared BEFORE the cell HorizontalLayout, so it sits behind the
per-cell TouchAreas (checkbox / play / title / subtitle / ⋯) and only catches the
empty gaps. The ⋯ menu (and the play overlay / title) now receive their clicks.

cc
2026-06-09 15:45:24 -06:00
vicrodh 8b0df8325d fix(myqbz): context menus — z-index, Remove/queue wiring, sidebar + album->collection
- Detail row ⋯ menu (and the hero overflow + filter popups) were PopupWindows
  with no explicit height, so they collapsed and the rows below painted over them.
  Gave each an explicit height (+ close-on-click) like every other app menu, so
  they paint above the rows.
- Row menu Remove now actually removes: it was a no-op ("remove" wasn't a RowMode)
  -> wired to remove the item (repo::remove_item by position, reload). Add-to-queue
  was also silently broken (emitted "play-later", unparsed) -> now "add-to-queue".
- Sidebar playlist right-click menu: added "Add to Mixtape/Collection" (opens the
  picker with the playlist payload).
- Album -> Collection: an album can now be added to ANY collection kind incl.
  artist_collection (removed the ArtistCollection exclusion in load_rows; Tauri
  allows adding an album to an artist discography).

Offline in the row menu is net-new (Tauri's collection row menu has none; it would
be a batch resolve-then-cache) and is NOT added here. Backend crates untouched.
cargo check -p qbz-slint green.

cc
2026-06-09 15:17:03 -06:00
vicrodh 47fdfe6622 fix(myqbz): centre the detail ToolBtn glyph (Filter / Select)
The ToolBtn row used an absolute `x: 8/10px` offset together with
`width: preferred + padding`, which pushed the icon (and label) off-centre —
the Filter and Select buttons read as misaligned. Replace the x offset with
symmetric padding-left/right inside the HorizontalLayout and size the button to
the layout's natural width, so the glyph is centred deterministically.

cc
2026-06-09 14:41:03 -06:00
vicrodh ea517326b7 fix(myqbz): visual round 2 — local art, checkbox-filter, circular checkboxes, quality skeleton, album header
Per the user's 2nd visual review:
- Local/Plex album art now renders in the detail rows: resolve_items backfills
  empty-art rows from the first resolved track (source-aware dispatch ->
  spawn_local_or_plex_loads with live Plex creds); to_item re-hydrates from cache.
- Type/source FILTER homologated: a LABELED filter button (FavGenreButton idiom,
  accent-fill active) + a checkbox popup; the "Select" toggle is now ICON-ONLY
  (square-check-big, no text) matching Favorites/LocalLibrary.
- New primitives/SelectionCheckbox.slint (the canonical CIRCULAR 13px selection
  checkbox, accent fill, check / minus for indeterminate). Replaced all 5 square
  MyQBZ checkboxes (detail list+grid, filter popup, builder per-row + tri-state
  select-all). KindRadio dots stay round radios (single-select, correct).
- Quality column shows a LoadingDots skeleton while resolving (not empty).
- Album view header buttons: deleted AlbumPageView's private divergent CircleAction
  copy -> the shared primitive, so it matches every other view's thickness.

CollectionsView kind-filter stays QbzSelect (single-select group-by = the app's
group-by idiom). Backend crates untouched. cargo check -p qbz-slint green.

cc
2026-06-09 13:31:31 -06:00
vicrodh 0b20a1a86a fix(myqbz): visual homologation — app components, colors, scrollbar, resolveItems
Per the user's visual review. Replaces the reinvented MyQBZ controls with the
app's existing primitives and resolves the per-item data left unresolved.

Cross-cutting (all MyQBZ views):
- Removed the per-view "Back" header (the app uses the global < > nav).
- 32px L/R content padding so the ListScrollbar never rides over rows/cards.
- Homologated page padding + Typography tokens to Favorites/LocalLibrary.
- No more blue accent-filled pills / non-app colors.

Component homologation (deleted the reinvented locals + dead code):
- Grid/list + detail view-mode toggle -> the app ViewToggle (ToggleButton,
  surface-hover active, never accent). Builder ORDER-BY segmented de-blued too.
- Sort/filter -> QbzSelect (sm); search -> ExpandableSearch (sm); "+ New" ->
  NewActionBtn (surface-elevated circle, single centered glyph). Header "+ New"
  hidden when empty so the empty-state CTA is the only one (no duplicate).
- Deleted PrimaryCta / MyQbzSearch / ControlBtn / ViewSeg / ViewModeGroup /
  SortItem / ColMenuItem + the hand-rolled sort/filter popups + reset.

Data (resolveItems, was deferred):
- Quality column populated from the resolved tracks (QualityBadgeFull).
- New ui/primitives/SourceGlyph.slint: full-color Qobuz/Plex glyphs + a
  text-muted local hard-drive -- distinguishes Local vs Plex, no accent blob.
  Reused in the detail + builder rows.
- Type column shows the real release type (ALBUM/EP/SINGLE), not always ALBUM.
- Local/Plex album art now loads in the detail rows (split artwork dispatch).
- Collection grid card renders custom_artwork_path.
- Builder rows: 44px + a hairline so quality badges read separated.

Backend crates untouched. cargo check -p qbz-slint green.

cc
2026-06-09 12:35:03 -06:00
vicrodh 4e0d1a005e fix(myqbz): final fidelity pass (uppercase, trim, expanded cache, bulk bar §13.1)
Phase-2 Slice 12d.
- Discography Builder: uppercase eyebrow + ORDER BY + 6 column labels (BUILD ARTIST
  COLLECTION / YEAR/TYPE/ALBUM/TRACKS/SOURCE/QUALITY); Create now disables on a
  whitespace-only name (name-trimmed-empty computed in the controller).
- Expanded-mode: per-item inline-track cache keyed by the stable source|source_item_id
  (survives filter/sort/search re-derive instead of re-fetching), cleared on open.
- Inline tracks: go-to-album passes the parent item_type (playlist parent routes to
  the playlist view); duration 0 -> "--:--"; track number from the resolver order.
- Detail bulk bar: completed the §13.1 set (Add to queue, Play next, Add to playlist,
  Add to Mixtape/Collection, Remove, Clear) via MultiSelectBar slots + bulk_enqueue
  (reuses the resolver) + the existing playlist picker; no invented primitive.
- Stale comments corrected (dj-mix wired; mix-modal primary = Add to queue).

Backend crates untouched. cargo check -p qbz-slint green.

cc
2026-06-09 08:49:39 -06:00
vicrodh 16e6232220 refactor(myqbz): button audit — app primitives + fix glyph centering
Phase-2 Slice 12c (user directive: use the app's circular controls, no invented
buttons; fix the glyph-stuck-left bug).
- Hero §3.6: deleted the local ActionCircle; the 4 hero controls are now the
  shared CircleAction (Play primary = filled white disc + black glyph; the loading
  spinner is now available = more 1:1). Added `enabled` to CircleAction (additive).
- All MyQBZ modal footers (Create / Edit / Mix / AddTo / Discography) now use the
  shared QbzPrimaryButton (+ a new `destructive` variant for Delete) and a new
  shared SecondaryButton (promoted from AppearanceSettings' inline copy).
  QbzPrimaryButton auto-sizes as a footer chip while staying full-width in login
  (verified: login stretches it in a VerticalLayout; footers take preferred width).
- Kept legitimately-local icon+label controls (grid "+ New" CTA, AddToMixtape
  picker chips, toolbar sort/filter/view-mode pills) but verified every glyph is
  centered.
- Fixed 3 glyph-stuck-left bugs (DiscographyBuilder checkboxes used non-snapped
  centering -> the Math.round(.../1px)*1px crisp idiom).

Backend crates untouched. cargo check -p qbz-slint green.

cc
2026-06-09 08:29:11 -06:00
vicrodh 6cbd51427d feat(myqbz): detail expanded-mode inline tracks + headless skip-to-item
Phase-2 Slice 12b.
- Expanded view-mode: replaced the "Inline tracks coming soon" placeholder with
  real per-item inline track expansion. Matches Tauri 1:1 (no chevron -- every
  album/playlist row auto-shows its tracks). fetch_item_tracks reuses the shared
  ProdItemResolver (display-only; spec §8 routing matrix); renders the app's
  TrackRow indented; inline-track media actions (play/play-next/play-later/go-to).
- skip-to-item: headless myqbz_play::skip_to_next_item/skip_to_previous_item over
  the qbz-core queue using the crate's next_item_index/previous_item_index (3s prev
  rule). UI trigger DEFERRED -- Tauri has zero frontend callsites for skip-to-item
  (no skip-album button exists), so wiring one would invent UI; the global next()/
  previous() are untouched.

Known polish (S12 fidelity pass): expanded-mode per-item cache should key on
source|source_item_id (survive filter/sort) + viewport-gate; inline go-to uses
item_type; duration "--:--". Backend crates untouched. cargo check green.

cc
2026-06-09 08:09:31 -06:00
vicrodh 0bffd32555 feat(myqbz): My QBZ rename/change-icon in Settings>Appearance (DQ3)
Phase-2 Slice 12a. Per the user's decision (DQ3), the "My QBZ" rename/change-icon
lives in Settings > Appearance, not a sidebar context menu:

- src/myqbz_prefs.rs: per-user {label, icon_path} persisted to
  users/<id>/myqbz_branding.json (scoped like plex_settings/tray_settings);
  init_for_user/seed/set_label/pick_icon(rfd)/reset_icon. Blank label coerces to
  "My QBZ"; a missing/deleted custom icon falls back to the default my-qbz.svg
  without crashing (path preserved). 4 unit tests.
- ui/settings/AppearanceSettings.slint: a "My QBZ" SettingRow (icon preview +
  label LineEdit + Change icon... + Reset).
- ui/shell/Sidebar.slint: the My QBZ row binds label/icon to MyQbzBrandingState
  (raw-icon flag renders a custom user image untinted; default glyph stays tinted).
- Seeded on enter_shell.

Backend crates untouched. cargo check -p qbz-slint green.

cc
2026-06-09 07:40:44 -06:00
vicrodh c6aa61a09e feat(myqbz): Discography Builder — artist_collection creation flow
Phase-2 Slice 11. The third MyQBZ kind (artist_collection / artist_discography),
1:1 with spec 13 (the 1445-line DiscographyBuilderView):
- ui/myqbz/DiscographyBuilderView.slint: header (artist avatar + name + eyebrow),
  name input, 3-segment order-by, the 7-col grouped release table
  (28 56 72 1fr 72 60 156) with tri-state select-all, primary + alternate rows
  (↳ connector, source indicator, QualityBadge, per-row type-override popover),
  footer (count + Create). Exact §0 dark colors + type tints.
- src/myqbz_builder.rs: candidate/group model + normalize/classify/quality-score;
  THREE sources fully wired (Qobuz get_artist_page + local + Plex via the unified
  metadata query); build_groups dedupes/buckets/quality-sorts (Qobuz wins ties);
  persisted release-type override sidecar. Save = create_collection (ArtistCollection,
  ArtistDiscography, source_ref=artist_id) + add_item per checked candidate in sort
  order (Plex->local) -> navigate to the new collection's detail.
- Entry: the artist page "Build Artist Collection" button (was unwired).

Known polish nits (S12): view-wide uppercase eyebrow/labels; Create should disable
on whitespace-only name. Toast uses real strings (toast.collectionCreated/Failed are
absent from en.json; DQ4). Backend crates untouched. cargo check -p qbz-slint green.

cc
2026-06-09 07:19:06 -06:00
vicrodh 1db40d87ef feat(myqbz): DJ-mix modal + detail multi-select bulk bar
Phase-2 Slice 10.
- ui/myqbz/MyQbzMixModal.slint + src/myqbz_mix.rs: the DJ-mix "random queue"
  modal. Open resolves the collection in-order + shuffle::unique_track_count
  (deterministic) for the slider max (buildSizeOptions). Shuffle re-resolves,
  runs dedup_by_similarity + hybrid_sample with a thread RNG kept in a SYNC scope
  (dropped before the next .await -- !Send confinement), then replace-plays +
  stamps queue-source + touch_play; toasts "Playing N of M" when the per-album cap
  shrinks the pool. Wired on_dj_mix.
- Detail bulk bar (MultiSelectBar, bottom-docked): add-selected -> AddToMixtapeModal
  with the selected payloads; remove-selected -> repo::remove_item (highest position
  first so compaction does not invalidate pending positions) -> reload + clear
  selection + toast.

Deferred to polish: the bulk bar's queue / add-to-playlist / clear-X groups (§13.1).
Backend crates untouched. cargo check -p qbz-slint green.

cc
2026-06-09 06:42:39 -06:00
vicrodh 5da3b8bfd5 feat(myqbz): AddToMixtapeModal + app-wide add-to-collection entry points
Phase-2 Slice 8. Adds "Add to Mixtape/Collection" everywhere "Add to playlist"
lives, so collections can be populated:

- ui/myqbz/AddToMixtapeModal.slint: the picker (search + collection list with
  kind icon/name/count + "Already added" via item_exists + kind tag) and the
  create-new-inline panel (name + kind), per spec 21. Mounted once at root.
- src/myqbz_add.rs: list_collections (kind-restricted: track/playlist->mixtapes,
  album->mixtapes+collections, never artist_collection; recency-sorted); add via
  add_item_with(allow_duplicate=false) counting added/skipped-duplicates + toast;
  create_and_add reuses create_collection. All via with_db -> with_connection.
- Entry points wired: per-track (universal via TrackContextMenu/TrackRow -> search,
  favorites, album rows, local tracks, now-playing), Qobuz album, local/plex album,
  playlist (PM), bulk local tracks. Replaced the PlaylistManager add-to-mixtape no-op.
  Qobuz bulk omitted by design (spec 50 negative-space).

Note: adversarial reviewers were blocked by a session limit; verified manually
(cargo green, confined to qbz-slint, backend untouched, ADR-005/006, coverage per
spec 50).

cc
2026-06-09 06:19:35 -06:00
vicrodh 2a72878a1d feat(myqbz): detail edit — overflow menu, modals, play-mode/kind/cover
Phase-2 Slice 7. The hero overflow (⋯) menu and the edit operations:
- Hero action row corrected to spec §3.6: exactly 4 controls (Play primary,
  filled white disc + black glyph; Shuffle; DJ-mix [turntable glyph]; ⋯ overflow)
  -- replaces the S3 flat-circles deviation.
- ⋯ overflow menu (spec §10): Rename, Edit description, Upload/Clear custom cover,
  play-mode toggle (label = the OTHER mode), Convert kind (hidden for
  artist_collection), Delete (destructive).
- ui/myqbz/MyQbzEditModal.slint: Rename / Description / Delete-confirm modals
  (mirrors EditPlaylistModal chrome).
- src/myqbz_edit.rs: rename / set_description / toggle play_mode / convert kind /
  delete via with_db -> with_connection -> qbz_mixtape::repo::*; each reloads the
  detail, delete navigates back. "Converted"/"Cannot convert this kind" toasts.
- src/myqbz_cover.rs: custom-cover upload (rfd picker, ext validate, resize
  1000x1000 Lanczos3, save to artwork cache, persist-before-delete-prev) + remove.

dj-mix + sync stay stubs (later slices). Authoritative copy (Upload cover / Clear
custom cover / In order / double-quote delete). Backend crates untouched.
cargo check -p qbz-slint green.

cc
2026-06-09 04:38:35 -06:00
vicrodh be832324bd feat(myqbz): create-new Mixtape/Collection modal + wiring
Phase-2 Slice 4. The grid "+ New" buttons open a create modal (name + kind
radio: mixtape/collection, artist_collection excluded), create via the shared
crate, and drop the user into the new collection's detail (1:1 with Tauri's
submitCreateModal -> openMixtapeDetail). The new row prepends to its kind.

- ui/myqbz/CreateMyQbzModal.slint (mirrors CreatePlaylistModal/CreateFolderModal
  idiom; spec 30 create-modal geometry).
- MyQbzCreateState/MyQbzCreateActions in state.slint; mounted in AppShell.
- src/myqbz.rs create_collection via with_db -> with_connection ->
  qbz_mixtape::repo::create_collection (Manual, 80-char cap).
- Un-stubbed on_create_mixtape/on_create_collection; trim + busy guards; toast.

Backend crates untouched. cargo check -p qbz-slint green.

cc
2026-06-09 03:57:30 -06:00
vicrodh ec2a52707f feat(myqbz): Slint collection/mixtape detail view (read-only)
Phase-2 Slice 3. Opening a grid card navigates to the detail view:
- NavEntry::MixtapeDetail(id) + back/forward + scroll-restore; nav-in
  un-stubs the grid card click.
- src/myqbz_detail.rs: get_collection via the shared crate (hydrated items),
  load/apply/refresh deriving the render model (filter -> search -> sort).
- ui/myqbz/MixtapeDetailView.slint: hero (CollectionMosaic/custom cover, title,
  kind eyebrow, meta), sticky toolbar (search/sort/type+source filter/reset/
  select/view-mode), the item list in list + grid modes -- exact 8-col grid
  (40 1fr 140 80 160 72 60 40), 56px rows, per item_type, source/quality cells.
  Empty/loading/not-found states.

Read-only boundary: hero CTAs + per-row play/menu actions are logging stubs;
expanded-mode track expansion, in-detail modals, DJ-mix, bulk bar, and live
resolveItems (quality/plex source) are deferred. KNOWN DEVIATION: the hero
action row shows flat circles; the spec-3.6 layout (Play/Shuffle/DJ-mix +
MoreHorizontal overflow menu) is folded into Slice 7 (its overflow items are
the edit actions). Backend crates untouched. cargo check -p qbz-slint green.

cc
2026-06-09 03:43:56 -06:00
vicrodh c0d980e9c7 fix(myqbz): replicate the empty-state flash during load (1:1)
Drop the `&& !MyQbzState.loading` gate on the zero-items empty states so
"No mixtapes yet" / "No collections yet" shows during load instead of a
blank, matching the Tauri/Svelte behavior (empty array -> empty state
flashes until the load resolves). Per user decision (DQ: replicate).

cc
2026-06-09 03:06:12 -06:00
vicrodh 698fb2fa7f feat(myqbz): Slint My QBZ sidebar nav + read-only Mixtapes/Collections grids
Phase-2 Slice 2 of the MyQBZ port. Wires the mixtape DB + the first
visually-reachable surface in Slint (read-only):

- Migrations: run qbz_mixtape::schema::run_mixtape_migrations in enter_shell
  (via library_db with_db -> with_connection), best-effort, on session open —
  same per-user library.db the Tauri build migrates.
- Glue: new src/myqbz.rs mirrors playlist_manager.rs — list_collections via the
  shared crate (items hydrated), model/sort/filter builders, mosaic artwork jobs,
  navigate. NavEntry::Mixtapes/Collections + back/forward + scroll-restore scopes.
- UI: ui/myqbz/{MixtapesView,CollectionsView,CollectionMosaic,MyQbzShared}.slint,
  1:1 with specs 10/11 (208px grid, mosaic custom->2x2/3x3->kind-glyph, toolbar,
  both empty states, ICU plural). MixtapeCardItem + MyQbzState/MyQbzActions in
  state.slint; mounted in AppShell. My QBZ sidebar entry routed to the two grids
  (branded mask-tinted icons), matching the port's existing flyout-nav idiom.

Read-only boundary: create/open-detail are logging stubs; titlebar My QBZ
dropdown and the Settings>Appearance rename (DQ3) are deferred to later slices.
Backend crates untouched. cargo check -p qbz-slint green.

cc
2026-06-09 02:49:44 -06:00
vicrodh f508758d32 feat(slint): Local Library folder management — compact table + per-folder sync/delete
- Replace the folder list with a compact table (sticky header: FOLDER / LAST SCAN /
  STATUS + actions, internal scroll).
- Per-row sync (scans only that folder) and per-row delete (removes the DB entry +
  indexed tracks, never the files); delete no longer relies on rfd/zenity (which
  fails-closed on zenity-less systems) — direct removal + a toast.
- Fix the row event order so the per-row buttons receive their clicks instead of
  toggling selection.
- Fix mis-centered button glyphs (Stop, change-path, scan, tag-editor search): an
  inner layout filled its parent so the centering collapsed to x:0 — use explicit
  padding.

cc
2026-06-09 01:44:48 -06:00
vicrodh 41c5261613 feat(slint): local album homologation, Recently Played fixes, local artist nav
Album view (local/Plex):
- Rebuild LocalAlbumView as a near copy of the Qobuz AlbumPageView (whole-page
  scroll, 224px cover, action row, toolbar with quality badge + track search,
  column header, source-aware rows) minus the label/awards sidebar and Qobuz
  context menus. Add a per-album client-side track search.

Recently Played (Home) — D:
- Carry the track source (qobuz|plex|local) through the recently-played store so
  the cards resolve source-aware artwork (PlexThumb / local file), the album-card
  play routes to local/Plex playback instead of the empty Qobuz path, and the
  card click opens the right album view.
- Fix a regression from the Plex version split: local_queue_track now derives the
  content-hash plex_album_key for album_id (navigation) while album_group_key keeps
  the per-edition split key, so "go to album" and recent Plex albums open correctly.

Local artist navigation — B:
- Implement open_artist (was a TODO): latch the requested artist and select it once
  the Artists tab loads. The shared open-artist is now source-aware (numeric id ->
  Qobuz artist; name -> LocalLibrary Artists tab). Wire the LocalAlbum header artist
  link and the now-playing "Go to artist" for Plex/local.

Folder delete -> Recently Played cascade:
- Removing a Local Library folder now also prunes its albums/tracks from the
  recently-played store (album_keys_in_folder + recently::prune_albums).

Covers — C:
- Robust on-disk folder cover lookup (cover/folder/front/art/<album>.jpg, any image
  as a last resort), shared by the Folders subcards and now-playing/album rows.

cc
2026-06-09 01:44:39 -06:00
vicrodh 46673b2077 feat(slint): extend scroll-position restore to all views
Wire the per-history-entry scroll memory (added for Local Library) into
every top-level view: Home/Discover, Album, Local Album, Artist, Musician,
Label, Label Releases, Location, Favorites, Mix, Playlist, Playlist Manager,
Offline Manager, Search, Settings.

Each view's primary scroll container reports its viewport-y and restores it
on back/forward via NavState.restore-scope (scope_for in main.rs maps each
entry to a stable id; tab views carry the tab, e.g. "fav:albums"). The
container restores once per arm, only after content lays out, then clears
the scope. A `changed sr-armed` trigger covers views that persist across
sub-tab navigation (Home's Discover tabs, Favorites' shared list, Playlist
A->B->A) where no fresh mount occurs.

ScrollView-based views (Offline Manager, Settings) use visible-height for
the overflow check; ListView/Flickable views use height. Favorites' shared
albums/artists/playlists/labels Flickable matches the active tab's scope
dynamically.

cc
2026-06-08 15:53:40 -06:00
vicrodh 641aa22c17 feat(slint): scroll-position restore on back/forward (Local Library)
Scroll-position restore was never implemented (only a TODO in nav.rs);
back/forward tracked which page was visited, not where it was scrolled.

Add per-history-entry scroll memory: the mounted scroll container reports
its live viewport-y via NavState.report-scroll, so every navigation (fresh
record + back/forward) stamps the outgoing entry without touching the ~30
record call sites. go_back/go_forward hand the saved scroll back to the
shell, which arms NavState.restore-scope + scroll-restore before switching
the view. A container restores only when its scope matches, once per mount,
and only after content has laid out (viewport overflows) to dodge the
clamp-to-0 on async data load; it clears the scope once consumed.

Wired the four Local Library lists (albums, tracks, folders, artists rail).
Tracks is paginated, so its restore is best-effort within loaded pages.

cc
2026-06-08 15:38:35 -06:00
vicrodh ed7891c697 feat(slint): play-fetching spinner (now-playing bar + track row + album button)
A play can take seconds before audio starts (esp. Plex, whose resolve downloads
the whole file) with no feedback today. Add a source-agnostic loading indicator:

- A loading flag keyed to PENDING_PLAY_ID, raised at the top of play_audible
  (before the Plex/Qobuz/local resolve — single chokepoint, covers auto-advance)
  and cleared by the poll loop when the requested track's audio actually starts
  (position > 0). Cleared on every play-error early-return and force-cleared on
  queue-finished / peer-controller / ephemeral-stop so it never sticks.
- Watchdog: force-clear after 45s if the engine accepted a play that silently
  never advances (undecodable-but-valid file) — otherwise the spinner is eternal.
- Spinner on three surfaces driven by NowPlayingState.loading: now-playing bar
  (cover overlay + play/pause button), the active track row's play cell, and the
  album big play button (gated to the album actually being launched).

Audio backend / playback routing untouched. The spinner only communicates the
wait; the underlying Plex fetch latency is fixed separately (progressive streaming).

cc
2026-06-08 12:40:06 -06:00
vicrodh 5939c090ca feat(slint): Plex quality hydration on album-open + consistent quality badges (port slice 6)
Hydration:
- qbz-plex: additive plex_hydrate_album_quality(base, token, rating_keys) — per
  key plex_get_track_metadata (batch 5, 5s/call timeout, skip failures) then the
  COALESCE write-back plex_cache_update_track_quality; awaited before re-read.
- qbz-player/qbz-core: patch_plex_quality / patch_plex_queue_quality patch the
  in-memory queue in place by rating_key (for the now-playing stamp).
- qbz-slint: album-open trigger (gated on DB-NULL, not a value heuristic) then
  refresh of all badge surfaces — album-card grid (re-read aggregate + patch
  every model + the raw filter source), album-detail header + rows, now-playing
  stamp. Re-sync stays routed through plex_cache_save_tracks (carry-over).

Quality badge consistency (the four surfaces must agree — and pre-hydration
show the filetype, not nothing):
- One shared classifier crate::quality::badge(format, bit_depth, sample_rate) ->
  (tier, detail, tooltip); the album card, album-detail header and track rows
  all route through it (kills the per-surface duplication that produced
  "96 kHz" vs "96000 kHz"). sample_rate normalized to kHz via a >=1000 guard so
  Qobuz's already-kHz values are never re-divided (no "0.041 kHz").
- New "lossless" tier: an un-hydrated lossless file (FLAC/WAV/ALAC… with unknown
  bit depth) shows a generic filetype badge ("FLAC") instead of an empty badge,
  rendered in QualityBadge (compact chip) + QualityBadgeFull; upgrades to
  "24-bit / 96 kHz" after hydration. Local/Qobuz/mp3 unchanged.

cc
2026-06-08 10:27:28 -06:00
vicrodh f7619a92fc feat(slint): Plex creds store + Settings auth/PIN UX (port slices 0-2)
Fresh Slint-only Plex integration foundation (Tauri untouched; the Tauri
code is slated for deletion at port end, so creds are not shared):

- slice 0: fix the false Plex docs in local_library.rs
- slice 1: add the qbz-plex dependency to qbz-slint
- slice 2a: qbz-app/settings/plex.rs per-user store (10 fields, idempotent
  4->11 column migration keeping creds, 8 tests) - frontend-agnostic,
  no tauri::State (ADR-006)
- slice 2b: Settings > Local Library Plex section, 1:1 with the complete
  Tauri feature - master toggle + collapse, LAN-only address gate, PIN auth
  (2500ms poll, self-stopping on section-leave/expiry/disconnect),
  manual-token fallback, ping + machine-id capture, multi-section pick
  (default-all), disconnect vs clear-cache; once-per-session background
  refresh (no cache_clear churn on section re-entry)

cc
2026-06-07 21:44:33 -06:00
vicrodh 82129cff35 feat(slint): LocalLibrary ephemeral folders + Folders tree 1:1 port
Ephemeral folders: open / browse / play a folder without indexing it —
album-grouped pane, CUE seek fast-path (no full re-decode per virtual track),
folder-path persistence + rehydrate, "already playing" add-vs-replace choice
dialog (ephemeral-only), and now-playing / queue / menu gating so an ephemeral
track exposes no persistence actions.

Folders tree brought 1:1 with Tauri:
- Rail header: select-mode, collapse-all, open-ephemeral, tree search.
- Resizable rail (default ~20% narrower; drag handle, min 50% default /
  max 50% of the content area).
- Subfolder cover grid + grid/list toggle, recursive track count, name search;
  compact icon bulk bar (folder-check / play-next / queue / playlist / clear).
- Tree is the default mode; circular checkboxes homologated with TrackRow;
  compact rows (smaller type + icon, no ambiguous trailing count).
- Flat/Tree icon toggle (disc-album / folder-tree), floated right.

Per-source TrackRow context menu (logic, not a blanket trim): Qobuz/offline =
full; local = play/next/queue + add-to-playlist; plex/ephemeral = play/next/
queue only.

App-wide standard: Math.round pixel-snap on icon centering across the UI to
kill sub-pixel blur (CircleAction, header back/forward/collapse, and the rest).
NowPlayingBar context icon -> turntable (frees disc-album for the Folders
toggle). New assets: folder-check, folder-tree, turntable.

cc
2026-06-07 10:12:59 -06:00
vicrodh 8a78c2250f feat(slint): Linux system tray (ksni) + Wayland-safe close-to-tray
Port the Tauri tray to Slint on Linux via ksni: themed icon (auto/mono/
color), left-click window toggle, middle-click play/pause, scroll volume,
the 5-item menu, and a live tooltip reflecting the now-playing track. Tray
actions call the playback controller directly (QConnect-aware) instead of
emitting webview events. Settings reuse the shared qbz-app tray store
(per-user) wired through AppearanceState.

Window hide/show uses Slint window.hide()/show() (winit set_visible is a
no-op on Wayland) and the loop runs via run_event_loop_until_quit so a
tray-hide keeps the app alive. close-to-tray is honored on both the custom
titlebar close and the WM close (on_close_requested). minimize-to-tray is
hidden until custom window chrome exists.

cc
2026-06-06 01:44:38 -06:00
vicrodh 5bc6552e98 feat(qbz-slint): queue drag-and-drop reorder (WS-authoritative when connected)
Add drag-and-drop reordering to the Queue sidebar: a per-row drag handle, a
floating drag ghost, and a drop indicator. When a QConnect transport is
connected the reorder is routed to the cloud (CtrlSrvrQueueReorderTracks) so
order stays WS-authoritative; otherwise it moves the local queue track via
core().move_track. The Flickable is non-interactive so a stale pressed_time
cannot strand the gesture and leave phantom scroll after release.

cc
2026-06-05 21:09:33 -06:00
vicrodh 712447eb00 revert(qbz-slint): remove hover-reveal scrollbar feature
Restore ListScrollbar, TrackRow and AlbumPageView to their pre-scrollbar
state (always-visible scrollbars). The per-row hover-reveal only revealed
the bar over track rows, not over the whole scrollable viewport, so on
mixed-content views (Artist, Label) it felt incomplete. Card sizing
(Album/Playlist/Radio at 200px) is kept.

cc
2026-06-04 16:17:10 -06:00
vicrodh af2ca1b815 feat(qbz-slint): RadioCard 200px + hover-reveal scrollbar base
- RadioCard enlarged to 200x246 to match Album/Playlist cards; ForYouView +
  Spotlight row-width math updated to 200.
- Scrollbar hover-reveal: ListScrollbar gains a reveal prop (default true, no
  regression) and fades on opacity; TrackRow exposes hover-changed; AlbumPageView
  wired as the verified template (row-hover count -> reveal).

cc
2026-06-04 13:15:45 -06:00
vicrodh cdb5aa0072 style(qbz-slint): album + playlist cards to 200px, fonts -2/-1
AlbumCard and PlaylistCard are now 200x246 (was 220x266), with the larger
texts -2px and the smaller ones -1px. All album/playlist carousels and grids
follow (card-width/height 200/246) so nothing reflows: Carousel,
PlaylistCarousel, AlbumCollectionView/AlbumGrid, ReleaseGrid, Spotlight,
HomeSkeleton, DiscoverBrowseView, SearchResultsView, FavoritesView. Artist /
radio cards are untouched.

cc
2026-06-04 12:32:47 -06:00
vicrodh 14e3e3689c feat(qbz-slint): Queue+Lyrics stacking, NowPlayingBar polish, shell fill fix
NowPlayingBar / SongCard:
- Audio stamp homologated New<->Classic + vertically centred (alignment:center
  on the stamp column; New had a binding-loop card height, now explicit).
- Dot-LED output indicators (circle-dot/circle) in both modes; render-target
  badge moved into the LED row when a peer/cast device owns playback.
- Right-cluster buttons made square (32x32); context glyph -> disc-album,
  flush-left; classic stamp right margin; quality tooltip = the quality text;
  heart tooltip = Add to library; Qobuz Connect tooltip always shows On/Off.

Qobuz Connect:
- Clear the session topology on disconnect so a late event cannot resurrect
  the golden render badge (badge-persists-after-disconnect fix).

Queue / Lyrics right panel:
- Queue and Lyrics can both be open at once, stacked vertically in one column
  (independent queue-open/lyrics-open flags replace the exclusive right-panel
  int); each closable from its bar button and its own X.
- Panel width 280; compact rows (zebra), now-playing card homologated with a
  row, flexible footer search, Lyrics title sized like the Queue tabs.

Shell layout:
- Pin AppShell to the window and size the content area arithmetically
  (window - header - player-bar - padding). A Slint VerticalLayout ignores an
  explicit height and does not auto-fill its parent Rectangle, so the content
  row collapsed once the right panels became conditionally mounted; a Rectangle
  takes the explicit height and it cascades up. Bar height references the single
  Layout.player-bar-height source (note left for the future Small/Large bars).

cc
2026-06-04 11:49:24 -06:00
vicrodh 485be7d16c fix(qbz-slint): Qobuz Connect controller-mode reflection + polish
From the controller-mode debug session. All peer-facing paths gate on
is_peer_renderer_active, so renderer / local / disconnected playback is
unaffected.

- Now-playing bar + queue follow the peer authoritatively: reflect the peer's
  current track / position / volume, refresh on the peer's track changes, and
  fetch a fresh renderer state on becoming a controller (so the bar isn't stale
  until the next track change; this also fixed remote pause, which had been
  sending a stale current_queue_item).
- Route play-next / add-to-queue (single track) to the peer via
  QueueInsertTracks / QueueAddTracks (insert-after the peer's current item);
  multi-track album/playlist enqueue stays local for now. Clear the superseded
  SetVolume pending to stop the volume-drag command spam.
- Clean disconnect: disarm the liveness watchdog + clear the device list / cast
  state, so no spurious "renderer unreachable" toast and no lingering badge.
- Volume lock only for local ALSA-Direct bit-perfect (backend Alsa + plugin Hw),
  lifted while controlling; reflect the peer's volume with a 50% safety clamp
  when unknown.
- Connect button golden whenever a session is on; +2px now-playing bar height.

cc
2026-06-03 17:28:53 -06:00
vicrodh e845c59fea feat(qbz-slint): per-device-type icons in the QConnect device picker
Mirror Qobuz Connect's device icons in the connect flyout: map each renderer's
device_type to an icon (6 -> smartphone, 5 -> monitor or globe when the name is a
web player/browser, else -> speaker), matching the Tauri QconnectBadge
resolveDeviceType. Adds lucide monitor/smartphone/speaker/globe svgs, an icon key
on the QconnectDevice model computed in the event sink, and a per-row icon in the
PlayerBar device list.

cc
2026-06-03 11:19:14 -06:00
vicrodh 9bb5e6db26 feat(qbz-slint): port the local bit-perfect volume lock + force-100 (QConnect parity)
Port the rest of Tauri's volume logic to the Slint bar.

PlayerBar gains a derived vol-locked reproducing Tauri's handleVolumeChange
formula exactly: (alsaDirectHw && !isRemote) || volumeLocked. The local
ALSA-Direct (bit-perfect) lock is lifted while controlling a peer (the
!is-remote term) so the user can adjust the remote renderer; the sink-pushed
NowPlayingState.volume-locked keeps meaning only the peer-disallow half. All
five volume-interaction gates and the slider opacity now read vol-locked.

Force the local player to unity gain (set_volume(1.0)) in bit-perfect, mirroring
Tauri's playerSetVolume(100): when the backend is ALSA + the hw plugin AND a
peer is not the active renderer, set core volume to 1.0 and push
NowPlayingState.volume = 1.0. Applied (a) at the startup audio-settings load and
(b) on backend / alsa-plugin change. Gated on a new is_peer_active() so the
force-100 is skipped while controlling a peer. core().set_volume is the safe
seam; the protected device-init is untouched.

cc
2026-06-03 10:47:38 -06:00
vicrodh 0d7ac7930d feat(qbz-slint): QConnect device picker + cast-aware now-playing state
Add the CONTROLLER-mode device picker. New QconnectDevice struct + devices /
active-renderer-id / set-active on the QconnectDevState global (re-exported via
app.slint so include_modules! generates the type). The connect flyout replaces
the "No devices found" placeholder with a plain-row list (ADR-008: no pills)
when session renderers exist: each row taps to set-active and closes the menu;
the local device renders as "Play here" to pull playback back to QBZ. A
"Playing on: {name}" header shows while a peer is active.

The event sink rebuilds the device list and pushes the cast-aware now-playing
state (is-remote / cast-target / volume-locked) from session topology after
every event, replacing the prior TODO. main.rs wires on_set_active to the new
SlintQconnectService::set_active_renderer (over send_set_active_renderer).

Wired but unverified (blind build, no phone+speaker); device round-trips need a
smoke test. Slice 4 (peer-state reflection in the poll loop) deferred.

cc
2026-06-03 07:08:46 -06:00
vicrodh b0274c39d4 feat(qbz-slint): move QConnect diagnostics to Settings > Developer
Relocate the QConnect diagnostics launcher out of the now-playing bar flyout
into a new opt-in Settings > Developer section (the modal + DEV log/status code
is unchanged). Also make the status block selectable (read-only TextEdit) so
session_uuid etc. can be copied, and relabel the event log.

cc
2026-06-03 00:36:59 -06:00
vicrodh 44876d01be feat(qbz-slint): Qobuz Connect renderer + controller integration
Port the QConnect adapter to the Slint frontend so qbz acts as a full peer
(renderer + controller), inheriting the shared qconnect-app hardening.

- Device-info / join payloads + transport config (piece a).
- SlintQconnectEventSink: queue materialize / renderer-command apply /
  session-management via the shared qconnect-app orchestration; renderer
  ready-report on SetActive(true).
- SlintSessionLoopHost + service connect recipe (subscribe-before-spawn) +
  controller bootstrap + deferred renderer-join; connect/disconnect toggle
  wired in the now-playing bar flyout.
- Renderer sync: outbound RndrSrvrStateUpdated reports (position / current
  queue item / file+device audio quality) while qbz is the active local
  renderer, and inbound UI refresh (now-playing card + queue sidebar) after
  remote queue / state changes.
- Controller-side queue push: when the local queue changes while connected,
  push it to the session (QueueLoadTracks) with all-or-nothing admission
  (refuse mixed local/Plex queues; offline qobuz_download is eligible) and
  echo suppression against the last-applied / last-pushed queue.
- Error + lifecycle toasts; DEV diagnostics modal (live session status +
  selectable rolling event log) opened from the connect flyout.

cc
2026-06-03 00:11:37 -06:00
vicrodh 1ad0d7c169 feat(slint): classic now-playing bar layout
Add the second of the four switchable now-playing bar layouts plus the
4-layout architecture behind it.

ShellState.npb-mode (0=New, 1=Classic) drives mode-conditional columns in
PlayerBar; the transport cluster is extracted into TransportControls.slint and
IconButton/PlayButton into primitives/BarControls (PlayButton gains a `circle`
prop) so both layouts share them without duplicating the popups. The choice
persists via UiPrefs.npb_mode ("new"/"classic") — restored on startup, written
on the "Now Playing view" menu select.

Classic = the Tauri arrangement: transport LEFT (plain play glyph, leading
track-info, inline add-to-playlist + favorite-with-state), glass song card
CENTRE, actions + volume RIGHT. SongCard.glass is a contained card capped at a
fixed 520px; the audio stamp is ABSOLUTELY pinned to the card's right edge
(cover 56 | song info that truncates with ellipsis | stamp 140) so it stays
flush right regardless of the text layout. Track info fires from both the
title click and the transport (i). Classic uses the full-size audio stamp; the
backend LED reads PIPEWIRE.

cc
2026-06-02 11:32:01 -06:00