// 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 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 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 title; // Rust-managed: the /discover/ endpoint + the next page offset, so // DiscoverBrowseActions.load-more knows what to fetch next. in-out property endpoint; in-out property 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 loading; in-out property loading-more; in-out property 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 search-query; // "grid" | "list" — the view toggle. in-out property 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 release-watch; in property <[RadioStationItem]> radio-stations: []; in property recent-albums; in property <[SlimItem]> recent-tracks: []; in property <[SlimItem]> top-artists: []; in property <[SlimItem]> artists-to-follow: []; in property more-from-library; in property rediscover; // Spotlight — one favorite artist highlighted with a hero + a row // of their content (top tracks card, radio card, albums). in property spotlight-visible: false; in property spotlight-artist-id; in property spotlight-name; in property spotlight-category; in property spotlight-image-url; in property spotlight-image; in property spotlight-has-top-tracks: false; in property <[AlbumCardItem]> spotlight-albums: []; in property loading: false; // True once a load has completed, so the tab does not refetch on // every visit. in-out property 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 selected-count: 0; in-out property open: false; in-out property remember: true; in-out property advanced: false; in-out property search-query; in property 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 id; in property title; in property artist; in property artist-id; // Network URL of the cover — used by the artwork right-click // menu for Open-in-browser and Save-as actions. in property artwork-url; in property has-custom-cover: false; in property info-line; in property quality-tier; // "24-bit / 96 kHz" — quality-badge detail line. in property quality-detail; // Editorial description / review (HTML stripped). May be empty. in property description; // Truncated description for the header; full text lives in the modal. in property description-short; in property artwork; // Artwork-derived tint for the header gradient. in property header-color: #181820; in property label; // Label id for the sidebar label card → label page navigation ("" = none). in property label-id; in property <[string]> awards: []; in property <[TrackItem]> tracks: []; in property 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 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 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 active: false; // Dragged-track count + ghost text (count == 1 shows title + // "artist · album"; count > 1 shows "N tracks"). in-out property count: 0; in-out property ghost-title; in-out property ghost-subtitle; // Global pointer position (window coords) while dragging. in-out property pointer-x; in-out property pointer-y; // The sidebar playlist id currently under the pointer (drop target). in-out property 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 } export global LocalLibraryState { // "albums" | "artists" | "folders" | "tracks" in-out property active-tab: "albums"; in property loading: false; in property load-error: ""; // Header stats / per-tab badge counts. in property album-count: 0; in property artist-count: 0; in property track-count: 0; in property 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 albums-total: 0; in property albums-loading: false; // initial page fetch in property albums-loading-more: false; // subsequent pages in property albums-has-more: false; in property albums-load-failed: false; in-out property albums-next-offset: 0; in-out property albums-search: ""; // "artist-asc" | "artist-desc" | "title-asc" | "title-desc" // | "year-desc" | "year-asc" in-out property albums-sort: "artist-asc"; // "grid" | "list" view toggle. in-out property 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 albums-group: "off"; // off | alpha | artist in property <[AlphaJump]> albums-alpha: []; // A-Z jumps (alpha group) in property 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 tracks-total: 0; in property tracks-loading: false; in property tracks-loading-more: false; in property tracks-has-more: false; in property tracks-load-failed: false; in-out property tracks-next-offset: 0; in-out property 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 tracks-group-mode: "off"; // off | album | artist | name in property <[AlphaJump]> tracks-alpha: []; // A-Z jumps (name grouping) in-out property tracks-multi-select: false; // checkbox column on in property tracks-selected-count: 0; // bar count + select-all // --- Folders tab --------------------------------------------------- // flat = album grid grouped by directory; tree = two-pane lazy filesystem. in-out property 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 folders-loading: false; in property folders-load-failed: false; in property <[AlbumCardItem]> folders-visible: []; in property <[DiscoverSection]> folders-grouped: []; in-out property folders-search: ""; in-out property folders-sort: "title-asc"; // title-asc|title-desc|artist-asc|year-desc in-out property folders-group: "off"; // off | alpha | artist in-out property 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 folder-tree-loading: false; in-out property folders-selected-path: ""; in property folders-selected-name: ""; in property <[FolderNode]> folder-detail-subfolders: []; in property <[TrackItem]> folder-detail-tracks: []; in property 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 artists-loading: false; in property artists-load-failed: false; in-out property artists-search: ""; in property <[LocalArtistSection]> artists-grouped: []; in property <[AlphaJump]> artists-alpha: []; in property 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 artists-selected-name: ""; // "" = none selected in property artists-selected-display: ""; in property <[AlbumCardItem]> artists-selected-albums: []; in property artists-selected-loading: false; // Artist-image background fetch (capped Qobuz portrait fill). in-out property artists-fetch-images: true; // default ON (mirrors Tauri) in property artists-images-fetching: false; // a fetch session is running in property 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 */); // --- 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 open: false; // Quality. in-out property hires: false; in-out property cd: false; in-out property lossy: false; // Format. in-out property flac: false; in-out property alac: false; in-out property ape: false; in-out property wav: false; in-out property mp3: false; in-out property aac: false; in-out property other: false; // Source. in-out property local: false; in-out property offline: false; in-out property plex: false; // Active-filter count (Rust-computed) for the toolbar badge. in property 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 id; // metadata group key in property title; in property artist; in property info-line; // "12 tracks · 48 min" in property quality-tier; // hires | cd | "" in property quality-detail; // "24-bit / 96 kHz" in property cover-url; in property cover; in property 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 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` 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 loading: false; in-out property filter: ""; // folder search (alias??path substring) in property selected-count: 0; // drives Edit(==1) / Remove(>0) enablement in property cleaning-missing: false; in property cleanup-status: ""; // "Removed N of M" / "" (auto-clears, Rust-side) in property clearing-library: false; } // Folder-settings modal state (separate from the playlist FolderEditState). export global LibFolderEditState { in-out property open: false; in property folder-id: 0; in-out property alias: ""; in property path: ""; // current path (updated after change-path) in-out property enabled: true; in-out property is-network: false; in-out property 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 fs-type-index: 0; in property accessible: true; in property checking-accessible: false; in property 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 scanning: false; // status == Scanning in property scan-status: 0; // 0 Idle,1 Scanning,2 Complete,3 Cancelled,4 Error in property total-files: 0; in property processed-files: 0; in property progress: 0.0; // 0..1 in property current-file: ""; // basename, or the cleanup label in property 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 */); } // ============================ 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). 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 open: false; in property album_group_key; // == LocalAlbum.id == directory_path in-out property album_title; in-out property album_artist; in-out property year_input; // raw text; controller parses in-out property genre; in-out property catalog_number; in-out property album_total_discs; // collected, NOT persisted (1:1) in property directory_path; // reference + is_dir gate in property can_direct_write; // false if any track has_cue in-out property persistence_index: 0; // 0 = sidecar, 1 = direct in-out property <[TagTrackEdit]> tracks: []; in-out property saving: false; in-out property write_progress_current: 0; in-out property write_progress_total: 0; // Remote lookup (MusicBrainz / Discogs). in-out property remote_provider_index: 0; // 0 musicbrainz, 1 discogs in-out property remote_searching: false; in-out property remote_loading: false; // apply (get full) in flight in-out property <[RemoteResultItem]> remote_results: []; in-out property selected_result_id: ""; // "" = none in-out property show_remote_panel: false; in-out property 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 visible: false; in-out property message; in-out property kind: ToastKind.info; in-out property persistent: false; in-out property 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 open: false; in-out property folder-id; in-out property folder-name; in-out property count: 0; // folder's playlist count (caps the height) in-out property anchor-x; // window x (sidebar right edge) in-out property 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 open: false; in-out property id; // owning row id (race-safe leave) in-out property text; // "Folder (N)" or "Playlist name" in-out property anchor-x; // window x (sidebar right edge) in-out property 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 open: false; in-out property anchor-x; // window x (header button left) in-out property 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 loading: false; // The currently-open playlist id, for the active-row highlight. in property active-id; // PLAYLISTS section collapse (the folder+playlist list hides). Mirrors // Tauri's `playlistsCollapsed`. in-out property 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 search-open: false; in-out property search-query; // Active sort option: "name" | "recent" | "tracks" | "playcount" // | "custom" (mirrors Tauri's SortOption). Persisted by crate::sidebar. in-out property 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 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 open: false; in-out property name; in-out property description; in-out property is-public: false; in property 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 folder-index: 0; } export global CreatePlaylistActions { callback submit(); callback close(); } // "Create folder" modal state. export global CreateFolderState { in-out property open: false; in-out property name; in property 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 id; in property name; in property owner; in property description; // Truncated description for the header (~2 lines); full text in // the Read-more modal. Equal to description when it fits. in property description-short; in property track-count: 0; in property total-duration; in property cover-url; in property cover; // True when a user-set custom artwork is shown (overrides the // track-cover collage). in property has-custom: false; in property is-favorite: false; // True when the playlist is one of the user's own (editable). in property 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 multi-select-mode: false; in property selected-count: 0; in property <[TrackItem]> tracks: []; in property loading: false; // Sort: "" / "default" = playlist order; else title|artist|album| // duration. `sort-asc` flips on re-selecting the active field. in property sort-field: "default"; in property 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 open: false; in property id; in-out property name; in-out property description; in property 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 folder-count: 0; in property playlist-count: 0; in property loading: false; // Toolbar state (session-scoped; persisted per-user where cheap). in-out property search-query; // "all" | "visible" | "hidden". in-out property filter: "all"; // "name" | "recent" | "playcount" | "tracks" | "custom". in-out property sort: "name"; // "grid" | "list" | "tree". in-out property view-mode: "grid"; // Folder mode on (folders shown + folder filtering) vs flat. in-out property folder-mode: true; // Folders section collapse. in-out property folders-collapsed: false; // Reorder is only allowed when sort == "custom". in property 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 loading: false; // Header stats. in property tracks-count: 0; in property size-text: ""; // "1.2 GB" in property limit-text: ""; // "of 5 GB" | "Unlimited" in property usage: 0.0; // 0..1 for the bar (0 = unlimited) in-out property 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 selected-artist: ""; // "" = all artists in property sort-index: 0; // 0 alpha / 1 recent / 2 largest / 3 smallest in property show-only-failed: false; // Multi-select: how many TRACK rows are checked (drives the bulk bar). in property 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 open: false; // Empty id = create mode; non-empty = editing that folder. in property id: ""; in-out property name; in-out property icon-preset: "folder"; // Selected solid color hex; "" = accent. in-out property icon-color: ""; in-out property is-hidden: false; // Custom image path (icon-type == "custom" when set) + decoded image. in-out property custom-image-path: ""; in property custom-image; in property 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 section: 0; in property 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 streaming-quality-index: 0; in-out property <[string]> sample-rates: []; in-out property sample-rate-index: 0; in-out property <[string]> backends: []; in-out property 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 device-index: 0; in-out property <[string]> alsa-plugins: []; in-out property alsa-plugin-index: 0; // Audio — toggles. in-out property limit-quality-to-device: false; in-out property alsa-hardware-volume: false; in-out property exclusive-mode: false; in-out property reserve-dac: false; in-out property dac-passthrough: false; in-out property pw-force-bitperfect: false; in-out property allow-quality-fallback: false; in-out property sync-audio-on-startup: false; in-out property skip-sink-switch: false; // Audio — Rust-computed conditional flags driving `if`-gated rows. in-out property backend-is-alsa: false; in-out property backend-is-pipewire: false; in-out property alsa-plugin-is-hw: false; // --- PLAYBACK ---------------------------------------------------------- // Playback — toggles. `continue-playback` maps to the autoplay mode // (on = ContinueWithinSource, off = PlayTrackOnly). in-out property continue-playback: true; in-out property show-context-icon: true; in-out property persist-session: false; in-out property resume-position: false; in-out property gapless: true; in-out property stream-uncached: false; in-out property 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 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 output-backend-label: "SYST"; // PIPE|ALSA|JACK|PULS|SYST in-out property output-mode-label: "DEFAULT"; // DACPASS|BITPERF|EXCL|DIRECT|LOCKED|ROUTED|SHARED|DEFAULT in-out property output-backend-active: false; in-out property output-mode-active: false; // Playback — Initial Buffer Size slider (seconds, 1-10). in-out property buffer-seconds: 3; // Playback — "When quality retries fail" dropdown. in-out property <[string]> retry-behaviors: []; in-out property 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 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 theme-filter: 0; in-out property 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 theme-is-system: false; in-out property <[string]> auto-theme-sources: ["System Colors", "Wallpaper Sync", "Custom Image"]; in-out property auto-theme-source-index: 0; in-out property auto-theme-custom-path: ""; in-out property auto-theme-generating: false; in-out property auto-theme-detected-de: ""; // --- TYPOGRAPHY / SCALE / LANGUAGE ------------------------------------- in-out property <[string]> languages: ["Auto", "English", "Español", "Français", "Deutsch", "Português"]; in-out property language-index: 0; in-out property <[string]> fonts: ["LINE Seed JP", "Montserrat", "Noto Sans", "Source Sans 3", "System"]; in-out property font-index: 0; in-out property <[string]> ui-scales: ["80%", "85%", "90%", "95%", "100%", "110%", "125%", "150%", "175%"]; in-out property 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 sidebar-playlist-collage: true; in-out property local-library-track-artwork: false; // Player volume +/- stepper buttons — opt-in, hidden by default. in-out property show-volume-steppers: false; // --- NOTIFICATIONS ----------------------------------------------------- in-out property in-app-toasts: true; in-out property system-notifications: true; // --- WINDOW TITLE ------------------------------------------------------ in-out property window-title-show: false; in-out property window-title-template: ""; // --- TITLE BAR / WINDOW CHROME ----------------------------------------- in-out property use-system-title-bar: false; in-out property hide-title-bar: false; in-out property 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 hardware-accel-enabled: true; in-out property 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 nav-tb-discover: false; in-out property nav-tb-favorites: false; in-out property nav-tb-library: false; in-out property nav-tb-myqbz: false; in-out property 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 nav-tb-position-index: 0; // Window controls group. in-out property <[string]> wc-positions: ["Left", "Right"]; in-out property wc-position-index: 1; in-out property <[string]> wc-styles: ["Rectangular", "Full-height rounded", "Circular", "Square"]; in-out property wc-style-index: 0; in-out property <[string]> wc-sizes: ["Small", "Normal", "Large"]; in-out property wc-size-index: 1; in-out property <[string]> wc-color-presets: ["Default", "macOS", "Adwaita", "Monochrome", "Custom"]; in-out property wc-color-preset-index: 0; in-out property show-window-controls: true; // --- MINIPLAYER -------------------------------------------------------- in-out property <[string]> miniplayer-views: ["Remember last used", "Micro", "Compact", "Artwork", "Queue", "Lyrics"]; in-out property miniplayer-view-index: 0; // --- STARTUP ----------------------------------------------------------- in-out property <[string]> startup-pages: ["Home", "Where you left off"]; in-out property startup-page-index: 0; in-out property show-purchases: false; // --- SYSTEM TRAY ------------------------------------------------------- in-out property tray-enable: true; in-out property tray-minimize-to-tray: false; in-out property tray-close-to-tray: false; in-out property <[string]> tray-icon-themes: ["Auto", "Mono light", "Mono dark", "Color"]; in-out property 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 immersive-view-index: 0; in-out property <[string]> immersive-background-modes: ["Full", "Lite", "Off"]; in-out property immersive-background-mode-index: 0; // Collapsible sub-panels (collapsed by default, matching Tauri). in-out property immersive-panels-collapsed: true; in-out property 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 open-index: -1; // Header x-coordinate of the open tab, so the overlay aligns the // menu panel under it. in-out property 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 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 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 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 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 home-item: -1; } export global NavState { in-out property view: ContentView.home; // Browser-like history flags, driven by the Rust nav module. in property can-back: false; in property 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 id; in property name; // Network URL of the portrait — used by the artwork right-click // menu for Open-in-browser and Save-as actions. in property 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 has-custom-image: false; in property 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 bio-short; in property bio-truncated: false; // Editorial source for the biography ("TiVo" etc); shown as a small // "Source: " line under the Read more toggle when present. in property bio-source; in property artwork; // Artwork-derived tint for the header gradient. in property 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 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 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 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 mb-mbid; // Origin (MB metadata) section in property origin-loading: false; in property origin; // Relationships (MB) section in property relationships-loading: false; in property relationships; // Discovery (MB tag-based) section — populated in phase 8. in property 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 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 name; in property role; // "confirmed" | "contextual" | "weak" | "none" — the sidebar // uses this to skip MusicianPageView entirely on Confirmed and // navigate to the matched artist instead. in property 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 total: 0; in property loading: false; in property 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 id; in property name; in property image-url; in property 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 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 has-more: false; in property loading: false; in property load-more-loading: false; // "grid" | "list" — the view toggle (mirrors DiscoverBrowseView). // UI-owned, like Discover's; no Rust round-trip needed. in-out property 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 description; in property description-short; in property description-truncated: false; // Header follow state — true when the label is in the user's favorites. in property is-following: false; // Popular tracks (album-thumbnail rows, multi-select capable). in property <[TrackItem]> top-tracks: []; // Releases + Critics' Picks carousels (reuse Carousel). in property releases-section; in property 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 page-loaded: false; // --- Releases sub-view toolbar (UI-owned, like Discover) ------------ // "newest" | "oldest" | "title-asc" | "title-desc" | "artist-asc". in-out property sort-by: "newest"; in-out property filter-hires: false; in-out property group-by-artist: false; // Releases sub-view in-view search query (drives the count line). in-out property 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 shown: 0; // Count of Hi-Res albums in the loaded catalog (count line). in property 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 scene-label; in property genre-summary; in property <[LocationArtistItem]> artists: []; in property total: 0; in property loading: false; in property 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 sidebar-state: 0; out property 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 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); } // 0 = none, 1 = queue, 2 = lyrics in-out property right-panel: 0; // Section navigation placement: the sections (Discover/Library/Local // Library/My QBZ) now live in the sidebar and open right-flyout menus. in-out property nav-in-sidebar: true; } // Authenticated user, for the header user element. export global SessionState { in property user-name: ""; in property 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 has-track: false; in property title; in property artist; in property album; in property album-id; in property artist-id; in property artwork; in property track-id; in property playing: false; in property position-secs: 0; in property duration-secs: 0; in property progress: 0.0; // 0..1 in property 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 seekable-max: 1.0; in property elapsed: "0:00"; in property remaining: "0:00"; in property volume: 0.7; // 0..1 in property muted: false; in property shuffle: false; in property repeat-mode: 0; // 0 off, 1 all, 2 one // Quality of the playing track, for the song-card badge. in property quality-tier; // "hires" | "cd" | "mp3" | "" in property 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 is-remote: false; in property volume-locked: 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 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 cast-active: false; in property 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 context-kind; // "album" | "playlist" | "artist" | "label" | "mix" in property 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(); } // 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 text; in-out property anchor-x; // control centre, window coords in-out property anchor-y; // control top, window coords in-out property 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 has-current: false; in property now-playing; // Whether the now-playing track is in the user's favorites. in property 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 upcoming-total: 0; // Tracks remaining after the current one (total - played - 1). in property upcoming-remaining: 0; // Zero-based page index and total page count for the paginator. in-out property page: 0; in property page-count: 1; // Index of the first upcoming entry shown, 1-based, for the counter. in property page-start: 0; // Index of the last upcoming entry shown, 1-based, for the counter. in property page-end: 0; // --- History tab ----------------------------------------------------- in property <[QueueItem]> history: []; // --- Shared ---------------------------------------------------------- // Active tab: 0 = Queue, 1 = History. in-out property tab: 0; // Live search query; the Rust controller re-filters `upcoming-page`. in-out property 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 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 open: false; in property track-id; in property <[string]> track-ids: []; // bulk add (favorites multi-select) in property <[PlaylistPickItem]> playlists: []; in property 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 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 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 tracks-search: ""; in-out property tracks-group-mode: "off"; // off|album|artist|name in property <[AlphaJump]> tracks-alpha: []; // A-Z jumps (name grouping) in-out property tracks-multi-select: false; in property 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 albums-view-mode: "grid"; // "grid" | "list" in-out property albums-search: ""; in-out property albums-sort-by: "default"; // default|title-asc|title-desc|artist-asc in-out property albums-group-mode: "off"; // off|alpha|artist in property <[AlphaJump]> albums-alpha: []; // A-Z jumps (alpha grouping) in property 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 playlists-sub-tab: "favorites"; // "favorites" | "following" (transient) in-out property playlists-search: ""; in-out property playlists-view-mode: "grid"; // "grid" | "list" (persisted) in property playlists-following-count: 0; // Following sub-tab badge in property <[FavoriteLabelItem]> labels: []; in property <[FavoriteLabelItem]> labels-visible: []; // after search filter (rendered) in-out property labels-search: ""; // Artists — toolbar / grid view in-out property artists-search: ""; in-out property artists-view-mode: "grid"; // grid | sidepanel in-out property 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 artists-shown: 0; // count after search // Artists — sidepanel (the selected artist's albums) in-out property selected-artist-id: ""; // "" = none selected in property selected-artist-name: ""; in property selected-albums-loading: false; in property selected-albums-error: ""; in property selected-albums-total: 0; // sum across sections in property <[DiscoverSection]> selected-artist-sections: []; // release-type sections in property tracks-total: 0; in property albums-total: 0; in property artists-total: 0; in property playlists-total: 0; in property labels-total: 0; in property loading: false; in property 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 kind; // daily | weekly | fav | top in property title; in property subtitle; in property <[TrackItem]> tracks: []; in property track-count: 0; in property total-duration; in property 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 query; // Active tab: 0 = All, 1 = Albums, 2 = Tracks, 3 = Artists, 4 = Playlists. in property tab: 0; in property 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 albums-total: 0; in property tracks-total: 0; in property artists-total: 0; in property playlists-total: 0; // Most-popular hero: `kind` selects which of the three items is shown. in property most-popular-kind; // "album" | "artist" | "track" | "" in property most-popular-album; in property most-popular-artist; in property 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 most-popular-quality-label; // Active searchType filter, as a dropdown index (0 = no filter). // The controller owns the index -> API-value mapping. in property 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); }