Commit Graph
712 Commits
Author SHA1 Message Date
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 a5383ba502 chore(slint): flag the remaining unwired Appearance settings red
Marks every non-functional Appearance row (Theme, Language, Font, UI Scale, notifications, window-title, player views, system tray, immersive) with the SettingRow 'unsupported' red label. Only the wired 'Show playlist cover collages' and the 'Detected' status row stay normal. Red = not actually connected, so what still needs wiring is visible at a glance.

cc
2026-05-26 16:36:02 -06:00
vicrodh 4e329b4c8b chore(slint): flag unwired Appearance settings for the Tauri to Slint audit
Add a SettingRow 'unsupported' flag that renders the label red. Mark the Title Bar and Window Controls sections: that Tauri custom-window-decoration paradigm does not carry over to Slint's TitleBar/Plasma chrome.

cc
2026-05-26 15:09:29 -06:00
vicrodh 178d135328 feat(slint): Playlist Manager + search most-popular hero polish
Playlist Manager: list view renders compact rows with total play time; folder grid cards no longer clip the playlist count (content-driven height); list view shows compact folder chips; the move-to-folder menu gains a folder search and a 4-row scroll cap. Search: the most-popular track hero is resized to the artist-card height with the quality shown as centered text instead of a clipped badge.

cc
2026-05-26 15:09:29 -06:00
vicrodh 93a7bbbc3c feat(slint): double-click a track row to play/pause
Mirrors the play-cell click on the row body: double-clicking a track plays it, or toggles play/pause when it is already the current track. Applies to every TrackRow (album, artist, search, playlist, mix).

cc
2026-05-26 15:09:29 -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 7aa3018cf6 feat(slint): replicate Settings > Appearance section (1:1 UI, mostly unwired)
Add an Appearance sub-section to the Slint settings, mirroring Tauri's:
theme + auto-theme, font/UI-scale/language, startup page, sidebar playlist
collage (wired to the sidebar flag), notifications/toasts/tray, title bar +
window controls, immersive + miniplayer. Controls reuse QbzToggle/QbzSelect
and the AudioSettings/PlaybackSettings layout. Most controls are visual
stubs (AppearanceState + TODO callbacks) pending backend wiring.

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 c5c124fb0c feat(slint): Discover "View all" pages + reusable AlbumListRow + recently-played metadata
Per-module discover See-All pages (new releases, press accolades, ideal
discography, qobuzissimes, albums of the week, most streamed) reachable
from each carousel's "View all" link: a paginated grid OR list view with
client-side search, genre filter, and a fixed header. Adds a reusable
AlbumListRow (columns #/ITEM/TYPE/SOURCE/QUALITY/TRACKS/YEAR, SOURCE
hideable, reuses QualityBadgeFull) with play-on-hover + even/odd zebra.
Recently Played albums now carry genre + localized release date + quality
(captured at play time, no extra fetch). Shared home::map_album /
card_to_item reused; grid-only AlbumCardItem literals get ..Default.

