Commit Graph
708 Commits
Author SHA1 Message Date
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 6d68e47daf feat(core): discover_artists_by_location for the scene view
Ports the core of v2_discover_artists_by_location to QbzCore,
unblocking the Slint ArtistsByLocationView. Reuses the affinity
helpers already living in qbz-integrations (compute_affinity_score,
extract_affinity_seeds, is_broad_genre, genre_summary).

Pipeline:
  - Smart area resolution — resolve a city area_id to its parent
    subdivision via resolve_parent_subdivision for broader results
    (Leyton → England), falling back to the raw area name.
  - Drop overly broad genre tags (rock / pop / latin) from the
    search set so the query doesn't return the whole country's
    catalog; fall back to the raw list only if everything was
    broad.
  - MB tag+area search per genre, dedupe candidates by mbid,
    accumulate affinity score + a multi-genre-hit bonus.
  - Sort by score (stable by mbid for paginated calls), page with
    offset/limit, validate each against Qobuz (exact normalized
    name match, most-albums wins for collisions).

Deferred vs the Tauri command: the scene cache (get/set) and the
`scene-discovery-progress` events. The pure discovery result is
identical otherwise.

cc
2026-05-20 20:15:41 -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 1b76ce3fa2 feat(core): musicbrainz_resolve_musician + musician appearances
Two new QbzCore methods unblocking the MusicianPageView port — both
direct ports of v2_resolve_musician and v2_get_musician_appearances
from src-tauri/commands_v2/integrations.rs.

musicbrainz_resolve_musician(name, role):
  - MB client resolve_artist for the MBID, then a Qobuz artist
    search for an exact name match. Confirmed when the Qobuz
    artist matches; Contextual when there are appears-on albums
    only; Weak when MB knows the artist but Qobuz has nothing;
    None otherwise.
  - The sidebar's musician-clicked dispatcher uses the confidence
    to decide whether to route the click into the artist page
    (Confirmed) or into MusicianPageView (Contextual / Weak).

musicbrainz_get_musician_appearances(name, role, limit, offset):
  - Wraps QbzCore::search_albums with the same per-row mapping
    Tauri does: album_id, title, artwork (large/small fallback),
    artist name, year, and the caller's role string stamped onto
    each row as `role_on_album`. Returns
    `MusicianAppearances { albums, total }`.

cc
2026-05-20 18:27:45 -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 69029238ee fix(slint): jump-nav natural-top at body-row top, no header overlap
The previous binding pulled the bar 44px up from body-row's top
(`body-row.y - self.height`), which landed it inside the artist
header — eating the action buttons row. The right value is
`body-row.y` itself: the bar takes the top 44px of the 52px
reservation slot that lives as body-row's first child, so it sits
exactly in the gap between the header above and the body below
(Popular Tracks header starts 52px into the body row, giving an
8px breathing gap below the bar). No overlap on either side.

Sidebar natural-top compensates with `+ jump-nav.height` so it
still starts flush with the bar's bottom edge as before.

cc
2026-05-20 17:09:50 -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 3429fe4ee2 fix(slint): popular tracks header below jumpnav, X inside input
Three targeted fixes from the latest screenshot.

Body offset:
  - The 24px head spacer before the body row only nudged content
    past the artist header, leaving Popular Tracks' first row sitting
    under the JUMP TO overlay (the play/select/more buttons in
    particular were behind the bar). Spacer is now 52px so the body
    column begins below the bar's natural y + 44 height + a small
    gap.
  - jump-nav natural-top tightens from 290 to 291 (purely cosmetic
    alignment with the new 52px slot above it). Sidebar natural-top
    follows to 335 (= 291 + 44) so it still sits flush below the
    bar.

Search input width:
  - max-open-width drops from 360px to 216px (60% of the previous
    cap). The expanded input now reads as a compact box on the
    right of the bar instead of a bar-wide slab.

X close affordance:
  - Moves INSIDE the search input on the right side, alongside the
    leading magnifier glyph. Clicking it both clears the query and
    closes the input (calls back into search-clicked, which the
    parent already wires to a reset).
  - The standalone X button that used to live on the bar's right
    edge is gone — the outer button now only ever shows the search
    icon when closed, and hides entirely while the inline input is
    open (no two competing search affordances).

cc
2026-05-20 14:03:24 -06:00
vicrodh 6d6a5c0e10 fix(slint): cap search input width; sidebar flush below jumpnav
Two precision fixes from the latest screenshot pass.

Search input:
  - The expanded input was claiming the full center-region width
    (everything between the JUMP TO label and the close icon),
    reading as a "gigante" slab. Caps the open width at 360px and
    keeps the right-edge anchor so it expands inward from the
    search icon, matching Tauri's `.search-input-container` shape.

