Commit Graph
5095 Commits
Author SHA1 Message Date
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 838085a492 fix(myqbz): per-collection view-prefs persistence (§18) + shuffle persists play_mode
Phase-2 Slice 12e (final fidelity touch-up).
- src/myqbz_view_prefs.rs: per-user JSON store (users/<id>/collection_view_prefs.json),
  keyed by collection id, persisting the five §18 fields (view_mode, sort_by, sort_dir,
  type_filter, source flags qobuz/plex/local). searchQuery + selectMode stay transient.
  Restore-on-open with a PREFS_HYDRATED gate (early setters can't clobber the load);
  persist on every toolbar change (sort/type/source/reset/view-mode); clear on delete.
  3 unit tests. 1:1 with Tauri's collection-view-prefs.{id} + prefsHydrated.
- myqbz_play::shuffle now persists play_mode='album_shuffle' (best-effort set_play_mode +
  reload so the overflow play-mode label flips) ONLY when not already album_shuffle --
  mirrors handleShuffle; normal Play never persists.

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

cc
2026-06-09 09:08:05 -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 b049b1cd5c feat(myqbz): detail playback — resolve collection + drive the queue
Phase-2 Slice 5. Wires the detail view's hero Play/Shuffle and per-row play
actions to the qbz-mixtape enqueue resolver:

- src/myqbz_play.rs: builds ProdItemResolver (qobuz client snapshot-cloned + a
  Send+Sync local closure that runs with_db SYNCHRONOUSLY, so &LibraryDatabase
  never crosses an .await), resolve_collection_tracks, drives the qbz-core queue.
- Hero Play = replace per the collection play_mode + stamp queue-source +
  touch_play. Hero Shuffle = force album_shuffle. Per-row Play/Play-next/
  Add-to-queue = single-item resolve with inline source_item_id_hint, no
  queue-source stamp/touch_play; play-next inserts in reverse. Failing items
  logged+skipped; empty resolve toasts "0 playable tracks".
- Un-stubbed on_play_all/on_shuffle/on_play_item/on_item_action; dj-mix/edit/
  delete/sync stay stubs (later slices). skip-to-item deferred.

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

cc
2026-06-09 04:08:31 -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 5458f3aa74 feat(mixtape): extract shared qbz-mixtape crate (MyQBZ backend, headless)
New crate crates/qbz-mixtape holding the Mixtapes+Collections backend, ported
1:1 from src-tauri/src/mixtape and re-typed against the real shared crates so it
runs headless (no tauri::State, no #[tauri::command] wrappers) per ADR-005/006:

- schema.rs / repo.rs / shuffle.rs copied verbatim (SQLite schema + migrations,
  CRUD repository, DJ-mix similarity sampler). enqueue.rs re-typed to
  qbz_qobuz::QobuzClient, qbz_library::LibraryDatabase and qbz_plex, dropping the
  src-tauri crate::api / crate::library shims.
- ItemResolver is frontend-agnostic: ProdItemResolver takes a caller-supplied
  sync local-resolution closure so &LibraryDatabase (!Sync) never crosses an
  .await. resolve_local_item centralizes the full Local dispatch matrix
  (Album / Track i64-parse / Playlist-unsupported) so frontends cannot drift.
- src-tauri left untouched: Tauri is removed at the end of the Slint port, so the
  temporary duplication is intentional and zero-risk to the working Tauri build.
  Adds qbz-mixtape to the workspace members + rand/strsim workspace deps.

Phase-2 Slice 1 of the MyQBZ Slint port. 57 crate tests pass.

cc
2026-06-09 02:16:20 -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 1409c9d917 feat(plex): split same-title Plex albums into selectable versions
A Plex album_key is a hash of artist+album, so two distinct Plex albums
that share a title (e.g. a matched album plus loose files added as a second
album) collapsed into one cache album and their tracks interleaved
(1,1,2,2,...) in the album view with no way to pick a version.

Capture parentRatingKey (the real per-album Plex id) on sync, store it in a
new plex_cache_tracks column, and key the album-view version grouping on it
so each Plex album becomes its own version in the existing header picker
(best quality first). Pre-resync rows lack the column and fall back to the
old single-group behaviour; a re-sync backfills it.

cc
2026-06-08 18:28:15 -06:00
vicrodh ca4b43d635 fix(slint): route Plex/local album opens to the LocalAlbum view
The shared open-album callback always recorded a Qobuz Album entry, so a
Plex/local item (Home "Recently played", the now-playing bar's "Go to
album") opened an empty Qobuz album view. Detect the Local-Library/Plex
metadata group key (album|artist, a folder path, plex:, or the
__unknown_album__ sentinel — none of which appear in numeric Qobuz ids) and
route those to the LocalAlbum view instead.

cc
2026-06-08 17:38:37 -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 b37c29af14 feat(slint): Plex progressive streaming (fast play) + fix re-sync wiping hydrated quality
Two Plex-port fixes (both in the Plex play/data path):

1. Progressive streaming — play starts in ~1s instead of ~10s. play_plex_audible
   no longer downloads the whole file before play_data; it resolves only the
   part URL (new additive qbz_plex::plex_resolve_part_url, no body GET) and feeds
   it into the player's streaming source (play_streaming_dynamic) via a shared
   feeder module (remote_stream.rs, extracted from the QConnect renderer's HTTP
   feeder — both now share it). Bit-perfect preserved: the original /file bytes
   are streamed (no transcode), device init derives from the decoded stream; the
   protected audio backend is untouched. Cast/DLNA keep the whole-file path.
   Falls back to whole-file play_data on a non-direct part / streaming-setup error.

2. Re-sync no longer wipes hydrated quality — the sync did plex_cache_clear()
   (full wipe) BEFORE the per-section save_tracks, defeating save_tracks' COALESCE
   hydration carry-over, so already-hydrated albums reverted to a bare FLAC badge
   on every settings-open / background-refresh / section-toggle. Replaced the full
   wipe with a new additive plex_cache_prune_sections(keep) that removes only the
   DE-selected sections, keeping the selected sections (and their hydration);
   save_tracks then re-applies its per-section carry-over.

cc
2026-06-08 14:39:05 -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 48a78192c0 chore: sync src-tauri Cargo.lock with qbz-models urlencoding dep
The Plex artwork-perf slice added urlencoding/url to qbz-models; the lockfile
sync only landed in crates/Cargo.lock. src-tauri depends on qbz-models, so its
lockfile needs the same entries (no behavior change).

cc
2026-06-08 10:27:28 -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 a680da5ad8 perf(slint): server-transcoded Plex thumbs + shared HTTP client for images
Plex cover images were fetched full-res on every surface (Slint built the raw
/library/.../thumb URL), where Tauri requests a server-transcoded thumb sized to
the render target. On a real library this made the views feel heavy and starved
the LAN bandwidth that track playback needs.

- qbz-models: ArtworkRef::PlexThumb gains a size: Option<u32>; new plex_thumb_url()
  emits the Tauri-format transcode URL when Some
  (/photo/:/transcode?url={enc}&width=N&height=N&minSize=1&X-Plex-Token=), raw
  tokenized path when None (album/MPRIS full-res preserved). urlencoding dep added
  to match encodeURIComponent. Tests extended.
- qbz-slint: each PlexThumb construction site passes its surface decode size
  (grid/detail 264, queue/now-playing-item 96, now-playing bar 160, MPRIS full-res);
  fetch_and_decode_ref uses plex_thumb_url.
- artwork.rs: static LazyLock<reqwest::Client> reused in fetch_cached_http instead
  of reqwest::get (per-image client) — keep-alive + no fd churn (EMFILE smell).

Audio path untouched; local/Qobuz artwork unchanged.

cc
2026-06-08 09:03:37 -06:00
vicrodh 381c0b1707 feat(slint): source-aware Plex artwork on now-playing bar, queue panel, MPRIS (port slice 4)
A Plex queue track's artwork was file://-prefixed onto its raw /library/ thumb
path, breaking the cover on all three surfaces. Approach (b) from the funnel
review: keep the raw path in QueueTrack.artwork_url (token never persisted) and
resolve to a fresh tokenized PlexThumb from live plex_settings creds at each
surface.

- qbz-models: additive QueueTrack::artwork_ref_with_plex(base_url, token) — a raw
  /library or /photo path + creds => ArtworkRef::PlexThumb, else falls back to
  artwork_ref() (http/file:// unchanged). 4 regression tests.
- playback.rs: local_queue_track keeps the raw path for Plex rows (no file://
  poison); refresh_now_playing_meta uses artwork_ref_with_plex -> fixes the
  now-playing bar, MPRIS (to_mpris_url), and the desktop notification.
- queue.rs: load_artwork threads creds + a PlexThumb branch for the queue panel.
- Local/Qobuz/ephemeral artwork byte-for-byte unchanged.

cc
2026-06-08 08:32:06 -06:00
vicrodh 334ddf315d feat(slint): Plex artists + tracks in Local Library Artists/Tracks tabs (port slice 3c)
Both tabs were Plex-blind. Loader-level merge (no qbz-library change):
- qbz-plex: additive PlexCachedArtist + plex_cache_get_artists() (aggregates
  the flat plex_cache_tracks by normalized artist) + plex_cache_count_tracks().
- Artists tab: fold Plex artists into the local set before merge_artists
  (same-name local+Plex collapse, summed tracks); artist-detail album grid +
  counts switch to the Plex-aware get_albums_metadata_page; portraits fall back
  custom/Qobuz -> Plex thumb -> album cover, dispatched via spawn_local_or_plex_loads.
- Tracks tab: prepend the bounded Plex set (<=5000) once at offset 0; later
  pages stay pure-local so the virtualized 16K-row LIMIT/OFFSET path is intact.
- Tracks badge now includes the Plex count (matches albums/artists badges).
- All gated on plex_settings enabled (Plex OFF = byte-for-byte local-only).

Plex track id namespaced into [2^40, 2^41) so it cannot collide with local
track ids (< 2^40) or ephemeral ids (>= 2^48) in the merged Tracks tab — the
string rating_key is preserved in file_path, so playback is unaffected.

cc
2026-06-07 23:43:34 -06:00
vicrodh 06b2bea932 feat(slint): Plex track playback (port slice 5)
Plex tracks produced no audio: local_queue_track had no "plex" arm so they were
enqueued as source="local", and there was no Plex resolve+play path.

- local_queue_track tags source="plex" and carries the string rating_key in
  source_item_id_hint (the numeric queue id is a hashed/parsed form; the resolve
  needs the original key); persisted in the session queue store.
- play_audible gains a "plex" arm -> play_plex_audible.
- play_plex_audible: qbz_plex::plex_resolve_track_media(base, token, rating_key)
  -> player.play_data(bytes, id), mirroring the local-file seam. Bit-perfect:
  play_data drives device init from the DECODED bytes; Plex metadata is
  display-only. Covers album play, single track, and auto-advance.

cc
2026-06-07 22:39:19 -06:00
vicrodh a45840d178 feat(slint): Plex albums in Local Library grid + detail + covers (port slice 3a/3b)
Albums tab only (Artists + flat Tracks tab remain Plex-blind — next slice):
- Albums loader switches to get_albums_metadata_page(.., Some(plex_cache_path))
  so the plex_aggregated union surfaces Plex albums alongside local; gated on
  plex_settings enabled (None path = local-only, no regression). seed_counts
  uses the page total so the badge includes Plex.
- Album-detail branches group_key "plex:" -> qbz_plex::plex_cache_get_album_tracks,
  mapped to LocalTrack (rating_key in file_path, source=plex, album name into
  album_group_title for the detail header).
- Source-aware grid + album-detail-header covers via spawn_local_or_plex_loads:
  a /library|/photo path with creds becomes ArtworkRef::PlexThumb, else LocalFile.
  Queue/now-playing/MPRIS artwork untouched (that is slice 4).
- Truthful module/Albums-tab docs.

cc
2026-06-07 22:39:19 -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 ee9a3c1c89 refactor(qbz-library): extract ephemeral-folder module to the shared crate
Lift the in-memory ephemeral-folder library (synthetic-id store + folder
scan / metadata / CUE handling) out of the Tauri binary into a
frontend-agnostic qbz-library module (ADR-006), so both the Tauri and Slint
frontends consume identical logic. src-tauri/ephemeral_library now re-exports
it as a thin shim. Adds tracks_snapshot() / current_folder_path() for the
Slint consumer.

cc
2026-06-07 10:12:44 -06:00
vicrodh d08c5f6569 feat(slint): system "now playing" notifications on track change
Port the Tauri desktop-notification path to a frontend-agnostic crate so the
Slint app (and any future TUI/CLI) fires it from native Rust instead of a
webview invoke.

New qbz-media-controls::notify module:
- Linux: XDG notification portal via ashpd (D-Bus); album art passed as
  Icon::Bytes after center-crop + downscale to <=512px + PNG re-encode (<=4 MiB).
- macOS: notify_rust with image_path (cover cached to disk).
- Windows: no-op (parity with Tauri).
- Self-contained artwork download + md5 cache + resize, matching the Tauri
  helpers, with no dependency on the frontend (ADR-006).

Wired in playback::refresh_now_playing_meta (the same point that pushes the
tray tooltip + OS media controls):
- De-dupe by track id so only an actual track change fires (the refresh also
  runs on resume/seek).
- Skip while a remote QConnect renderer drives playback (matches the Svelte
  skipIfRemote). Fire-and-forget on a tokio task.

Enabled by default, matching the Tauri default; a settings toggle is a
follow-up.

cc
2026-06-06 16:45:04 -06:00
vicrodh b9434a5034 fix(slint): hand-rolled macOS tray menu (objc2 0.5) with correct icon size and click routing
Replace the tray-icon/muda macOS backend with a hand-rolled NSStatusItem +
NSMenu built directly on objc2 0.5 / app-kit 0.2 (the same objc2 generation
winit 0.30 uses), so the menu lives in winit's NSApplication runtime and the
target-action dispatches. tray-icon's bundled muda collided with the muda
Slint's winit backend already registers and never fired the menu items. This
drops the tray-icon dependency and its whole GTK tree from the lockfile.

- Menu-bar icon pinned to 18pt via NSImage::setSize; the 44px PNG was
  rendering at native pixel size (giant icon).
- The status button gets its own target-action firing on left AND right
  mouse-up: left-click toggles the window, right/control-click pops the menu
  transiently (setMenu -> performClick -> clear, the non-deprecated
  replacement for popUpStatusItemMenu:). The menu is no longer permanently
  attached, so a left-click no longer opens it.
- Add the NSEvent feature to objc2-app-kit.

cc
2026-06-06 16:43:38 -06:00
vicrodh 2a2328e6c6 feat(slint): MPRIS album art (mpris:artUrl) — Qobuz pass-through + local file://
Port the Tauri normalizeCoverUrlForMetadata workaround as ArtworkRef::to_mpris_url
(qbz-models): remote Qobuz covers pass through as-is, local-file covers become a
percent-encoded file:// URI (MPRIS clients cannot read bare paths or asset://),
Plex thumbs become a tokenized HTTP URL. Wired into the Slint now-playing push so
the media widget shows album art. The app icon was already fixed via DesktopEntry.

cc
2026-06-06 11:58:17 -06:00
vicrodh ee8700db1a fix(packaging): Arch installs com.blitzfc.qbz.desktop for the MPRIS app icon
GNOME resolves the MPRIS media-widget app icon via DesktopEntry -> the installed
<id>.desktop -> Icon=. Rename the Arch desktop file to com.blitzfc.qbz.desktop,
set Icon=com.blitzfc.qbz, and install the hicolor icon as com.blitzfc.qbz.png so
the basename matches the published DesktopEntry. Other channels (AUR/Gentoo/Snap
+ Tauri bundle/release workflow) documented in qbz-nix-docs (release-time,
build-test required). Flatpak was already correct.

cc
2026-06-06 11:03:34 -06:00
vicrodh 7a7e5f82c4 feat(slint): system media controls — MPRIS with GNOME app icon + SMTC/MediaRemote
New frontend-agnostic crate qbz-media-controls (ADR-006): one MediaIntegration
trait, cfg-split backends. Linux uses mpris-server and publishes the MPRIS
org.mpris.MediaPlayer2.DesktopEntry property as com.blitzfc.qbz — the only way
GNOME Shell resolves the app icon for its media widget (souvlaki, the old
cross-platform crate, never set it, so GNOME showed no icon; KDE is lenient).
macOS/Windows keep souvlaki (MediaRemote/SMTC), where the icon comes from the
app bundle, not a DesktopEntry. The Linux server runs on a dedicated thread
with its own tokio runtime; state arrives over an async channel.

Wired into qbz-slint via src/media_controls.rs: started on shell entry,
metadata/playback pushed from the playback paths (mirroring the tray), and
inbound control events (media keys, the desktop media widget, Now Playing)
dispatched to the player through the existing QConnect-aware helpers. Both
Linux and macOS compile clean.

cc
2026-06-06 11:03:11 -06:00
vicrodh 682e735dc2 fix(slint): macOS tray menu — force Regular activation policy (UNVERIFIED)
Attempt to fix macOS tray menu items doing nothing: a bare `cargo run`
binary is not a bundled .app, so macOS does not dispatch NSStatusItem
menu-item target-actions (the menu shows, items enabled, but clicks fire
nothing). Force the app to a Regular, active application at tray creation
via objc2-app-kit setActivationPolicy(.regular) + activate(), and switch
menu handling to MenuEvent::set_event_handler with a diagnostic log.

NOTE: NOT verified at runtime on macOS yet (pending the user's GUI test). If
the menu still does nothing, look for the '[tray] menu item activated' log
line to tell whether the handler fires.

cc
2026-06-06 09:28:21 -06:00
vicrodh a91d629c2b feat(slint): macOS/Windows system tray (tray-icon) + macOS Dock policy
Add the NSStatusItem (macOS) / Shell_NotifyIcon (Windows) tray via the
tray-icon crate: the same 5-item menu, left-click window toggle, a themed
icon (auto uses a macOS template image that adapts to the menu bar), a
static QBZ tooltip, and live icon-variant switching. tray_icon::TrayIcon is
!Send and (on macOS) must be built on the main thread with NSApplication
running, so it lives in a thread_local created via invoke_from_event_loop;
its events are drained from a slint::Timer.

macOS close-to-tray reuses the cross-platform Slint hide/show path. The
opt-in Hide Dock icon toggle switches the NSApplication activation policy to
.accessory while closed to the menu bar (.regular by default, Spotify-style)
via objc2-app-kit.

cc
2026-06-06 02:15:40 -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 5b3ba963b0 feat(settings): tray store gains mac_hide_dock + close-to-tray opt-out
Add a mac_hide_dock column (with migration) to the shared tray settings
store for the upcoming macOS close-to-tray Dock-icon toggle, and flip the
close_to_tray default to true (opt-out: closing sends the window to the
tray and keeps the app running).

cc
2026-06-06 01:44:30 -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 02fdd94c46 fix(qconnect): take the render on idle connect (normalize -1 active id)
The take-renderer-when-idle path never fired: capture_session_state_takeover_input
parsed active_renderer_id raw. The cloud encodes "nobody active" as -1, which was
read as Some(-1) and classified as a peer being active (OtherPaused), so
compute_connection_state returned should_set_active_renderer: false and the
auto-take was suppressed. Normalize identically to the apply path
(normalize_active_renderer_id filters >= 0 -> None) so -1 maps to
ServerActiveState::None and the idle auto-take claims the render. Adds the
auto_take_when_idle decision to compute_connection_state plus regression tests
for the -1 / active-peer classification.

cc
2026-06-05 21:09:26 -06:00
vicrodh 1287d4feaf fix(qbz-slint): cold-start current track on play when no audio is loaded
Local toggle_play always called resume(), which fails with "cannot resume -
no audio data available" when the player holds no loaded stream — e.g. a
freshly materialized QConnect renderer queue whose cursor sits on a track that
was never loaded. Detect the no-loaded-audio case via Player::has_loaded_audio()
and load+play the current queue track instead of resuming. A normal pause keeps
the stream loaded, so the pause/resume path is unchanged.

cc
2026-06-05 21:09:14 -06:00
vicrodh b4c3bab91b fix(qconnect): WS-authoritative shuffle/repeat gating, button reflection, pause-to-iOS
Shuffle and Repeat now go to the CLOUD whenever connected (renderer OR controller),
gating on transport_connected like Tauri's v2_toggle_shuffle / v2_set_repeat_mode,
instead of the old peer-active gate that fell through to a LOCAL reshuffle when QBZ
was the active renderer — that did nothing visible AND risked a divergent local
order. QBZ sends only the seed + pivot; the cloud owns the order; the toggle reads
the cloud-authoritative queue.shuffle_mode. The local shuffle path is now reachable
only when NOT connected.

- shuffle/repeat button reflects the cloud queue flag (queue.shuffle_mode for the
  controller branch, core queue state via refresh_transport_modes for the renderer
  branch), not the per-renderer field the cloud never populates for a peer — fixes
  the iOS-reflects / WebPlayer-doesn't asymmetry.
- pause/play to a peer renderer: send a BARE SetPlayerState (playing_state only).
  iOS reports current_queue_item_id=null while playing, so resolving the qid landed
  on queue_item_id 0 — the protocol's "unknown" placeholder — and iOS could not
  anchor the pause, auto-resuming. A bare transport toggle is spec-sanctioned: the
  renderer pauses/resumes its own current item in place.
- skip_serializing_if on the SetPlayerState request structs so a None field is
  OMITTED, not serialized as null; and the mapper now treats a null current_queue_item
  as absent. Without this the bare command failed to encode and was never sent.
- log the outbound SetPlayerState payload for controller diagnostics.

cc
2026-06-05 17:44:11 -06:00
vicrodh 1072195740 merge: bring v1.2.15 Tauri fixes (shared HTTP client / fd exhaustion, qobuz bundle-token cache + bounded fetch) into slint-mvp
cc
2026-06-05 15:11:48 -06:00
vicrodh 31a4023f0b chore: bump Nix flake hashes for v1.2.15
cc
2026-06-05 13:01:58 -06:00