Commit Graph
26 Commits
Author SHA1 Message Date
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 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 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 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
vicrodh 7e6e548598 feat(slint): minimal playback wiring + Queue, plus detail/Settings fixes
Playback (minimal, album-focused):
- playback.rs controller: play an album (set queue + start audio),
  play a single track, enqueue, play a queue index, transport
  (play/pause, next, previous, seek, volume)
- the audible step uses Player::play_track (fetch -> download -> play);
  the protected audio backend is untouched
- a 450ms poll loop pushes position/progress/times/playing onto
  NowPlayingState and auto-advances at end of track
- NowPlayingState / QueueState globals; NowPlayingBar bound to real
  state (song card, seek bar, transport, volume); QueueSidebar lists
  the queue and rows play on click
- a played track is recorded into the recently-played store

Detail views:
- album description and artist biography stay with the metadata,
  filling the width to the right of the artwork (not below the header)
- header nav tabs gained a dropdown caret and 2px side padding

Settings > Playback defaults are now opt-out: show context icon,
restore session, resume position and stream-uncached default on;
streaming-only stays opt-in; initial buffer defaults to 2s.

Known gaps: queue/now-playing artwork not yet threaded (Image is not
Send); autoplay-mode preference not consulted by auto-advance; buffer
progress line stays at 0.

cc
2026-05-18 22:16:13 -06:00
vicrodh 33df7bce76 feat(slint): collapsible sidebar, Queue/Lyrics panels, nav menus
Shell chrome (steps 1-3):
- Left sidebar is collapsible — a toggle in the nav cluster (left of the
  [<] [>] pair) and a draggable right edge that resizes 200-420px and
  collapses below 160px. ShellState global drives it.
- Queue and Lyrics right sidebars: 320px panels rendered as layout
  siblings, so opening one pushes the content area narrower rather than
  overlaying it. The header captions/queue buttons toggle them
  (mutually exclusive) and light up when active.
- The four nav tabs (Discover/Library/Local Library/My QBZ) open
  dropdown menus listing their sub-sections; Discover -> Home is wired.

Detail views (steps 4-5):
- Track rows: the even-row tint is now clearly fainter than the hover
  tint so the two states no longer look identical.
- The favorite and offline-cache glyphs in album track rows are real
  buttons (hover state, emit media-action).
- Album header artist link uses the metadata text color, bold, instead
  of the accent blue.
- Album description and artist biography span the full content width
  below the header block.

cc
2026-05-18 21:54:31 -06:00
vicrodh 99e3513d68 feat(slint): NowPlayingBar rebuild, Settings constraints, Discover fixes
NowPlayingBar (start of the playback-bar work):
- height raised to 110px
- SeekBar component: three-line track (total / cached / progress), a
  hover thumb, elapsed/remaining times, and a hover time bubble with a
  downward caret marking the seek position
- song card: background and border dropped, gains the extra height,
  album/artist links and the context icon kept (context icon now
  always shown)
- transport + secondary buttons copied from Tauri (shuffle, prev,
  play, next, repeat, add, favorite; cast, picture-in-picture,
  fullscreen, normalization; volume cluster). Queue/Lyrics stay in the
  header — and the header now orders them lyrics-then-queue.

Settings constraints (1:1 with Tauri):
- Exclusive Mode enabled only on ALSA; DAC Passthrough only on
  PipeWire; Lock Output Device disabled while DAC Passthrough is on;
  Gapless disabled while Streaming Only is on
- backend change cascades: forces off DAC passthrough / force-bit-
  perfect / exclusive / gapless as appropriate; DAC-passthrough and
  streaming-only cascades; initial-buffer slider right-aligned

Discover fixes:
- navigation [<] [>] pair homologated to the same top/left margin
  across every view
- album/artist description moved back into the metadata column (it now
  fills the width there) with uniform secondary text color
- genre filter button hugs its label instead of stretching
- Recently Played Albums and Tracks modules always render, with a
  placeholder until the play history has data
- wider spacing between Discover rows; the Discover gear is no longer
  a Settings shortcut (it is the per-tab sort/show-hide control)

cc
2026-05-18 21:22:40 -06:00
vicrodh 1089761e74 fix(slint): widen the album/artist description area
Move the album description and the artist biography out of the metadata
column to span the full content width below the header block, so the
text is no longer cramped into the narrow column beside the artwork.