Network sidebar top edge:
  - natural-top moves from 320px to 334px (= jump-nav natural-top
    290 + jump-nav height 44). The sidebar now starts flush with
    the bottom edge of the JUMP TO bar instead of sliding behind
    its last 14px ("se come un cachito").

cc
2026-05-20 13:37:43 -06:00
vicrodh 2b5e4aead1 feat(slint): search input slides into the JUMP TO bar inline
Moves the in-page search out of the body column into the JUMP TO
bar itself, with a width animation so the input slides over the
tabs from the right edge on open (and back to 0 on close), matching
Tauri's `.page-search.open` behaviour.

JumpNavBar:
  - New `search-text` (in-out) and `search-text-edited` callback so
    the parent owns the query string + filter wiring while the bar
    owns the input UI.
  - Center region is a single Rectangle whose `clip: true` hides the
    expanding search-overlay until it has width. The overlay's
    width binds to `search-open ? parent.width : 0px` with a 200ms
    ease-in-out animation, so the tabs visually disappear behind it.
  - Tabs' TouchAreas are disabled while the search is open, so the
    visible click target is just the input.
  - Search icon flips to an X glyph and stays accent-tinted while
    open, signalling "click again to close".

ArtistPageView:
  - The standalone in-page search Rectangle in the body column is
    removed. The 24px head spacer stays so the loading state /
    Popular Tracks header sit at the same y as before.
  - JumpNavBar wired: search-text-edited routes through to
    ArtistActions.search; search-clicked toggles search-visible and
    also clears the query (and re-runs the empty search) on close
    so the filter releases as the input disappears.

cc
2026-05-20 13:24:29 -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 ccebf2833c feat(slint): sticky JUMP TO bar, search toggle, View less
Three follow-on refinements after the layout restructure.

JUMP TO bar (`ui/primitives/JumpNavBar.slint`):
  - New reusable component — same shape Tauri ships above
    section-rich pages (artist, library, future label / musician
    views): "JUMP TO" label, horizontal list of tabs with an accent
    underline on the active one, search icon on the right.
  - Presentation-only — sticky positioning is the parent's
    responsibility. Tabs and search icon emit callbacks; reuse by
    passing a fresh tab list and routing the callbacks.

Artist page wiring:
  - JumpNavBar mounted as an overlay sibling of the page Flickable
    with the same sticky math as the sidebar: y =
    `max(290px + page-flickable.viewport-y, 0px)`. At rest the bar
    sits below the artist header; once the user scrolls past the
    header, it pins to the top of the page. (This is the reusable
    sticky recipe the user asked for — it's wired here for artist
    and lifts straight into the next section that needs it.)
  - jump-tabs hardcoded with About / Popular Tracks / Discography /
    EPs & Singles / Compilations / Playlists — same labels as
    Tauri. tab-clicked updates the active-tab visual state; actual
    scroll-to-section wiring is a follow-up (needs anchor positions
    that depend on per-artist data).
  - search-visible flag flips on the bar's search-clicked. The
    existing in-page search input is now gated by it, so the search
    is closed by default (matches Tauri's `.page-search.open`
    behaviour) and opens on click of the JUMP TO bar's search icon.

Popular Tracks "View less":
  - The Load more affordance now flips its label to "View less" once
    expanded so users can collapse back to the 5-track preview
    without scrolling. A small improvement over the Tauri flow
    (which only shows Load more).

cc
2026-05-20 12:26:01 -06:00
vicrodh 50b92031cb fix(slint): header spans full width, sticky-style sidebar overlay
Restructures the artist page so the gradient header truly covers the
full window width and the network sidebar behaves like Tauri's
sticky aside: anchored to natural-y below the header while the page
is at the top, then pinned to sticky-top once the user scrolls past
the header. The bottom edge stays flush with the now-playing bar.

Layout:
  - The outer HorizontalLayout wrapping (Flickable + sidebar column)
    is gone. The Flickable is now a direct child of ArtistPageView at
    full root.width, so the gradient Rectangle inside it also runs
    edge-to-edge. (Tables analogy: header row is colspan=2.)
  - Inside the page VerticalLayout, body content (search, popular
    tracks, release grids) is wrapped in a HorizontalLayout with a
    horizontal-stretch main column on the left and a 300px empty
    placeholder on the right when the sidebar is open. The main
    column never extends under the sidebar.

Sidebar:
  - The sidebar block is no longer a sibling of the Flickable inside
    a HorizontalLayout — it's an absolutely-positioned Rectangle
    overlay anchored at x = root.width - 300, y bound to
    `max(natural-top + page-flickable.viewport-y, sticky-top)`. At
    scroll = 0 the sidebar sits at natural-top = 320 (just below the
    header). As the user scrolls, the sidebar tracks the page upward
    until it pins at sticky-top = 44 (reserving room for the future
    sticky JUMP TO bar — harmless until that lands).
  - height = root.height - self.y, so the sidebar always extends to
    the bottom of the page area (top of the now-playing bar). The
    32px bottom inset is gone — no more gap under the sidebar.

