Commit Graph
267 Commits
Author SHA1 Message Date
vicrodh 12048bcee0 fix(slint): label albums missing artist/quality/date + favorite + load-more
/label/getAlbums returns the V2 nested album shape (DiscographyAlbumDto:
artists[], audio_info, dates, track_count) — confirmed against the Android
decompiled sources — but it was deserialized into the flat Album model,
which dropped artist, quality and release date (Unknown Artist in
group-by-artist, no quality badge, no date, empty list-row columns).

qbz-models Album: capture the nested V2 fields (audio_info, dates,
track_count, release_type) as optional serde(default) — backward
compatible with the flat-shape endpoints.
label.rs map_album: read nested-first (quality from audio_info, date from
dates, count from track_count, artist from artists[] main-artist, explicit
release_type) with flat fallback. Fixes the releases grid + list columns,
the landing Releases carousel, and Critics' Picks.
Wire ("album","favorite") -> add_favorite + toast (works on every album
card via the shared component).
LabelReleases load-more now driven by has_more (the endpoint caps each
page well below the catalog, so total was an unreliable gate).

cc
2026-05-26 20:10:09 -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 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 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 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 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 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 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 449d3c9e2a feat(slint): enrich recently-played store with quality + ids
The play-history store kept only id/title/artist/artwork, so the
Recently Played Albums cards rendered bare (no quality badge) and a
re-play had no stored context to fall back on.

RecentTrack now also persists quality_tier, quality_label and
artist_id; RecentAlbum carries the quality through to its card.
record_recent derives the quality from the live queue track's
bit_depth / sample_rate (same Hi-Res/CD badge format as the discover
cards) and copies its artist_id. home.rs maps the stored quality
onto the Recently Played Albums card so the badge now shows.

cc
2026-05-20 22:15:10 -06:00
vicrodh 2dc6726c3b feat(slint): discover home loading skeleton + paginated slim carousels
Three of the four Discover > Home quick-wins.

