fix(slint): smooth sticky scroll + artwork right-click menu

Two unrelated things landing together.

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

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

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

cc
This commit is contained in:
vicrodh committed 2026-05-20 18:24:39 -06:00
1 parent 69029238ee
commit c4b6acb760
12 files changed
+480 -15

No files matched your search

+24
View File
@@ -66,6 +66,10 @@ export global AlbumState {
in property <string> title;
in property <string> artist;
in property <string> artist-id;
// Network URL of the cover — used by the artwork right-click
// menu for Open-in-browser and Save-as actions.
in property <string> artwork-url;
in property <bool> has-custom-cover: false;
in property <string> info-line;
in property <string> quality-tier;
// "24-bit / 96 kHz" — quality-badge detail line.
@@ -213,6 +217,13 @@ export struct SimilarEntry {
export global ArtistState {
in property <string> id;
in property <string> name;
// Network URL of the portrait — used by the artwork right-click
// menu for Open-in-browser and Save-as actions.
in property <string> artwork-url;
// True when the user has registered a local image to override
// the Qobuz-supplied portrait. Drives the menu's
// "Add image" -> "Change image / Remove image" toggle.
in property <bool> has-custom-image: false;
in property <string> bio;
// Truncated bio (word-boundary cut + ellipsis) for the at-rest view;
// the full bio in `bio` is shown when the Read-more toggle expands.
@@ -242,6 +253,19 @@ export global ArtistState {
in property <bool> loading: false;
}
// Artwork right-click actions — shared by ArtistPageView and
// AlbumPageView for their respective headers. `kind` is "artist" or
// "album"; `key` is the artist name or album id; `url` is the
// current network URL (for open-in-browser and save-as).
export global ArtworkActions {
callback open-in-browser(string /* url */);
callback save-as(string /* url */, string /* default file name */);
// Opens the system file picker and registers the chosen image
// as the custom override for this artist/album.
callback add-custom(string /* kind */, string /* key */);
callback remove-custom(string /* kind */, string /* key */);
}
// Artist-view actions wired to Rust callbacks.
export global ArtistActions {
// Filter the visible Popular Tracks and release-section albums by