cc
2026-05-20 12:19:45 -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 6642b5ef13 feat(slint): release sections render as wrapping grids, not carousels
Tauri's artist page lays Discography / EPs & Singles / Live Albums /
Compilations / Others as grids that wrap into rows — never as
horizontally paginated carousels. The Slint port was reusing the
Discover Carousel here, which paginated horizontally and hid most of
the discography behind chevrons.

New `ui/artist/ReleaseGrid.slint`, modeled on `discover/GridSection`:
  - Same DiscoverSection input + album-clicked / media-action
    callbacks the Carousel exposed, so swapping is one import + one
    component name in ArtistPageView.
  - Card grid uses absolute positioning inside a sized Rectangle so a
    `for` can render a flow that Slint's layout primitives don't
    support directly. Columns recompute on resize:
      max(1, floor((width + gap) / (card_width + gap)))
    Card sizes match the Carousel for visual continuity (220x266 +
    24px gap), so the wider viewport lands ~5 cards per row, the
    narrower one ~2.
  - Header shows the section title and the count next to it
    (mirrors Tauri's `<count>` badge); no sort dropdown yet — per-
    section sorting can land in a follow-up.

cc
2026-05-20 11:22:38 -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
vicrodh 8b526049bb fix(slint): drop redundant ARTIST label above the artist name
The small uppercased "ARTIST" eyebrow above the artist name in the
header is redundant — the context (artist page) and the prominent
name below already say it. Removed the Text node and the 6px spacer
that followed.

cc
2026-05-20 11:02:22 -06:00
vicrodh cccb0c2af0 feat(core, slint): wire MusicBrainz cache for metadata and relationships
QbzCore gains an Arc<Mutex<Option<MusicBrainzCache>>> field plus a
`set_musicbrainz_cache(cache)` setter so the frontend (which owns
the data-dir path) can install the cache after construction.

musicbrainz_get_artist_metadata and musicbrainz_get_artist_relation-
ships now check the cache before fetching and persist successful
results on the way out — mirrors the v2_musicbrainz_get_artist_*
behavior in src-tauri/src/commands_v2/integrations.rs. Missing cache
degrades gracefully: methods skip the lookup and the persist hook
silently when no cache is set.

qbz-slint opens the cache at startup at <data-dir>/qbz/cache/
musicbrainz_cache.db (same path Tauri uses) and hands it to the
core. Failure to open the directory or the SQLite file just falls
back to direct network calls — a warn line in the log surfaces it,
but the sidebar still works.

cc
2026-05-20 10:58:53 -06:00
vicrodh 5c9de3e4d1 fix(slint): sidebar starts below the artist header, always opens
Two adjustments to land the sidebar truly 1:1 with the Tauri visual.

Top inset now 320px — clears the entire artist header (NavButtons
row + ARTIST label + name + truncated bio + Read more + action
buttons + the 36px spacer before Popular Tracks). The sidebar's top
edge sits inline with where Popular Tracks starts, matching Tauri's
`.network-sidebar-column` natural flow position inside the body
(alongside the sections, never overlapping the header).

Sidebar is always open by default on every artist navigation, with
no cross-session persistence — closing it is a per-view choice only:
  - reset_network_sidebar (called at the start of navigate_artist)
    flips NetworkSidebarState.open back to true alongside the other
    per-artist state resets.
  - network_sidebar_prefs.rs is removed; the on_toggle Rust handler
    is a no-op (Slint already mutates the open flag directly in the
    button click). The previous `<data-dir>/qbz/network_sidebar.json`
    persistence file is no longer written or read.

cc
2026-05-20 10:56:26 -06:00
vicrodh d68772e3e7 fix(slint): artist bio matches album description; sidebar 1:1 height
Two refinements requested after the sidebar landed.

Artist biography:
  - Adopts the AlbumPageView description pattern. The truncated body
    is sized with an explicit width = content-width - 296px (page
    padding 32+32 + portrait 200 + header gap 32), so it fills the
    horizontal band to the right of the artwork instead of wrapping
    narrow.
  - content-width is derived: shrinks by 300px when the network
    sidebar is open, so the bio band stays inside the visible main
    column and never gets clipped behind the sidebar.
  - Read more no longer toggles an inline expand. It opens a
    centered modal with a Flickable for the full bio plus the
    optional "Source: <name>" attribution line — same scrim, panel
    sizing, and close-X treatment as the album modal.

Network sidebar height:
  - Top spacer goes from 22px to 44px to clear the NavButtons row
    at the top of the artist body — same offset Tauri's sticky
    jump-nav uses (`top: 44px`).
  - Bottom spacer goes from 22px to 32px to mirror the small gap
    above the now-playing bar that Tauri's `height: calc(100vh -
    180px)` produces. The sidebar is now visibly shorter than the
    main column, matching the WebKit build.

cc
2026-05-20 10:50:40 -06:00
vicrodh 165674d8a8 feat(slint): you-may-also-like discovery section with sticky dismiss
Closes the network sidebar with the fifth section — tag-based "You may
also like" discovery, with a per-tag dismiss store so a thumbs-down
sticks across artist pages that share the same primary genre tag.

Backend (`qbz-integrations` + `qbz-core`):
  - DiscoveryArtist and DiscoveryResponse DTOs added to
    qbz-integrations::musicbrainz::models.
  - QbzCore::musicbrainz_discover_artists ports
    v2_get_discovery_artists end to end (sans the RecoState local
    "play count > 2" filter, which has no equivalent in the Slint
    shell yet): primary-tag search, exclude seed + similar +
    dismissed, deterministic shuffle keyed on seed_mbid, validate
    each candidate on Qobuz, fall back to the secondary tag if we
    have fewer than 5 matches.
  - normalize_artist_name re-exported at the qbz-core crate root so
    the dismiss handler in qbz-slint can use the same key shape that
    the discovery pipeline filters on.
  - Deterministic shuffle implemented with a simple xorshift PRNG
    seeded from a stable hash of seed_mbid — keeps qbz-core free of
    the rand dep.

Slint controller (`crates/qbz-slint/src/`):
  - discovery_dismiss.rs — small JSON store at
    `<data-dir>/qbz/discovery_dismiss.json` keyed by lowercased tag
    -> list of normalized artist names. dismissed_for_tag is passed
    into the discovery call as the filter source; dismiss() is
    invoked by the thumbs-down handler.
  - artist::load_mb_discovery / apply_mb_discovery /
    remove_discovery_artist plumb the pipeline result through the
    standard load-off-thread / apply-on-UI split. Discovery state
    includes primary_tag so the dismiss handler can persist under
    the right key.
  - main.rs chains discovery after relationships in the same MB
    enrichment task. NetworkSidebarActions.discovery-dismissed (now
    carrying mbid + name) normalizes the name, persists the
    dismiss, and removes the row from the visible list.

Slint UI (`ui/state.slint`, `ui/artist/ArtistPageView.slint`):
  - discovery-tag property added to NetworkSidebarState.
  - YOU MAY ALSO LIKE section after Relationships — one row per
    candidate (SidebarLink + a 24x24 thumbs-down button on the
    right). Clicking the link navigates via the artist-clicked
    callback when a Qobuz id is present.
  - thumbs-down.svg icon added to ui/assets/icons (Lucide
    ThumbsDown).

cc
2026-05-20 10:39:53 -06:00
vicrodh 72083adb7f feat(slint): MB relationships section in the network sidebar
Wires the third MB-driven section: Members & Former, Member Of, and
Collaborators, mirroring Tauri's RELATIONSHIPS block.

Controller (`crates/qbz-slint/src/artist.rs`):
  - load_mb_relationships(runtime, mbid) — calls QbzCore's
    musicbrainz_get_artist_relationships, then maps each section
    independently with group_relations.
  - group_relations dedupes by mbid combining roles, preserves first-
    seen order, and computes the per-row role + tooltip:
      * role = first attribute, or the section default ("Band Member"
        / "Band" / "Collaborator") when MB has no attributes.
      * tooltip = roles joined with ", " plus the period in parens
        ("Vocalist, Guitarist (1965 - 1969)"); falls back to just the
        period or to the name.
  - format_period mirrors Tauri's `{begin || '?'} - {end || 'present'}`
    when at least one of the two is set, otherwise empty.

main.rs:
  - The MB enrichment task now chains: resolve + metadata first, then
    relationships with the resolved mbid. Both loading flags flip
    together so the sidebar shows a single "Loading…" line per section
    while the calls run.

ArtistPageView.slint:
  - RELATIONSHIPS section, gated by mb-available + (loading || has-
    data). Three sub-groups, each with a secondary label
    ("Members & Former" / "Member Of" / "Collaborators"), shown only
    when their list is non-empty.
  - Members and Collaborators use the User icon, Member Of uses the
    Music icon (matches Tauri's Lucide imports).
  - Each row reuses SidebarLink, emits NetworkSidebarActions.musician-
    clicked(name, role) — logged-only until MusicianPage lands.

cc
2026-05-20 10:29:26 -06:00
vicrodh 6d25b25d1d feat(slint): labels and similar artists sections in the sidebar
Extracts the two Qobuz-driven sidebar sections from /artist/page data
the artist controller already receives and wires them into the
NetworkSidebar.

Controller (`crates/qbz-slint/src/artist.rs`):
  - Labels collected during the existing release-bucketing loop,
    matching Tauri's extractLabelsFromPageReleases: only the "album"
    release group, only releases attributed to this artist, dedupe by
    label id, sorted by name. Each label exposes {id, name}.
  - Similar artists pulled from page.similar_artists.items as
    {id, name}. No artwork on the sidebar rows (Tauri only renders a
    User icon + name there).
  - ArtistData / ArtistState gained `labels` and `similar_artists`;
    reset_artist clears them on artist change.

Slint (`ui/state.slint`, `ui/artist/ArtistPageView.slint`):
  - New LabelEntry and SimilarEntry structs alongside ArtistState.
  - New SidebarLink local component — small leading icon (12px) +
    elided name, surface-elevated hover, mirrors Tauri's
    `.sidebar-artist-link`. Used by Labels (Disc), Similar (User),
    and the upcoming Relationships rows.
  - Labels section: title + items or "No label info" placeholder.
  - Similar Artists section: title + items, with "Loading…" while
    the artist page itself is loading and "No similar artists found"
    once loaded with an empty list.

main.rs:
  - on_artist_clicked now navigates: records nav history and calls
    navigate_artist, same flow as the top-level on_open_artist. The
    target view exists in Slint already (unlike LabelReleases /
    ArtistsByLocation / MusicianPage, which stay logged-only).

cc
2026-05-20 09:59:02 -06:00
vicrodh d107d4f577 feat(slint): origin section + MB metadata wiring on artist navigate
Wires the artist controller to call QbzCore's MusicBrainz orchestration
and feed the Origin section of the network sidebar, plus typed click
callbacks for every section (logged-only until the target views land).

Controller (`crates/qbz-slint/src/artist.rs`):
  - reset_network_sidebar() — clears sidebar MB state on artist change.
  - load_mb_metadata(runtime, name) — checks the client's enabled flag,
    resolves name -> mbid via musicbrainz_resolve_artist, fetches
    metadata via musicbrainz_get_artist_metadata, maps into MbOrigin
    (is_person derived from artist_type, dates formatted as "1976",
    "May 1976" or "May 14, 1976" matching Tauri's en-US output).
  - apply_mb_metadata() / apply_mb_unavailable() — push into
    NetworkSidebarState on the event loop.

main.rs:
  - navigate_artist now resets the sidebar before loading and spawns
    the MB enrichment in parallel with artwork. Origin loading flag
    flips to true before the resolve, back to false on success or
    on MB-unavailable.
  - NetworkSidebarActions click callbacks (location, label, artist,
    musician, discovery dismiss) wired to log() with full payload —
    placeholder until ArtistsByLocation/LabelReleases/MusicianPage land
    in Slint.

ArtistPageView.slint:
  - Sidebar body becomes a Flickable + VerticalLayout (so the upcoming
    Labels / Similar / Relationships / Discovery sections can overflow
    cleanly).
  - Origin section renders three optional rows — Born/Founded,
    Born in/Founded in, Died/Disbanded — with the same uppercased
    label + right-aligned value pattern Tauri uses. The location row
    is clickable when the location precision is finer than country
    or a city is present; click emits NetworkSidebarActions.location-
    clicked with the artist's mbid.
  - Section is gated by NetworkSidebarState.mb-available, so a
    disabled MB integration or an unmatched artist hides Origin
    entirely (next phases fill Labels/Similar with Qobuz data that
    does not depend on MB).

Adds qbz-integrations as a direct dep of qbz-slint so the controller
can match on ArtistType / LocationPrecision without going through a
re-export shim.

cc
2026-05-20 09:51:22 -06:00
vicrodh 11cdc0d238 fix(slint): open network sidebar by default and inset it vertically
Two adjustments to land the sidebar shell closer to the Tauri visual.

1. Default open. The artist network sidebar is one of the visible
   improvements over a generic Linux music player, so it should be on
   out of the box. `network_sidebar_prefs::load_open()` now returns
   `true` when the prefs file is missing, the Slint global also
   defaults to open, and main.rs reads the persisted value at startup
   to seed NetworkSidebarState.open. Users who close it have the
   choice persisted as before.

2. Vertical inset. The sidebar is now wrapped in a VerticalLayout with
   a 22px spacer at the top and bottom, so it does not occupy the full
   ArtistPageView height. Mirrors Tauri's `height: calc(100vh - 180px)`
   which leaves room for the sticky jump-nav at the top and the
   now-playing bar at the bottom.

cc
2026-05-20 09:27:11 -06:00
vicrodh 5a76196f4e feat(slint): artist network sidebar shell with header
Restructures ArtistPageView's outer layout into a HorizontalLayout
where the existing Flickable becomes a horizontal-stretch column and a
fixed 300px sidebar opens on the right when NetworkSidebarState.open
is true — mirroring Tauri's .artist-body { content + .network-sidebar-
column } flex row.

Sidebar shell:
  - 300px column with a 1px left border in border-subtle, surface-card
    background.
  - Header row (44px tall, surface-main background, separator line
    below): uppercased "ARTIST NETWORK" label on the left, 28x28 close
    button on the right using the panel-right-close icon (same icon
    Tauri renders via Lucide PanelRightClose).
  - Close button mirrors the header toggle: flips
    NetworkSidebarState.open to false and emits
    NetworkSidebarActions.toggle so the Rust side persists.
  - Body is an empty stretch placeholder; Origin / Labels / Similar /
    Relationships / Discovery sections land in the following phases.

Asset: panel-right-close.svg ported from Lucide (24x24 viewBox,
stroke-width 2, same path as Tauri's lucide-svelte import).

cc
2026-05-20 09:16:32 -06:00
vicrodh a8c8947658 feat(slint): network sidebar toggle in artist header
Adds the Network button to the artist action cluster (Heart / Build
collection / Radio / Network), wired to NetworkSidebarState.open.
While the sidebar is open the button stays "active" — accent-tinted
icon over the same surface a hover would produce — mirroring Tauri's
`.network-btn.active`. The CircleAction primitive grew an `active`
prop to express this without bespoke per-button styling; existing
buttons default it to false and are unaffected.

The click toggles NetworkSidebarState.open and emits
NetworkSidebarActions.toggle(). Rust persists the new flag through
network_sidebar_prefs::set_open() so the sidebar reopens in the same
state the user left it.

Asset: element-connect.svg copied from the Tauri `static/` folder
verbatim — same icon Tauri renders for the button.

Hide (blacklist) button stays out of the cluster until the
artist_blacklist module migrates out of src-tauri. The sidebar
itself, its content, and the controllers that fill the state are
landed in the following phases.

cc
2026-05-20 09:11:10 -06:00
vicrodh a768cce5e5 feat(slint): NetworkSidebarState globals and open/closed persistence
Defines the state shape and callbacks the artist network sidebar will
fill in. No view changes yet — the sidebar itself, its toggle button,
and the controllers that populate the state are landed in the following
phases.

Slint side (`ui/state.slint`):
  - MbOriginData / MbRelationship / MbRelationshipsData / DiscoveryArtist
    structs.
  - NetworkSidebarState global with open + mb-available + mb-mbid +
    per-section loading flags + the typed data structs.
  - NetworkSidebarActions global with the six callbacks the sidebar
    will emit (location-clicked, label-clicked, artist-clicked,
    musician-clicked, discovery-dismissed, toggle).

Rust side (`crates/qbz-slint/src/network_sidebar_prefs.rs`):
  - Tiny JSON store at <data-dir>/qbz/network_sidebar.json with two
    functions, load_open() and set_open(open), mirroring the recently.rs
    pattern. Failures degrade silently.

cc
2026-05-20 09:05:02 -06:00
vicrodh 4430d7fb2f feat(core): add MusicBrainz orchestration methods to QbzCore
Adds qbz-integrations as a dependency of qbz-core and embeds a
MusicBrainzClient on the QbzCore struct (initialized in new()). The
client's own enabled flag governs whether requests fire; calls return
the integration-layer ServiceUnavailable error when disabled.

New methods on QbzCore<A>:

  musicbrainz_is_enabled()
  musicbrainz_set_enabled(enabled)
  musicbrainz_resolve_artist(name) -> Option<ResolvedArtist>
  musicbrainz_get_artist_metadata(mbid) -> ArtistMetadata
  musicbrainz_get_artist_relationships(mbid) -> ArtistRelationships

The metadata method calls location::extract_metadata on the full artist
response and then resolves the real country from the begin_area
hierarchy when a city-level location was found (MB's "country" field is
where the artist is currently active, not where they were born/formed).
The relationships method splits relations into members / past_members /
groups / collaborators by relation_type and direction, mirroring
v2_musicbrainz_get_artist_relationships exactly.

No cache wiring yet — every call hits the network. A cache wrapper can
be plugged in later via a setter without changing the method
signatures.

cc
2026-05-20 08:57:50 -06:00
vicrodh 508609a5d8 refactor(integrations): move MB location and genre helpers to qbz-integrations
Pure-logic move of two helper modules out of src-tauri/src/musicbrainz/
into the qbz-integrations crate, where the MusicBrainzClient and DTOs
already live:

  src-tauri/src/musicbrainz/location_discovery.rs
    -> crates/qbz-integrations/src/musicbrainz/location.rs
  src-tauri/src/musicbrainz/genre_normalization.rs
    -> crates/qbz-integrations/src/musicbrainz/genre.rs

The functions exposed are unchanged: extract_metadata,
compute_affinity_score, build_scene_cache_key, format_life_span_date
(from location); normalize_genre, is_broad_genre, extract_affinity_seeds,
genre_summary (from genre). Imports in src-tauri/src/commands_v2/
integrations.rs are repointed to the new home; the legacy file
declarations are removed from src-tauri/src/musicbrainz/mod.rs. No
behavior change.

Unblocks the Slint side from reaching the same helpers via qbz-core
without depending on src-tauri.

cc
2026-05-20 08:50:50 -06:00
vicrodh b3acb21c37 feat(slint): bucket artist releases by type and surface bio source
Groups releases into five sections (Discography, EPs and Singles, Live
Albums, Compilations, Others). Releases attributed to a different
main artist inside a group are filtered out, and the release id is
deduped across groups so the same album does not appear twice when
Qobuz lists it in multiple buckets. Compilations come from
tracks_appears_on (albums by other artists that include this artist),
deduped by album id.

ArtistData and the ArtistState global gain a bio_source field; the
ArtistPageView renders it as a small "Source: <name>" line under the
Read-more toggle when present (TiVo etc).

cc
2026-05-20 08:21:11 -06:00
vicrodh d01b1c10d8 feat(slint): artist popular tracks - select button + 5+load more
The Popular Tracks list now caps at 5 rows with a Load more button
instead of dumping 30. Each row gains a Select button (foundation for
multi-select — first surface), and the more-button is always visible
(no longer hover-revealed) so all three controls (play / select /
context) read together. AlbumTrackItem gained a selected: bool and
media-action(\"track\", id, \"toggle-select\") flips the row state via
the handler on ArtistState.top-tracks.

cc
2026-05-19 21:48:45 -06:00
vicrodh e116438023 fix(slint): artist view album covers + bio Read-more toggle
Artist release cards were rendered with empty artwork because no
artwork-pipeline target existed for ArtistState. Added
ArtworkTarget::ArtistRelease and an artist::artwork_jobs builder;
navigate_artist now spawns the cover-download jobs after applying the
page data, so every section grid fills in progressively.

Biography truncation: artist.rs precomputes a word-boundary bio_short
(360 chars), ArtistState exposes bio-short and bio-truncated, and
ArtistPageView shows the short variant with an inline Read more /
Read less toggle.

cc
2026-05-19 21:12:01 -06:00
vicrodh 6d8a2e1fff fix(slint): spinner on home/album/artist + dedupe only in All-tab carousel
Home, Album and Artist views now show the rotating LoadingSpinner (same
as search) above a label while the initial load runs, replacing the
bare Loading… text.

The top-result-artist dedupe moved from map_search_all into apply_search
and replace_category: the full artists list is kept in SearchState.artists
(used by the Artists tab grid), and a separate artists-carousel drops the
duplicate next to the Most-popular hero on the All tab.

cc
2026-05-19 21:00:40 -06:00
vicrodh e2c0d812fe fix(slint): search refinements — artist card, history, spinner, hover
- ArtistCard rebuilt 1:1 with Tauri SearchView.artist-card: 160x220
  inside a surface-card container, 12px corners, 120px circular avatar
  with a gradient placeholder, hover swaps to surface-elevated. The
  Follow chip (rectangular per ADR-008) sits below the name when the
  user does not follow the artist.
- Search loading state uses a rotating spinner (Lucide LoaderCircle on
  Slint transform-rotation) matching the Tauri spinner — 1s linear
  infinite. The search header (title + tabs + filter radios) collapses
  while the initial load runs so the chrome does not show through.
- Search history: live debounce + submit now use
  nav::push_or_replace_search, so the latest query is the single Search
  entry in history. Back from an opened album lands on that search.
- Track row backgrounds use the OR-d row-hovered (cell / fav / dl /
  more), so hovering the more button no longer drops the row tint.
- replace_category artists arm re-applies the top-result dedupe, so
  filtering does not bring the duplicate back.

cc
2026-05-19 20:37:52 -06:00
vicrodh b874661bbf feat(slint): search loading fade + 1s idle-close on header menus
New LoadingDots primitive (three pulsing dots over an optional label)
shown while a search is running. The result sections fade in once the
results arrive instead of showing empty placeholder colors. The
LoadingDots component is reusable for other heavy loads (large grids,
playlist manager) when they get the same treatment.

The header nav-tab menu now closes after 1s of pointer idle (was 1.5s)
per the project rule that no menu stays open more than 1s idle.
PopupWindow context menus already close on click-outside, which is the
idle behaviour for click-triggered popups.

cc
2026-05-19 20:13:14 -06:00
vicrodh e1aa0a71dc feat(slint): full track context menu (13 items, ported from Tauri)
New shared TrackContextMenu primitive with every option the Tauri
TrackMenu shows (flat — no submenus): Play now / Play next / Add to
queue / Create radio / Add to library / Add to mixtape / Add to
playlist / Share Qobuz link / Share Song.link / Make available offline
/ Go to album / Go to artist / Track info. Items already wired in the
MVP (play / play-next / queue / favorite) fire as before; the rest emit
their media-action and stay no-op until the backend lands. Used by
SearchTrackRow, AlbumPageView TrackRow, and ArtistPageView TrackRow.

cc
2026-05-19 20:04:53 -06:00
vicrodh c7129de096 fix(slint): follow chip, playlist card size, track row hover, dedupe
- Follow button on the artist card is no longer a pill — Radius.sm
  rounded rectangle, matching the rest of the app (new ADR-008).
- PlaylistCard sized identically to AlbumCard (220x266) with the same
  220px artwork area; collage and the grid/carousel updated to match.
- Track row play overlay now appears anywhere over the row, not only
  when the pointer is directly over the cell. Slint does not propagate
  has-hover to ancestor TouchAreas, so each row OR-s its own ta with
  every nested TouchArea (play-cell, fav/dl/more) into row-hovered.
- EqualizerBars sit on a fixed 16px baseline so the animation stays
  perfectly centered while the bar heights oscillate.
- TrackPlayCell exposes out hovered so rows can OR it cleanly.
- Search dedupes a top-result artist that is also the first carousel
  entry, removing the redundant duplicate.

cc
2026-05-19 19:53:15 -06:00
vicrodh 7efb83c075 feat(slint): functional play button with playing state on track rows
New shared TrackPlayCell for the track-row leading slot, ported 1:1 from
the Tauri track rows: at rest it shows the cover thumbnail or track
number; on row hover a play triangle; for the current track an animated
three-bar equalizer, swapping to a pause glyph on hover. Clicking a
non-current cell plays the track, clicking the current one toggles
play/pause. Wired into the search, album-page and artist-page track
rows.

cc
2026-05-19 18:25:46 -06:00
vicrodh 95fb45650e fix(slint): widen artists carousel and make the filter apply
- The most-popular row used alignment:start, which packed the artists
  carousel to its preferred width (one card). Dropped it so the
  carousel stretches and shows as many cards as fit.
- The searchType filter only re-queried on per-type tabs (none on All,
  so it did nothing). It now re-queries albums/tracks/artists with the
  search_type and replaces all three lists, so it applies on every tab.

cc
2026-05-19 14:39:28 -06:00
vicrodh 5707601174 fix(slint): search results 1:1 with Tauri
- Most popular: a crowned hero inline with the artists carousel; its
  cover now resolves (a dedicated artwork target was missing).
- Artists: a real carousel on the All tab, ArtistCard restyled to the
  Tauri For-You tile with a Follow chip (favorites the artist) shown
  when the user does not follow it.
- Playlists: a PlaylistCarousel of PlaylistCard, with a count-based
  PlaylistCollage (1/2/3/4-up) instead of a flat row list.
- Carousels gained a View all link; result counts removed from titles.
- searchType filter is a styled radio row (ring + accent dot) at the
  top right with a clear button, not a dropdown.
- Title fixed to Search Results.
- QbzCore::favorite_artist_ids backs the follow state.

cc
2026-05-19 13:59:56 -06:00
vicrodh 61232c05ce fix(slint): rebuild search results view to match Tauri
The results view was bare text rows — nothing like the Tauri search.
Rebuilt it from the components the successful views already use:

- Albums: a Carousel of AlbumCard cover cards (All tab) and an AlbumCard
  grid (Albums tab).
- Artists: new ArtistCard (circular portrait) + ArtistCarousel; an
  ArtistCard grid on the Artists tab.
- Tracks: SearchTrackRow with a position number.
- The history nav pair (NavButtons) added to the view toolbar.

cc
2026-05-19 12:53:46 -06:00
vicrodh fa5f22e609 feat(slint): searchType filter dropdown for per-type tabs
The Albums / Tracks / Artists tabs get a QbzSelect filter (No filter /
Main artist / Performer / Composer / Label / Release name). Changing it
re-queries that category from offset 0 with the search_type and replaces
the list; load-more carries the active filter forward. The filter
resets on every new search. search_type_for_filter is unit-tested.

cc
2026-05-19 12:35:20 -06:00
vicrodh 6a584803f1 fix(slint): trim header nav-tab width by 6px
Drop 2px of left padding and 4px of right padding (after the caret) on
the Discover / Library / Local Library / My QBZ tabs.

cc
2026-05-19 12:30:04 -06:00
vicrodh 0ae66ee57c feat(slint): progressive artwork loading for search results
The artwork module gains SearchAlbum/Track/Artist/Playlist targets;
navigate_search and load-more build cover-download jobs and hand them to
spawn_loads. Covers resolve through the shared qbz_cache image cache
(disk-backed, never re-downloaded) and apply per row, so a late arrival
never resets a list.

cc
2026-05-19 12:27:57 -06:00