Commit Graph
101 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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 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 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
vicrodh da7758b9d6 feat(slint): new now-playing bar layout
Land the "New" now-playing bar as the first of the four switchable
layouts. A dedicated SongCard component holds the cover (or an idle
sleeping cat), the elidable title, the artist·album links, and the
vertical audio stamp (compact quality badge over the backend/mode LEDs).
A full-width seek bar sits on top with a buffer-driven stream lock.

The controls row uses responsive symmetric column zones (39/30/25% by
window width); the side columns share one fraction so the transport stays
absolutely centred and PLAY lands on the window centre. The song-card /
controls divider is a hard wall: the text column stretches to push the
audio stamp to its right edge, the album elides first (and never collapses
to zero) so the stamp is always visible and never crosses the wall.

Adds QualityBadgeFull compact mode, backend/mode output LED labels from
settings, a global TooltipOverlay so clipped columns keep their tooltips,
and the supporting icons.

cc
2026-06-02 06:48:36 -06:00
vicrodh 34eaebd8ef feat(slint): play-from-clicked-track, track-row quality badge, album link, playlist layout
Committed together because these changes share main.rs, playlist.rs and
PlaylistView.slint, and the new quality field is atomic across every
TrackItem builder, so they cannot compile as separate whole-file commits.

- Universal play-from-here: a single play_track_in_context dispatcher builds
  the queue from the current view's VISIBLE list and starts at the clicked
  track (playlist, album, artist, favorites, label, mix, search). Cached
  views order by the visible model; re-fetch views (album / artist top
  tracks) reorder to the in-page filter; the search "most popular" hero
  prepends to the results queue. Drops the playlist-track / mix-track arms
  that always started at track 1 (the clicked id was read from the wrong
  media-action slot).

- Album-name links in track rows open the album page (the ("album","open")
  media-action arm was missing and fell through to a no-op).

- Unified track-row quality badge: a new quality-detail field on TrackItem,
  populated from bit-depth / sample-rate in every builder through a shared
  crate::quality::detail, rendered with a bare (no chip background/border,
  centered text) QualityBadgeFull. Restores the exact bit-depth / sample-rate
  line that had regressed to a plain CD / Hi-Res tier.

- Playlist track list: reserve the favorite / download / context-menu columns
  in the column header, match the row's 36px cover cell, and anchor the
  scrollbar to the view edge (matches Local Library > Tracks).