Loading skeleton (#1):
  - New HomeSkeleton replaces the spinner + "Loading your music…"
    text shown while the first fetch is in flight. Grey placeholder
    blocks shaped like two real carousels (title bar + a row of
    square art with two text lines), gently pulsing via a shared
    900ms Timer phase + animate opacity — no more artifact titles
    during load.

Recently Played Tracks + Popular albums (#3, #4):
  - New SlimCarousel: a paginated 4x3 slim-card grid (12 per page),
    capped at two pages, sliding one viewport width per page. The
    prev/next controls only appear when there is a second page
    (>12 items); 12 or fewer renders as a single static page with
    no arrows. Replaces the old non-paginated GridSection that grew
    into a tall wall of rows.
  - home.rs caps `recent` and `popular` to 24 items (two pages) so
    the carousel never has hidden overflow; the Popular/Recent
    artwork-job indices are unchanged.

GridSection is retained — the artist ReleaseGrid still uses it.

Note on #2 (Recently Played Albums "spurious AlbumCard"): there is
only one AlbumCard component (discover/AlbumCard.slint, used by 5
call sites incl. this carousel via Carousel) — no duplicate to
delete. The cards look bare because the local play-history store
keeps only id/title/artist/artwork (no quality/year/ribbon), not
because of a different component.

cc
2026-05-20 21:58:36 -06:00
vicrodh 1efa6fc745 feat(slint): Discover Editor's Picks + For You tabs
Makes the three-tab Discover pill (Home / Editor's Picks / For You)
functional, plus the matching Discover header-dropdown rows. Mirrors
Tauri's DiscoveryView: all tabs draw from one fetch and swap which
curated section set is shown — no extra round-trips on tab switch.

home.rs:
  - load_home now builds three section sets from a single
    get_discover_index call (cloning the editorial containers):
      * home — the existing balanced set.
      * editorPicks — editorial only (New Releases, Qobuzissimes,
        Press Accolades, Most Streamed, Ideal Discography, Albums of
        the Week), ordered per Tauri's DEFAULT_PREFS.editorPicks.
      * forYou — Release Watch (get_release_watch "artists"). The
        deeper personalized sections (Qobuz mixes, similar albums,
        spotlight) need reco-DB backend the Slint MVP lacks, so this
        is the honest first cut.
  - apply_home caches all three sets in a thread-local; select_tab
    swaps HomeState.sections to the chosen set and returns the
    Section-targeted artwork jobs to re-fire. Reuses the existing
    Section artwork target — no new targets.
  - map_full_album maps release-watch's catalog Album to a card.

Slint:
  - HomeState gains active-tab; HomeActions.select-tab wired to the
    pill (active state derived from active-tab) and to the Discover
    dropdown rows (route "discover-<tab>") via header-menu-navigate.
  - The Home-only sections (Recently Played albums/tracks, Popular)
    are gated to active-tab == "home" so Editor's Picks / For You
    render just their own set.

cc
2026-05-20 21:33:06 -06:00
vicrodh 93edce305c feat(slint): Favorites Playlists + Labels tabs
Completes the five Favorites tabs. Both lazy-load like the others.

Playlists:
  - Sourced from QbzCore::get_user_playlists (the getUserFavorites
    envelope has no playlists branch; Tauri splits owned-vs-following
    locally — the MVP shows the full user playlist list for now).
  - FavoritePlaylistItem card: cover (images300 > images150 >
    images, first entry), name, "N tracks". Cover streams via the
    artwork pipeline (new ArtworkTarget::FavoritePlaylist). Click is
    logged — no playlist detail view in the Slint MVP yet.

Labels:
  - Parsed from get_favorites("labels"); the qbz-models Label is
    just {id,name}, so a local FavLabel shape picks up the
    albums_count. FavLabelRow (icon + name + "N releases") routes
    the click into the existing LabelReleasesView via
    FavoritesActions::open-label -> navigate_label.

FavTab gains Playlists + Labels; from_route/from_tab_id now resolve
them so the header dropdown and tab bar both lazy-load. Tab bar
shows live counts for all five tabs.

Still deferred (per "finish then polish one by one"): the richer
Tauri options (sort, group-by-artist, owned/following split,
per-row context menus, bulk select, download status).

cc
2026-05-20 21:20:53 -06:00
vicrodh a40c6ce569 feat(slint): Library > Favorites view (Tracks / Albums / Artists)
Wires the Library header dropdown to a real Favorites view. Mirrors
Tauri's FavoritesView.svelte core: a tab bar over per-tab content,
each tab lazy-loaded on first open.

Slint:
  - ContentView gains `favorites`. NavMenuEntry gains a `route`
    field; the Library dropdown rows carry "favorites-tracks" /
    "-albums" / "-artists" / "-playlists" / "-labels". The
    HeaderMenuOverlay click bubbles the route up through AppShell ->
    AppWindow -> Rust (new header-menu-navigate callback).
  - FavoritesState / FavoritesActions globals. Tracks reuse
    SearchTrackItem (so the shared SearchTrackRow renders them with
    per-row cover art), albums reuse AlbumCardItem + the Discover
    AlbumCard, artists use a new FavoriteArtistItem round-portrait
    grid.
  - New ui/favorites/FavoritesView.slint: title, tab bar with live
    counts + active underline, and the three populated tabs.
    Playlists / Labels show "Coming soon" pending their backing
    views. AppShell routes ContentView.favorites.

Rust:
  - crate::favorites controller — load_favorites parses the raw
    get_favorites JSON (`{ <type>: { items, total } }`) into typed
    qbz-models Track / Album / Artist, maps to the Slint structs.
    Lazy per-tab: select-tab and the header route both call
    navigate_favorites, which fetches only the opened tab.
  - artwork::ArtworkTarget gains FavoriteTrack / FavoriteAlbum /
    FavoriteArtist; covers stream in via the shared pipeline.
  - main.rs: navigate_favorites + on_header_menu_navigate +
    FavoritesActions (select-tab / open-album / open-artist /
    play-track / track-action). No new qbz-core method —
    get_favorites already existed.

Deferred: Playlists + Labels tab content, full pagination (one 500
page is fetched per tab), favorites preferences (tab order).

cc
2026-05-20 20:53:37 -06:00
vicrodh cdb93341d0 feat(slint): ArtistsByLocationView (Port 3 complete — third click target)
Wires the Origin section's location link to the scene-discovery
view. Completes the three artist-network click targets (Musician,
Label, Location).

Slint:
  - ContentView gains `location`; NavEntry mirrors with the full
    `Location { mbid, area_id, area_name, country, genres, tags }`
    payload so back/forward replays the scene.
  - LocationViewState / LocationViewActions globals carry the scene
    header (label + genre summary) and a paginated artist grid
    (LocationArtistItem: qobuz id, name, genres line, image).
  - New `ui/location/ArtistsByLocationView.slint`: a scene header
    (map-pin + location + genre summary) over a wrapping round-
    portrait artist grid with load-more. open-artist routes the
    card click to the artist page.
  - AppShell routes ContentView.location; new map-pin.svg (Lucide).

Rust:
  - `crate::location_view` controller — load_scene, apply_scene,
    append_scene, reset_scene, artwork_jobs. Maps LocationCandidate
    rows to the grid (qobuz_name fallback to mb_name).
  - artwork::ArtworkTarget gains LocationArtist { index }.
  - The Origin metadata load now caches LocationParams (area +
    affinity seeds) in a cross-thread Mutex; reset_network_sidebar
    clears it so a stale click can't fire for the wrong artist.
    artist::location_params() exposes it to the click handler.
  - main.rs: navigate_location + apply_entry route +
    on_location_clicked (reads the cached params) +
    LocationViewActions open-artist / load-more.

Backend (prior commit) supplied discover_artists_by_location;
this commit is the Slint + wiring layer.

cc
2026-05-20 20:31:08 -06:00
vicrodh 302f708694 feat(slint): LabelReleasesView (Port 3, second of three click targets)
Wires the artist network sidebar's Labels section to a real
destination. Mirrors Tauri's LabelReleasesView.svelte core: a label
header (image + name + release count) over a wrapping album grid
with load-more pagination. (Sort / filter Hi-Res / group-by-artist
from the Tauri view are deferred — the core browse experience is in.)

Slint:
  - ContentView gains `label`; NavEntry mirrors with
    `Label { id, name }`.
  - LabelState / LabelActions globals carry the header (id, name,
    image-url + decoded image) and the AlbumCardItem grid + total.
  - New `ui/label/LabelReleasesView.slint`: label header card,
    then the same absolute-position album grid + load-more pattern
    the artist/musician views use. Reuses the Discover AlbumCard
    (artist-as-subtitle is correct here — a label's catalog is
    various artists).
  - AppShell routes ContentView.label and forwards open-album +
    media-action.

Rust:
  - `crate::label` controller — load_label (parallel-ish
    get_label_page + get_label_albums), load_more_albums,
    apply_label, append_albums, apply_image (label header), and
    artwork_jobs. extract_label_image handles the flexible
    /label/page image value (string or mega/extralarge/.../small
    object), same precedence as the Svelte.
  - artwork::ArtworkTarget gains LabelAlbum { index }.
  - main.rs: navigate_label + apply_entry route + on_label_clicked
    (sidebar) + LabelActions::on_load_more.
  - No new qbz-core methods — get_label_page / get_label_albums
    already existed.

cc
2026-05-20 20:09:09 -06:00
vicrodh cfe083898d feat(slint): MusicianPageView (Port 3, first of three click targets)
Wires the sidebar's musician-clicked callback into a real
destination. Mirrors Tauri's MusicianPageView.svelte plus the
"jump to artist page when Confirmed" shortcut.

Slint:
  - ContentView gains a `musician` variant; NavEntry mirrors with
    `Musician { name, role }` so the back/forward history can
    re-fire the page with the original payload.
  - MusicianState / MusicianActions globals carry the resolved
    musician metadata (name, role, confidence) + the paginated
    Appears On grid (MusicianAppearanceItem mirrors the Tauri
    interface — album_id, title, artist_name, year,
    role_on_album, artwork URL/handle).
  - New `ui/musician/MusicianPageView.slint`: header (avatar +
    name + role), Bands & Projects chips when populated, and an
    Appears On grid built from the same absolute-position pattern
    the artist release grid uses. Load more button drives
    MusicianActions::load-more.
  - AppShell routes ContentView.musician to MusicianPageView and
    forwards open-album.

Rust:
  - `crate::musician` controller — load_musician, apply_musician,
    load_more_appearances, append_appearances, reset_musician,
    artwork_jobs. Pure data + UI plumbing; the orchestration lives
    in qbz-core.
  - artwork::ArtworkTarget gains MusicianAppearance { index } so
    the existing artwork pipeline can target the new grid.
  - main.rs gains `navigate_musician` (the navigation entry point
    apply_entry now also routes to) and `on_musician_clicked`
    (does the resolve, jumps to navigate_artist on Confirmed,
    otherwise records + navigates to the Musician view).
  - MusicianActions::on_load_more wired to load_more_appearances
    + append.

cc
2026-05-20 19:17:51 -06:00
vicrodh c4b6acb760 fix(slint): smooth sticky scroll + artwork right-click menu
Two unrelated things landing together.

Sticky scroll fix:
  - The natural-top binding on both jump-nav and the sidebar reads
    from `body-row.absolute-position.y` (a live, scroll-aware
    lookup). The y formula was `max(natural-top + viewport-y, ...)`
    — adding viewport-y on top of an already scroll-aware value
    double-counted the scroll. During mid-scroll the bar drifted
    ahead of the body row, opening a visible gap and briefly
    overlapping the header.
  - Drops `+ viewport-y` from both formulas. natural-top alone IS
    the viewport-relative y the elements want to occupy; the
    max() clamp just keeps them from going above the top.
  - Initial position and sticky position are unchanged; only the
    transition is fixed.

Artwork right-click menu (artist portrait + album cover):
  - Both headers get a TouchArea on the artwork that opens a
    PopupWindow + ContextMenu on right-click, positioned at the
    cursor. Mirrors Tauri's `oncontextmenu` flow on the artist
    portrait and album cover.
  - Menu items match Tauri 1:1: Add image / cover, Change + Remove
    (when a custom image is registered), divider, Open in
    browser, Save as.
  - New `crate::custom_artwork` JSON-backed store at
    `<data-dir>/qbz/custom_artwork.json` keyed by artist name +
    album id (mirror of the Tauri library's
    `v2_library_set/remove_custom_*` persistence).
  - ArtistState / AlbumState expose `artwork-url` (network URL for
    Open / Save) + `has-custom-image` / `has-custom-cover` (drives
    the Add vs Change/Remove menu rendering), both populated from
    Rust at apply_artist / apply_album time.
  - ArtworkActions global + main.rs handlers:
      * open-in-browser → `open::that(url)`.
      * save-as → `rfd::AsyncFileDialog::save_file()` + reqwest
        download + tokio::fs::write.
      * add-custom → `rfd::AsyncFileDialog::pick_file()` then
        `custom_artwork::set_*` + flip the has-custom flag.
      * remove-custom → `custom_artwork::remove_*` + reset flag.
  - rfd added with xdg-portal + tokio features. external-link and
    image-plus SVGs ported from Lucide.

Custom-image RENDERING (using the picked file in place of the
network artwork) is deferred — the store persists the choice and
the menu reflects it, but the displayed image still comes from
Qobuz. Next pass.

cc
2026-05-20 18:24:39 -06:00
vicrodh 06a763443a feat(slint): jump-nav scroll-to-section + hide empty tabs + UX fixes
Brings the JUMP TO bar from a visual placeholder to a working nav,
plus two interaction fixes the user flagged on the previous build.

JUMP TO bar — functional scroll-to:
  - JumpNavTab gains an `anchor-y: length` field. JumpNavBar's
    tab-clicked callback now emits (id, anchor-y); the parent
    (ArtistPageView) scrolls page-flickable to land the section
    just below the sticky bar.
  - Tab list is built in Rust (`build_jump_tabs` in artist.rs)
    from the artist's actual data and pushed to a new
    `ArtistState.jump-tabs` model. Tabs are emitted only for
    sections that exist (top tracks present, "Discography" /
    "EPs & Singles" / "Live Albums" / "Compilations" / "Others"
    only when the corresponding release_sections entry is there);
    the Playlists tab — not implemented yet — never appears.
  - anchor-y estimates use layout-derived constants per section
    type (popular-tracks header + visible row count, release grid
    headers + ceil(album_count / 5) rows). Not pixel-perfect for
    long bios or grids that wrap differently, but lands the
    viewport inside the right section.
  - Removed the hardcoded ArtistPageView property — tabs and
    visibility are now data-driven.

Sticky positioning — dynamic:
  - jump-nav.natural-top now binds to
    `body-row.absolute-position.y - page-flickable.absolute-y -
    self.height`, so the bar always sits flush in the gap above
    the body row regardless of artist-header height (long-bio
    artists stop pushing the row out from under the bar).
  - Sidebar natural-top follows the same body-row anchor, so it
    too tracks where the body actually starts.

Search input UX:
  - The outer search-icon slot is now removed from layout (via
    `if !search-open`, not `visible:`) when the input opens, so
    the expanded input runs all the way to the bar's right
    padding — right under the user's cursor where the lupa just
    was, no leftover gap on the right.
  - `changed search-open` auto-focuses the inline TextInput, so
    typing starts immediately after clicking the lupa with no
    second click in the input.

cc
2026-05-20 14:20:47 -06:00
vicrodh df3ef14c81 fix(slint): release cards show year + quality badge; Load more polished
Two release-grid issues from the latest user pass.

Release cards (Discography / EPs & Singles / Live / Compilations /
Others):
  - map_release was leaving year, quality_tier and quality_label as
    empty strings, so the shared AlbumCard had nothing to render for
    the badge (no Hi-Res / CD pill) and showed the artist name as
    the subtitle (redundant on the artist's own page).
  - Now populates `year` from `release.dates.original` (first 4
    chars), `quality_tier` from `audio_info.maximum_bit_depth` via
    the existing `tier()` helper, and `quality_label` as the
    "24-bit / 96 kHz" string the badge tooltip expects.
  - card_to_item re-routes the year into the AlbumCard's `artist`
    field so the subtitle reads "2026" / "2020" / "1997" instead of
    "Julieta Venegas" — matches Tauri's artist page.

Load more / View less:
  - Centered (was left-aligned), no background and no border, 4px
    gap above so it sits separated from the last visible row
    instead of looking glued to it. Hover treatment is a simple
    text-color flip (Theme.text-primary on hover) rather than the
    panel-style bg + border the button had before.

cc
2026-05-20 13:17:48 -06:00
vicrodh 63295583b7 fix(slint): restore Popular Tracks buttons, wire Play All, checkbox left
Three follow-ups to the body-row restructure.

Popular Tracks header:
  - The header HorizontalLayout had `alignment: space-between` but
    no horizontal-stretch on the title cell, so the action cluster
    (Play / Select / More) collapsed to zero width inside the new
    main-col container. Now uses an explicit horizontal-stretch on
    the title cell with the action cluster pinned to its preferred
    width, so the three buttons reappear on the right.

Play All wired:
  - ArtistState gains an `id` property, set in navigate_artist
    before the page data loads. The Play CircleAction now passes
    `ArtistState.id` through the media-action callback as the
    artist id payload.
  - main.rs routes `("artist", "play-top")` to a new
    playback::play_artist_top_tracks, which re-fetches the artist
    page (so the queue carries the same audio metadata the page
    row uses), builds QueueTracks from page.top_tracks, set_queues
    them, and starts audio on the first one. Other artist-row
    actions (Select / More) stay as their current placeholders for
    now.

Multi-select layout:
  - Per-track checkboxes move from the right side of the row to
    the leading column, replacing the play-cell when multi-select
    is on (so the checkbox stays vertically aligned with the same
    column the play number normally sits in). This is the project
    standard the user called out — checkboxes belong on the
    leading edge.
  - When multi-select is on, a "Select all" check-all row appears
    immediately above the track list, also leading-aligned. The
    visual click emits media-action("artist", "",
    "select-all-top-tracks") — handler stub will land with the
    rest of the selection wiring.

cc
2026-05-20 13:13:56 -06:00
vicrodh ed8bf0054d fix(slint): strip_html iterates by char, not byte (preserves UTF-8)
The previous byte-walking implementation pushed `bytes[i] as char`
for every non-tag byte, which mangled every multi-byte UTF-8
sequence into its constituent Latin-1 codepoints. A single "ó"
(U+00F3 = 0xC3 0xB3) came out as "ó"; longer artist biographies
in Spanish / Portuguese were riddled with the glitch chars the
user just flagged ("establecíĀĀª" instead of "estableció", etc).

Rewrites the three byte-walking helpers (normalize_breaks,
decode_entities, and the underlying matchers) to walk by `char`
via `.chars()` + `&str` slicing. Tag detection still uses
`as_bytes()` for fast ASCII matching against `<br...>` and `</p>`,
but only when the leading `<` is ASCII (which it always is) — the
rest of the body passes through char-by-char so accented letters
and curly quotes land verbatim in the output.

Added a regression test (`preserves_multibyte_characters`) that
runs the corrupted biography fragment through strip_html and
asserts the round-trip is lossless.

cc
2026-05-20 13:01:56 -06:00
vicrodh 381ab04ef0 feat(slint): in-page search for the artist view
Mirrors Tauri's `.page-search` substring filter — Popular Tracks
(title + artist) and every release-section album (title) hide on
each keystroke when they don't match, restore when the query is
cleared.

UI:
  - Search row added between the artist header and Popular Tracks,
    always visible while artist data is present. Search icon +
    inline TextInput, surface-card background with a thin border
    that highlights on focus. Placeholder "Search this page…".

State:
  - New ArtistActions global with `callback search(string)`, re-
    exported through app.slint so main.rs can wire it.

Controller (artist.rs):
  - Thread-local FULL_TOP_TRACKS and FULL_RELEASE_SECTIONS cache the
    rendered models on the UI thread (same pattern as
    AlbumPageView's FULL_TRACKS), populated in apply_artist so the
    filter never round-trips to the backend.
  - filter_artist(window, query): trim+lowercase the query, then
    rebuild ArtistState.top_tracks and ArtistState.release_sections.
    Sections whose albums all filter out are dropped from the visible
    list, matching Tauri's behavior.

main.rs:
  - ArtistActions::on_search wired to artist::filter_artist (same
    shape as the existing AlbumActions::on_search wiring).

cc
2026-05-20 11:27:11 -06:00
vicrodh 275d0e8121 feat(slint): +1px typography scale and shared paragraph-preserving strip_html
Two text-side refinements to the artist + album views.

Typography:
  - Bumps every scale level by 1px so body text reads at 15px instead
    of 14px (legal 13, link 14, body/subtitle 15, button 17, heading
    19, title 25, wordmark 29). All views adopt the new scale through
    the existing tokens — the artist + album descriptions feel
    noticeably more readable without per-component overrides.

strip_html (new shared module `crates/qbz-slint/src/strip_html.rs`):
  - Single implementation now used by both album::map_album and
    artist::map_artist for editorial copy (reviews, biographies).
    The previous album-local helper handled only <br> and </p>; the
    artist-local helper handled neither, so multi-paragraph
    biographies collapsed into one wall of text.
  - <br>, <br/>, <br /> (any case + spacing) -> \n. </p>, </P> ->
    \n\n. Stripped tags like <strong>, <em>, <a> still leave their
    text inline. (Slint's `Text` is single-style, so true inline
    bold/italic isn't possible without splitting into multiple Text
    elements — deferred.)
  - Broader entity table: amp/lt/gt/quot/apos/nbsp/copy/reg/mdash/
    ndash/hellip/curly quotes plus numeric copy variants.
  - Runs of 3+ newlines collapse to a paragraph break so stacked tags
    don't blow the layout open. Unit tests for each path.

cc
2026-05-20 11:13:04 -06:00
vicrodh 669785ae01 feat(slint, core): per-artist play history feeds the discovery filter
Ports the minimum of src-tauri/src/reco_store needed to honour the
"skip artists I already listen to" gate in the discovery pipeline.

play_history (`crates/qbz-slint/src/play_history.rs`):
  - Tiny SQLite store at <data-dir>/qbz/play_history.db with WAL
    (ADR-002), holding play_events (artist_id + occurred_at, one row
    per audible track start) and artist_names (id -> name upsert).
    Lazy open, errors collapsed to log::warn so a missing DB just
    means an empty filter — same default Tauri lands on a fresh
    profile.
  - record_play(artist_id, name) wired into playback::record_recent
    so every audible track start increments the per-artist count.
  - known_artists(threshold) returns the (HashSet<u64>,
    HashSet<String>) pair Tauri's filter expects — qobuz ids for
    suppressing validated matches, normalized names for suppressing
    MB candidates upstream.

qbz-core::musicbrainz_discover_artists:
  - New `known_artists` closure parameter, called once per
    invocation. Threshold matches Tauri (>2 plays).
  - Filter applied at both stages: MB candidates dropped when their
    normalized name is in known_names; Qobuz matches dropped when
    their id is in known_ids (mirrors local_known_qobuz_ids check
    in v2_get_discovery_artists).

The Tauri reco_store has many other features (favorites, top
genres, home seeds) the Slint shell does not use yet — those stay
out of this commit; only the play-count surface needed by the
sidebar's "You may also like" filter is ported.

cc
2026-05-20 11:06:38 -06:00