Commit Graph
690 Commits
Author SHA1 Message Date
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
vicrodh 573c479d4b feat(slint): load-more pagination for per-type search tabs
The Load more button now fetches the next 20-item page for the active
category via the per-type QbzCore search methods and appends onto the
existing VecModel, so already-loaded rows and their artwork are
untouched. category_for_tab maps the tab index to a category and is
unit-tested.

cc
2026-05-19 12:17:20 -06:00
vicrodh 80f861779c fix(slint): symmetric spacing on the header sidebar toggle
The collapse toggle is a bare icon; the window-edge margin (Spacing.md)
was wider than its ~10px gap to the Discover tab. Drop the tab cluster
x to Spacing.xs so the toggle glyph sits symmetrically.

cc
2026-05-19 12:13:36 -06:00
vicrodh df2ffd2190 feat(slint): tabbed search results view
SearchResultsView is now a real results page: a five-tab strip (All /
Albums / Tracks / Artists / Playlists), an All-tab dashboard (top-result
hero plus a capped preview of each category with View more), and
per-type tabs showing the full loaded list. Album and playlist result
rows are file-local components; tracks reuse SearchTrackRow, artists
reuse SlimCard. The load-more button is in place; its callback is wired
in the next task.

cc
2026-05-19 12:01:34 -06:00
vicrodh 7a40fd660c feat(slint): SearchTrackRow with configurable artwork leading slot
A search-owned track row modelled on the album-page track row, but the
leading slot is a cover thumbnail (search tracks span many albums).
show-artwork keeps the slot configurable for a future shared component.
Compiled once SearchResultsView imports it (next task).

cc
2026-05-19 11:54:36 -06:00
vicrodh bb6fd1e0f3 feat(slint): minimal search results view, functional end to end
SearchResultsView renders the four SearchState categories; mounted in
AppShell on ContentView.search. Phase 1 is now functional end to end:
header input -> debounced search -> QbzCore::search_all -> results.
Phase 2 builds this into the tabbed dashboard.

cc
2026-05-19 11:46:16 -06:00
vicrodh 7073b6553f feat(slint): wire header search input with debounced live search
The header TextInput now submits on Enter (records history, opens the
results page) and live-searches on edit. Live search debounces 300 ms
via a Slint Timer and requires 2+ characters. A monotonic version
counter guards navigate_search so a stale load cannot overwrite newer
results.

cc
2026-05-19 11:42:12 -06:00
vicrodh 37ba47434a feat(slint): wire navigate_search into navigation
navigate_search mirrors navigate_album: reset SearchState, switch to the
search view, run the search off-thread, apply on the event loop. The
apply_entry stub arm is replaced with the real call so history
back/forward re-runs searches.

cc
2026-05-19 11:36:16 -06:00
vicrodh 99d45a9e55 feat(slint): search controller with load/map/apply stages
search.rs follows the album.rs three-stage pattern: load_search fetches
a combined search via QbzCore on a worker thread, the map_* layer turns
domain types into plain Send rows, and apply_search writes SearchState
on the event loop. The map_* functions are unit-tested. Wired into
navigation in the next task.

cc
2026-05-19 11:34:32 -06:00
vicrodh acfb6a05cb feat(slint): add NavEntry::Search history variant
The Search variant carries the query. apply_entry gets a stub arm that
switches the view; Task 6 upgrades it to a full navigate_search call.

cc
2026-05-19 11:26:48 -06:00
vicrodh b040a1d359 feat(slint): add SearchState globals and search ContentView
SearchState/SearchActions globals, SearchTrackItem/SearchPlaylistItem
structs, and a search variant on ContentView. Re-exported from app.slint
so the Rust layer can populate them. Nothing consumes them yet.

cc
2026-05-19 11:18:16 -06:00
vicrodh 9365eb450d feat(core): add QbzCore::search_all with parameterized blacklist
search_all wraps catalog_search, parses the four category pages and the
most_popular hero, and drops blacklisted artists. The blacklist is a
parameter (BlacklistFilter) so Search does not depend on the un-migrated
artist_blacklist module.