cc
2026-05-31 21:50:51 -06:00
vicrodh d1ebf8ea1c feat(audio): release/refresh output-device button (#263)
After bit-perfect ALSA Direct grabs a DAC exclusively (hw: + D-Bus
reservation), the device stays sequestered and is invisible to PipeWire,
so switching back to the PipeWire backend can't see it without restarting
the app. Tauri had a refresh affordance for hot-plugged DACs; this is the
refresh plus an explicit release, in one button.

Backend: Player::release_device() / AudioCommand::ReleaseDevice tears the
stream down immediately (cancelling the deferred drop), freeing the hw:
grab + its reservation, then un-suspends the parked sink and resets any
forced graph clock (both self-gating). It does NOT reopen, unlike
reinit_device which would re-grab the device.

UI: a refresh-cw icon button beside the Output device select, propagated
through the settings callback chain to handle_release_device, which
releases, waits for PipeWire to reclaim, then re-enumerates so a freed or
hot-plugged DAC reappears without an app restart.

cc
2026-05-31 17:41:01 -06:00
vicrodh ac192a0c4c feat(slint): playlists flyout from the header when the sidebar is closed
With the sidebar fully closed there was no way to reach playlists. Add a
playlists button to the closed-sidebar header cluster that opens a flyout
over SidebarState's entries (folders + playlists), six rows visible with
the rest scrolling, plus an always-visible footer linking to the Playlist
Manager. Rendered at the AppShell level (SidebarPlaylistsPopup +
SidebarPlaylistsPopupState); header button needs no absolute-position
correction (not inside a Flickable).

cc
2026-05-30 21:02:53 -06:00
vicrodh f249558573 feat(slint): head collapsed-sidebar nav flyouts with the section name
SidebarNavRow (mini sidebar) opened its section flyout with no header;
the closed-sidebar CompactNavBtn dropdowns show the section name + a
hairline. Set HeaderMenuState.title to the row label so the mini flyouts
match (same @tr strings).

cc
2026-05-30 20:18:11 -06:00
vicrodh 63a80f49dd feat(slint): hover tooltip on collapsed-sidebar folder/playlist rows
In the mini (icon-only) sidebar the rows show no name. Add a hover bubble
to the right of each folder/top-level-playlist row surfacing the name
(folders also append their playlist count), mirroring the native title
Tauri puts on its collapsed buttons. Rendered at the AppShell level
(SidebarTooltip + SidebarTooltipState) because the sidebar clips its
overflow; anchored with the same Flickable absolute-position correction as
the folder flyout. Closes on click so it does not linger behind the flyout.

cc
2026-05-30 19:48:29 -06:00
vicrodh 4c0a849371 fix(slint): correct absolute-position double-count inside Flickable
absolute-position of an element nested in a Flickable double-counts the
element's own layout offset (measured: abs.y = true_y + root.y), so the
collapsed-sidebar folder flyout drifted down by one row-pitch per row
(lower folders left the viewport) and the track drag ghost plus the
playlist drop-target hit box drifted the same way. Subtract root.{x,y}
to recover the true window position; scroll-safe because root.{x,y} is
the row's fixed in-layout offset, not the viewport scroll. Rows outside
a Flickable (the section nav rows) are unaffected and left as-is.

Sites: SidebarRow folder-flyout anchor + recompute-drop hit box;
TrackRow drag down/threshold/start/move coordinates.

cc
2026-05-30 19:03:11 -06:00
vicrodh 13f43b802b fix(slint): flyouts/idle, header nav layout, toasts, settings, search
Flyouts & idle (main effort):
- Header dropdowns now glue under the tab: open-menu resets from-sidebar +
  title (a stale sidebar flyout state was flinging the panel far away).
- Idle-close now pauses while the cursor rests on the TRIGGER too (full
  header tabs gained the trigger-hovered logic the others already had).
- Dropped the dropdown caret (saves width on small screens).

Header nav layout:
- Section nav moved INTO the left-controls cluster, after the three sacred
  buttons, so it never overlaps them. Full text tabs when nav-in-sidebar off
  and sidebar not closed; compact icons when closed — mutually exclusive (no
  more duplicate/cluttered header).
- Search field gives up 60px (animated) when the header hosts the nav.

Settings made functional:
- Toast component mounted in AppShell (it was never rendered) + the in-app
  toasts toggle drives ToastState.enabled.
- "Show album artwork in Local Library track list" now drives the Tracks/
  Folders rows. Both rows un-flagged as unsupported.

Collapsible search (ExpandableSearch, app-wide):
- Smooth open AND close (width animates both ways; magnifier fades).
- Reliable auto-focus on open (deferred via a one-shot timer).

cc
2026-05-30 15:59:25 -06:00
vicrodh 682719cab0 feat(slint): library/sidebar UX overhaul + theme switching + fixes
Shell & navigation:
- Three-state sidebar (open/mini/closed); collapse cycles via header button.
- Section nav as right-flyouts (sidebar) + compact header nav (closed) +
  full header tabs (nav-in-sidebar off), via a shared HeaderMenuOverlay.
- Collapsed-folder right-flyout popup (max 4 + scrollbar), Rust-populated.
- Thin auto-hiding sidebar scrollbar pinned to the edge (Flickable, drag-safe).
- Slack-style bezel: rounded inset content panel over surface-card chrome.
- Header restructure: nav buttons, window-centered search, app menu.

Themes & components:
- Live theme switching: Dark / OLED Black / Tokyo Night / System (std-widgets).
- SegmentedTabBar (Discovery-style) with count badges for Library + Local Library.
- MP3 quality-badge tier (format-first classification) across badges.
- QbzSelect (Group/Sort) trimmed 20px.

Behavior fixes:
- Artist + Local Library Folder cover art now reach the rendered (visible/
  grouped) models, not just the source set.
- Favorites Playlists/Labels random buttons.
- Playlist track click plays from the clicked track in visible order.
- Dropdown idle-close pauses while the cursor rests on the menu.

cc
2026-05-30 15:40:35 -06:00
vicrodh 201082d4a1 feat(locallibrary): complete Albums 1:1 + dedicated local album view with versions
Albums tab — full 1:1 with Tauri in one pass:
- Switch from the chunked-paginated grid to a full-load + in-memory derive so
  grouping/filtering span the whole set. derive_albums = search + sort + group
  (off / by letter with A-Z jump / by artist, both with section headers) + the
  quality/format/source filter (1:1 with matchesQualityFilters: OR within a
  group, AND between groups; empty group passes).
- Grid/list view toggle; the list view shows a SOURCE column (Local/Offline/
  Plex), placed after TYPE. Filter popup now FLOATS (sibling of the content
  layout, not a layout slot) with an active-count badge. Fixed the year sort
  (sort_album_items maps year-asc/desc — was also silently broken in Folders).
- Album covers dual-set by id across the full set + visible + grouped sections.

Dedicated local album view — SEPARATE from the Qobuz AlbumView:
- New ContentView::local-album + NavEntry::LocalAlbum + ui/album/LocalAlbumView.slint,
  reusing TrackRow/NavButtons/CircleAction/QualityBadge internals but ONLY local
  actions (play all / shuffle / edit tags / add to playlist) — no Qobuz chrome.
- Versions: an album's tracks are split by source directory into versions, so
  multiple copies of the same album no longer merge into a duplicated track
  list. Best-quality version is the default; a source-icon version picker
  (hard-drive / Qobuz / Plex glyph in both the selected display and the options)
  switches the shown copy in place. The album cover is the best-quality version
  that has one, falling through versions, else a placeholder (covers backfilled
  from disk via fill_missing_covers). Header padding-top 11px matches the app.

Fix: Artists tab deadlock — fetch_missing_artist_images snapshotted names with a
blocking rx.recv() called from inside an event-loop closure, freezing the UI
(static spinner, no error). Names are now snapshotted on the UI thread and
passed in; the worker never blocks the event loop.

cc
2026-05-29 19:16:09 -06:00
vicrodh 4e79386daf feat(locallibrary): tag editor modal — manual edit + sidecar/direct save
The local-album tag editor (no remote lookup yet — that lands with the
remote-metadata crate move). Opens from a pencil on the local album detail.

- TagEditorModal.slint: album title/artist + year/genre/catalog + per-track
  title/track#/disc# table (indexed model write-back) + persistence QbzSelect
  (sidecar default / direct opt-in) + save-with-progress. Mounted last in
  AppShell (ADR-009 by mount order). ADR-008: dropdown, no pills.
- tag_editor.rs controller: open_tag_editor (off-thread pre-fetch via
  fetch_album_tracks_blocking + D1 year seed + can-direct-write CUE guard),
  save_tags (validate, is_dir gate, CUE guard, one-time direct-write confirm
  via rfd + library_kv ack, sidecar/direct branch on spawn_blocking with
  per-file progress, update_album_group_metadata via new with_db_mut, post-save
  refresh of the album + browse models), close.
- AlbumPageView: edit pencil CircleAction, visible is-local && source != plex;
  AlbumState gains `source`, set in apply_local_album.
- library_db: with_db_mut for the transactional metadata update.
- main.rs: on_media_action("album","edit") + TagEditorActions wiring (close/
  save/set-persistence with the CUE gate).

cc
2026-05-29 15:03:58 -06:00
vicrodh 3826298f77 feat(locallibrary): Settings > Local Library — folder management (Slice A)
Port the Tauri folder-management surface to a new Settings > Local Library
sub-section (index 4); the browse-view gear routes here and pre-selects it.

UI:
- LocalLibrarySettings.slint: folder list (icon, display name, disabled/
  unavailable badges, multi-select checkboxes) + toolbar (scan-all/add/edit/
  remove icon buttons) + filter + Maintenance (cleanup missing) + two-step
  Danger Zone clear. ADR-008: plain bordered icon buttons + square badges.
- LibFolderEditModal.slint: alias, enabled, network override + fs-type,
  change-path, accessibility status, last-scanned, scan-this-folder. Mounted
  at the AppShell root (overlays the window, ADR-009 by mount order).
- SettingsView gains the section-4 sub-nav + panel; app.slint re-exports the
  new globals so the Rust layer can populate them.

Controller (local_library_settings.rs): load/add/remove/edit/save/change-path,
cleanup-missing (stats outside the DB lock), two-step clear, filter, select.
All DB access via qbz-library through library_db::with_db on spawn_blocking;
native dir picker + confirms via rfd. Selection preserved across reloads;
network folders get an async 6s-timeout accessibility check (exists() fallback
on timeout, mirroring Tauri).

Core (qbz-library): network_fs_label() for fs-type parity; separator-safe
delete_tracks_in_folder_prefixed() + remove_folder_with_tracks() (fixes the
prefix-collision bug without changing the legacy fn). Scan engine lands in
Slice B.

cc
2026-05-29 13:07:19 -06:00
vicrodh c50ae17102 feat(locallibrary): nav scaffolding for the Local Library Slint view
Greenfield Local Library port, slice 1 — the view now mounts and navigates
end to end (empty placeholder body; per-tab data lands next):

- state.slint: `ContentView::local-library` + `LocalLibraryState`
  (active-tab / loading / load-error / per-category counts) +
  `LocalLibraryActions` (select-tab, open-settings). Re-exported from
  app.slint so the globals reach the Rust API.
- ui/locallibrary/LocalLibraryView.slint: header (title + stats + gear) +
  4-tab bar (Albums / Artists / Folders / Tracks) + placeholder body.
- shell: AppShell mounts the view on `local-library`; HeaderBar's
  Local Library menu items now carry routes (local-albums / -artists /
  -folders / -tracks) instead of being inert.
- nav.rs: `NavEntry::LocalLibrary { tab }` — per-tab history entry,
  mirroring Favorites / Discover (back/forward moves between tabs).
- src/local_library.rs: `LibTab` controller stub (route/tab-id parsing,
  round-trip tested).
- main.rs: apply_entry arm + `navigate_local_library` + header-route
  branch + in-view tab-bar (select-tab) and gear (open-settings ->
  Settings; management lives under Settings > Local Library) wiring.

qbz-slint cargo check green. Built blind — needs a runtime smoke test
(./scripts/slint-dev.sh): click the Local Library header menu, confirm the
view shows and the 4 tabs switch.

cc
2026-05-28 17:51:52 -06:00
vicrodh 3e0a718e0c feat(slint): offline cache manager + remove/redownload + Settings Offline page
Slices 3 + 4 of the offline-cache port.

Slice 3 (controller, offline_cache.rs):
- remove_album (maintenance::remove_album_cached_tracks + library rows + live
  row update), redownload_track / redownload_album(failed_only) (via
  select_redownload_targets + reset_track_for_redownload + spawn), clear_all
  (purge), open_folder (xdg-open / open the cache dir).

Slice 4 (manager + Settings):
- OfflineManagerView.slint: a standalone route (NavEntry::OfflineManager ->
  ContentView.offline-manager, mounted in AppShell, mirrors PlaylistManager).
  Stats bar (tracks count / size / limit / usage bar + edit-limit GB input +
  open-folder + clear-all) over a scrolling album-header + track-row list with
  a spinning/check/alert status glyph and per-row re-download / remove.
- offline_manager.rs: builds the album->track rollup + stats from index.db
  (get_all_tracks), pushes [OfflineRow] + stats to OfflineManagerState; set_limit
  (in-memory, session-scoped) + reload_soon (refresh after a mutation).
- OfflineManagerActions wired in main.rs (open/refresh/remove/redownload/
  set-limit/clear-all/open-folder/play).
- Settings gains an "Offline" section (OfflineSettings.slint): Manage offline
  cache (-> the manager), Open cache folder, Clear all. The Local-Library
  rows (show-in-library, repair) arrive with the Local Library port.

qbz-slint compiles clean.

cc
2026-05-28 11:23:55 -06:00
vicrodh 56c924d8ea feat(slint): Label landing page + All Releases sub-view
Port the Tauri LabelView module to native Slint, reusing existing
components and extending where needed (no duplication).

Landing (new LabelPageView): circular header + description read-more +
Follow toggle, sticky JUMP TO bar, Popular Tracks (multi-select, 5->20->50
reveal, Play all), and Releases (+ See all) / Critics' Picks / Playlists /
Artists / More Labels carousels.

All Releases sub-view (LabelReleasesView): circular header + album count,
toolbar (local search, group-by-artist, Hi-Res filter, sort) + grid/list
toggle, load-more, states. TYPE column now populated.

Split the label route: ContentView::label -> LabelPageView (landing),
ContentView::label-releases -> LabelReleasesView; add NavEntry::LabelReleases.
label.rs load_label_page parses /label/page + getAlbums + explore + the
favorite-labels set; derive_releases re-derives the sub-view list.
Reuse Carousel/PlaylistCarousel/ArtistCarousel/TrackRow/AlbumCard/
AlbumListRow/JumpNavBar/QbzSelect/ExpandableSearch; extend ArtistCard/
ArtistCarousel (follow-mode/follow-kind); promote CircleAction to a shared
primitive; JumpNavBar gains show-search. Wire label media-actions:
follow, play-top, open (more-labels), see-all.

cc
2026-05-26 18:35:42 -06:00
vicrodh b2004e6867 feat(slint): TrackRow favorites + artist/album links, PlaylistView, LabelView, scrollbar
CHECKPOINT — this batch is NOT yet acceptable; substantial feedback is pending. Committed so the work is preserved for rework next session, after a max-effort Tauri code review. Areas: TrackRow favorite toggle/state + clickable artist/album (TrackItem gains is-favorite/artist-id/album-id + fav_cache, populated across all builders); PlaylistView header buttons + cover-art hover overlay; LabelView full AlbumCard + grid/list toggle; vertical scrollbar moved to the right gutter across ~12 views.

cc
2026-05-26 16:36:02 -06:00
vicrodh d30e3d658d feat(slint): sidebar context menus, in-app toast, SearchView + Playlist Manager polish
Sidebar:
- Fix "Recursion detected" panic from focusing the playlist search input in init (defer via a single-shot Timer).
- Right-click context menus: playlist (Edit, Hide from sidebar, Move to folder with folder search + Move to root) and folder (Edit, Hide from sidebar), built on ContextMenu/ContextMenuItem; remove the unreachable hover "..." button.
- Filter hidden playlists/folders out of the list; add folders::set_folder_hidden.
- Folder list in the move-to-folder menu scrolls (cap 5 rows); close-on-click-outside so the folder-search input no longer dismisses the menu.
- Sort-menu Refresh now uses a dedicated refresh-cw icon.

Toast: in-app ToastKind/ToastState + Toast overlay mounted at the app root, crate::toast (show/hide, per-kind auto-hide via a reusable Timer, cross-thread *_weak helpers); surface play album/artist load failures.

SearchView: title inline with NavButtons + searchType filters floated right; tab strip on its own row; Most-popular hero for a track uses the new SearchTrackHero, sized to the artist card.

Playlist Manager: list view renders compact PmListRow rows instead of cards; grid/folder card fonts tightened to match Tauri; folder card centered with hover-reveal edit; search input narrowed to 70%.

cc
2026-05-26 12:21:59 -06:00
vicrodh 2bf6feaa6d feat(slint): Playlist Manager view + shared nav/state/shell wiring
Port the Playlist Manager: header/toolbar (search/filter/sort/folder-flat
toggle/view-mode/new-folder/counter), FOLDERS card grid (accent icon, count,
edit), PLAYLISTS grid/list/tree, folder CRUD (FolderEditModal), move-to-
folder, per-card actions, and arrow-button reorder (custom sort). Reached
from the sidebar "Manage playlists" item; backend 100% reused via
QbzCore/library.db (no new commands). Pointer-event drag-and-drop and
gradient folder colors are deferred follow-ups.

This commit also carries the cross-cutting wiring the session's features all
share — state.slint globals/structs, NavEntry variants, main.rs dispatch +
action wiring, artwork targets, app.slint exports, AppShell mounts — so HEAD
is the verified-green tree (cargo check + release build clean).

cc
2026-05-26 01:07:08 -06:00
vicrodh 9f70a5ac5a feat(slint): sidebar Playlists toolbar + folders + micro-collage refinements
PLAYLISTS header: smaller title, recursive search (hides title), create
modal at 1:1 with Tauri (name/description/folder/public), sort menu, and a
collapse chevron. Folders: accent-tinted icon, open/close chevron as the
primary affordance (no more purposeless "..."), count shown on hover only.
Playlists render a 2x2 micro-collage of track covers (opt-out) instead of
the glyph, sourced from the existing playlist images (no extra fetch).

cc
2026-05-26 01:07:08 -06:00
vicrodh de826bf560 feat(slint): playlist detail polish, sort (incl. custom) + WIP track drag
Polish + consistency:
  - Compact playlist header (cover 150px; nav->header spacing
    homologated with AlbumView); description truncated to ~2 lines +
    "Read more" modal; custom-artwork buttons only on cover hover.
  - New Typography.section (18px) token for hero/section titles,
    applied across Album / Artist / Mix / Playlist + Home carousels.
  - Shorter track rows app-wide (50px row, 36px artwork, -1 fonts);
    +3px column-header/divider gap; wider track-number column.
  - Reusable ExpandableSearch (the JumpNavBar page-search pattern:
    magnifier -> opens left -> X to close) in the playlist action row.
  - Custom ListScrollbar (thicker, min thumb height, own gutter) since
    Slint's ListView scrollbar can't be styled.

Sort:
  - Default / Title / Artist / Album / Duration with asc/desc per
    field, plus Custom (manual order persisted in library.db with
    up/down reorder). Client-side search + sort combined.

WIP: track drag-and-drop onto sidebar playlists
  - DragState/DragActions globals, drag source on the shared TrackRow,
    follow-cursor DragGhost, sidebar playlist rows self-detect the drop
    via absolute-position, multi-select group drag, add_tracks_to_playlist
    on drop, Flickables freeze scroll during a drag.
  - KNOWN ISSUE: the ghost is offset from the cursor (the row
    absolute-position + pointer mapping is off, esp. inside Flickables).
    Diagnostic logging is left in (TrackRow debug() + a start log) to
    pin down the wrong term before the generic fix.

cc
2026-05-22 18:26:55 -06:00
vicrodh 4fe034d86b feat(slint): playlist folders in the sidebar
Local, Tauri-shared playlist folders (library.db) now organize the
sidebar:

  - New folders.rs controller (create / rename / delete / move via
    qbz-library); sidebar.rs reworked to a flattened entries model
    (folder headers + grouped playlists + root playlists) with a
    session expand cache, rebuilt without a refetch on expand/move.
  - Sidebar renders folders (expandable, with playlist counts) and
    playlists (indented inside folders); a hover "..." opens a context
    popup: Move to folder (root + each folder) for playlists, Delete
    folder for folders.
  - Header gains a New-folder button + a CreateFolderModal; folders
    reload after create/delete; moves are optimistic + persisted.

Adds folder-open / folder-plus icons.

cc
2026-05-22 08:44:37 -06:00
vicrodh 75cdb10ee1 feat(slint): rename + delete playlist (edit modal)
Completes playlist CRUD. The detail header shows an edit button only
for the user's own playlists (is-owner = the open id is in the sidebar
list); it opens an EditPlaylistModal:

  - Save -> update_playlist (rename); refreshes the detail header name
    and the sidebar list.
  - Delete -> delete_playlist; closes the modal, navigates back, and
    refreshes the sidebar.

