Replaces the thin name+counts grid (which was built without a Tauri review) with the real structure: a two-column master/detail, 1:1 with the Svelte Artists tab. Left rail: a merged/deduped, A-Z grouped master list of compact rows (round avatar + display name + "N albums · M tracks") with a proportional alpha jump strip and in-place search. The name-merge collapses normalized-equal spellings into one canonical row (canonical = most albums, tie by most tracks) and computes accurate unique album counts from the album set's all_artists, mirroring Tauri's artistMergeResult. Diacritics are folded best-effort (no unicode-normalization dep). Right pane: the selected artist's albums, filtered IN PLACE from the loaded album set by primary/all_artists/multi-credit match (no new backend call) and rendered via the shared AlbumCollectionView; covers load through a new LocalArtistAlbumCard artwork target. Clicking an album opens the local album detail; the play action queues the album. Custom DB artist images are resolved into the rail model; rendering them (plus the capped/sequential Qobuz photo fetch) is the remaining image follow-up — the rail shows the placeholder avatar until then. cc
1877 lines
82 KiB
Plaintext
1877 lines
82 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);
|
|
}
|
|
|
|
// 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,
|
|
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;
|
|
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;
|
|
}
|
|
|
|
// 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 }
|
|
|
|
// 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 for a custom/cached photo ("" = none)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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" — the list view toggle lands with the next slice.
|
|
in-out property <string> albums-view-mode: "grid";
|
|
|
|
// --- 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: "";
|
|
|
|
// --- Folders tab ---------------------------------------------------
|
|
// flat = album grid grouped by directory; tree = two-pane lazy filesystem.
|
|
in-out property <string> folders-view-mode: "flat"; // "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).
|
|
in property <[FolderNode]> folder-tree: [];
|
|
in property <bool> folder-tree-loading: false;
|
|
in-out property <string> folders-selected-path: "";
|
|
in property <string> folders-selected-name: "";
|
|
in property <[FolderNode]> folder-detail-subfolders: [];
|
|
in property <[TrackItem]> folder-detail-tracks: [];
|
|
in property <bool> folder-detail-loading: false;
|
|
|
|
// --- 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;
|
|
}
|
|
|
|
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-query page 1 (server-side ORDER BY).
|
|
callback albums-set-sort(string /* sort key */);
|
|
// Fetch + append the next page (driven by scroll-near-bottom).
|
|
callback albums-load-more();
|
|
// 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 — local playback lands with the source-aware slice.
|
|
callback track-action(string /* track id */, 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 */);
|
|
|
|
// --- 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 */);
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
|
|
// 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 */);
|
|
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();
|
|
}
|
|
|
|
// 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;
|
|
|
|
// 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 -------------------------------------------------------------
|
|
in-out property <[string]> themes: [
|
|
"Dark", "OLED Black", "Light", "System",
|
|
"Warm", "Nord", "Dracula", "Tokyo Night", "Catppuccin Mocha",
|
|
"Breeze Dark", "Adwaita Dark", "Alucard", "Aurora", "Ikari",
|
|
"Ayanami", "Iscariot", "Stratego", "Rumi", "Zoey", "Mira",
|
|
"Rose Pine Dawn", "Breeze Light", "Adwaita Light", "Duotone Snow",
|
|
"Snow Storm", "Frost", "Langley", "Kurosaki",
|
|
"WCAG Light", "WCAG Dark", "High Contrast", "Colorblind"
|
|
];
|
|
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;
|
|
|
|
// --- 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 -------------------------------------------------------
|
|
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;
|
|
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;
|
|
// 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();
|
|
}
|
|
|
|
// 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 {
|
|
in-out property <bool> sidebar-collapsed: false;
|
|
in-out property <length> sidebar-width: 280px;
|
|
// 0 = none, 1 = queue, 2 = lyrics
|
|
in-out property <int> right-panel: 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,
|
|
}
|
|
|
|
// 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;
|
|
in property <bool> playing: 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
|
|
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
|
|
|
|
// 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();
|
|
}
|
|
|
|
// 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);
|
|
// 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;
|
|
}
|
|
|
|
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 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);
|
|
}
|