cc
2026-05-26 01:07:08 -06:00
vicrodh 9f1e036308 feat(slint): album cards show genre + localized full release date (#469)
Album-card hover overlay now shows the full release date as "MMM D, YYYY"
(e.g. "Nov 6, 2025") instead of the year alone. New dates::release_label
helper keeps the month/day/year order fixed but localizes the month via
chrono format_localized (the app ships 5 languages; current_locale() is the
single hook to thread the real UI language once Slint wires translations —
English for now). Applied to the Discover and Favorites album mappings.

Also fixes Favorites > Albums cards, which hardcoded an empty genre and a
year-only label: they now read album.genre and the full date, matching the
Discover cards 1:1. The data was already in the getUserFavorites response
(qbz-models Album parses genre + release_date_original) — no backend change.

cc
2026-05-25 17:42:36 -06:00
vicrodh 21af0f63f8 feat(slint): favorites tabs as history pages + homologated nav-bar/title
Each favorites tab is now its own history entry (NavEntry::Favorites{tab},
mirroring Discover) recorded from both the header nav and the in-view tab
bar, so back/forward moves between tabs instead of exiting Favorites. Add
the back/forward NavButtons at the same position as every other page
(11px top padding + 22px gap) — FavoritesView was the only view missing
it — and shrink the "Favorites" heading to Typography.section (the album
name size in AlbumView).

cc
2026-05-25 17:41:35 -06:00
vicrodh abff875a0e fix(slint): album card title no longer clamped to the artist name width
The clickable-artist change bound the artist TouchArea width to the artist
text's preferred-width, which clamped the whole stretch:1 title column to
the (often short) artist name — truncating titles to a few letters and
shoving the quality badge inward instead of letting it float right. Drop
the explicit width; the inner Text fills the column (width: 100%) and elides.

cc
2026-05-25 17:05:44 -06:00
vicrodh 11a78ed2f6 feat(slint): resolve Auto backend to a concrete one in audio settings (#470)
Port #470's UI behavior: add an "Auto" entry at the top of the backend
dropdown that, on select, resolves to the best available backend (PipeWire
if present, else System) and persists that concrete value. The rebuilt
snapshot then moves the dropdown onto the resolved backend, so backend_type
is never left null/Auto. Concrete backends keep mapping by their shifted
index; System wiring comes from available_backends() (merged backend half).

cc
2026-05-25 17:05:44 -06:00
vicrodh e38dfb6235 chore(slint): restore qbz-slint deps in crates/Cargo.lock after the 1.2.14 merge
The catch-up merge staged the pre-release lock (--theirs) before cargo
regenerated it, so the committed lock dropped qbz-slint's GUI dependency
tree (slint / accesskit / winit / skia / ab_glyph, ...). Commit the
regenerated lock so the checked-in slint-mvp lock matches the workspace.

cc
2026-05-25 17:05:44 -06:00
vicrodh 022b3e46e6 Merge pre-release into slint-mvp (1.2.14 backend catch-up)
One-way backend catch-up bringing the 1.2.14 maintenance work into
slint-mvp: audio (#470/#449/#466), player/offline/prefetch (#467),
qobuz retry/backoff, credential-store + image-cache refactor, dep
bumps (cpal 0.17.3 / alsa 0.11 / tokio / ashpd / tower-http), and the
qbzd daemon removal.

Conflicts resolved:
- qbz-audio settings.rs: keep slint-mvp stream_first_track/buffer
  defaults (1/2, opt-out); adopt #470's CREATE-TABLE seed removal
  (row is seeded with backend_type below).
- crates/Cargo.toml: keep qbz-slint workspace member (qbzd removed
  upstream).
- crates/Cargo.lock: regenerated (no ratatui/qbzd; cpal/alsa bumped).

cargo check --workspace green.

cc
2026-05-25 15:12:58 -06:00
vicrodh 47db92ff92 chore: remove qbzd headless daemon and its client integration
qbzd was a premature daemon experiment that never shipped (gated off
behind QBZD_ENABLED, no user access). Its ratatui 0.29 dependency pinned
unicode-width =0.2.0, which conflicts with slint's annotate-snippets in a
shared workspace. Remove it entirely; it will be reimplemented from
scratch later on the clean, UI-agnostic shell.

- Delete crates/qbzd daemon crate + workspace member (drops ratatui,
  crossterm and the other qbzd-only deps from the lock).
- Delete src-tauri qbzd_discovery (mDNS) + its commands/state wiring in
  lib.rs; drop the direct mdns-sd dep (Chromecast keeps its own via
  qbz-cast).
- Delete remoteApi/remoteEvents/playbackTargetStore; neutralize
  commandRouter to local-only dispatch (isRemote/skipIfRemote are now
  always-false no-ops so the ~100 guard call-sites keep compiling).
- Strip qbzd branches from playbackService and the remote banner from
  NowPlayingBar; excise qbzd from CastPicker (Chromecast/DLNA kept).
- Remove qbzd i18n keys (5 locales), README daemon node, gitignore note.

cargo check (crates + src-tauri) and svelte-check all green.

cc
2026-05-25 15:08:14 -06:00
vicrodh a0051ab513 fix(audio): default to shared System output and resolve Auto to a concrete backend (#470)
OOTB playback froze (seekbar advancing, no audio) on hosts where the
configured backend was not usable — e.g. PipeWire as the Linux default but
`pactl` absent (minimal Debian/Ubuntu). "Auto" (a null backend_type) fell
through to a legacy CPAL path that forced the track rate onto the shared
default device, producing a suspended/silent stream and leaving a
process-wide stuck audio handle that survived Reset.

- Add a cross-platform "System" backend (CpalDefaultBackend now on Linux
  too): opens the OS default output at its negotiated rate with rodio
  resampling and an explicit ~100ms CPAL buffer (MixerDeviceSink has zero
  internal buffering, so the bare default underran and stayed suspended).
  Shared, no exclusivity, no `pactl` — mixes with other apps like any
  normal player.
- Make System the OOTB default and what Reset lands on; seed it on first
  run. Drop the per-startup #375 NULL->PipeWire backfill migration.
- Resolve a null backend_type to System in the player so the legacy CPAL
  path is never reached (covers legacy installs and headless callers).
- "Auto" in Settings is now a resolve-and-set action: it picks the best
  available backend (PipeWire if available, else System), stores that
  concrete value and updates the dropdown + device picker, so backend_type
  is never null.
- Explicit PipeWire (DAC passthrough) and ALSA Direct (exclusive) configs
  are untouched and remain bit-perfect.

cc
2026-05-24 21:13:11 -06:00
vicrodh bf56eefd7a chore(deps): bump cpal 0.17.1 -> 0.17.3 and alsa 0.10 -> 0.11
cpal 0.17.3 requires alsa 0.11, which in turn pulls alsa-sys 0.4. The
`links = "alsa"` constraint permits only one alsa-sys in the dependency
graph, so qbz-audio's direct alsa stack is raised in lockstep:
alsa 0.10 -> 0.11 and alsa-sys 0.3 -> 0.4. Both workspace lockfiles
(crates/ and src-tauri/) are updated to cpal 0.17.3 / alsa 0.11.0 /
alsa-sys 0.4.0.

No source changes required: alsa_direct.rs uses only the stable
high-level alsa API (pcm, HwParams, Format, Direction, ValueOr, mixer),
and the snd_lib_error_set_handler FFI in alsa_error_handler.rs is
unchanged in alsa-sys 0.4. Both workspaces pass cargo check.

cc
2026-05-23 22:25:26 -06:00
vicrodh ee4c889c55 fix(player): rebuild output stream on sample-rate change for all backends (#449)
The stream-recreate decision only forced a rebuild on a decoded-format change
for bit-perfect backends (DAC passthrough, ALSA Direct, CoreAudio exclusive).
The default/PipeWire path relied on Stop dropping the stream to get a fresh one
at the new rate; once that drop was deferred to avoid a track-change click
(e93fcaec), the PipeWire node stayed locked to the first track's rate for all
subsequent tracks — a regression vs 1.2.10. Make any decoded-format change
rebuild the stream on every backend; same-rate tracks still reuse it, so the
click fix is preserved.

Verified on hardware via pw-top: PipeWire without passthrough now switches rate
per track; PipeWire with passthrough and ALSA Direct unchanged (no regression).

cc
2026-05-23 08:35:38 -06:00
vicrodh 3a09ce7fe7 fix(audio): detect PipeWire via runtime socket so the backend is not hidden without pactl (#466)
is_pipewire_available() shelled out to `pactl info`, which lives in
pulseaudio-utils and is absent on PipeWire-only systems — hiding the PipeWire
backend even though PipeWire was running. Detect via the
$XDG_RUNTIME_DIR/pipewire-0 socket first (no extra package required), with
pw-cli and pactl as fallbacks.

cc
2026-05-22 23:16:53 -06:00
vicrodh bdaee1af1a fix(qobuz): retry transient fetch failures with backoff instead of skipping (#467)
A single transient 5xx / timeout / reset on the next track's file/url or a CMAF
segment used to be terminal and skipped the track. Add a retry wrapper (3
attempts, exponential backoff + jitter) around get_file_url, the init segment,
and per-segment fetches. Retry only transient errors (network/timeout, 5xx, 429
honoring Retry-After); a real 404 is classified as TrackUnavailable and
propagates immediately to the bounded skip path.

cc
2026-05-22 21:46:33 -06:00
vicrodh 133e90f752 fix(offline): hysteresis + cheap probe + streaming liveness override (#467)
Long sessions died when a single transient blip flipped the app to offline and
the queue then skipped every cached-but-not-downloaded track. Harden the
detector:

- Consecutive-failure hysteresis: declare offline only after 2 failed polls,
  not a single blip.
- Replace the HEAD https://1.1.1.1 probe with a concurrent race of cheap
  generate_204 endpoints, so a saturated link (long Hi-Res tracks) no longer
  times the probe out.
- Positive liveness override: if a segment downloaded within the last 45s we
  are online by definition, regardless of the probe.

cc
2026-05-22 21:46:33 -06:00
vicrodh e4fa90c1b2 feat(prefetch): adaptive throttle based on observed bandwidth + underrun signal
Recovered from 63ecc324 (authored on the native-port branch, never merged to
pre-release). Backend-only, no Slint. Scales the prefetch cap down on slow
links and during audio underruns, and exposes the per-segment download signal
the offline detector reuses as a positive liveness override (#467).

cc
2026-05-22 21:46:07 -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 85223b03ff perf(slint): virtualize playlist track list + lighter artwork
Large playlists (2000+ tracks) were sluggish. Addressed the root
causes; this strategy is reusable for the library / mixtapes / tracks
views.

  - Virtualization: the track list is now a ListView (only visible
    rows are instantiated) instead of a Flickable with a for over all
    rows. The header + column header are pinned above it.
  - Smaller images: track rows use ImageSet::smallest() (new) instead
    of best() — they are 40px thumbnails, so best() was downloading
    mega/large covers.
  - Per-target decode size: row thumbnails decode to 96px instead of
    the 264px card size, so a 2000-row model holds ~75MB of pixels
    rather than ~550MB.
  - Track-number column widened to fit 4-digit positions.
  - Back-to-top button: appears once scrolled past one viewport
    (bottom-right), scrolls the list home. A shared rule for big lists.
  - Custom-artwork buttons only show on cover hover.

cc
2026-05-22 09:27:45 -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 5ac5c60c57 feat(slint): playlist collage + custom artwork + edit description
Wires qbz-library into the Slint app (new library_db module) so the
local, Tauri-shared organization data is read/written from the same
per-user library.db (<data_dir>/qbz/users/<id>/library.db). The DB is
opened per-operation on a blocking thread (rusqlite Connection is
!Send).

  - Header collage: the playlist cover is now a 4-up collage of the
    first tracks' covers (Tauri parity); falls back to the server
    image, then the first track, then a placeholder.
  - Custom artwork: owner-only buttons on the cover set/clear a custom
    image. Set copies the picked file into <cache_dir>/qbz/artwork and
    stores the path in playlist_settings.custom_artwork_path (shared
    with Tauri); it's loaded directly from disk and overrides the
    collage.
  - Edit description: the edit modal gains a description field
    (prefilled), saved through update_playlist alongside the name.

cc
2026-05-22 08:30:16 -06:00
vicrodh 5aae0195f6 feat(slint): playlist track editing (multi-select remove)
The playlist detail view gains an edit mode (owner-only), reusing
TrackRow's multi-select-mode:

  - A "Select" toggle in the header turns on per-row checkboxes and a
    bulk action bar (N selected, Select all, Remove).
  - toggle-select is now context-aware (playlist vs artist Popular);
    PlaylistState tracks selected-count for the bar.
  - Remove calls remove_tracks_from_playlist with the selected ids,
    then reloads the playlist and leaves edit mode.

Track reordering is the remaining playlist-editing piece (deferred —
it is the most involved).

cc
2026-05-22 07:01:21 -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 75d8571be3 feat(slint): clickable artist name on AlbumCard -> artist page
Ports Tauri's behavior where clicking an album card's artist name
navigates to that artist (the card body still opens the album).

  - AlbumCardItem gains artist-id; populated in every album-card
    constructor (home/search/label/foryou/favorites). Artist-page
    release cards leave it empty (their subtitle slot shows the year,
    not the artist, so it stays non-clickable).
  - AlbumCard wraps the artist name in a TouchArea that emits
    media-action("artist", artist-id, "open") and brightens on hover
    only when an id is known.
  - main.rs handles ("artist","open") -> open the artist page;
    FavoritesView (which only forwarded clicked) now forwards the
    artist action.

cc
2026-05-22 02:54:52 -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 1ebec6cbc1 refactor(slint): one shared TrackRow + TrackItem across all surfaces
Collapses the three near-identical track rows (AlbumView's inline
TrackRow, ArtistPage's inline TrackRow with multi-select, and the
shared SearchTrackRow) into a single primitives/TrackRow driven by
flags, backed by one TrackItem (superset). Foundation for the
upcoming playlist module, which renders the artwork variant heavily.

  - New primitives/TrackRow.slint with: show-artwork (cover+number vs
    number cell), show-album, show-favorite, show-download,
    multi-select-mode (selection checkbox). One media-action wires
    play / play-next / queue / favorite / download / toggle-select +
    the TrackContextMenu.
  - TrackItem is the unified model (superset of the old AlbumTrackItem
    + SearchTrackItem: number, album, selected, artwork all present;
    unused fields stay empty per surface).
  - Migrated AlbumView (show-artwork:false, show-download:true),
    ArtistPage Popular (show-artwork:false, multi-select-mode),
    Search, Favorites, Mix (show-artwork:true, +album/+favorite).
    Deleted SearchTrackRow.slint and both inline TrackRows.
  - Rust: AlbumTrackItem + SearchTrackItem -> TrackItem across
    album/artist/search/favorites/mix/main; search's internal row
    struct renamed TrackRow -> TrackRowData to free the TrackRow name
    for the Slint component. 46 tests pass.

cc
2026-05-22 00:31:42 -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
vicrodh 4a9cbdeb90 feat(qbz-radio): agnostic smart-radio pool builder crate
Ports the Tauri radio_engine into a standalone, frontend-agnostic
crate so any client (Slint, TUI, CLI) can build and play a smart
radio without Tauri.

  - New crate qbz-radio (builder + db + engine + tests), copied
    verbatim from src-tauri/radio_engine with the only change being
    the dependency rewrite: crate::api::{QobuzClient, Track,
    TracksContainer} -> qbz_qobuz::QobuzClient + qbz_models. Depends
    only on qbz-qobuz + qbz-models + rusqlite + dirs. All four engine
    tests pass.
  - QbzCore::create_smart_artist_radio builds the session pool + pulls
    up to 50 track ids + resolves them to full tracks. The radio DB's
    rusqlite Connection is !Send, so the build + pull run on
    spawn_blocking threads (the async builder driven via block_on),
    mirroring the Tauri command.
  - The artist "radio" action (Spotlight RADIO card + artist radio)
    now uses the smart builder (playback::play_smart_artist_radio);
    album radio (Radio Stations) keeps the Qobuz /radio/album
    endpoint, so both radio systems are in use. play_artist_radio
    (Qobuz /radio/artist) is retained as the simpler alternative.

cc
2026-05-21 07:49:01 -06:00
vicrodh c19a5b84dd feat: For You — radio, album suggestions, rediscover, more-from-library
Completes the remaining For You sections by porting two Qobuz API
endpoints down into qbz-core and adding the local "forgotten
favorites" approximation.

Backend ports (qbz-models + qbz-qobuz + qbz-core):
  - /album/suggest -> AlbumSuggestResponse, QobuzClient::get_album_suggest,
    QbzCore::get_album_suggest.
  - /radio/artist + /radio/album -> RadioResponse, get_radio_artist /
    get_radio_album on the client + core. (Faithful ports of the legacy
    src-tauri/api/client.rs methods — same paths, params, shapes.)

For You sections:
  - Radio Stations — album-seeded radio tiles (recent + favorite
    albums, deduped, capped 12), rendered with RadioCard; click starts
    a real Qobuz album radio.
  - More From Your Library — /album/suggest seeded from the most recent
    played album (else first favorite).
  - Rediscover Your Library — favorite albums not in the local
    play-history (approximation of Tauri's reco-DB forgotten
    favorites).
  - Spotlight's RADIO card + the artist "radio" action now create a
    real Qobuz artist radio (playback::play_artist_radio /
    play_album_radio build a queue from the radio track list and play).

Section order now matches Tauri: Qobuz Mixes, Release Watch, Radio
Stations, Recently Played Tracks/Albums, Your Top Artists, More From
Your Library, Rediscover, Artists to Follow, Spotlight.

Still backend-blocked: the Qobuz mix detail views (DailyQ/WeeklyQ/etc.)
and the smart radio_engine (the Qobuz radio used here is the simpler
/radio/* endpoint, not the local pool builder).

cc
2026-05-21 06:43:53 -06:00
vicrodh 83b7721b10 feat(slint): For You Qobuz Mixes row
Ports Tauri's QobuzMixesRow as the first For You section — four
220px navigation tiles (DailyQ / WeeklyQ / FavQ / TopQ) with the
1:1 gradients, badge, name, and description. Pure navigation, no
per-tile data. The mix detail views they open don't exist in the
Slint MVP yet, so the click is logged for now.

cc
2026-05-21 02:01:23 -06:00
vicrodh c27f6e1b9f feat(slint): For You Spotlight section
Ports Tauri's SpotlightLite — one favorite artist highlighted with a
hero and a content row. Backed by get_artist_page.

  - Spotlight component: header (title + "Shine a light…" subtitle),
    hero (round portrait + ARTIST label + name + play-top-tracks /
    open-artist circle buttons), and a draggable Flickable row
    mixing a TOP TRACKS card, a RADIO card, and the artist's albums.
  - New RadioCard (port of RadioCardLite): a 180px tile with the seed
    artwork centered on an elevated panel, a wordmark label over it,
    and a title/subtitle below; hover reveals play. (The Tauri
    palette-derived panel colour is approximated with a neutral
    elevated panel.)
  - foryou::load_spotlight rotates among the top-5 favorites by
    wall-clock seconds, fetches get_artist_page, and extracts up to 6
    albums (album > live > ep-single > compilation, deduped) plus
    whether the artist has top tracks. ForYouState gains the
    spotlight-* fields; two new artwork targets feed the portrait +
    album covers.
  - Wiring: ForYouView renders Spotlight after Artists to Follow.
    play-top-tracks routes to the existing artist play-top action;
    "start radio" has no artist-radio backend yet so it falls back to
    playing top tracks (logged).

Remaining For You: Radio Stations section (old card design), Qobuz
Mixes row, and the reco-DB sections (More from library, Rediscover).

cc
2026-05-21 01:51:31 -06:00
vicrodh 85ecf79a2d feat(slint): dedicated For You view with artist sections
Replaces the ad-hoc For You (a single album set crammed into
HomeState.sections) with a dedicated, ordered ForYouView/ForYouState
so the tab can mix album, track, and artist carousels — closer to
Tauri's For You. Lazy-loaded once on first open.

Sections (all reuse existing cards, no new card components):
  - Release Watch (album Carousel) — get_release_watch.
  - Recently Played Tracks (SlimCarousel) — local play-history.
  - Recently Played Albums (album Carousel) — local play-history.
  - Your Top Artists (ArtistCarousel) — the user's favorite artists
    via get_favorites.
  - Artists to Follow (ArtistCarousel) — similar artists seeded from
    a few favorites via get_similar_artists, excluding ones already
    followed (Follow chip shown).

Plumbing:
  - crate::foryou controller (load/apply/artwork). Five ForYou
    artwork targets feed the dedicated models.
  - select_tab("forYou") clears HomeState.sections (the home-content
    `for` renders nothing) and the For You content comes from
    ForYouView; HomeView routes the For You tab to ForYouView and
    gains an open-artist callback (wired through AppShell).
  - ensure_for_you_loaded lazy-loads on tab open (pill, header
    dropdown, and back/forward history).
  - home.rs no longer builds a For You section set (removed the
    redundant release-watch/favorites fetch on every home load).

Still to come (next increments): Qobuz Mixes row, Radio Stations
(old card design), Spotlight hero, and the reco-DB sections (More
from library, Rediscover) — those need backend the MVP lacks or the
mix detail views.

cc
2026-05-21 00:33:11 -06:00
vicrodh 066890a7dd feat(slint): Discover tabs are history entries
Home / Editor's Picks / For You were plain tab swaps with no history
footprint. Each is now a NavEntry::Discover { tab } pushed on click
(both the in-view pill and the Discover header dropdown), so
back/forward navigates between the three Discover pages. apply_entry
replays a Discover entry by selecting that tab's cached section set
and re-firing artwork.

cc
2026-05-20 23:35:26 -06:00
vicrodh 21a909185f fix(slint): genre popup grew to full window height
The popup root Rectangle set width but no height, so as a direct
child of the home overlay it defaulted to the parent's full height —
the panel ballooned vertically. Bind height to the content layout's
preferred-height so it hugs its content.

cc
2026-05-20 23:35:26 -06:00
vicrodh b4c99e1d88 feat(slint): flesh out Discover For You with real personalized sections
For You was a single Release Watch row. Adds the personalized album
sections the Slint MVP can source today, matching Tauri's forYou
defaults:
  - Release Watch (followed-artist new releases) — kept.
  - Favorite Albums — the user's hearted albums via get_favorites.
  - Recently Played Albums — from the local play-history store
    (carries the stored quality badge).

The remaining Tauri For You sections — Qobuz mixes, similar albums,
rediscover library, essentials by genre, artists to follow, artist
spotlight, continue listening — depend on the reco-DB / home-resolved
backend (v2_reco_*) that the Slint MVP does not have yet, plus an
artist-tile section type for the follow/spotlight rows. Those are the
next increment.

cc
2026-05-20 23:22:03 -06:00
vicrodh 9fce0939d1 feat(slint): genre filter advanced view + child-genre filtering
Adds the advanced view to the Discover genre filter and recovers the
child-genre filtering the discovery-v2 home redesign dropped.

Child-genre recovery:
  - The old (pre-redesign) home expanded a parent selection to its
    descendant genre NAMES and filtered content client-side; the
    discovery-v2 redesign passed only the explicitly-selected ids to
    the discover index, so selecting a parent stopped covering its
    children. Here filter_ids() expands the selection to all loaded
    descendant ids and feeds that to get_discover_index; selecting a
    genre eager-loads its descendant subtree so the expansion is
    complete.

Advanced view:
  - Search box + a scrollable 3-level genre tree (parent / child /
    grandchild), flattened in Rust into visible GenreTreeRow rows
    honoring per-node expansion. Each row: indent by level, expand
    arrow (lazy-loads children on first expand), checkbox, name, and
    loaded child count. Search returns a flat match list across all
    loaded genres.
  - Opening the advanced view eager-loads every parent's children so
    counts show up front; grandchildren stay lazy until a child is
    expanded.

State/wiring:
  - GenreTreeRow struct; GenreFilterState gains tree + search-query;
    GenreFilterActions gains toggle-expand + search.
  - genre_filter STATE moves to LazyLock (HashMap/HashSet aren't
    const) and gains children map, expanded set, and search query.
  - main.rs wires toggle-expand (lazy child load), search, and the
    advanced-open eager child load; toggle now eager-loads
    descendants before the re-fetch.

cc
2026-05-20 23:06:43 -06:00
vicrodh 880493b73d feat(slint): Discover genre filter (simple view)
Ports Tauri's Filter-by-genre popup (simple grid view). The selection
is shared across the three Discover tabs — matching Tauri's single
"home" genre context — and re-fetches the discover index with the
chosen genre ids.

UI:
  - New GenreFilterPopup: header (icon + title + close), Remember
    selection + Advanced view toggles (QbzToggle), a 3-column grid of
    parent-genre chips with an accent fill + check circle when
    selected, and a Clear filter footer. Anchored top-right under the
    button, with a click-away backdrop.
  - GenreButton turns accent-filled and shows "N genres" / "1 genre"
    when a filter is active (else "Filter by genre"), 1:1 with Tauri.
  - Advanced view (search + 3-level tree + counts) is the next
    increment; its toggle is present and shows a "coming soon" stub.

State / backend:
  - GenreFilterState + GenreFilterActions globals; GenreChip struct.
  - crate::genre_filter loads the parent genres (QbzCore::get_genres)
    and owns the shared selection, persisted to
    <data-dir>/qbz/genre_filter.json when Remember is on (dropped
    when off). Stale ids are pruned against the loaded genres.
  - load_home now takes genre_ids and threads them into
    get_discover_index. The home-load path is extracted into a shared
    `reload_home(active_tab)` used by the initial shell load and by
    genre toggle/clear; it rebuilds all three section sets, restores
    the active tab via select_tab, and re-fires artwork.

cc
2026-05-20 22:43:16 -06:00
vicrodh 235c61bf80 feat(slint): drag-to-scroll the slim grid carousels
Applies the same Flickable drag pattern to SlimCarousel (Recently
Played Tracks, Popular albums). The 4x3 page blocks now live inside a
horizontal Flickable — each block is one viewport width wide, cards
positioned absolutely within it — so the grid drags between pages
with kinetic momentum while slim-card taps still register. prev/next
nudge viewport-x by a page width; enabled state derives from
viewport-x. Two-page (24-item) cap unchanged.

cc
2026-05-20 22:23:53 -06:00
vicrodh 20b13dbbf1 feat(slint): drag-to-scroll album carousels
Introduces the horizontal drag concept (to be reused widely) on the
discover album Carousel. Replaces the manual page-slide (a clipped
Rectangle translated by -page*step) with a horizontal Flickable:
the row now drags by touch/pointer with kinetic momentum, and card
taps still register because Flickable distinguishes a flick from a
tap — unlike nested TouchAreas, where the card's own TouchArea would
swallow the drag.

  - viewport-height is pinned to the row height so the Flickable
    only scrolls horizontally and never fights the home page's
    vertical Flickable.
  - prev/next buttons nudge viewport-x by one page width (clamped to
    [-max-scroll, 0]); enabled state + the edge fades derive from
    viewport-x.
  - No animate on viewport-x — that would rubber-band the per-frame
    drag updates; button jumps are instant in exchange for smooth
    dragging.

cc
2026-05-20 22:15:10 -06:00