Adds the pen-line icon (Lucide) used by the edit button.

cc
2026-05-22 06:47:57 -06:00
vicrodh f387114ecf feat(slint): sidebar playlists list + create-playlist modal
- SidebarState/SidebarActions + crate::sidebar: the user's playlists
    load into the left-nav list on shell entry; clicking a row opens
    the playlist detail view, with the open playlist highlighted
    (active-id). Empty state shown when there are none.
  - The sidebar "+" opens a CreatePlaylistModal (name field + public
    toggle); submit calls create_playlist, refreshes the sidebar, and
    navigates to the new playlist.

cc
2026-05-22 03:57:54 -06:00
vicrodh 1be78193e2 feat(slint): playlist detail view
First piece of the playlist module: a detail view at the AlbumView /
MixView level, reusing the shared TrackRow (show-artwork + show-album +
context menu, so Add to playlist / Go to album / etc. work in it).

  - PlaylistState + crate::playlist controller (get_playlist -> header
    metadata + TrackItem rows; cached track list backs play-all /
    per-track play; artwork jobs resolve row covers + the header cover).
  - PlaylistView.slint: cover + "Playlist" label + name + description +
    "owner - N tracks - duration" info line + circular actions (Play
    all, Shuffle, Favorite) over a column-headed track list.
  - ContentView.playlist + NavEntry::Playlist (back/forward history) +
    navigate_playlist; the Favorites > Playlists card now opens it.
  - media-action: playlist play-all / shuffle / favorite (add/remove
    favorite) + per-track play-from-here.