cc
2026-05-19 11:03:42 -06:00
vicrodh 1e17032d03 feat(models): move combined-search result types to qbz-models
SearchAllResults and MostPopularItem now live in qbz-models; the Tauri
helpers re-export them under the legacy V2 aliases so command code and
its response serialization are unchanged.

cc
2026-05-19 10:51:14 -06:00
vicrodh 862d408789 fix(slint): crisp button icons, tighter tabs, homologate nav buttons
Icon buttons (header lyrics/queue/hamburger, queue footer) centered
their glyph at a fractional offset (e.g. (36-19)/2 = 8.5px), which
renders blurry — the offset is now rounded to whole pixels. The
header tab spacing dropped from 4px to 2px. The Discover toolbar's
back/forward pair drifted 3px from where the album/artist/settings
views place it; both toolbar clusters now anchor their vertical
center to y=25 so the [<] [>] pair holds one position across views.

cc
2026-05-19 05:05:17 -06:00
vicrodh 9a964477e5 fix(slint): unblock header menu hover-switch, tighten panel + tabs
The click-outside scrim was a full-window TouchArea — it covered the
tabs and intercepted their hover events, so hover-switching between
menus still did not work (same failure mode as the old PopupWindow
grab). The scrim now starts below the header, leaving the tabs
hoverable. The panel also dropped from header-height + 6px to 45px,
removing the unjustified gap between the tab and its dropdown. Header
tab labels lost another 2px of right padding.

cc
2026-05-19 04:53:03 -06:00
vicrodh 5972e8052b feat(slint): in-window overlay for nav-tab dropdowns with hover-switch
Add HeaderMenuOverlay, a plain in-window Rectangle rendered as the last
child of the AppShell root so it draws on top of the header and content
region. Unlike a PopupWindow it does not grab the pointer, so the header
tabs stay hoverable while a menu is open — hovering from one tab to
another overwrites HeaderMenuState and switches the menu instantly.

Behaviour preserved from the old PopupWindow menus:
  - hover-to-open and click-to-open
  - 1.5s idle auto-close, restarted on pointer activity over the panel
    or its rows and on every tab (re)publish via a changed handler
  - click-outside-close via a full-window transparent scrim
  - Discover home-item row still switches to ContentView.home
  - glyphs on all menus except Discover
Menu panel visuals are unchanged: surface-main bg, 1px border-muted,
Radius.sm, 24px drop shadow, 188px wide.

cc
2026-05-19 04:41:11 -06:00
vicrodh b6f5631f51 refactor(slint): publish nav-tab menus into HeaderMenuState global
The four header nav tabs each owned a PopupWindow for their dropdown.
A PopupWindow grabs the pointer while open, so hovering a sibling tab
delivered no hover event and the menus could not be hover-switched.

Add a HeaderMenuState global (open-index, anchor-x, items, home-item)
and the NavMenuEntry struct to state.slint. Tabs now write their
identity into the global on hover/click instead of showing a popup;
the menu panel itself will be rendered by a separate in-window overlay.
Move NavMenuItem into its own file so the overlay can reuse it.

cc
2026-05-19 04:37:26 -06:00
vicrodh 3aa902292e fix(slint): toolbar vertical align, header toggle, queue menu clicks
The Discover toolbar's side layouts had no explicit height, so they
filled the 56px bar and y resolved to 0 — the tab pill, genre filter
and gear stuck to the top instead of centering. Both now carry
height = preferred-height.

The header sidebar toggle dropped from a 19px to a 14px icon: it now
matches the nav-tab icon size and tint, and the 30px box centers it
on whole pixels (was 5.5px → blurry).

The queue row's click TouchArea was declared after the content, so it
sat on top and ate the context-menu button's clicks — the [...] just
skipped to the track. It is now declared before the content, so the
menu button keeps its own clicks.

cc
2026-05-19 04:32:37 -06:00
vicrodh c73e7ac767 feat(slint): add Play next to track and album context menus
Add a "Play next" ContextMenuItem above "Add to queue" in every menu
that offers queueing: the album page track menu, the artist page track
menu, and the discover album card menu. Play next uses the list-plus
icon and Add to queue switches to list-end, matching Tauri's ListPlus /
ListEnd icon mapping. The queue sidebar track menu is left untouched.

cc
2026-05-19 04:18:47 -06:00