Wire the per-history-entry scroll memory (added for Local Library) into
every top-level view: Home/Discover, Album, Local Album, Artist, Musician,
Label, Label Releases, Location, Favorites, Mix, Playlist, Playlist Manager,
Offline Manager, Search, Settings.
Each view's primary scroll container reports its viewport-y and restores it
on back/forward via NavState.restore-scope (scope_for in main.rs maps each
entry to a stable id; tab views carry the tab, e.g. "fav:albums"). The
container restores once per arm, only after content lays out, then clears
the scope. A `changed sr-armed` trigger covers views that persist across
sub-tab navigation (Home's Discover tabs, Favorites' shared list, Playlist
A->B->A) where no fresh mount occurs.
ScrollView-based views (Offline Manager, Settings) use visible-height for
the overflow check; ListView/Flickable views use height. Favorites' shared
albums/artists/playlists/labels Flickable matches the active tab's scope
dynamically.
cc
Ephemeral folders: open / browse / play a folder without indexing it —
album-grouped pane, CUE seek fast-path (no full re-decode per virtual track),
folder-path persistence + rehydrate, "already playing" add-vs-replace choice
dialog (ephemeral-only), and now-playing / queue / menu gating so an ephemeral
track exposes no persistence actions.
Folders tree brought 1:1 with Tauri:
- Rail header: select-mode, collapse-all, open-ephemeral, tree search.
- Resizable rail (default ~20% narrower; drag handle, min 50% default /
max 50% of the content area).
- Subfolder cover grid + grid/list toggle, recursive track count, name search;
compact icon bulk bar (folder-check / play-next / queue / playlist / clear).
- Tree is the default mode; circular checkboxes homologated with TrackRow;
compact rows (smaller type + icon, no ambiguous trailing count).
- Flat/Tree icon toggle (disc-album / folder-tree), floated right.
Per-source TrackRow context menu (logic, not a blanket trim): Qobuz/offline =
full; local = play/next/queue + add-to-playlist; plex/ephemeral = play/next/
queue only.
App-wide standard: Math.round pixel-snap on icon centering across the UI to
kill sub-pixel blur (CircleAction, header back/forward/collapse, and the rest).
NowPlayingBar context icon -> turntable (frees disc-album for the Folders
toggle). New assets: folder-check, folder-tree, turntable.
cc
AlbumCard and PlaylistCard are now 200x246 (was 220x266), with the larger
texts -2px and the smaller ones -1px. All album/playlist carousels and grids
follow (card-width/height 200/246) so nothing reflows: Carousel,
PlaylistCarousel, AlbumCollectionView/AlbumGrid, ReleaseGrid, Spotlight,
HomeSkeleton, DiscoverBrowseView, SearchResultsView, FavoritesView. Artist /
radio cards are untouched.
cc
Insert a 20px spacer before the body row. The JUMP TO bar's natural-top tracks body-row.absolute-position.y, so it drops with the row while its sticky fix-on-scroll clamp stays at 0 (unchanged).
cc
CHECKPOINT — this batch is NOT yet acceptable; substantial feedback is pending. Committed so the work is preserved for rework next session, after a max-effort Tauri code review. Areas: TrackRow favorite toggle/state + clickable artist/album (TrackItem gains is-favorite/artist-id/album-id + fav_cache, populated across all builders); PlaylistView header buttons + cover-art hover overlay; LabelView full AlbumCard + grid/list toggle; vertical scrollbar moved to the right gutter across ~12 views.
cc
Polish + consistency:
- Compact playlist header (cover 150px; nav->header spacing
homologated with AlbumView); description truncated to ~2 lines +
"Read more" modal; custom-artwork buttons only on cover hover.
- New Typography.section (18px) token for hero/section titles,
applied across Album / Artist / Mix / Playlist + Home carousels.
- Shorter track rows app-wide (50px row, 36px artwork, -1 fonts);
+3px column-header/divider gap; wider track-number column.
- Reusable ExpandableSearch (the JumpNavBar page-search pattern:
magnifier -> opens left -> X to close) in the playlist action row.
- Custom ListScrollbar (thicker, min thumb height, own gutter) since
Slint's ListView scrollbar can't be styled.
Sort:
- Default / Title / Artist / Album / Duration with asc/desc per
field, plus Custom (manual order persisted in library.db with
up/down reorder). Client-side search + sort combined.
WIP: track drag-and-drop onto sidebar playlists
- DragState/DragActions globals, drag source on the shared TrackRow,
follow-cursor DragGhost, sidebar playlist rows self-detect the drop
via absolute-position, multi-select group drag, add_tracks_to_playlist
on drop, Flickables freeze scroll during a drag.
- KNOWN ISSUE: the ghost is offset from the cursor (the row
absolute-position + pointer mapping is off, esp. inside Flickables).
Diagnostic logging is left in (TrackRow debug() + a start log) to
pin down the wrong term before the generic fix.
cc
Collapses the three near-identical track rows (AlbumView's inline
TrackRow, ArtistPage's inline TrackRow with multi-select, and the
shared SearchTrackRow) into a single primitives/TrackRow driven by
flags, backed by one TrackItem (superset). Foundation for the
upcoming playlist module, which renders the artwork variant heavily.
- New primitives/TrackRow.slint with: show-artwork (cover+number vs
number cell), show-album, show-favorite, show-download,
multi-select-mode (selection checkbox). One media-action wires
play / play-next / queue / favorite / download / toggle-select +
the TrackContextMenu.
- TrackItem is the unified model (superset of the old AlbumTrackItem
+ SearchTrackItem: number, album, selected, artwork all present;
unused fields stay empty per surface).
- Migrated AlbumView (show-artwork:false, show-download:true),
ArtistPage Popular (show-artwork:false, multi-select-mode),
Search, Favorites, Mix (show-artwork:true, +album/+favorite).
Deleted SearchTrackRow.slint and both inline TrackRows.
- Rust: AlbumTrackItem + SearchTrackItem -> TrackItem across
album/artist/search/favorites/mix/main; search's internal row
struct renamed TrackRow -> TrackRowData to free the TrackRow name
for the Slint component. 46 tests pass.
cc
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
- 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
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
- 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
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
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
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
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
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
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
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
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
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
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
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