cc
2026-05-18 20:36:04 -06:00
vicrodh 12ed5a7c4a feat(slint): browser-like back/forward navigation history
Add a navigation history so pages can be walked back and forward.

- nav.rs: a back/forward stack of NavEntry (home, album, artist,
  settings); record() on fresh navigation, go_back/go_forward to walk it
- NavButtons: a [<] [>] pair styled like the Discover tab group; inline
  beside the Discover tabs and in place of the per-page Back button on
  album, artist and settings views
- All navigation routes through Rust (open-album/-artist record entries;
  the gear and header menu use NavState.request-settings) so the history
  stays accurate; back/forward re-apply entries, re-loading entity pages
- AppShell listens for the mouse back/forward buttons
- can-back / can-forward drive the buttons' enabled state

Also in this round:

- Album header description: render a pre-truncated short string so it
  wraps to a couple of lines instead of overflowing the header; the full
  text stays in the Read-more modal
- Header queue button uses an ordered-list icon (the previous one read
  as a playlist)

cc
2026-05-18 20:17:54 -06:00
vicrodh b51ec1509e feat(slint): album view detail parity
Bring the album view closer to 1:1 with the Tauri original.

- Track list: favorite and offline-cache columns, and the context-menu
  button now always shows in the muted column color (it brightens on
  hover) instead of being hidden until hover
- Toolbar above the track list: a QualityBadgeFull port of the Tauri
  QualityBadgeStatic (icon + tier + exact bit-depth/sample-rate), plus
  a track search box
- Editorial album description, truncated, with a Read more link that
  opens the full text in a modal
- Label/awards sidebar: drop the resting card background; awards use
  the Tauri laurels SVG
- Control buttons on both album and artist views are ghost circles
  (translucent fill, faint ring) instead of solid fills

cc
2026-05-18 19:25:33 -06:00
vicrodh 49cd8f6a87 feat(slint): album and artist detail view parity pass
Bring the album and artist pages closer to the Tauri originals.

Shared:
- Artwork-tinted gradient header — header_tint() derives an average,
  darkened color from the decoded cover; the page header renders a
  linear-gradient from it. Both pages scroll on a Flickable (no
  scrollbar gutter).

Album view:
- Full header action row: play, shuffle, favorite, radio, credits,
  booklet, mixtape, multi-select, more (visual where playback/feature
  wiring is pending; play and favorite emit media-action)
- Right sidebar with the record label and editorial awards, each a
  round-avatar card; AlbumData now carries id, label and awards

Artist view:
- Gradient header, fuller action row, and an "ARTIST" eyebrow
- Discography split into titled release sections (Albums, EPs &
  Singles, Live Albums, Compilations, ...) instead of one flat list
- Artist track rows gained the more-button context menu

cc
2026-05-18 18:23:50 -06:00
vicrodh 7eb9627eab feat(slint): context menus for album cards and track rows
Add a first cut of context menus, opened from the "more" (ellipsis)
button on Discover album cards and on album track rows.

- ContextMenu / ContextMenuItem primitives (PopupWindow-hosted panel)
- AlbumCard: the overlay action buttons are now interactive (they gate
  their pointer handling so non-hovered cards still open on click); the
  more button opens a menu — Open album, Play, Add to queue, Add to
  favorites
- TrackRow: a hover-revealed more button opens a menu — Play, Add to
  queue, Add to favorites; AlbumTrackItem and TrackData carry the
  track id
- Menu actions bubble up as a single media-action(kind, id, action)
  callback through the shell; main.rs logs it for now — playback and
  favorites wiring lands with the playback session
- user and list-plus icons

cc
2026-05-18 17:32:56 -06:00
vicrodh 4e143bbcd5 feat(qbz-slint): artist detail page
Clicking an artist (from the album page's artist line) opens the
artist detail view: a circular portrait, name, biography, action
buttons, a Popular Tracks list, and a Discography carousel.

- artist.rs: fetches the artist page via QbzCore::get_artist_page and
  maps name, biography (HTML-stripped), portrait URL, top tracks and
  releases; the portrait loads through the shared image cache.
- ArtistPageView.slint reuses AlbumCard for the discography (through
  the Carousel) and AlbumTrackItem for the track list.
- NavState becomes a three-way ContentView (home / album / artist) and
  AppShell routes the content region accordingly. The album page's
  artist line and the artist page's release cards are navigable.

cc
2026-05-18 15:59:47 -06:00