Files
qbz/crates/qbz-slint/ui/state.slint
T
vicrodh 641aa22c17 feat(slint): scroll-position restore on back/forward (Local Library)
Scroll-position restore was never implemented (only a TODO in nav.rs);
back/forward tracked which page was visited, not where it was scrolled.

Add per-history-entry scroll memory: the mounted scroll container reports
its live viewport-y via NavState.report-scroll, so every navigation (fresh
record + back/forward) stamps the outgoing entry without touching the ~30
record call sites. go_back/go_forward hand the saved scroll back to the
shell, which arms NavState.restore-scope + scroll-restore before switching
the view. A container restores only when its scope matches, once per mount,
and only after content has laid out (viewport overflows) to dodge the
clamp-to-0 on async data load; it clears the scope once consumed.

Wired the four Local Library lists (albums, tracks, folders, artists rail).
Tracks is paginated, so its restore is best-effort within loaded pages.

cc
2026-06-08 15:38:35 -06:00

2523 lines
115 KiB
Plaintext

// Shared UI state — structs and globals the Rust controllers populate.
//
// The Rust side builds these models on a worker thread, then sets them
// on the global from the Slint event loop (see src/home.rs, src/main.rs).
import { JumpNavTab } from "primitives/JumpNavBar.slint";
export { JumpNavTab }
export struct AlbumCardItem {
id: string,
title: string,
artist: string,
artist-id: string, // for the clickable artist name -> artist page
genre: string, // shown in the hover overlay
year: string, // localized "MMM D, YYYY" — shown in the hover overlay
quality-tier: string, // "hires" | "cd" | "" — drives the badge icon
quality-label: string, // "Hi-Res: 24-bit / 96 kHz" — badge hover tooltip
ribbon: string,
ribbon-kind: string, // "press" | "qobuzissime" | "albumOfTheWeek" | ""
artwork-url: string,
artwork: image, // empty until the artwork pipeline resolves it
is-favorite: bool, // filled accent heart on the card when favorited
removing: bool, // fading out (un-favorited in the favorites view)
// --- List-row extras (AlbumListRow; ignored by the grid AlbumCard) ----
release-type: string, // "Album" | "EP" | "Single" | "Live" | "Compilation"
source: string, // "qobuz" | "local" | "plex" | "" — hideable column
quality-detail: string,// "24-bit / 96 kHz" — bare detail for QualityBadgeFull
track-count: string, // "12" — TRACKS column
plain-year: string, // "1973" — bare year for the list-row YEAR column
}
// One entry in an A-Z jump strip (favorites tracks name-group / albums
// A-Z group). `index` is the first row index (tracks); albums jump
// proportionally by the entry's ordinal so it needs no exact offset.
export struct AlphaJump {
letter: string,
index: int,
}
// One button in the shared multi-select bulk bar (MultiSelectBar).
export struct BulkAction {
id: string, // emitted verbatim via MultiSelectBar.action(id)
label: string, // already-translated button label
danger: bool, // red-bordered destructive button (e.g. Remove)
needs-selection: bool, // dimmed + disabled while nothing is selected
}
export struct DiscoverSection {
title: string,
// Discover endpoint path this section maps to (e.g. "/discover/newReleases").
// Empty for sections with no full-list page; non-empty shows the "View all"
// link and drives the DiscoverBrowse page.
endpoint: string,
albums: [AlbumCardItem],
}
// Compact "slim" card item — a small thumbnail with a title/subtitle,
// optionally rank-numbered (Popular albums, Recently Played).
export struct SlimItem {
id: string,
title: string,
subtitle: string,
rank: string, // "" = no rank
artwork-url: string,
artwork: image,
// Artist follow state — drives the ArtistCard Follow button.
following: bool,
}
// A playlist result in search (and the label landing's Playlists row).
// Qobuz returns up to four cover URLs per playlist; the card renders them
// as a count-based collage. Defined here (before LabelState) so the label
// landing can reuse it — Slint structs must be declared before first use.
export struct SearchPlaylistItem {
id: string,
title: string,
subtitle: string, // owner name + track count
cover-count: int, // number of cover images, 0-4
url1: string,
url2: string,
url3: string,
url4: string,
cover1: image,
cover2: image,
cover3: image,
cover4: image,
}
// Discover / Home screen state.
export global HomeState {
in property <[DiscoverSection]> sections: [];
in property <[SlimItem]> popular: [];
in property <[SlimItem]> recent: [];
in property <[AlbumCardItem]> recent-albums: [];
in property <bool> loading: false;
// Active Discover tab: "home" | "editorPicks" | "forYou". The
// three tabs draw from the same fetch; switching swaps which
// cached section set is shown (see crate::home::select_tab).
in-out property <string> active-tab: "home";
}
// Discover tab switching. Wired in main.rs to crate::home::select_tab.
export global HomeActions {
callback select-tab(string);
}
// Discover "View all" full-list page — one album module (new releases,
// qobuzissimes, ...) opened from a Carousel's See-All link. Paginated.
export global DiscoverBrowseState {
in-out property <string> title;
// Rust-managed: the /discover/<x> endpoint + the next page offset, so
// DiscoverBrowseActions.load-more knows what to fetch next.
in-out property <string> endpoint;
in-out property <int> next-offset;
// Full (unfiltered) loaded page set. The view binds to `visible`
// below; the Rust side rebuilds `visible` from `albums` whenever the
// search query changes (client-side title/artist filter).
in-out property <[AlbumCardItem]> albums;
// The albums actually rendered — equals `albums` with no search, or a
// filtered subset while the search box is non-empty.
in-out property <[AlbumCardItem]> visible;
in-out property <bool> loading;
in-out property <bool> loading-more;
in-out property <bool> has-more;
// Header tools (1:1 with Tauri DiscoverBrowseView).
// Client-side search over the loaded albums; while non-empty,
// infinite-scroll load-more is disabled (Tauri does the same).
in-out property <string> search-query;
// "grid" | "list" — the view toggle.
in-out property <string> view-mode: "grid";
}
export global DiscoverBrowseActions {
// Fetch the next page (offset = DiscoverBrowseState.next-offset).
callback load-more();
// Re-filter `visible` after `search-query` changed (UI thread).
callback search-changed();
}
// Discover > For You state. The For You tab has heterogeneous,
// ordered sections (album carousels, a track carousel, artist
// carousels), so it renders from its own state rather than the
// uniform HomeState.sections used by Home / Editor's Picks.
// Populated lazily by crate::foryou the first time the tab opens.
// One album-seeded radio tile in the For You "Radio Stations" row.
export struct RadioStationItem {
album-id: string,
title: string,
artist: string,
artwork-url: string,
artwork: image,
}
export global ForYouState {
in property <DiscoverSection> release-watch;
in property <[RadioStationItem]> radio-stations: [];
in property <DiscoverSection> recent-albums;
in property <[SlimItem]> recent-tracks: [];
in property <[SlimItem]> top-artists: [];
in property <[SlimItem]> artists-to-follow: [];
in property <DiscoverSection> more-from-library;
in property <DiscoverSection> rediscover;
// Spotlight — one favorite artist highlighted with a hero + a row
// of their content (top tracks card, radio card, albums).
in property <bool> spotlight-visible: false;
in property <string> spotlight-artist-id;
in property <string> spotlight-name;
in property <string> spotlight-category;
in property <string> spotlight-image-url;
in property <image> spotlight-image;
in property <bool> spotlight-has-top-tracks: false;
in property <[AlbumCardItem]> spotlight-albums: [];
in property <bool> loading: false;
// True once a load has completed, so the tab does not refetch on
// every visit.
in-out property <bool> loaded: false;
}
// One genre in the Filter-by-genre popup's simple grid.
export struct GenreChip {
id: string,
name: string,
selected: bool,
}
// One row of the advanced-view genre tree, flattened (level 0/1/2)
// with only currently-visible rows emitted by the Rust side.
export struct GenreTreeRow {
id: string,
name: string,
level: int, // 0 parent, 1 child, 2 grandchild
selected: bool,
expanded: bool,
has-children: bool,
count: int, // loaded child count (0 = none / unknown)
}
// Filter-by-genre state. The selection is shared across the three
// Discover tabs (matching Tauri's single "home" genre context) — it
// re-fetches the discover index with the chosen genre ids. Populated
// by crate::genre_filter.
export global GenreFilterState {
in property <[GenreChip]> genres: [];
in property <[GenreTreeRow]> tree: [];
in property <int> selected-count: 0;
in-out property <bool> open: false;
in-out property <bool> remember: true;
in-out property <bool> advanced: false;
in-out property <string> search-query;
in property <bool> loading: false;
}
export global GenreFilterActions {
// Set the context the popup edits ("discover" | "favorites"), called
// when a surface opens the shared popup.
callback set-context(string);
callback toggle(string /* genre id */);
callback toggle-expand(string /* genre id */);
callback search(string /* query */);
callback clear();
callback set-remember(bool);
callback set-advanced(bool);
}
// Small string helpers backed by Rust. Slint 1.16 has no `contains`
// builtin, so the searchable QbzSelect filter (long device lists) needs a
// native case-insensitive substring test. Pure so it can be called from
// property bindings; reusable by any searchable list.
export global TextUtil {
pure callback contains-ci(string /* haystack */, string /* needle */) -> bool;
}
// The single, shared track-row model. Superset of every track-list
// surface (album, artist popular, search, favorites, mix, playlist),
// rendered by the shared primitives/TrackRow component. Fields that a
// given surface doesn't use stay empty/false:
// - `number` explicit track number (album/playlist); rows fall
// back to their list index when empty.
// - `album` shown only by rows with show-album (mix/playlist).
// - `selected` multi-select state (artist Popular, playlist editor).
// - `artwork` the cover thumbnail for show-artwork rows.
export struct TrackItem {
id: string,
number: string,
title: string,
artist: string,
album: string,
duration: string,
quality-tier: string,
// Exact bit-depth / sample-rate line, e.g. "24-bit / 96 kHz" — feeds the
// track-row quality badge (empty for unknown / where only the tier shows).
quality-detail: string,
explicit: bool,
selected: bool,
artwork-url: string,
artwork: image,
// Favorite state — drives the heart fill and the optimistic toggle.
// Populated from the user's favorite-track set when each row is built.
is-favorite: bool,
// Ids for the clickable artist/album links in the row. Empty = the
// text is plain (not a link). artist-id -> artist view, album-id ->
// album view, both via the media-action bubble channel.
artist-id: string,
album-id: string,
// "qobuz" | "local" | "plex" | "" — drives the Local Library source
// indicator (shown instead of the favorite/offline columns).
source: string,
// Fading out (un-favorited in the favorites view) before removal.
removing: bool,
// Offline-cache status: 0 none, 1 queued, 2 downloading, 3 ready, 4 failed.
// Drives the per-row offline affordance (make-offline / ring / check / warn).
cache-status: int,
// Download progress 0.0..1.0 — meaningful only while cache-status == 2.
cache-progress: float,
// True while this track's offline CMAF bundle is being decrypted for
// playback — drives the padlock glyph (swaps the play/equalizer icon).
unlocking: bool,
}
// Album detail screen state.
export global AlbumState {
in property <string> id;
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.
in property <string> quality-detail;
// Editorial description / review (HTML stripped). May be empty.
in property <string> description;
// Truncated description for the header; full text lives in the modal.
in property <string> description-short;
in property <image> artwork;
// Artwork-derived tint for the header gradient.
in property <color> header-color: #181820;
in property <string> label;
// Label id for the sidebar label card → label page navigation ("" = none).
in property <string> label-id;
in property <[string]> awards: [];
in property <[TrackItem]> tracks: [];
in property <bool> loading: false;
// True when this is a Local Library album (local files / offline copies).
// Routes the play actions to local playback + hides Qobuz-only chrome.
in property <bool> is-local: false;
// Local source ("user" | "qobuz_download" | "plex" | ""). Gates the
// tag-editor pencil (hidden for Plex; tag write isn't supported there).
in property <string> source;
}
// Album-view actions wired to Rust callbacks.
export global AlbumActions {
// Filter the visible track list by a query (client-side).
callback search(string);
}
// Track drag-and-drop onto sidebar playlists (a star QBZ feature).
// Slint has no HTML5 DnD, so this is pointer-based: the source row
// grabs the pointer and feeds global coords here; the floating ghost
// + each sidebar playlist row read this to self-detect the drop.
export global DragState {
in-out property <bool> active: false;
// Dragged-track count + ghost text (count == 1 shows title +
// "artist · album"; count > 1 shows "N tracks").
in-out property <int> count: 0;
in-out property <string> ghost-title;
in-out property <string> ghost-subtitle;
// Global pointer position (window coords) while dragging.
in-out property <length> pointer-x;
in-out property <length> pointer-y;
// The sidebar playlist id currently under the pointer (drop target).
in-out property <string> over-playlist-id;
}
export global DragActions {
// Begin a track drag from a row. `track-id` is the row; the Rust
// side expands it to the multi-selection when applicable.
callback start(string /* track id */, string /* title */, string /* subtitle */, length, length);
callback move(length /* gx */, length /* gy */);
callback end();
}
// Which content view the shell shows.
export enum ContentView { home, album, artist, settings, search, musician, label, label-releases, location, favorites, mix, playlist, discover-browse, playlist-manager, offline-manager, local-library, local-album }
// In-app toast (transient notification). Mirrors Tauri's toastStore:
// a single toast at a time (a new one replaces the current), auto-hidden
// by the Rust layer per kind. `buffering` is persistent (no auto-hide).
export enum ToastKind { success, error, info, warning, buffering }
// Local Library (on-disk + Plex music). Greenfield Slint port — the view
// shows header stats + the browse tabs (Albums / Artists / Folders /
// Tracks). All management/maintenance/danger actions live under
// Settings > Local Library; the gear button routes there.
// One card in the Local Library Artists grid (name + "N albums · M tracks").
// Images + click-to-detail are a later slice.
export struct LocalArtistItem {
name: string, // raw/canonical key (used for selection + matching)
display-name: string, // canonical display name (after name-merge)
album-count: int,
track-count: int,
image-path: string, // local file path OR cached Qobuz URL ("" = none)
image: image, // decoded portrait (default-empty = show placeholder)
}
// One A-Z section of the local Artists left rail (master list grouped by the
// display name's first letter, "#" bucket for non-alphabetic). 1:1 with the
// Tauri Artists tab, which groups the compact rows under sticky letters.
export struct LocalArtistSection {
letter: string,
artists: [LocalArtistItem],
}
// One visible row in the Folders tree (flattened-model lazy tree — Slint has
// no native TreeView). Folders expand to fetch children lazily.
export struct FolderNode {
path: string, // absolute filesystem path (the node id)
segment: string, // last path component (display)
depth: int, // indent level
is-folder: bool, // folder vs. track row
expanded: bool, // folder open?
can-expand: bool, // has children (track_count_under > 0)
track-count: int, // recursive track count under a folder
// Select-mode: track rows carry `selected`; folder rows carry a tri-state
// `select-state` (0 none / 1 partial / 2 all) over their descendant tracks.
selected: bool,
select-state: int,
}
// One subfolder cover card in the tree-mode folder-detail pane (1:1 with
// Tauri's `.subfolder-card`). `artwork` is loaded async from the folder cover.
export struct FolderSubcardItem {
path: string, // absolute path (the node id)
name: string, // last path component (display)
track-count: int, // recursive track count under the folder
artwork: image,
artwork-url: string, // cover file path (artwork-pipeline source)
}
// One album block in the ephemeral-folder pane (an ad-hoc folder can hold
// several albums). `artwork` is loaded from the cached cover file on the UI
// thread; `tracks` are the album's tracks in scan order.
export struct EphemeralAlbum {
group-key: string,
title: string,
artist: string,
// "year · N tracks" (already formatted).
meta: string,
quality-tier: string,
// This album is a single-file CUE rip (one audio file, virtual tracks).
is-cue: bool,
artwork: image,
tracks: [TrackItem],
}
export global LocalLibraryState {
// "albums" | "artists" | "folders" | "tracks"
in-out property <string> active-tab: "albums";
in property <bool> loading: false;
in property <string> load-error: "";
// Header stats / per-tab badge counts.
in property <int> album-count: 0;
in property <int> artist-count: 0;
in property <int> track-count: 0;
in property <int> folder-count: 0;
// --- Albums tab (server-paginated, metadata-grouped local albums) ----
// `albums` is the accumulated paged set (also the artwork target). Sort
// + search are pushed to SQL (the performant path); paging fetches the
// next chunk on scroll. List / group / A-Z / filters land with the next
// slice — this slice ships the chunked grid.
in property <[AlbumCardItem]> albums: [];
in property <int> albums-total: 0;
in property <bool> albums-loading: false; // initial page fetch
in property <bool> albums-loading-more: false; // subsequent pages
in property <bool> albums-has-more: false;
in property <bool> albums-load-failed: false;
in-out property <int> albums-next-offset: 0;
in-out property <string> albums-search: "";
// "artist-asc" | "artist-desc" | "title-asc" | "title-desc"
// | "year-desc" | "year-asc"
in-out property <string> albums-sort: "artist-asc";
// "grid" | "list" view toggle.
in-out property <string> albums-view-mode: "grid";
// `albums` is the full mapped set (artwork-index target). `albums-visible`
// / `albums-grouped` are the search+filter+sort+group-derived render sets.
in property <[AlbumCardItem]> albums-visible: [];
in property <[DiscoverSection]> albums-grouped: [];
in-out property <string> albums-group: "off"; // off | alpha | artist
in property <[AlphaJump]> albums-alpha: []; // A-Z jumps (alpha group)
in property <int> albums-shown: 0; // count after filter
// --- Tracks tab (server-paginated flat list — the 16K-freeze fix) ---
// Group-by / multi-select / per-row play land with the playback slice.
in property <[TrackItem]> tracks: [];
in property <int> tracks-total: 0;
in property <bool> tracks-loading: false;
in property <bool> tracks-loading-more: false;
in property <bool> tracks-has-more: false;
in property <bool> tracks-load-failed: false;
in-out property <int> tracks-next-offset: 0;
in-out property <string> tracks-search: "";
// Group-by + multi-select (this slice). `tracks-visible` is the
// group-ordered + search-filtered render model (the list renders THIS).
in property <[TrackItem]> tracks-visible: [];
in-out property <string> tracks-group-mode: "off"; // off | album | artist | name
in property <[AlphaJump]> tracks-alpha: []; // A-Z jumps (name grouping)
in-out property <bool> tracks-multi-select: false; // checkbox column on
in property <int> tracks-selected-count: 0; // bar count + select-all
// --- Folders tab ---------------------------------------------------
// flat = album grid grouped by directory; tree = two-pane lazy filesystem.
// Tree is the Slint default (user 2026-06-06).
in-out property <string> folders-view-mode: "tree"; // "flat" | "tree"
// Flat mode: `folders` is the full set (artwork target); `folders-visible`
// / `folders-grouped` are the search/sort/group-derived render sets.
in property <[AlbumCardItem]> folders: [];
in property <bool> folders-loading: false;
in property <bool> folders-load-failed: false;
in property <[AlbumCardItem]> folders-visible: [];
in property <[DiscoverSection]> folders-grouped: [];
in-out property <string> folders-search: "";
in-out property <string> folders-sort: "title-asc"; // title-asc|title-desc|artist-asc|year-desc
in-out property <string> folders-group: "off"; // off | alpha | artist
in-out property <string> folders-grid-view: "grid"; // grid | list
// Tree mode: a flattened visible-node list + the selected folder's detail
// (its direct tracks; subfolders shown as a grid). `folder-tree` is the full
// flattened set; `folder-tree-visible` is the search-filtered render set.
in property <[FolderNode]> folder-tree: [];
in property <[FolderNode]> folder-tree-visible: [];
in property <bool> folder-tree-loading: false;
// Tree rail width (resizable). Default ~20% narrower than the old 340px;
// clamped in the UI to [50% of default, 50% of the content area].
in-out property <length> tree-sidebar-width: 272px;
// Tree rail header: search box + multi-select mode (1:1 with Tauri).
in-out property <string> folders-tree-search: "";
in-out property <bool> tree-select-mode: false;
in property <int> tree-selected-count: 0;
in-out property <string> folders-selected-path: "";
in property <string> folders-selected-name: "";
// Subfolder cover cards (1:1 with Tauri): `folder-detail-subfolders` is the
// full set (artwork target); `-visible` is the search-filtered render set.
in property <[FolderSubcardItem]> folder-detail-subfolders: [];
in property <[FolderSubcardItem]> folder-detail-subfolders-visible: [];
in property <[TrackItem]> folder-detail-tracks: [];
in property <bool> folder-detail-loading: false;
// Recursive track count under the selected folder (the header "N tracks").
in property <int> folder-detail-track-count: 0;
// Per-pane grid/list toggle + subfolder name filter.
in-out property <string> folder-detail-view: "grid"; // "grid" | "list"
in-out property <string> folder-detail-search: "";
// --- Ephemeral folder (ad-hoc play, NOT indexed into library.db) ----
// When `ephemeral-active`, the Folders tab shows the ephemeral pane
// (album-grouped) instead of the normal flat/tree browse. The session is
// in-memory; the folder path is persisted so it rehydrates on restart.
in property <bool> ephemeral-active: false;
in property <bool> ephemeral-loading: false;
in property <string> ephemeral-path: "";
in property <string> ephemeral-name: "";
in property <int> ephemeral-track-count: 0;
// True when the session spans more than one album → show per-album play.
in property <bool> ephemeral-multi-album: false;
in property <[EphemeralAlbum]> ephemeral-albums: [];
// --- Artists tab — two-column master/detail (1:1 with Tauri) --------
// Left rail: the merged/deduped artist master list. `artists` is the full
// merged set (post name-merge); `artists-grouped` / `artists-alpha` are
// the search-and-group-derived render sets; `artists-shown` is the count
// after the search filter (drives the rail header).
in property <[LocalArtistItem]> artists: [];
in property <bool> artists-loading: false;
in property <bool> artists-load-failed: false;
in-out property <string> artists-search: "";
in property <[LocalArtistSection]> artists-grouped: [];
in property <[AlphaJump]> artists-alpha: [];
in property <int> artists-shown: 0;
// Right pane: the selected artist's albums (filtered in place from the
// loaded album set — no new backend call, mirroring Tauri).
in-out property <string> artists-selected-name: ""; // "" = none selected
in property <string> artists-selected-display: "";
in property <[AlbumCardItem]> artists-selected-albums: [];
in property <bool> artists-selected-loading: false;
// Artist-image background fetch (capped Qobuz portrait fill).
in-out property <bool> artists-fetch-images: true; // default ON (mirrors Tauri)
in property <bool> artists-images-fetching: false; // a fetch session is running
in property <int> artists-images-fetched: 0; // painted this session
}
export global LocalLibraryActions {
// Switch browse tab — records a per-tab history entry (mirrors Favorites
// / Discover) so back/forward moves between Local Library tabs.
callback select-tab(string /* "albums"|"artists"|"folders"|"tracks" */);
// Open Settings > Local Library (the gear button — management lives there).
callback open-settings();
// --- Albums tab ---
// Re-query page 1 with the current search text (server-side LIKE).
callback albums-search(string /* query */);
// Set the sort key + re-derive (in-memory, full-load).
callback albums-set-sort(string /* sort key */);
// Set group mode (off|alpha|artist) + re-derive.
callback albums-set-group(string /* mode */);
// Set grid|list view.
callback albums-set-view(string /* mode */);
// A quality/format/source filter toggle changed -> re-derive.
callback albums-filter-changed();
// Clear all quality/format/source filters.
callback albums-clear-filter();
// Retry after a load error.
callback albums-retry();
// Open a local album's detail view (lands with the album-detail slice).
callback open-album(string /* album id */);
// Open an artist (lands with the Artists slice).
callback open-artist(string /* artist id */);
// Per-card action (play / queue / play-next) — lands with the playback
// slice. Wired now so the shared AlbumCollectionView contract is honored.
callback album-action(string /* album id */, string /* action */);
// --- Tracks tab ---
callback tracks-search(string /* query */);
callback tracks-load-more();
callback tracks-retry();
// Per-row play/queue/toggle-select (action: play|play-next|queue|toggle-select).
callback track-action(string /* track id */, string /* action */);
// Group-by mode (off|album|artist|name), persisted.
callback tracks-set-group(string /* mode */);
// Toggle the multi-select checkbox column.
callback tracks-toggle-multi-select();
// Bulk bar action: select-all | clear | queue | play-next | add-to-playlist.
callback tracks-bulk-action(string /* action */);
// --- Folders tab ---
callback folders-search(string /* query */);
callback folders-set-sort(string /* sort key */);
callback folders-set-group(string /* off|alpha|artist */);
callback folders-set-mode(string /* flat|tree */);
// Tree: expand/collapse a folder node (lazy child fetch on expand).
callback folders-toggle-node(string /* path */, bool /* now-expanded */);
// Tree: select a folder -> load its detail (direct tracks + subfolders).
callback folders-select(string /* path */, string /* segment */);
// Tree: play everything under a folder (recursive).
callback folders-play-node(string /* path */);
// Tree: play one track from the selected folder's direct tracks (the
// folder's direct list becomes the queue, starting at this track).
callback folders-play-track(string /* track id */);
// Tree detail pane: filter the subfolder cards by name.
callback folder-detail-search(string /* query */);
// Tree rail header: filter the tree by name; collapse every folder.
callback folders-tree-search(string /* query */);
callback folders-collapse-all();
// Multi-select mode (tree rail): toggle the mode; toggle a folder's
// descendant tracks / a single track; run a bulk action on the selection.
callback folders-toggle-select-mode();
callback folders-toggle-folder-select(string /* folder path */);
callback folders-toggle-track-select(string /* track path */);
callback folders-bulk-action(string /* select-all|clear|queue|play-next|add-to-playlist */);
// --- Ephemeral folder (ad-hoc play) ---
// Open the native folder picker → scan → show the ephemeral pane.
callback ephemeral-open();
// Play the whole ephemeral folder (all albums, scan order).
callback ephemeral-play-all();
// Play one ephemeral track — its album group becomes the queue.
callback ephemeral-play-track(string /* track id */);
// Play one ephemeral album (its tracks become the queue).
callback ephemeral-play-album(string /* album group key */);
// Clear the ephemeral session (drop the pane + forget the path).
callback ephemeral-clear();
// --- Artists tab ---
// Filter the master list (two-way bound to artists-search; re-derive).
callback artists-search(string /* query */);
// Select an artist -> filter their albums into the right pane.
callback artists-select(string /* artist name */);
}
// Albums tab quality/format/source filter (1:1 with Tauri matchesQualityFilters:
// OR within each group, AND between groups; an empty group matches all).
export global LibAlbumFilterState {
in-out property <bool> open: false;
// Quality.
in-out property <bool> hires: false;
in-out property <bool> cd: false;
in-out property <bool> lossy: false;
// Format.
in-out property <bool> flac: false;
in-out property <bool> alac: false;
in-out property <bool> ape: false;
in-out property <bool> wav: false;
in-out property <bool> mp3: false;
in-out property <bool> aac: false;
in-out property <bool> other: false;
// Source.
in-out property <bool> local: false;
in-out property <bool> offline: false;
in-out property <bool> plex: false;
// Active-filter count (Rust-computed) for the toolbar badge.
in property <int> count: 0;
}
// Dedicated Local Library album detail (separate from the Qobuz AlbumView).
// Reuses TrackRow / NavButtons / QualityBadge internals, but ONLY local
// actions (play / shuffle / edit tags / add-to-playlist) — no Qobuz chrome.
// When an album has multiple physical versions (distinct source folders),
// `versions` drives a picker and the track list shows ONE version (no dups).
// One physical version of a local album (a distinct source folder). The
// picker shows the source icon + a quality/format label.
export struct LocalAlbumVersion {
label: string, // "24-bit / 96 kHz · FLAC"
source: string, // "user" | "qobuz_download" | "plex" | ""
}
export global LocalAlbumState {
in property <string> id; // metadata group key
in property <string> title;
in property <string> artist;
in property <string> info-line; // "12 tracks · 48 min"
in property <string> quality-tier; // hires | cd | ""
in property <string> quality-detail; // "24-bit / 96 kHz"
in property <string> cover-url;
in property <image> cover;
in property <bool> loading: false;
in property <[TrackItem]> tracks: [];
// Versions (distinct copies). >1 -> show the picker. Sorted best-quality
// first, so version 0 is the highest-quality copy.
in property <[LocalAlbumVersion]> versions: [];
in-out property <int> version-index: 0;
}
export global LocalAlbumActions {
callback play-all();
callback shuffle();
callback edit-tags();
callback add-to-playlist();
callback play-track(string /* track id */);
callback select-version(int);
}
// ============================ Local Library management ====================
// The folder list + scan + maintenance + danger zone live under
// Settings > Local Library (NOT the browse view). One row per registered
// library folder; `Option<T>` maps to a "" / 0 sentinel + a Rust-computed
// label where the raw value is ambiguous.
// One registered library folder. Mirrors `qbz_library::LibraryFolder`
// (8 DB columns) + a UI-owned `selected` checkbox + Rust-computed labels.
export struct LibraryFolderItem {
id: int, // i64 folder id (small)
path: string, // absolute path
alias: string, // "" when none
display-name: string, // alias if non-empty else path (Rust-computed)
enabled: bool,
is-network: bool,
network-fs-type: string, // "" when none (e.g. "nfs","cifs")
user-override-network: bool,
last-scan: int, // unix seconds; 0 = never
last-scan-label: string, // "Never" / localized date (Rust-computed)
accessible: bool, // from the accessibility check; true until resolved
selected: bool, // UI multi-select state
}
// The folder list + maintenance + danger-zone state (Settings > Local Library).
export global LibraryFoldersState {
in property <[LibraryFolderItem]> folders: [];
in property <bool> loading: false;
in-out property <string> filter: ""; // folder search (alias??path substring)
in property <int> selected-count: 0; // drives Edit(==1) / Remove(>0) enablement
in property <bool> cleaning-missing: false;
in property <string> cleanup-status: ""; // "Removed N of M" / "" (auto-clears, Rust-side)
in property <bool> clearing-library: false;
}
// Folder-settings modal state (separate from the playlist FolderEditState).
export global LibFolderEditState {
in-out property <bool> open: false;
in property <int> folder-id: 0;
in-out property <string> alias: "";
in property <string> path: ""; // current path (updated after change-path)
in-out property <bool> enabled: true;
in-out property <bool> is-network: false;
in-out property <bool> user-override-network: false;
// QbzSelect index: 0 auto,1 cifs,2 nfs,3 sshfs,4 rclone,5 webdav,6 glusterfs,7 ceph,8 other.
in-out property <int> fs-type-index: 0;
in property <bool> accessible: true;
in property <bool> checking-accessible: false;
in property <string> last-scan-label: ""; // "Never" / date
}
// Library scan progress — a single global poll target (scan is not per-row,
// unlike the offline cache). `progress` is computed Rust-side (total==0 guard).
export global LibraryScanState {
in property <bool> scanning: false; // status == Scanning
in property <int> scan-status: 0; // 0 Idle,1 Scanning,2 Complete,3 Cancelled,4 Error
in property <int> total-files: 0;
in property <int> processed-files: 0;
in property <float> progress: 0.0; // 0..1
in property <string> current-file: ""; // basename, or the cleanup label
in property <int> error-count: 0;
}
export global LibraryManageActions {
callback load(); // (re)load the folder list (panel init)
callback add-folder(); // native dir picker (Rust)
callback remove-folders(); // bulk-remove selected (confirm)
callback toggle-folder-select(int /* id */);
callback edit-folder(int /* id */); // open the settings modal
callback save-folder-settings(int /* id */, string /* alias */, bool /* enabled */,
bool /* is-network */, string /* fs-type */, bool /* user-override */);
callback change-folder-path(int /* id */); // picker -> update path
callback scan-all();
callback scan-folder(int /* id */);
callback stop-scan();
callback cleanup-missing();
callback clear-library(); // two-step confirm
callback set-filter(string /* query */);
}
// ============================ Plex connection =============================
// Settings > Local Library > Plex. A LAN-only Plex Media Server connection:
// PIN-based auth (or manual token), library-section multi-select, and the
// per-section track caching that downstream browse views read. State is
// Rust-backed (qbz_app::settings::plex store + qbz-plex core); the controller
// lives in `plex_auth`. ADR-008: no pills. Status is a (key, args) pair so the
// Rust side can localize without 5-file JSON sync — `status-text` is the
// already-resolved label, `status-kind` drives the highlight style.
// One Plex music-library section row + its UI selection + cached track count.
export struct PlexSectionItem {
key: string, // Plex Directory@key
title: string, // section title
count: int, // cached track count (plex_cache_get_tracks(key).len())
selected: bool, // membership in selected_section_keys
}
export global PlexSettingsState {
// --- Persisted toggles -------------------------------------------------
in-out property <bool> enabled: false; // master toggle (default OFF)
in-out property <bool> ui-collapsed: false; // collapse chevron
in-out property <bool> metadata-write-enabled: false;
// --- Connection --------------------------------------------------------
// server-url is the RAW host-form text the user types; base-url is the
// Rust-resolved proto://host:port (set after resolve).
in-out property <string> server-url: "";
in property <string> base-url: "";
in property <string> token: ""; // "" = not authenticated
in-out property <bool> manual-token-mode: false;
in-out property <string> token-input: ""; // manual-token field buffer
// Derived gates (computed Rust-side, pushed in).
in property <bool> is-local-address: true; // isLocalPlexAddress(server-url)
in property <bool> can-use: false; // enabled && local && base!="" && token!=""
// --- PIN flow ----------------------------------------------------------
in property <bool> pin-busy: false; // PIN start / poll in flight
in property <string> pin-code: ""; // "" = no active code
in property <string> pin-auth-url: "";
// --- General busy + status --------------------------------------------
in property <bool> busy: false; // ping / sync / auto-setup
// Status as a resolved label + kind (0 none, 1 info, 2 connected/ok, 3 error).
in property <string> status-text: "";
in property <int> status-kind: 0;
// --- Library sections --------------------------------------------------
in property <[PlexSectionItem]> sections: [];
}
export global PlexAuthActions {
callback enable-toggle(bool);
callback collapse-toggle(bool);
callback set-server-url(string /* host-form */);
callback generate-code(); // PIN start + begin poll
callback open-auth-url();
callback copy-code();
callback manual-token-toggle(bool);
callback set-token(string /* token */); // save manual token
callback ping(); // check connection
callback load-sections(); // get libraries
callback toggle-section(string /* key */);
callback metadata-write-toggle(bool);
callback disconnect(); // confirm + reset
callback clear-cache(); // confirm + plex_cache_clear
callback load(); // panel init: seed from store + warm cache
}
// ============================ Tag editor ==================================
// Local-album metadata editor (Settings-independent modal launched from the
// local album detail). Sidecar (default) + opt-in direct file-write. Remote
// MusicBrainz/Discogs lookup lands as a follow-up sub-slice (needs the
// remote-metadata crate move).
// One editable track row. `cue_start_secs` is f64 (-1.0 = none); `has_cue`
// is precomputed (cue_file_path != "" || cue_start_secs >= 0) and disables
// direct file-write. disc/track 0 = unset sentinel (Option<u32>).
export struct TagTrackEdit {
id: int,
file_path: string,
cue_file_path: string,
cue_start_secs: float,
has_cue: bool,
title: string,
disc_number: string, // raw text (LineEdit); controller parses on save ("" = unset)
track_number: string, // raw text (LineEdit); controller parses on save ("" = unset)
}
// One remote (MusicBrainz/Discogs) search-result card. Option fields map to
// a 0/"" sentinel + a has_* companion where the zero value is valid.
export struct RemoteResultItem {
provider: string, // "musicbrainz" | "discogs"
provider_id: string, // selection key + open-in-browser
title: string,
artist: string,
year: int,
has_year: bool,
track_count: int,
has_track_count: bool,
country: string,
format: string,
label: string,
catalog_number: string,
}
export global TagEditorState {
in-out property <bool> open: false;
in property <string> album_group_key; // == LocalAlbum.id == directory_path
in-out property <string> album_title;
in-out property <string> album_artist;
in-out property <string> year_input; // raw text; controller parses
in-out property <string> genre;
in-out property <string> catalog_number;
in-out property <int> album_total_discs; // collected, NOT persisted (1:1)
in property <string> directory_path; // reference + is_dir gate
in property <bool> can_direct_write; // false if any track has_cue
in-out property <int> persistence_index: 0; // 0 = sidecar, 1 = direct
in-out property <[TagTrackEdit]> tracks: [];
in-out property <bool> saving: false;
in-out property <int> write_progress_current: 0;
in-out property <int> write_progress_total: 0;
// Remote lookup (MusicBrainz / Discogs).
in-out property <int> remote_provider_index: 0; // 0 musicbrainz, 1 discogs
in-out property <bool> remote_searching: false;
in-out property <bool> remote_loading: false; // apply (get full) in flight
in-out property <[RemoteResultItem]> remote_results: [];
in-out property <string> selected_result_id: ""; // "" = none
in-out property <bool> show_remote_panel: false;
in-out property <bool> has_searched: false;
}
export global TagEditorActions {
callback close();
callback save();
callback set-persistence(int); // QbzSelect index -> mode (controller maps)
callback set-provider(int); // provider QbzSelect index
callback search-remote();
callback select-result(string /* provider_id */);
callback apply-remote();
callback open-in-browser();
}
// Global toast state, driven by crate::toast. The Toast overlay (mounted
// at the app root) renders whatever is here. The Rust layer owns timing
// (show + auto-hide); the UI only sets `visible = false` on the close
// button. `enabled` gates non-error toasts (errors always show), to match
// Tauri's "in-app notifications" preference.
export global ToastState {
in-out property <bool> visible: false;
in-out property <string> message;
in-out property <ToastKind> kind: ToastKind.info;
in-out property <bool> persistent: false;
in-out property <bool> enabled: true;
}
// One row in the sidebar playlists list (also used for the folder
// list in the move-to-folder menu).
export struct SidebarPlaylistItem {
id: string,
name: string,
}
// One flattened entry in the sidebar nav list — a folder header or a
// playlist row (indented when inside a folder).
//
// Playlist rows carry up to four cover-art URLs + decoded images so the
// row can render a micro-collage instead of the generic list-music glyph
// (mirrors Tauri's `sidebarPlaylistCollage`). The covers come from the
// same `get_user_playlists()` payload (images300 / images150 / images),
// so no extra per-playlist fetch is needed.
export struct SidebarEntry {
kind: string, // "folder" | "playlist"
id: string,
name: string,
expanded: bool, // folders
count: int, // folder playlist count
indent: bool, // playlist nested in a folder
folder-id: string, // playlist's current folder id ("" = root; folders "")
cover-count: int, // number of cover images, 0-4 (playlists)
url1: string,
url2: string,
url3: string,
url4: string,
cover1: image,
cover2: image,
cover3: image,
cover4: image,
}
// Flyout shown when a folder is clicked while the sidebar is collapsed
// (mini): a popup to the right listing that folder's playlists. The list is
// populated from Rust because collapsed folders' children are NOT present in
// the flat `SidebarState.entries`.
export global SidebarFolderPopupState {
in-out property <bool> open: false;
in-out property <string> folder-id;
in-out property <string> folder-name;
in-out property <int> count: 0; // folder's playlist count (caps the height)
in-out property <length> anchor-x; // window x (sidebar right edge)
in-out property <length> anchor-y; // window y (folder row top)
in property <[SidebarEntry]> playlists: [];
}
// Collapsed (mini) sidebar hover tooltip — the row shows only an icon, so a
// hover bubble surfaces the name (folders also show their playlist count).
// Mirrors Tauri's native `title` on the collapsed folder/playlist buttons.
// Rendered at the AppShell level because the sidebar clips its overflow; the
// anchor is the row's true window position (the collapsed rows live inside a
// Flickable, so SidebarRow corrects the absolute-position double-count).
export global SidebarTooltipState {
in-out property <bool> open: false;
in-out property <string> id; // owning row id (race-safe leave)
in-out property <string> text; // "Folder (N)" or "Playlist name"
in-out property <length> anchor-x; // window x (sidebar right edge)
in-out property <length> anchor-y; // window y (row top)
}
// Closed-sidebar playlists flyout. When the sidebar is fully closed there is no
// playlist list on screen; a header button opens this popup over SidebarState's
// entries (folders + playlists), capped at six visible rows with the rest
// scrolling, plus an always-visible footer link to the Playlist Manager.
export global SidebarPlaylistsPopupState {
in-out property <bool> open: false;
in-out property <length> anchor-x; // window x (header button left)
in-out property <length> anchor-y; // window y (below the button)
}
// The sidebar's playlists + folders. Populated by crate::sidebar on
// shell entry and after create/delete/move.
export global SidebarState {
in property <[SidebarEntry]> entries: [];
// Flat folder list, for the move-to-folder menu.
in property <[SidebarPlaylistItem]> folders: [];
// Folder list filtered by the move-to-folder search box. Equals
// `folders` when the query is empty; a case-insensitive substring
// subset otherwise. Filled by the Rust side (Slint strings have no
// `contains`, so the substring filter can't live in markup). The
// context menu iterates this; the current-folder exclusion stays in
// markup via the per-row `folder-id`.
in property <[SidebarPlaylistItem]> menu-folders: [];
in property <bool> loading: false;
// The currently-open playlist id, for the active-row highlight.
in property <string> active-id;
// PLAYLISTS section collapse (the folder+playlist list hides). Mirrors
// Tauri's `playlistsCollapsed`.
in-out property <bool> playlists-collapsed: false;
// Inline playlist-name search in the header. When open the title is
// hidden and the input takes the full header width.
in-out property <bool> search-open: false;
in-out property <string> search-query;
// Active sort option: "name" | "recent" | "tracks" | "playcount"
// | "custom" (mirrors Tauri's SortOption). Persisted by crate::sidebar.
in-out property <string> sort-option: "name";
// Micro-collage of track covers on playlist rows. Opt-out (ON by
// default) to match Tauri's `sidebarPlaylistCollage`. The Settings >
// Appearance toggle wires into this in a later step.
in-out property <bool> playlist-collage: true;
}
export global SidebarActions {
callback open-playlist(string /* id */);
// Populate SidebarFolderPopupState.playlists for the collapsed-sidebar
// folder flyout (the folder's children aren't in the flat entries).
callback load-folder-popup(string /* folder id */);
callback create-playlist();
callback create-folder();
callback toggle-folder(string /* folder id */);
callback delete-folder(string /* folder id */);
// Move a playlist into a folder ("" folder id = root).
callback move-playlist(string /* playlist id */, string /* folder id */);
// Right-click context menu actions (folder + playlist rows).
callback edit-playlist(string /* id */);
callback edit-folder(string /* id */);
callback hide-playlist(string /* id */);
callback hide-folder(string /* id */);
// Filter `menu-folders` by a case-insensitive substring of the
// move-to-folder search box (Rust does the substring match).
callback search-folders(string /* query */);
// Pick a sort option (re-sorts the list). Same five values as Tauri.
callback set-sort(string /* option */);
// Re-run the playlist-name filter (called as the search input edits).
callback search-changed(string /* query */);
// Re-fetch the playlist list from the network.
callback refresh-playlists();
// Open the full Playlist Manager view.
callback manage-playlists();
}
// "Create playlist" modal state. Mirrors Tauri's PlaylistModal (create
// mode): name, optional description, target folder, public flag.
export global CreatePlaylistState {
in-out property <bool> open: false;
in-out property <string> name;
in-out property <string> description;
in-out property <bool> is-public: false;
in property <bool> creating: false;
// Folder dropdown options — index 0 is always "No folder", the rest
// are the user's folder names (parallel to `folder-ids`). Built by
// the create-playlist handler from the sidebar folder list.
in property <[string]> folder-options: ["No folder"];
// Folder ids parallel to `folder-options` ("" for "No folder").
in property <[string]> folder-ids: [""];
// Selected index into `folder-options` (0 = No folder).
in-out property <int> folder-index: 0;
}
export global CreatePlaylistActions {
callback submit();
callback close();
}
// "Create folder" modal state.
export global CreateFolderState {
in-out property <bool> open: false;
in-out property <string> name;
in property <bool> creating: false;
}
export global CreateFolderActions {
callback submit();
callback close();
}
// Ephemeral "play with active queue" choice — shown when the user triggers an
// ephemeral play action while a queue is already playing. Two outcomes:
// add to the queue, or clear the queue and start playing.
export global EphemeralPlayChoiceState {
in-out property <bool> open: false;
// "all" | "album" | "track" — what the user pressed play on.
in-out property <string> intent-kind: "";
// group key (album) or track id (track); empty for "all".
in-out property <string> intent-arg: "";
// Whether "Add to queue" is offered — only when the current queue is itself
// entirely ephemeral (queue ops may not mix ephemeral with persistent tracks).
in-out property <bool> enqueue-allowed: false;
}
export global EphemeralPlayChoiceActions {
callback enqueue(); // add the selection to the current queue
callback replace(); // clear the queue and start playing the selection
callback close();
}
// Playlist detail screen state. Rows reuse the shared TrackRow
// (show-artwork + show-album); populated by crate::playlist.
export global PlaylistState {
in property <string> id;
in property <string> name;
in property <string> owner;
in property <string> description;
// Truncated description for the header (~2 lines); full text in
// the Read-more modal. Equal to description when it fits.
in property <string> description-short;
in property <int> track-count: 0;
in property <string> total-duration;
in property <string> cover-url;
in property <image> cover;
// True when a user-set custom artwork is shown (overrides the
// track-cover collage).
in property <bool> has-custom: false;
in property <bool> is-favorite: false;
// True when the playlist is one of the user's own (editable).
in property <bool> is-owner: false;
// Track edit mode — rows show a selection checkbox and a bulk bar
// appears (remove selected). Only available to the owner.
in-out property <bool> multi-select-mode: false;
in property <int> selected-count: 0;
in property <[TrackItem]> tracks: [];
in property <bool> loading: false;
// Sort: "" / "default" = playlist order; else title|artist|album|
// duration. `sort-asc` flips on re-selecting the active field.
in property <string> sort-field: "default";
in property <bool> sort-asc: true;
}
export global PlaylistActions {
// Client-side filter of the visible track list by query.
callback search(string);
// Sort by a field (re-selecting the active field toggles asc/desc).
callback set-sort(string);
}
// "Edit playlist" modal state (rename + description + delete).
export global EditPlaylistState {
in-out property <bool> open: false;
in property <string> id;
in-out property <string> name;
in-out property <string> description;
in property <bool> busy: false;
}
export global EditPlaylistActions {
callback save();
callback delete();
callback close();
}
// === Playlist Manager ===================================================
// A full playlist + folder organization surface (Tauri's
// PlaylistManagerView). Rows are precomputed in Rust (crate::playlist_manager)
// and pushed as ready-to-render models — no per-row map lookups in .slint.
// One folder card / chip / tree-folder. `icon-color` is a solid hex (or
// "" → use the accent). Gradient folder colors are not supported here
// (Slint brush limitation); a gradient string falls back to the accent.
export struct PmFolderItem {
id: string,
name: string,
count: int, // playlists in this folder
// "preset" or "custom".
icon-type: string,
// Lucide preset name ("heart" | "star" | "music" | "folder" | "disc"
// | "library" | "headphones"); drives which glyph the card shows.
icon-preset: string,
// Resolved solid color for the icon square (parsed from the stored
// hex by the Rust controller). When `has-color` is false the card
// uses the accent instead (gradient / empty / unparseable colors
// fall back to accent — Slint has no gradient-brush-in-struct).
icon-color: color,
has-color: bool,
is-hidden: bool,
// Decoded custom image (icon-type == "custom"); empty otherwise.
custom-image: image,
has-custom-image: bool,
}
// One playlist card / list-row / tree-row. All display strings are
// precomputed (track line, local line) so the view never does lookups.
export struct PmPlaylistItem {
id: string, // playlist id (may be negative for offline pending)
name: string,
// "12 tracks" — total (remote + local).
tracks-line: string,
// "1h 43m" — total playtime, "" when zero (list-row meta only).
duration-line: string,
// "(3 local)" — local-only suffix, "" when none.
local-line: string,
local-count: int,
total-count: int,
play-count: int,
// "" | "all_local" | "some_local" | "no" — availability badge.
local-status: string,
is-favorite: bool,
is-hidden: bool,
folder-id: string, // "" = root
// Micro-collage covers (same 0-4 scheme as the sidebar).
cover-count: int,
url1: string,
url2: string,
url3: string,
url4: string,
cover1: image,
cover2: image,
cover3: image,
cover4: image,
}
// One flattened row of the tree view — a folder header or a nested /
// root playlist. The Rust side emits only currently-visible rows
// (collapsed folders hide their children).
export struct PmTreeRow {
kind: string, // "folder" | "playlist"
expanded: bool, // folders
folder: PmFolderItem, // populated when kind == "folder"
playlist: PmPlaylistItem, // populated when kind == "playlist"
indent: bool, // playlist nested under a folder
}
export global PlaylistManagerState {
in property <[PmFolderItem]> folders: [];
// Filtered folder list for the list-row move-to-folder menu. Starts as
// the full folder set; the menu's search box narrows it via
// `search-folders` (Slint strings have no `contains`, so Rust filters).
in property <[PmFolderItem]> menu-folders: [];
in property <[PmPlaylistItem]> playlists: []; // the filtered+sorted grid/list set
in property <[PmTreeRow]> tree: [];
// Header counters: total folders / total playlists (post-filter for
// the visible playlist count).
in property <int> folder-count: 0;
in property <int> playlist-count: 0;
in property <bool> loading: false;
// Toolbar state (session-scoped; persisted per-user where cheap).
in-out property <string> search-query;
// "all" | "visible" | "hidden".
in-out property <string> filter: "all";
// "name" | "recent" | "playcount" | "tracks" | "custom".
in-out property <string> sort: "name";
// "grid" | "list" | "tree".
in-out property <string> view-mode: "grid";
// Folder mode on (folders shown + folder filtering) vs flat.
in-out property <bool> folder-mode: true;
// Folders section collapse.
in-out property <bool> folders-collapsed: false;
// Reorder is only allowed when sort == "custom".
in property <bool> can-reorder: false;
}
export global PlaylistManagerActions {
// Open a playlist (-> PlaylistView).
callback open-playlist(string /* id */);
// Toolbar.
callback search-changed(string /* query */);
callback set-filter(string);
callback set-sort(string);
callback set-view-mode(string);
// Filter the list-row move-to-folder menu's folder list (substring,
// case-insensitive). Empty query restores the full list.
callback search-folders(string /* query */);
callback toggle-folder-mode();
callback toggle-folders-collapsed();
// Tree-view folder expand/collapse.
callback toggle-tree-folder(string /* folder id */);
// Folder CRUD — "new folder" opens the editor in create mode; edit
// opens it for an existing folder.
callback new-folder();
callback edit-folder(string /* folder id */);
// Per-card playlist actions.
callback toggle-favorite(string /* playlist id */);
callback toggle-hidden(string /* playlist id */);
callback edit-playlist(string /* playlist id */);
callback add-to-mixtape(string /* playlist id */);
// Arrow reorder (custom sort only): move a playlist up / down within
// the current visible order, persisting the new order.
callback move-up(string /* playlist id */);
callback move-down(string /* playlist id */);
// Move a playlist into a folder ("" = root). Used by the tree's
// folder rows and the future DnD path.
callback move-to-folder(string /* playlist id */, string /* folder id */);
}
// === Offline Cache Manager =============================================
// The standalone manage-downloads surface (Tauri's OfflineCacheManagerView).
// Rows are precomputed in Rust (crate::offline_manager): an album-header row
// followed by its track rows, flattened (mirrors PmTreeRow).
export struct OfflineRow {
kind: string, // "album" | "track"
album-id: string, // album-header: the album id; track: its album id
track-id: string, // track rows
title: string, // album title (album) | track title (track)
subtitle: string, // artist
meta: string, // "8 tracks · 320 MB" (album) | "3:45" (track)
// 0 none | 2 downloading | 3 ready | 4 failed. Album = worst across tracks.
status: int,
progress: float, // track downloading 0..1
cover: image, // album-header cover (loaded from the on-disk cover.jpg)
number: string, // track position within its album ("" on album rows)
selected: bool, // multi-select checkbox
}
// One entry in the manager's left A-Z artist rail.
export struct OfflineArtist {
name: string,
meta: string, // "3 albums · 21 tracks"
selected: bool,
}
export global OfflineManagerState {
in property <[OfflineRow]> rows: [];
in property <[OfflineArtist]> artists: [];
in property <bool> loading: false;
// Header stats.
in property <int> tracks-count: 0;
in property <string> size-text: ""; // "1.2 GB"
in property <string> limit-text: ""; // "of 5 GB" | "Unlimited"
in property <float> usage: 0.0; // 0..1 for the bar (0 = unlimited)
in-out property <int> limit-gb: 5; // edit-limit input
// Toolbar — set by the controller from the Rust-side filter state so the
// dropdown selection / toggle / rail highlight reflect the current view.
in property <string> selected-artist: ""; // "" = all artists
in property <int> sort-index: 0; // 0 alpha / 1 recent / 2 largest / 3 smallest
in property <bool> show-only-failed: false;
// Multi-select: how many TRACK rows are checked (drives the bulk bar).
in property <int> selected-count: 0;
}
export global OfflineManagerActions {
// Navigate to the manager (from the Settings "Offline Playback" entry).
callback open();
callback refresh();
// Toolbar.
callback select-artist(string /* name, "" = all */);
callback set-sort(int /* index */);
callback toggle-failed();
// Multi-select.
callback toggle-select(string /* track id */);
callback select-all();
callback clear-selection();
callback bulk-redownload();
callback bulk-remove();
callback remove-track(string /* track id */);
callback remove-album(string /* album id */);
callback redownload-track(string /* track id */);
callback redownload-album(string /* album id */);
callback redownload-failed(string /* album id */);
callback set-limit(int /* gb */);
callback clear-all();
callback open-folder();
callback play-track(string /* track id */);
}
// Folder editor modal — create + edit (icon preset, solid color, custom
// image, hidden toggle, delete). Ports Tauri's FolderEditModal. Gradient
// color presets are intentionally omitted (Slint brush limitation).
export struct PmIconPreset {
id: string,
icon: image,
}
export struct PmColorSwatch {
// The stored hex value ("" = the "Accent" preset), used as the
// selection key and persisted into the folder record.
value: string,
// Resolved Slint color for the swatch fill (ignored when is-accent).
color: color,
is-accent: bool,
}
export global FolderEditState {
in-out property <bool> open: false;
// Empty id = create mode; non-empty = editing that folder.
in property <string> id: "";
in-out property <string> name;
in-out property <string> icon-preset: "folder";
// Selected solid color hex; "" = accent.
in-out property <string> icon-color: "";
in-out property <bool> is-hidden: false;
// Custom image path (icon-type == "custom" when set) + decoded image.
in-out property <string> custom-image-path: "";
in property <image> custom-image;
in property <bool> busy: false;
// The icon-preset grid + the solid-color swatch grid, built once by
// the Rust controller.
in property <[PmIconPreset]> icon-presets: [];
in property <[PmColorSwatch]> color-swatches: [];
}
export global FolderEditActions {
callback select-preset(string /* preset id */);
callback select-color(string /* color hex, "" = accent */);
callback pick-image();
callback clear-image();
callback toggle-hidden();
callback save();
callback delete();
callback close();
}
// Settings screen state. Audio and Playback preferences are loaded from
// the persistence stores by the Rust settings controller and pushed here;
// changes flow back through the settings-bool / settings-select /
// settings-slider callbacks.
export global SettingsState {
// Sub-section: 0 = Audio, 1 = Playback.
in-out property <int> section: 0;
in property <bool> loading: true;
// --- AUDIO -------------------------------------------------------------
// Audio — dropdowns. Each list is paired with a selected index; the
// Rust controller owns the index -> value mapping.
in-out property <[string]> streaming-qualities: [];
in-out property <int> streaming-quality-index: 0;
in-out property <[string]> sample-rates: [];
in-out property <int> sample-rate-index: 0;
in-out property <[string]> backends: [];
in-out property <int> backend-index: 0;
in-out property <[string]> devices: [];
// Parallel to `devices`: true marks a bit-perfect-capable device.
in-out property <[bool]> device-bp: [];
// Parallel to `devices`: a non-empty entry is a section-header label
// shown above that device row (empty = no header).
in-out property <[string]> device-groups: [];
in-out property <int> device-index: 0;
in-out property <[string]> alsa-plugins: [];
in-out property <int> alsa-plugin-index: 0;
// Audio — toggles.
in-out property <bool> limit-quality-to-device: false;
in-out property <bool> alsa-hardware-volume: false;
in-out property <bool> exclusive-mode: false;
in-out property <bool> reserve-dac: false;
in-out property <bool> dac-passthrough: false;
in-out property <bool> pw-force-bitperfect: false;
in-out property <bool> allow-quality-fallback: false;
in-out property <bool> sync-audio-on-startup: false;
in-out property <bool> skip-sink-switch: false;
// Audio — Rust-computed conditional flags driving `if`-gated rows.
in-out property <bool> backend-is-alsa: false;
in-out property <bool> backend-is-pipewire: false;
in-out property <bool> alsa-plugin-is-hw: false;
// --- PLAYBACK ----------------------------------------------------------
// Playback — toggles. `continue-playback` maps to the autoplay mode
// (on = ContinueWithinSource, off = PlayTrackOnly).
in-out property <bool> continue-playback: true;
in-out property <bool> show-context-icon: true;
in-out property <bool> persist-session: false;
in-out property <bool> resume-position: false;
in-out property <bool> gapless: true;
in-out property <bool> stream-uncached: false;
in-out property <bool> streaming-only: false;
// Volume normalization (loudness leveling). Applied in the shared player
// and bypassed automatically when bit-perfect is active. Surfaced in the
// now-playing bar's normalization group as well as Settings > Playback.
in-out property <bool> normalization: false;
// Audio-output indicators for the now-playing song card (computed in Rust
// from the real backend + bit-perfect constraints). `*-label` is the fixed
// short text; `*-active` colours the led when a deliberate / bit-perfect
// mode is engaged (vs the shared system default).
in-out property <string> output-backend-label: "SYST"; // PIPE|ALSA|JACK|PULS|SYST
in-out property <string> output-mode-label: "DEFAULT"; // DACPASS|BITPERF|EXCL|DIRECT|LOCKED|ROUTED|SHARED|DEFAULT
in-out property <bool> output-backend-active: false;
in-out property <bool> output-mode-active: false;
// Playback — Initial Buffer Size slider (seconds, 1-10).
in-out property <int> buffer-seconds: 3;
// Playback — "When quality retries fail" dropdown.
in-out property <[string]> retry-behaviors: [];
in-out property <int> retry-behavior-index: 0;
}
// Appearance settings — backs the Settings > Appearance panel. 1:1 with
// Tauri's appearance section. This is a visual/structural replica: every
// row is bound to a property here, but the Rust side does NOT yet persist
// or apply most of them (wiring is a later step). The few that map onto an
// existing, already-functional flag are wired through (see notes inline).
//
// Dropdowns follow the same pattern as SettingsState: an option list paired
// with a selected index; the Rust controller (when wired) owns the
// index -> value mapping. Lists are populated with sensible defaults below
// so the UI renders standalone even while unwired.
export global AppearanceState {
// --- THEME -------------------------------------------------------------
// Only the themes that are actually ported/wired. Order is load-bearing:
// the index maps 1:1 to ThemeState.mode (0 Dark, 1 OLED, 2 Tokyo, 3 System).
in-out property <[string]> themes: [
"Dark", "OLED Black", "Tokyo Night", "System"
];
in-out property <int> theme-index: 0;
// Theme list filter cycle: 0 = All, 1 = Dark, 2 = Light. Mirrors the
// SunMoon/Moon/Sun toggle button next to the theme dropdown.
in-out property <int> theme-filter: 0;
in-out property <bool> album-header-gradient: true;
// Auto-Theme (shown when the "System" theme is selected). Source:
// 0 = System Colors, 1 = Wallpaper Sync, 2 = Custom Image.
in-out property <bool> theme-is-system: false;
in-out property <[string]> auto-theme-sources: ["System Colors", "Wallpaper Sync", "Custom Image"];
in-out property <int> auto-theme-source-index: 0;
in-out property <string> auto-theme-custom-path: "";
in-out property <bool> auto-theme-generating: false;
in-out property <string> auto-theme-detected-de: "";
// --- TYPOGRAPHY / SCALE / LANGUAGE -------------------------------------
in-out property <[string]> languages: ["Auto", "English", "Español", "Français", "Deutsch", "Português"];
in-out property <int> language-index: 0;
in-out property <[string]> fonts: ["LINE Seed JP", "Montserrat", "Noto Sans", "Source Sans 3", "System"];
in-out property <int> font-index: 0;
in-out property <[string]> ui-scales: ["80%", "85%", "90%", "95%", "100%", "110%", "125%", "150%", "175%"];
in-out property <int> ui-scale-index: 4;
// --- VISUAL EXTRAS -----------------------------------------------------
// sidebar-playlist-collage is wired to SidebarState.playlist-collage in
// the panel (the only already-functional flag here).
in-out property <bool> sidebar-playlist-collage: true;
in-out property <bool> local-library-track-artwork: false;
// Player volume +/- stepper buttons — opt-in, hidden by default.
in-out property <bool> show-volume-steppers: false;
// --- NOTIFICATIONS -----------------------------------------------------
in-out property <bool> in-app-toasts: true;
in-out property <bool> system-notifications: true;
// --- WINDOW TITLE ------------------------------------------------------
in-out property <bool> window-title-show: false;
in-out property <string> window-title-template: "";
// --- TITLE BAR / WINDOW CHROME -----------------------------------------
in-out property <bool> use-system-title-bar: false;
in-out property <bool> hide-title-bar: false;
in-out property <bool> match-system-chrome: false;
// True when GPU acceleration is on (gates match-system-chrome). Default
// true so the row is interactive in the unwired replica.
in-out property <bool> hardware-accel-enabled: true;
in-out property <bool> search-in-title-bar: false;
// Navigation-in-title-bar group: the parent label row plus per-target
// toggles (Discover/Home, Favorites, Library, My QBZ, Purchases).
in-out property <bool> nav-tb-discover: false;
in-out property <bool> nav-tb-favorites: false;
in-out property <bool> nav-tb-library: false;
in-out property <bool> nav-tb-myqbz: false;
in-out property <bool> nav-tb-purchases: false;
// Nav buttons position: 0 = Auto, 1 = Left, 2 = Right.
in-out property <[string]> nav-tb-positions: ["Auto (opposite to window controls)", "Left", "Right"];
in-out property <int> nav-tb-position-index: 0;
// Window controls group.
in-out property <[string]> wc-positions: ["Left", "Right"];
in-out property <int> wc-position-index: 1;
in-out property <[string]> wc-styles: ["Rectangular", "Full-height rounded", "Circular", "Square"];
in-out property <int> wc-style-index: 0;
in-out property <[string]> wc-sizes: ["Small", "Normal", "Large"];
in-out property <int> wc-size-index: 1;
in-out property <[string]> wc-color-presets: ["Default", "macOS", "Adwaita", "Monochrome", "Custom"];
in-out property <int> wc-color-preset-index: 0;
in-out property <bool> show-window-controls: true;
// --- MINIPLAYER --------------------------------------------------------
in-out property <[string]> miniplayer-views: ["Remember last used", "Micro", "Compact", "Artwork", "Queue", "Lyrics"];
in-out property <int> miniplayer-view-index: 0;
// --- STARTUP -----------------------------------------------------------
in-out property <[string]> startup-pages: ["Home", "Where you left off"];
in-out property <int> startup-page-index: 0;
in-out property <bool> show-purchases: false;
// --- SYSTEM TRAY -------------------------------------------------------
// `is-macos` is set once from Rust (cfg!(target_os = "macos")) so the tray
// section can swap the header to "Menu Bar" and show macOS-only controls.
in-out property <bool> is-macos: false;
in-out property <bool> tray-enable: true;
in-out property <bool> tray-minimize-to-tray: false;
in-out property <bool> tray-close-to-tray: false;
// macOS-only: hide the Dock icon when closed to the menu bar (.accessory
// activation policy). Off = Spotify-style (keep the Dock icon).
in-out property <bool> tray-mac-hide-dock: false;
in-out property <[string]> tray-icon-themes: ["Auto", "Mono light", "Mono dark", "Color"];
in-out property <int> tray-icon-theme-index: 0;
// --- IMMERSIVE ---------------------------------------------------------
in-out property <[string]> immersive-views: [
"Remember last used", "Coverflow", "Static", "Vinyl", "Visualizer",
"Neon Flow", "Tunnel", "Comet", "Lyrics", "Queue",
"Split: Lyrics", "Split: Track Info", "Split: Suggestions", "Split: Queue"
];
in-out property <int> immersive-view-index: 0;
in-out property <[string]> immersive-background-modes: ["Full", "Lite", "Off"];
in-out property <int> immersive-background-mode-index: 0;
// Collapsible sub-panels (collapsed by default, matching Tauri).
in-out property <bool> immersive-panels-collapsed: true;
in-out property <bool> immersive-fps-collapsed: true;
// Callbacks. Unwired in this replica — the Rust side may bind them
// later. Kept generic (key + value) so the controller can dispatch.
callback appearance-bool(string /* key */, bool /* value */);
callback appearance-select(string /* key */, int /* index */);
callback appearance-text(string /* key */, string /* value */);
callback appearance-action(string /* key */);
}
// One entry of a nav-tab dropdown.
export struct NavMenuEntry {
label: string,
icon: image,
has-glyph: bool,
// Destination route fired when the row is clicked, e.g. "home",
// "favorites-tracks", "favorites-albums". Empty = no-op (the
// destination isn't implemented yet).
route: string,
}
// Header nav-tab dropdown state.
//
// The four header tabs (Discover / Library / Local Library / My QBZ) no
// longer own a PopupWindow each — a PopupWindow grabs the pointer while
// open, which blocks hover-switching between tabs. Instead a single
// in-window overlay (HeaderMenuOverlay, a late child of the AppShell
// root) draws the open tab's menu. Tabs write their identity into this
// global on hover/click; hovering a different tab overwrites it, giving
// an instant switch. `open-index` of -1 means no menu is open.
export global HeaderMenuState {
// Which tab's menu is open: -1 = none, 0..3 = the tab index.
in-out property <int> open-index: -1;
// Header x-coordinate of the open tab, so the overlay aligns the
// menu panel under it.
in-out property <length> anchor-x: 0px;
// Window y-coordinate of the anchor — used for sidebar flyouts, which
// open to the RIGHT of the row rather than under the header.
in-out property <length> anchor-y: 0px;
// True when the open menu is a sidebar section flyout (opens right of
// the row) rather than a header dropdown (opens under the tab).
in-out property <bool> from-sidebar: false;
// Optional section-name header shown as the first row (with a hairline
// separator below it). Empty = no header row.
in-out property <string> title: "";
// True while the pointer rests on the TRIGGER (the header tab / compact
// nav icon / sidebar nav row). Pauses the idle-close like hovering the
// menu does, so resting on the trigger never lets it close.
in-out property <bool> trigger-hovered: false;
// The open tab's menu rows.
in-out property <[NavMenuEntry]> items: [];
// Index of the row wired to Home (-1 = none); only Discover uses it.
in-out property <int> home-item: -1;
}
export global NavState {
in-out property <ContentView> view: ContentView.home;
// Browser-like history flags, driven by the Rust nav module.
in property <bool> can-back: false;
in property <bool> can-forward: false;
// Navigation requests routed to Rust so history is recorded.
callback request-back();
callback request-forward();
callback request-settings();
// ---- Scroll-position restore (browser-like, per history entry) ----
// The mounted scroll container reports its live viewport-y here on every
// `changed viewport-y`; the Rust nav module stamps it onto the outgoing
// entry so back/forward can put it back.
callback report-scroll(length /* viewport-y */);
// Armed by Rust right before a back/forward view switch: `restore-scope`
// names which scroll container should restore (a stable per-list id, see
// nav::scope_for), and `scroll-restore` is the viewport-y to apply. A
// container restores only when its own scope matches, exactly once per
// mount, and only after its content has laid out (viewport overflows).
in-out property <string> restore-scope: "";
in-out property <length> scroll-restore: 0;
}
// Artist detail screen state.
// One label row in the artist network sidebar's Labels section.
export struct LabelEntry {
id: string,
name: string,
}
// One similar-artist row in the artist network sidebar's Similar
// Artists section.
export struct SimilarEntry {
id: string,
name: string,
}
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.
in property <string> bio-short;
in property <bool> bio-truncated: false;
// Editorial source for the biography ("TiVo" etc); shown as a small
// "Source: <name>" line under the Read more toggle when present.
in property <string> bio-source;
in property <image> artwork;
// Artwork-derived tint for the header gradient.
in property <color> header-color: #181820;
in property <[TrackItem]> top-tracks: [];
// Releases grouped into titled sections (Albums, EPs & Singles, ...).
in property <[DiscoverSection]> release-sections: [];
// Labels collected from the artist's own album releases (deduped by
// id, alphabetically sorted) — feed the Labels section of the
// network sidebar.
in property <[LabelEntry]> labels: [];
// Similar artists from the /artist/page response — feed the Similar
// Artists section of the network sidebar.
in property <[SimilarEntry]> similar-artists: [];
// Filtered JUMP TO tabs for this artist — only the sections
// that actually have content. Built in artist::apply_artist
// from the page response. anchor-y is the page-local y the
// ArtistPageView will scroll to when the tab is clicked.
in property <[JumpNavTab]> jump-tabs: [];
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
// a query string (client-side substring match, mirrors the album
// track search in AlbumActions).
callback search(string);
}
// Artist network sidebar — Origin / Labels / Similar / Relationships /
// Discovery. The first three lists come from artist state already; this
// global covers the MusicBrainz-driven sections plus the open/closed
// flag the network button toggles.
// One row of the Origin section. Empty strings mark absent fields so the
// view can hide individual rows without an Option wrapper.
export struct MbOriginData {
// True when the artist is a person (vs group/orchestra/etc). Drives
// the row labels: "Born / Born in / Died" vs "Founded / Founded in /
// Disbanded".
is-person: bool,
// Begin date — "1976" / "May 1976" / "May 14 1976". Empty when MB
// has no life_span.begin.
begin-date: string,
// End date — same shape as begin. Empty when life_span.end is
// missing (or the artist is still active).
end-date: string,
// Location display string ("Berlin, Germany" / "United Kingdom").
// Empty when MB has no location.
location-display: string,
// True when the location is precise enough to drill in (city/state
// precision, or precision==country but a city is present somehow).
// When false, the row renders as plain text.
location-clickable: bool,
}
// One member, group, or collaborator row.
export struct MbRelationship {
mbid: string,
name: string,
// Primary role string for the musician-click callback ("Band Member",
// "Guitar", "Producer", ...). Always populated — defaults to "Band
// Member" / "Band" / "Collaborator" by section when MB has no
// attributes for the relation.
role: string,
// Tooltip — roles joined with ", " plus the period in parens when
// present. Falls back to the name.
tooltip: string,
}
// All MB relationship rows for the sidebar. Each list is independently
// hideable.
export struct MbRelationshipsData {
members: [MbRelationship],
groups: [MbRelationship],
collaborators: [MbRelationship],
// Convenience flag — true when any of the three lists has at least
// one row, so the section header can hide on no-data.
has-data: bool,
}
// One row of the "You may also like" discovery section.
export struct DiscoveryArtist {
// MB id — used by the dismiss callback to remember the rejection.
mbid: string,
name: string,
// Qobuz artist id (string form) when the MB candidate matched a
// Qobuz artist; empty when only available on MB. Drives whether the
// row is clickable.
qobuz-id: string,
}
export global NetworkSidebarState {
// Open/closed. The header network button toggles this; persistence
// is handled by the Rust side (`network_sidebar_prefs::set_open`).
// Defaults to open — Rust overrides on startup from the persisted
// value if any.
in-out property <bool> open: true;
// True until proven otherwise. Set to false when MB is disabled in
// settings, or when the name → mbid resolution fails. Drives whether
// the MB-sourced sections render at all (Origin, Relationships,
// Discovery).
in property <bool> mb-available: true;
// Resolved MB id for the current artist; empty before resolve
// completes or when the artist could not be matched. Used by the
// Rust side as the key for relationship/discovery loads.
in property <string> mb-mbid;
// Origin (MB metadata) section
in property <bool> origin-loading: false;
in property <MbOriginData> origin;
// Relationships (MB) section
in property <bool> relationships-loading: false;
in property <MbRelationshipsData> relationships;
// Discovery (MB tag-based) section — populated in phase 8.
in property <bool> discovery-loading: false;
in property <[DiscoveryArtist]> discovery-artists: [];
// Primary tag the current discovery batch was seeded with —
// delivered as the first argument of the discovery-dismissed
// callback so the Rust side can persist the dismiss under the
// right key.
in property <string> discovery-tag;
}
// Callbacks the sidebar emits. Each delivers the minimum payload the
// Rust controller needs; the controller reconstructs richer payloads
// (location precision, affinity seeds) from cached metadata when it
// has to call out to a target view.
export global NetworkSidebarActions {
// Origin location row clicked — opens ArtistsByLocation view.
callback location-clicked(string /* current artist mbid */);
// Label row clicked — opens LabelReleases view.
callback label-clicked(string /* label id */, string /* label name */);
// Similar / discovery / similar-row clicked — opens the Qobuz
// artist page for `qobuz-id`.
callback artist-clicked(string /* qobuz id */);
// Relationship row clicked — opens MusicianPage with the role.
callback musician-clicked(string /* name */, string /* role */);
// Discovery dismiss — removes the row and remembers the rejection.
// Carries both the MBID (model key for removal) and the name
// (persisted into the per-tag dismiss store after normalization).
callback discovery-dismissed(string /* mbid */, string /* name */);
// Sidebar toggled — Rust persists the new open state.
callback toggle();
}
// One row of MusicianPageView's "Appears On" grid — same shape as
// the album cards on Discover, plus a role label stamped onto each
// album by the Qobuz search wrapping (e.g. "Drums" / "Guitar").
export struct MusicianAppearanceItem {
album-id: string,
album-title: string,
artist-name: string,
year: string,
role-on-album: string,
artwork-url: string,
artwork: image,
}
// MusicianPageView state — opened by the artist network sidebar
// when a relationship row is clicked and the musician has no
// confident Qobuz artist match. Populated by `crate::musician`
// from the resolve + appearances backend calls.
export global MusicianState {
in property <string> name;
in property <string> role;
// "confirmed" | "contextual" | "weak" | "none" — the sidebar
// uses this to skip MusicianPageView entirely on Confirmed and
// navigate to the matched artist instead.
in property <string> confidence;
// Bands / projects from MusicBrainz; not yet populated but
// kept in the data shape so the section renders cleanly when
// wired.
in property <[string]> bands: [];
in property <[MusicianAppearanceItem]> appearances: [];
in property <int> total: 0;
in property <bool> loading: false;
in property <bool> load-more-loading: false;
}
export global MusicianActions {
callback load-more();
}
// LabelReleasesView state — opened by the artist network sidebar's
// Labels section. Header (name + image) from /label/page, the
// album grid from /label/getAlbums. Populated by `crate::label`.
export global LabelState {
in property <string> id;
in property <string> name;
in property <string> image-url;
in property <image> image;
// Releases sub-view (LabelReleasesView) — the paginated album grid.
// `albums` is the full loaded catalog + the artwork-pipeline target
// (stable indices); `visible` is the sorted/filtered/searched subset
// the view actually renders (shares `albums` when no filter is active,
// so artwork stays live — mirrors DiscoverBrowseState).
in property <[AlbumCardItem]> albums: [];
in property <[AlbumCardItem]> visible: [];
in property <int> total: 0;
// Whether the catalog has more pages to fetch (drives Load more). The
// /label/getAlbums endpoint caps each page well below the full catalog,
// so this is the reliable signal — `total` is often unset/underset.
in property <bool> has-more: false;
in property <bool> loading: false;
in property <bool> load-more-loading: false;
// "grid" | "list" — the view toggle (mirrors DiscoverBrowseView).
// UI-owned, like Discover's; no Rust round-trip needed.
in-out property <string> view-mode: "grid";
// --- Landing page (LabelPageView) -----------------------------------
// Header description (HTML-stripped) + truncated form + the flag that
// gates the READ MORE affordance (same pattern as ArtistState.bio).
in property <string> description;
in property <string> description-short;
in property <bool> description-truncated: false;
// Header follow state — true when the label is in the user's favorites.
in property <bool> is-following: false;
// Popular tracks (album-thumbnail rows, multi-select capable).
in property <[TrackItem]> top-tracks: [];
// Releases + Critics' Picks carousels (reuse Carousel).
in property <DiscoverSection> releases-section;
in property <DiscoverSection> critics-section;
// Playlists / Artists / More-labels carousels. More-labels reuses
// ArtistCarousel (SlimItem already carries `following`).
in property <[SearchPlaylistItem]> playlists: [];
in property <[SlimItem]> artists: [];
in property <[SlimItem]> more-labels: [];
// Sticky JUMP TO tabs — only the sections that have content.
in property <[JumpNavTab]> jump-tabs: [];
// True once the landing page-data load resolves (gates the spinner).
in property <bool> page-loaded: false;
// --- Releases sub-view toolbar (UI-owned, like Discover) ------------
// "newest" | "oldest" | "title-asc" | "title-desc" | "artist-asc".
in-out property <string> sort-by: "newest";
in-out property <bool> filter-hires: false;
in-out property <bool> group-by-artist: false;
// Releases sub-view in-view search query (drives the count line).
in-out property <string> search-query: "";
// Group-by-artist rendering — one section per artist (reuses the
// DiscoverSection + grid pattern). Empty unless group-by-artist is on.
in property <[DiscoverSection]> grouped: [];
// Count of albums actually shown after filter/search (for the count line).
in property <int> shown: 0;
// Count of Hi-Res albums in the loaded catalog (count line).
in property <int> hires-count: 0;
}
export global LabelActions {
// Releases sub-view: fetch the next album page.
callback load-more();
// Landing: toggle follow on the header label or a more-labels card.
callback toggle-follow(string /* label id */);
// Landing: play all popular tracks.
callback play-top();
// Landing: top-tracks header overflow menu (play-next / queue / shuffle).
callback top-tracks-menu();
// Landing: enter/leave multi-select for popular tracks (select-all).
callback select-all-top-tracks();
// Landing: open the full releases sub-view ("See all").
callback open-releases();
// Releases sub-view toolbar.
callback set-sort(string /* option */);
callback set-hires(bool);
callback set-group(bool);
callback search(string /* query */);
}
// One artist in the scene-discovery grid (artists from the same
// place as the source artist).
export struct LocationArtistItem {
qobuz-id: string,
name: string,
genres-line: string,
image-url: string,
image: image,
}
// ArtistsByLocationView state — opened from the Origin section's
// location link. Populated by `crate::location_view`.
export global LocationViewState {
in property <string> scene-label;
in property <string> genre-summary;
in property <[LocationArtistItem]> artists: [];
in property <int> total: 0;
in property <bool> loading: false;
in property <bool> load-more-loading: false;
}
export global LocationViewActions {
callback open-artist(string /* qobuz id */);
callback load-more();
}
// Shell chrome state — collapsible left sidebar and right-side panels.
// Globals are imported directly by the shell components, so toggles
// need no callback plumbing.
export global ShellState {
// Three-state sidebar: 0 = open (240px, labels), 1 = mini (64px, icons
// only, Tauri-style), 2 = closed (0px). The collapse toggle cycles
// open -> mini -> closed -> open.
in-out property <int> sidebar-state: 0;
out property <length> sidebar-rendered-width:
root.sidebar-state == 2 ? 0px
: root.sidebar-state == 1 ? 64px
: 240px;
// True while in the icons-only mini state — gates label/header rendering.
out property <bool> sidebar-mini: root.sidebar-state == 1;
// Advance the sidebar through its three states.
public function cycle-sidebar() {
root.sidebar-state = Math.mod(root.sidebar-state + 1, 3);
}
// Right-side panels — independent now: Queue and Lyrics can BOTH be open
// at once, stacked vertically in a shared right column (Feishin-style). Each
// is toggled from its bar button and closed from its own X. The column is
// visible when either is open. (Replaces the old exclusive `right-panel` int
// where lyrics closed queue and vice-versa.)
in-out property <bool> queue-open: false;
in-out property <bool> lyrics-open: false;
// Section navigation placement: the sections (Discover/Library/Local
// Library/My QBZ) now live in the sidebar and open right-flyout menus.
in-out property <bool> nav-in-sidebar: true;
// Now-playing bar layout mode: 0 = New (default), 1 = Classic. Persisted
// in ui_prefs.json (`npb_mode` "new"|"classic"); set on startup from the
// store and on the player bar's "Now Playing view" menu select.
in-out property <int> npb-mode: 0;
}
// Authenticated user, for the header user element.
export global SessionState {
in property <string> user-name: "";
in property <string> subscription: "";
}
// One entry in the playback queue, for the Queue sidebar list.
export struct QueueItem {
id: string,
title: string,
artist: string,
artwork: image,
playing: bool,
duration: string, // "M:SS"
explicit: bool,
// From an ephemeral folder — gates persistence actions in the row menu.
is-ephemeral: bool,
}
// Now-playing / transport state. The Rust playback poll loop pushes the
// live values here; the PlayerBar and SeekBar bind to it directly.
export global NowPlayingState {
in property <bool> has-track: false;
in property <string> title;
in property <string> artist;
in property <string> album;
in property <string> album-id;
in property <string> artist-id;
in property <image> artwork;
in property <string> track-id;
// True when the playing track is from an ephemeral folder (synthetic id):
// it has no DB row, so favorite / add-to-playlist / track-info are gated off.
in property <bool> is-ephemeral: false;
in property <bool> playing: false;
// Fetch/resolve in flight for the track being started — no audio yet.
// Set the instant a play is requested (before the Plex/Qobuz/local
// resolve) and cleared once the poll loop sees the audio advancing.
// Drives the fetch spinner on the bar, the active track row, and the
// album play button.
in property <bool> loading: false;
in property <int> position-secs: 0;
in property <int> duration-secs: 0;
in property <float> progress: 0.0; // 0..1
in property <float> cache: 0.0; // 0..1 buffered
// Furthest seekable fraction. 1.0 for fully-available (local/cached)
// tracks; equal to the streamed buffer while a track is still downloading,
// so the user can't seek into not-yet-downloaded territory.
in property <float> seekable-max: 1.0;
in property <string> elapsed: "0:00";
in property <string> remaining: "0:00";
in property <float> volume: 0.7; // 0..1
in property <bool> muted: false;
in property <bool> shuffle: false;
in property <int> repeat-mode: 0; // 0 off, 1 all, 2 one
// Quality of the playing track, for the song-card badge.
in property <string> quality-tier; // "hires" | "cd" | "mp3" | ""
in property <string> quality-detail; // "24-bit / 96 kHz"
// Cast / render-aware state (Qobuz Connect). `is-remote` = a peer
// renderer owns playback (controls route to it); `volume-locked` =
// the active renderer disallows remote volume control. Both default
// off and are wired by the QConnect epic; the bar respects them now.
in property <bool> is-remote: false;
in property <bool> volume-locked: false;
// Whether THIS device currently has a Qobuz Connect session running (drives
// the connect flyout's Connect/Disconnect button). Distinct from `is-remote`,
// which means a PEER renderer owns playback. Set by the connect callback.
in property <bool> qconnect-connected: false;
// Name of the active Qobuz Connect renderer (shown under the cast button
// when remote). Empty when local. Populated by the QConnect epic.
in property <string> cast-target;
// Cast (Chromecast / DLNA) — when casting, the two output leds fuse into a
// single purple CAST / DLNA led (mirroring Tauri). Populated by the Cast epic.
in property <bool> cast-active: false;
in property <string> cast-protocol; // "cast" | "dlna"
// Playback CONTEXT — the source the queue was launched from (album /
// playlist / artist / label / mix). Drives the song-card layers button
// (navigate back to that page). Set at play time, persists across track
// changes; falls back to the track's album when no richer source is known.
in property <string> context-kind; // "album" | "playlist" | "artist" | "label" | "mix"
in property <string> context-id;
// Transport callbacks, wired to the Rust playback controller.
callback toggle-play();
callback next();
callback previous();
callback seek(float); // 0..1
callback set-volume(float); // 0..1
callback toggle-mute();
callback toggle-shuffle();
callback cycle-repeat();
// Qobuz Connect — toggle the session on/off from the bar's connect flyout.
// Wired in main.rs to SlintQconnectService::connect/disconnect.
callback qconnect-toggle();
}
// One row in the QConnect device picker (a session renderer). Populated by the
// event sink from `session.renderers`.
export struct QconnectDevice {
renderer-id: int,
name: string,
is-local: bool, // this is QBZ itself ("Play here")
is-active: bool, // currently the active renderer
// Device-type icon key (mirrors Qobuz Connect): "computer" | "web" | "mobile" | "speaker".
icon: string,
}
// Qobuz Connect DEV diagnostics — live session topology + a rolling event log,
// rendered by QconnectDevModal (opened from the connect flyout). Lets us inspect
// QC behavior at runtime WITHOUT a rebuild (Slint builds are slow). `open` is
// in-out so the flyout button + the modal backdrop toggle it directly.
//
// Also hosts the device-picker model (CONTROLLER mode): the available session
// renderers + the active renderer id, plus the set-active callback the flyout
// rows invoke to switch playback to another renderer (or back to "Play here").
export global QconnectDevState {
in-out property <bool> open: false;
in property <string> status;
// Joined log text (newest first, one entry per line). A single string so the
// modal can render it in a read-only TextEdit (selectable + copyable).
in property <string> log-text;
callback clear();
// Device picker.
in property <[QconnectDevice]> devices: [];
in property <int> active-renderer-id: -1;
callback set-active(int); // renderer-id; the local id = "Play here"
}
// Single global hover-tooltip channel. Each hoverable control writes its text
// + absolute anchor (window coords: centre-x and top-y of the control) here on
// hover; one TooltipOverlay (mounted last, on top of everything) renders the
// bubble, clamped to the window so it never clips or hides behind a neighbour.
export global TooltipState {
in-out property <string> text;
in-out property <length> anchor-x; // control centre, window coords
in-out property <length> anchor-y; // control top, window coords
in-out property <bool> show: false;
}
// Playback queue model, for the Queue sidebar.
//
// The sidebar shows two tabs. The Queue tab renders `now-playing` plus a
// paginated slice of `upcoming` (40 per page). The History tab renders
// `history` (most-recent first). The Rust queue controller owns the full
// upcoming/history lists and the pagination maths; it pushes the current
// page's slice into `upcoming-page` and the counters that label it.
export global QueueState {
// --- Queue tab -------------------------------------------------------
// Current track card. `has-current` gates the NOW PLAYING section.
in property <bool> has-current: false;
in property <QueueItem> now-playing;
// Whether the now-playing track is in the user's favorites.
in property <bool> now-playing-favorite: false;
// The slice of upcoming tracks for the current page (<= 40 entries).
in property <[QueueItem]> upcoming-page: [];
// Total upcoming-track count (post-filter when a search is active).
in property <int> upcoming-total: 0;
// Tracks remaining after the current one (total - played - 1).
in property <int> upcoming-remaining: 0;
// Zero-based page index and total page count for the paginator.
in-out property <int> page: 0;
in property <int> page-count: 1;
// Index of the first upcoming entry shown, 1-based, for the counter.
in property <int> page-start: 0;
// Index of the last upcoming entry shown, 1-based, for the counter.
in property <int> page-end: 0;
// --- History tab -----------------------------------------------------
in property <[QueueItem]> history: [];
// --- Shared ----------------------------------------------------------
// Active tab: 0 = Queue, 1 = History.
in-out property <int> tab: 0;
// Live search query; the Rust controller re-filters `upcoming-page`.
in-out property <string> search-query: "";
// --- Callbacks (wired to the Rust queue controller) ------------------
// Play the upcoming entry at `index` within the current page slice.
callback play-upcoming(int);
// Play a history entry by its `index` in the `history` list.
callback play-history(int);
// Remove the upcoming entry at `index` within the current page slice.
callback remove-upcoming(int);
// Reorder the upcoming list: move the page-local row `from` to the insertion
// slot `to` (0 = before the first upcoming row, N = after the last). The Rust
// controller resolves the page-local indices to queue-wide upcoming indices
// and routes to the cloud when connected (WS-authoritative).
callback reorder(int /* from */, int /* to */);
// Empty the queue.
callback clear-queue();
// Toggle the favorite state of the now-playing track.
callback toggle-now-playing-favorite();
// Save the current queue as a new playlist.
callback save-as-playlist();
// Toggle infinite-play (auto-refill the queue with similar tracks).
callback toggle-infinite-play();
// Re-filter the upcoming list after `search-query` changed.
callback search-changed();
// Move to the previous / next paginator page.
callback prev-page();
callback next-page();
// Switch the active tab (0 = Queue, 1 = History).
callback set-tab(int);
// Fired when the sidebar becomes visible, so the Rust controller can
// pull a fresh queue snapshot (the queue may have changed while the
// panel was closed, e.g. after a session restore).
callback panel-opened();
// Reflects whether infinite-play is currently enabled.
in property <bool> infinite-play: false;
}
// One row in the "Add to playlist" picker.
export struct PlaylistPickItem {
id: string,
name: string,
tracks-line: string, // "N tracks"
}
// "Add to playlist" picker — a global overlay opened from the track
// context menu. Holds the pending track id while the user picks a
// destination playlist. Populated by crate::playlist_picker.
export global PlaylistPickerState {
in-out property <bool> open: false;
in property <string> track-id;
in property <[string]> track-ids: []; // bulk add (favorites multi-select)
in property <[PlaylistPickItem]> playlists: [];
in property <bool> loading: false;
// True when track-ids are LocalLibrary row ids (i64) -> route to
// add_local_track_to_playlist instead of the Qobuz endpoint.
in property <bool> local-mode: false;
}
export global PlaylistPickerActions {
callback pick(string /* playlist id */);
callback close();
}
// One artist tile in the Favorites > Artists grid.
export struct FavoriteArtistItem {
id: string,
name: string,
image-url: string,
image: image,
}
// An A-Z section of favorite artists (grid grouped mode + sidepanel left
// list). `key` is the bucket letter ("#", "A".."Z"); `title` the header.
export struct FavArtistSection {
key: string,
title: string,
artists: [FavoriteArtistItem],
}
// One label card in the Favorites > Labels grid.
export struct FavoriteLabelItem {
id: string,
name: string,
albums-line: string, // "N albums"
image-url: string, // resolved URL ("" -> gradient placeholder)
image: image, // decoded cover, filled by the artwork pipeline
}
// Library > Favorites state. The active tab decides which model is
// shown; all are populated lazily by `crate::favorites` the first
// time their tab is opened. Albums reuse AlbumCardItem, tracks reuse
// TrackItem (artwork-on), artists use FavoriteArtistItem.
export global FavoritesState {
// "tracks" | "albums" | "artists" | "playlists" | "labels"
in-out property <string> active-tab: "tracks";
// Tracks reuse TrackItem so the shared SearchTrackRow can
// render them (artwork-on, per-row cover thumbnail). `tracks` is the
// full fetched set (the artwork pipeline targets it); `tracks-visible`
// is what the list renders after the search filter (mirrors
// LabelState.albums vs .visible).
in property <[TrackItem]> tracks: [];
in property <[TrackItem]> tracks-visible: [];
in-out property <string> tracks-search: "";
in-out property <string> tracks-group-mode: "off"; // off|album|artist|name
in property <[AlphaJump]> tracks-alpha: []; // A-Z jumps (name grouping)
in-out property <bool> tracks-multi-select: false;
in property <int> tracks-selected-count: 0;
in property <[AlbumCardItem]> albums: [];
in property <[AlbumCardItem]> albums-visible: []; // after search + sort (flat render)
in property <[DiscoverSection]> albums-grouped: []; // group-by sections
in-out property <string> albums-view-mode: "grid"; // "grid" | "list"
in-out property <string> albums-search: "";
in-out property <string> albums-sort-by: "default"; // default|title-asc|title-desc|artist-asc
in-out property <string> albums-group-mode: "off"; // off|alpha|artist
in property <[AlphaJump]> albums-alpha: []; // A-Z jumps (alpha grouping)
in property <int> albums-shown: 0; // count after filter (grouped or flat)
in property <[FavoriteArtistItem]> artists: [];
// Playlists has two internal sub-tabs (mirror Tauri favoritePlaylists /
// followingPlaylists), both rendered with the rich SearchPlaylistItem
// (4-cover collage). `playlists-visible` is the current sub-tab after the
// search filter (what the grid/list renders).
in property <[SearchPlaylistItem]> playlists-favorites: []; // Library (locally favorited)
in property <[SearchPlaylistItem]> playlists-following: []; // Qobuz-followed, not owned
in property <[SearchPlaylistItem]> playlists-visible: [];
in-out property <string> playlists-sub-tab: "favorites"; // "favorites" | "following" (transient)
in-out property <string> playlists-search: "";
in-out property <string> playlists-view-mode: "grid"; // "grid" | "list" (persisted)
in property <int> playlists-following-count: 0; // Following sub-tab badge
in property <[FavoriteLabelItem]> labels: [];
in property <[FavoriteLabelItem]> labels-visible: []; // after search filter (rendered)
in-out property <string> labels-search: "";
// Artists — toolbar / grid view
in-out property <string> artists-search: "";
in-out property <string> artists-view-mode: "grid"; // grid | sidepanel
in-out property <bool> artists-group-enabled: false; // A-Z grouping (grid)
in property <[FavoriteArtistItem]> artists-visible: []; // after search (flat grid)
in property <[FavArtistSection]> artists-grouped: []; // A-Z sections (grouped grid + sidepanel list)
in property <[AlphaJump]> artists-alpha: []; // A-Z jump strip
in property <int> artists-shown: 0; // count after search
// Artists — sidepanel (the selected artist's albums)
in-out property <string> selected-artist-id: ""; // "" = none selected
in property <string> selected-artist-name: "";
in property <bool> selected-albums-loading: false;
in property <string> selected-albums-error: "";
in property <int> selected-albums-total: 0; // sum across sections
in property <[DiscoverSection]> selected-artist-sections: []; // release-type sections
in property <int> tracks-total: 0;
in property <int> albums-total: 0;
in property <int> artists-total: 0;
in property <int> playlists-total: 0;
in property <int> labels-total: 0;
in property <bool> loading: false;
in property <string> load-error: ""; // non-empty -> show error + retry
}
export global FavoritesActions {
// Switch tab; the Rust side lazy-loads the tab's data if it
// hasn't been fetched yet.
callback select-tab(string);
callback open-album(string);
callback open-artist(string);
callback open-label(string /* label id */, string /* name */);
callback open-playlist(string /* playlist id */);
callback play-track(string /* track id */);
callback track-action(string /* track id */, string /* action */);
callback search-tracks(string /* query */);
callback tracks-set-group(string /* off | album | artist | name */);
callback play-all-tracks();
callback shuffle-tracks();
callback toggle-multi-select();
callback bulk-action(string /* select-all | remove-selected | clear */);
callback album-action(string /* album id */, string /* action */);
callback albums-search(string /* query */);
callback search-labels(string /* query */);
callback playlists-set-sub-tab(string /* favorites | following */);
callback playlists-random();
callback labels-random();
callback search-playlists(string /* query */);
callback playlists-set-view(string /* grid | list */);
callback playlist-action(string /* playlist id */, string /* play | play-next | queue | share | favorite */);
callback search-artists(string /* query */);
callback artists-set-view(string /* grid | sidepanel */);
callback artists-set-group(string /* off | alpha */);
callback artists-shuffle();
callback select-artist(string /* artist id */, string /* name */);
callback albums-set-view(string /* grid | list */);
callback albums-set-sort(string /* sort key */);
callback albums-set-group(string /* off | alpha | artist */);
callback albums-shuffle();
callback unfavorite-track(string /* track id */);
callback unfavorite-album(string /* album id */);
callback retry-load();
}
// A Qobuz mix detail page (DailyQ / WeeklyQ / FavQ / TopQ), opened
// from the For You Qobuz Mixes tiles. Populated by crate::mix. Rows
// reuse the shared SearchTrackRow (with show-album), so the track
// context menu works here too.
export global MixState {
in property <string> kind; // daily | weekly | fav | top
in property <string> title;
in property <string> subtitle;
in property <[TrackItem]> tracks: [];
in property <int> track-count: 0;
in property <string> total-duration;
in property <bool> loading: false;
}
// Search results screen state. The Rust search controller populates the
// four category lists, their totals, and the most-popular hero entry.
export global SearchState {
in property <string> query;
// Active tab: 0 = All, 1 = Albums, 2 = Tracks, 3 = Artists, 4 = Playlists.
in property <int> tab: 0;
in property <bool> loading: false;
in property <[AlbumCardItem]> albums: [];
in property <[TrackItem]> tracks: [];
in property <[SlimItem]> artists: [];
// Carousel-only variant of artists — skips the top-result artist when
// it is also the first list entry, so the All-tab carousel does not
// duplicate the Most-popular hero. The Artists tab uses `artists`.
in property <[SlimItem]> artists-carousel: [];
in property <[SearchPlaylistItem]> playlists: [];
in property <int> albums-total: 0;
in property <int> tracks-total: 0;
in property <int> artists-total: 0;
in property <int> playlists-total: 0;
// Most-popular hero: `kind` selects which of the three items is shown.
in property <string> most-popular-kind; // "album" | "artist" | "track" | ""
in property <AlbumCardItem> most-popular-album;
in property <SlimItem> most-popular-artist;
in property <TrackItem> most-popular-track;
// Detailed quality label for the most-popular TRACK hero, e.g.
// "Hi-Res 24-bit / 192 kHz". Shown as text (the hero dropped the icon
// badge). "" when unknown / not a track.
in property <string> most-popular-quality-label;
// Active searchType filter, as a dropdown index (0 = no filter).
// The controller owns the index -> API-value mapping.
in property <int> filter-index: 0;
}
// Search actions wired to the Rust search controller.
export global SearchActions {
// Enter / explicit submit — navigate to the full results page.
callback submit(string);
// Live (debounced) search as the user types.
callback live(string);
// User picked a results tab (0..4).
callback tab-changed(int);
// searchType filter changed — the new dropdown index.
callback filter-changed(int);
// Request the next page for the given tab.
callback load-more(int);
}