cc
2026-05-22 03:02:09 -06:00
vicrodh db8a6ac5b1 feat(slint): wire track context-menu actions (favorite/radio/share/playlist/nav)
Connects the previously-inert track context-menu items now that the
backends exist:

  - Add to library -> add_favorite("track").
  - Go to album / Go to artist -> resolve the track (get_track) to its
    album / performer and navigate there.
  - Create radio -> Qobuz track radio (new get_radio_track core wrapper
    over the existing qbz-qobuz endpoint + playback::play_track_radio).
  - Share Qobuz link -> https://www.qobuz.com/track/{id} to clipboard
    (new arboard dep + share module).
  - Share Song.link -> Odesli (api.song.link) pageUrl resolution to
    clipboard.
  - Add to playlist -> a global picker modal (PlaylistPickerModal +
    PlaylistPickerState/Actions + playlist_picker controller) listing
    the user's playlists; picking one calls add_tracks_to_playlist.

Add to mixtape / Make available offline / Track info stay unwired
(no backend yet).

cc
2026-05-22 02:54:52 -06:00
vicrodh 469c2495bf refactor(slint): mix rows reuse SearchTrackRow; For You uses HomeSkeleton
Track row reuse: the mix views had a bespoke MixTrackRow with a dead
favorite/more controls (no context menu), while search / favorites /
artist already share SearchTrackRow (which has the TrackContextMenu).
Consolidated:
  - SearchTrackRow gains optional show-album (album column) +
    show-favorite (standalone heart) — off by default, so search /
    favorites rows are unchanged. SearchTrackItem gains an `album`
    field.
  - MixView drops MixTrackRow/MixTrackItem and renders SearchTrackRow
    with show-album + show-favorite. A row "play" starts the mix from
    that track; the context-menu actions (play next / queue / favorite
    / view album / view artist / ...) now bubble through media-action
    and work in the mix views too.

For You skeleton: the For You tab showed a bare spinner during its
lazy load; it now shows the same pulsing HomeSkeleton as Home (the
static Qobuz Mixes tiles stay visible above it). Editor's Picks is
served from the shared discover-index cache (Tauri parity) and shows
the skeleton during the initial load via the existing HomeView guard.

cc
2026-05-21 23:46:13 -06:00
vicrodh 61882166f7 feat(slint): rebuild mix views to AlbumView-level rendering
The mix detail pages were a thin header + flat track list. Rebuilt to
mirror Tauri's playlist-detail layout at the AlbumView port quality.

  - Header: a per-mix gradient artwork (224px, same gradients as the
    QobuzMixesRow tiles) + "Qobuz Mixes" label + title + description +
    "N tracks • duration" info line + circular action buttons (Play
    all, Shuffle, Save-as-playlist, Refresh) — matching AlbumView's
    header anatomy.
  - Track list with a real column header (#, Title, Album, Duration,
    Quality) over rich MixTrackRows: artwork thumbnail + number/play
    cell (TrackPlayCell, artwork-on), title + explicit badge, artist,
    album column, duration, quality, favorite + more controls.
  - MixTrackItem gains an album column; MixState carries track-count +
    total-duration for the info line.
  - Play all / per-track play (from that track), Shuffle (plays a
    random order without mutating the list), and Refresh (re-fetches)
    wired through media-action.

Fixes a wiring bug: the tile click sends id=kind/action="open", but
the handler matched on the action and passed "open" as the mix kind —
now it reads the kind from id.

cc
2026-05-21 22:48:49 -06:00
vicrodh 3b8f0a4597 feat(slint): Qobuz mix detail views (DailyQ / WeeklyQ / FavQ / TopQ)
The For You Qobuz Mixes tiles now open functional mix detail pages.

  - New MixView: header (mix name + description + Play all) over the
    mix's track list (reusing SearchTrackRow). Clicking a track plays
    the whole mix from there; Play all starts at the top.
  - crate::mix resolves each mix to a track list from available
    backend: DailyQ/WeeklyQ -> /dynamic/suggest seeded from the local
    play-history track ids; FavQ -> favorite tracks shuffled (xorshift,
    no rng dep); TopQ -> tracks aggregated from the user's playlists.
    The loaded list is cached so play-all / play-from-track build the
    queue without re-fetching.
  - Backend: ported /dynamic/suggest into qbz-qobuz
    (QobuzClient::get_dynamic_suggest — POST, items parsed leniently
    into Vec<Track>) + QbzCore::get_dynamic_suggest.
  - ContentView.mix + NavEntry::Mix { kind } so each mix is a history
    entry; the tile click records + navigates. New MixTrack artwork
    target. playback::play_tracks generalizes the radio queue builder
    with a start index (radio reuses it at 0).

Tauri's exact generation (listened-track analysis payloads, playlist
play-stats ranking) is approximated; same surfaces + playback, sourced
from the backend the MVP has.

cc
2026-05-21 08:16:31 -06:00