// 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, mixtapes, collections, mixtape-detail, discography-builder } // In-app toast (transient notification). Mirrors Tauri's toastStore: // a single toast at a time (a new one replaces the current), auto-hidden // by the Rust layer per kind. `buffering` is persistent (no auto-hide). export enum ToastKind { success, error, info, warning, buffering } // Local Library (on-disk + Plex music). Greenfield Slint port — the view // shows header stats + the browse tabs (Albums / Artists / Folders / // Tracks). All management/maintenance/danger actions live under // Settings > Local Library; the gear button routes there. // One card in the Local Library Artists grid (name + "N albums · M tracks"). // Images + click-to-detail are a later slice. export struct LocalArtistItem { name: string, // raw/canonical key (used for selection + matching) display-name: string, // canonical display name (after name-merge) album-count: int, track-count: int, image-path: string, // local file path OR cached Qobuz URL ("" = none) image: image, // decoded portrait (default-empty = show placeholder) } // One A-Z section of the local Artists left rail (master list grouped by the // display name's first letter, "#" bucket for non-alphabetic). 1:1 with the // Tauri Artists tab, which groups the compact rows under sticky letters. export struct LocalArtistSection { letter: string, artists: [LocalArtistItem], } // One visible row in the Folders tree (flattened-model lazy tree — Slint has // no native TreeView). Folders expand to fetch children lazily. export struct FolderNode { path: string, // absolute filesystem path (the node id) segment: string, // last path component (display) depth: int, // indent level is-folder: bool, // folder vs. track row expanded: bool, // folder open? can-expand: bool, // has children (track_count_under > 0) track-count: int, // recursive track count under a folder // Select-mode: track rows carry `selected`; folder rows carry a tri-state // `select-state` (0 none / 1 partial / 2 all) over their descendant tracks. selected: bool, select-state: int, } // One subfolder cover card in the tree-mode folder-detail pane (1:1 with // Tauri's `.subfolder-card`). `artwork` is loaded async from the folder cover. export struct FolderSubcardItem { path: string, // absolute path (the node id) name: string, // last path component (display) track-count: int, // recursive track count under the folder artwork: image, artwork-url: string, // cover file path (artwork-pipeline source) } // One album block in the ephemeral-folder pane (an ad-hoc folder can hold // several albums). `artwork` is loaded from the cached cover file on the UI // thread; `tracks` are the album's tracks in scan order. export struct EphemeralAlbum { group-key: string, title: string, artist: string, // "year · N tracks" (already formatted). meta: string, quality-tier: string, // This album is a single-file CUE rip (one audio file, virtual tracks). is-cue: bool, artwork: image, tracks: [TrackItem], } export global LocalLibraryState { // "albums" | "artists" | "folders" | "tracks" in-out property 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. // Tree is the Slint default (user 2026-06-06). in-out property folders-view-mode: "tree"; // "flat" | "tree" // Flat mode: `folders` is the full set (artwork target); `folders-visible` // / `folders-grouped` are the search/sort/group-derived render sets. in property <[AlbumCardItem]> folders: []; in property 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). `folder-tree` is the full // flattened set; `folder-tree-visible` is the search-filtered render set. in property <[FolderNode]> folder-tree: []; in property <[FolderNode]> folder-tree-visible: []; in property folder-tree-loading: false; // Tree rail width (resizable). Default ~20% narrower than the old 340px; // clamped in the UI to [50% of default, 50% of the content area]. in-out property tree-sidebar-width: 272px; // Tree rail header: search box + multi-select mode (1:1 with Tauri). in-out property folders-tree-search: ""; in-out property tree-select-mode: false; in property tree-selected-count: 0; in-out property folders-selected-path: ""; in property folders-selected-name: ""; // Subfolder cover cards (1:1 with Tauri): `folder-detail-subfolders` is the // full set (artwork target); `-visible` is the search-filtered render set. in property <[FolderSubcardItem]> folder-detail-subfolders: []; in property <[FolderSubcardItem]> folder-detail-subfolders-visible: []; in property <[TrackItem]> folder-detail-tracks: []; in property folder-detail-loading: false; // Recursive track count under the selected folder (the header "N tracks"). in property folder-detail-track-count: 0; // Per-pane grid/list toggle + subfolder name filter. in-out property folder-detail-view: "grid"; // "grid" | "list" in-out property folder-detail-search: ""; // --- Ephemeral folder (ad-hoc play, NOT indexed into library.db) ---- // When `ephemeral-active`, the Folders tab shows the ephemeral pane // (album-grouped) instead of the normal flat/tree browse. The session is // in-memory; the folder path is persisted so it rehydrates on restart. in property ephemeral-active: false; in property ephemeral-loading: false; in property ephemeral-path: ""; in property ephemeral-name: ""; in property ephemeral-track-count: 0; // True when the session spans more than one album → show per-album play. in property ephemeral-multi-album: false; in property <[EphemeralAlbum]> ephemeral-albums: []; // --- Artists tab — two-column master/detail (1:1 with Tauri) -------- // Left rail: the merged/deduped artist master list. `artists` is the full // merged set (post name-merge); `artists-grouped` / `artists-alpha` are // the search-and-group-derived render sets; `artists-shown` is the count // after the search filter (drives the rail header). in property <[LocalArtistItem]> artists: []; in property 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 */); // Tree detail pane: filter the subfolder cards by name. callback folder-detail-search(string /* query */); // Tree rail header: filter the tree by name; collapse every folder. callback folders-tree-search(string /* query */); callback folders-collapse-all(); // Multi-select mode (tree rail): toggle the mode; toggle a folder's // descendant tracks / a single track; run a bulk action on the selection. callback folders-toggle-select-mode(); callback folders-toggle-folder-select(string /* folder path */); callback folders-toggle-track-select(string /* track path */); callback folders-bulk-action(string /* select-all|clear|queue|play-next|add-to-playlist */); // --- Ephemeral folder (ad-hoc play) --- // Open the native folder picker → scan → show the ephemeral pane. callback ephemeral-open(); // Play the whole ephemeral folder (all albums, scan order). callback ephemeral-play-all(); // Play one ephemeral track — its album group becomes the queue. callback ephemeral-play-track(string /* track id */); // Play one ephemeral album (its tracks become the queue). callback ephemeral-play-album(string /* album group key */); // Clear the ephemeral session (drop the pane + forget the path). callback ephemeral-clear(); // --- Artists tab --- // Filter the master list (two-way bound to artists-search; re-derive). callback artists-search(string /* query */); // Select an artist -> filter their albums into the right pane. callback artists-select(string /* artist name */); } // Albums tab quality/format/source filter (1:1 with Tauri matchesQualityFilters: // OR within each group, AND between groups; an empty group matches all). export global LibAlbumFilterState { in-out property 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 add-to-mixtape(); callback play-track(string /* track id */); callback select-version(int); // Client-side track filter (mirrors AlbumActions.search), applied over the // current version's tracks. callback search(string /* query */); } // ============================ 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 remove-folder(int /* id */); // remove ONE folder directly (confirm) callback toggle-folder-select(int /* id */); callback edit-folder(int /* id */); // open the settings modal callback save-folder-settings(int /* id */, string /* alias */, bool /* enabled */, bool /* is-network */, string /* fs-type */, bool /* user-override */); callback change-folder-path(int /* id */); // picker -> update path callback scan-all(); callback scan-folder(int /* id */); callback stop-scan(); callback cleanup-missing(); callback clear-library(); // two-step confirm callback set-filter(string /* query */); } // ============================ Plex connection ============================= // Settings > Local Library > Plex. A LAN-only Plex Media Server connection: // PIN-based auth (or manual token), library-section multi-select, and the // per-section track caching that downstream browse views read. State is // Rust-backed (qbz_app::settings::plex store + qbz-plex core); the controller // lives in `plex_auth`. ADR-008: no pills. Status is a (key, args) pair so the // Rust side can localize without 5-file JSON sync — `status-text` is the // already-resolved label, `status-kind` drives the highlight style. // One Plex music-library section row + its UI selection + cached track count. export struct PlexSectionItem { key: string, // Plex Directory@key title: string, // section title count: int, // cached track count (plex_cache_get_tracks(key).len()) selected: bool, // membership in selected_section_keys } export global PlexSettingsState { // --- Persisted toggles ------------------------------------------------- in-out property enabled: false; // master toggle (default OFF) in-out property ui-collapsed: false; // collapse chevron in-out property metadata-write-enabled: false; // --- Connection -------------------------------------------------------- // server-url is the RAW host-form text the user types; base-url is the // Rust-resolved proto://host:port (set after resolve). in-out property server-url: ""; in property base-url: ""; in property token: ""; // "" = not authenticated in-out property manual-token-mode: false; in-out property token-input: ""; // manual-token field buffer // Derived gates (computed Rust-side, pushed in). in property is-local-address: true; // isLocalPlexAddress(server-url) in property can-use: false; // enabled && local && base!="" && token!="" // --- PIN flow ---------------------------------------------------------- in property pin-busy: false; // PIN start / poll in flight in property pin-code: ""; // "" = no active code in property pin-auth-url: ""; // --- General busy + status -------------------------------------------- in property busy: false; // ping / sync / auto-setup // Status as a resolved label + kind (0 none, 1 info, 2 connected/ok, 3 error). in property status-text: ""; in property status-kind: 0; // --- Library sections -------------------------------------------------- in property <[PlexSectionItem]> sections: []; } export global PlexAuthActions { callback enable-toggle(bool); callback collapse-toggle(bool); callback set-server-url(string /* host-form */); callback generate-code(); // PIN start + begin poll callback open-auth-url(); callback copy-code(); callback manual-token-toggle(bool); callback set-token(string /* token */); // save manual token callback ping(); // check connection callback load-sections(); // get libraries callback toggle-section(string /* key */); callback metadata-write-toggle(bool); callback disconnect(); // confirm + reset callback clear-cache(); // confirm + plex_cache_clear callback load(); // panel init: seed from store + warm cache } // ============================ Tag editor ================================== // Local-album metadata editor (Settings-independent modal launched from the // local album detail). Sidecar (default) + opt-in direct file-write. Remote // MusicBrainz/Discogs lookup lands as a follow-up sub-slice (needs the // remote-metadata crate move). // One editable track row. `cue_start_secs` is f64 (-1.0 = none); `has_cue` // is precomputed (cue_file_path != "" || cue_start_secs >= 0) and disables // direct file-write. disc/track 0 = unset sentinel (Option). 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 */); // Add a playlist to a Mixtape/Collection (opens the AddToMixtapeModal with // the playlist as a 1-item payload; restricts to mixtapes since a playlist // can't go in a Collection). callback add-to-mixtape(string /* playlist 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(); } // "Create Mixtape / Collection" modal state (My QBZ). Opened from either // grid's "+ New" CTA / empty-state button. `kind` is preset by the opening // grid ("mixtape" | "collection") and can be flipped via the radio toggle // (artist_collection is created via the Discography Builder, not here). // `name` is capped at 80 chars (mirrors Tauri's maxlength="80"). export global MyQbzCreateState { in-out property open: false; in-out property kind: "mixtape"; in-out property name; in property creating: false; } export global MyQbzCreateActions { callback submit(); callback close(); } // === "Add to Mixtape/Collection" picker modal (global singleton) ========= // Spec 21/50. A global overlay (mounted ONCE at the window root) that adds one // or more items (album / track / playlist, from qobuz or local) into an // existing Mixtape/Collection — or creates a brand-new one inline and adds // into it. Every app-wide "Add to Mixtape/Collection" trigger calls // MyQbzAddActions.open(...) with a payload it built; the Rust controller // (crate::myqbz_add) holds the pending items and drives the picker. // // One picker row = one collection (mixtape | collection | artist_collection), // with its kind label, "N albums" meta, and an `already-has` flag computed // from `item_exists` so the row can show an "already added" hint. export struct MyQbzAddRow { id: string, name: string, // "mixtape" | "collection" | "artist_collection". kind: string, // Row icon by kind ("mixtape" -> cassette, "artist_collection" -> user, // else library-big). Resolved in Rust to a key the row maps to a glyph. icon: string, // Right-aligned kind tag, uppercase ("MIXTAPE" | "COLLECTION" | "ARTIST"). kind-label: string, // "N albums" / "1 album" — ICU plural rendered in Rust (always "album(s)"). meta: string, // True when this collection already contains ALL of the pending items // (per item_exists). Drives the per-row "already added" hint. already-has: bool, } export global MyQbzAddState { in-out property open: false; // Whether the picker is loading the collection list. in property loading: false; // The collections offered as targets (already kind-restricted + // recency-sorted + item_exists-resolved by the Rust controller). in property <[MyQbzAddRow]> rows: []; // Header title + subtitle (precomputed: single-item title or bulk // "{count} items" / "{first} + N more"). in property header-title; in property header-subtitle; // True when >1 pending item (bulk add). in property bulk-mode: false; // True when ANY pending item is a track/playlist -> targets restricted to // Mixtapes; the "Create Collection" button is hidden. in property restrict-to-mixtape: false; // Free-text filter over collection names. in-out property search; // Id of the collection currently being written to (disables rows/buttons). in property busy-id; // --- Create-new sub-panel --- in-out property creating: false; in-out property create-name; // "mixtape" | "collection". in-out property create-kind: "mixtape"; in property create-busy: false; } export global MyQbzAddActions { // Click a collection row -> add the pending items to it. callback pick(string /* collection id */); // Filter the list by name. callback search-changed(string /* query */); // Open the create-new sub-panel preset to a kind ("mixtape"|"collection"). callback show-create(string /* kind */); // Back out of the create sub-panel to the picker. callback create-back(); // Create a new collection (create-kind/create-name) then add the items. callback create-and-add(); callback close(); } // Ephemeral "play with active queue" choice — shown when the user triggers an // ephemeral play action while a queue is already playing. Two outcomes: // add to the queue, or clear the queue and start playing. export global EphemeralPlayChoiceState { in-out property open: false; // "all" | "album" | "track" — what the user pressed play on. in-out property intent-kind: ""; // group key (album) or track id (track); empty for "all". in-out property intent-arg: ""; // Whether "Add to queue" is offered — only when the current queue is itself // entirely ephemeral (queue ops may not mix ephemeral with persistent tracks). in-out property enqueue-allowed: false; } export global EphemeralPlayChoiceActions { callback enqueue(); // add the selection to the current queue callback replace(); // clear the queue and start playing the selection callback close(); } // Playlist detail screen state. Rows reuse the shared TrackRow // (show-artwork + show-album); populated by crate::playlist. export global PlaylistState { in property 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 */); } // === My QBZ — Mixtapes & Collections (read-only index grids) ============ // Mirrors PlaylistManagerState/PmPlaylistItem. Both the Mixtapes grid and the // Collections grid render `MixtapeCardItem`s; the Rust controller (crate::myqbz) // filters by kind and precomputes every display string, so the views do no // per-row lookups. Mosaic covers (up to 4, or 9 for a 3x3 Collection) are // decoded into the item by the artwork pipeline. // // READ-ONLY SCOPE (Phase-2 Slice 2): the grids DISPLAY rows + route from the // sidebar. Card click (open detail) and "+ New" (create) are STUBS that log // "not yet wired" — wired in later slices. export struct MixtapeCardItem { id: string, name: string, // "mixtape" | "collection" | "artist_collection". kind: string, // Eyebrow label, uppercase ("MIXTAPE" | "COLLECTION" | "ARTIST"). label: string, // "5 albums" / "1 album" — ICU-plural rendered in Rust (always "album(s)" // regardless of item_type, matching the PSD). meta: string, // Item count (drives the 3x3-vs-2x2 mosaic rule + sort). item-count: int, play-count: int, // updated_at epoch ms (sort key). updated-at: int, // Custom user-uploaded cover (decoded local file); empty when none. When // present it full-bleeds the mosaic (precedence over the collage). custom-cover: image, has-custom-cover: bool, // Mosaic cell count actually used: 0 (empty -> kind glyph), or 4 / 9. cover-count: int, // Up to 9 collage cover slots (only 0-3 used for 2x2; 0-8 for a 3x3 // Collection with >=9 items). URLs are pre-downscaled in Rust. url1: string, url2: string, url3: string, url4: string, url5: string, url6: string, url7: string, url8: string, url9: string, cover1: image, cover2: image, cover3: image, cover4: image, cover5: image, cover6: image, cover7: image, cover8: image, cover9: image, } export global MyQbzState { // The Mixtapes grid set (kind == mixtape), filtered + sorted. in property <[MixtapeCardItem]> mixtapes: []; // The Collections grid set (kind == collection | artist_collection), // filtered (by kind-filter) + sorted. in property <[MixtapeCardItem]> collections: []; in property loading: false; // --- Mixtapes toolbar (session-scoped) --- in-out property mix-search; // "position" | "name" | "items" | "updated". in-out property mix-sort: "position"; // "asc" | "desc". in-out property mix-sort-dir: "asc"; // "grid" | "list". in-out property mix-view: "grid"; // --- Collections toolbar (session-scoped) --- in-out property col-search; in-out property col-sort: "position"; in-out property col-sort-dir: "asc"; in-out property col-view: "grid"; // "all" | "collection" | "artist_collection". in-out property col-kind-filter: "all"; } export global MyQbzActions { // Card click -> open detail (STUB: logs in this slice). callback open-card(string /* id */); // "+ New Mixtape" / "+ New Collection" (STUB: logs in this slice). callback create-mixtape(); callback create-collection(); // --- Mixtapes toolbar --- callback mix-search-changed(string /* query */); callback mix-set-sort(string /* field */); callback mix-set-view(string /* grid|list */); callback mix-reset(); // --- Collections toolbar --- callback col-search-changed(string /* query */); callback col-set-sort(string /* field */); callback col-set-view(string /* grid|list */); callback col-set-kind-filter(string /* all|collection|artist_collection */); callback col-reset(); } // === My QBZ — Collection / Mixtape DETAIL view (read-only) ============== // Spec 12. One row per collection item (album / track / playlist), rendered // in the 8-col list / the grid. The Rust controller (crate::myqbz_detail) // precomputes every display string (type label, source kind, quality detail, // tracks/year columns, downscaled artwork URL) so the view does no lookups. // READ-ONLY SCOPE (Phase-2 Slice 3): the hero action buttons + per-row // context-menu items + grid/list view modes render 1:1, but every click is a // logging STUB. The "expanded" view-mode toggle + shell render; per-item // inline track expansion is DEFERRED (needs the enqueue resolver — a later // slice). Edit/rename/delete/cover/DJ-mix modals + the BulkActionBar are also // deferred. export struct MixtapeDetailItem { // 0-based persisted position (the row's stable key; display is +1). position: int, // "album" | "track" | "playlist". item-type: string, // "qobuz" | "local". source: string, source-item-id: string, title: string, subtitle: string, // True for qobuz items whose subtitle is a clickable artist link // (spec 12 §6.3 col-2 — local subtitles are plain text). subtitle-is-link: bool, // Resolved source kind for the source indicator + source filter // ("qobuz" | "plex" | "local"). Defaults from `source` until resolveItems // lands (deferred — for now equals the raw source mapping). source-kind: string, // Type-cell label (already localized): "ALBUM" / "TRACK" / "PLAYLIST" // (or an EP/Single/… release-type override — overrides are a later slice). type-label: string, // Quality badge inputs (resolved by the resolveItems pass). quality-tier: string, // "hires" | "cd" | "" quality-detail: string, // "24-bit / 96 kHz" | "" // True while the async resolveItems pass is still pending for this row (no // cached resolution yet). Drives the quality-cell LoadingDots skeleton — // shown only during the resolve window, collapsing to the real badge (or an // empty cell for a no-quality item) once resolution completes. quality-resolving: bool, // TRACKS column: "1" for a track, else the count or "—". tracks-text: string, // YEAR column: the year or "". year-text: string, // Downscaled (_50) row artwork; decoded into `artwork` by the pipeline. artwork-url: string, artwork: image, // UI-owned multi-select state (select-mode — deferred wiring; the column // renders when select-mode is on so geometry matches the PSD). selected: bool, // --- Expanded view-mode inline tracks (spec 12 §8) -------------------- // In the "expanded" view-mode every album / playlist item shows its // tracks rendered directly underneath — always visible, no chevron (1:1 // with Tauri: "like LocalLibrary's Tracks page with Group-by-album"). // Whether this item can host inline tracks (album / playlist; never a // bare track) — gates the inline section + the auto-fetch. can-expand: bool, // True once the per-item track fetch has completed (success OR empty), so // the view can distinguish "still loading" from "loaded, no results". tracks-loaded: bool, // True while the per-item track fetch is in flight (spinner state). expand-loading: bool, // The resolved inline tracks (rendered as shared TrackRows under the row). // Fetched once on entering expanded mode, then cached on the row. inline-tracks: [TrackItem], } export global MyQbzDetailState { in property id; // "mixtape" | "collection" | "artist_collection". in property kind; // Eyebrow label, uppercase ("MIXTAPE" | "COLLECTION" | "ARTIST"). in property kind-label; in property name; in property description; // "5 albums" / "1 album" meta line (ICU plural, rendered in Rust). in property meta; in property item-count: 0; // play_mode ("in_order" | "album_shuffle") — drives the overflow toggle // label (deferred wiring; shown for parity). in property play-mode: "in_order"; // True when the user set a custom cover (full-bleeds the hero mosaic). in property has-custom-cover: false; in property custom-cover; in property loading: false; // False once the load completes with no collection (not-found state). in property found: true; // --- Hero mosaic (CollectionMosaic inputs, same scheme as the card) ---- // cover-count 0 (empty -> kind glyph) / 4 / 9; up-to-9 downscaled URLs + // decoded covers. Mirrors MixtapeCardItem so CollectionMosaic renders it. in property cover-count: 0; in property url1; in property url2; in property url3; in property url4; in property url5; in property url6; in property url7; in property url8; in property url9; in property cover1; in property cover2; in property cover3; in property cover4; in property cover5; in property cover6; in property cover7; in property cover8; in property cover9; // --- Item list (the render model: filter -> search -> sort applied) ---- in property <[MixtapeDetailItem]> items: []; // --- Toolbar (session-scoped) ------------------------------------------ in-out property search; // "position" | "name" | "year" | "tracks". in-out property sort: "position"; in-out property sort-dir: "asc"; // "all" | "album" | "track" | "playlist" (single-select). in-out property type-filter: "all"; // Multi-select source filter membership (spec 12 §5.5). Three flags // (Slint has no Set); Rust derives the badge count. in-out property src-qobuz: false; in-out property src-plex: false; in-out property src-local: false; // "list" | "grid" | "expanded". in-out property view-mode: "list"; // Multi-select edit mode (renders the per-row checkbox column; bulk-bar // wiring is DEFERRED). in-out property select-mode: false; in property selected-count: 0; // Rust-computed filter badge count (sources + (type-filter != all)). in property filter-count: 0; // Whether any filter/sort differs from the defaults (drives the reset btn). in property has-any-filter: false; } export global MyQbzDetailActions { // Client-side toolbar — re-derive the visible item list (UI thread). callback search-changed(string /* query */); callback set-sort(string /* field */); callback set-type-filter(string /* all|album|track|playlist */); callback toggle-source-filter(string /* qobuz|plex|local */); callback set-view-mode(string /* list|grid|expanded */); callback toggle-select-mode(); callback reset-filters(); // Open an item's detail (album / track-album / playlist). Routed to the // existing open-album / open-playlist paths in main.rs. callback open-item(string /* source */, string /* item-type */, string /* source-item-id */); // Open an item's artist page by name (qobuz items only). callback open-artist(string /* source */, string /* artist-name */); // --- Hero CTAs -------------------------------------------------------- callback play-all(); callback shuffle(); callback dj-mix(); // STUB (the DJ-mix modal is a later slice). callback sync(); // STUB / no-op (no discography sync impl, spec §8). // --- Hero overflow (⋯) menu (spec 12 §10) ----------------------------- // Each opens the matching modal or fires the matching mutation + reload. callback open-rename(); // -> Rename modal (prefilled with name). callback open-description(); // -> Description modal (prefilled). callback upload-cover(); // native file picker -> resize -> set + reload. callback remove-cover(); // clear custom cover -> reload. callback toggle-play-mode(); // in_order <-> album_shuffle -> reload. callback convert-kind(); // mixtape <-> collection -> reload + toast. callback open-delete(); // -> Delete-confirm modal. // Per-row affordances. callback play-item(string /* source-item-id */); callback item-action(string /* source-item-id */, string /* action */); // Remove ONE item from the collection by its 0-based persisted position // (the row menu's "Remove from collection"). Routes to the audited bulk // remover with a single position so compaction + reload + toast are reused. callback remove-item(int /* position */); // Toggle one row's selection (select-mode). callback toggle-item-select(int /* position */); // --- Expanded view-mode inline tracks (spec 12 §8) -------------------- // Fired when the "expanded" view-mode becomes active (and on the initial // open in expanded mode): the Rust side fetches every expandable item's // tracks (via the enqueue resolver) that aren't already cached, populating // each row's inline-tracks model. Idempotent — cached rows are skipped, so // re-entering expanded mode is instant. callback ensure-expanded(); // Play / queue / navigate from an inline track row. `action` is one of // "play" | "play-next" | "play-later" | "go-to-album". // The parent item's source-item-id scopes the fetch + go-to-album target. callback inline-track-action(string /* item-source-item-id */, string /* track-id */, string /* action */); // --- Bulk action bar (select-mode, spec 12 §13) ----------------------- // The bar emits the chosen action id verbatim; the Rust side maps it: // "add-to-mixtape" -> open the global AddToMixtapeModal with the // selected items' payloads. // "remove-selected" -> repo::remove_item each selected position // (highest-first so compaction doesn't shift), // then reload the detail + clear selection. callback bulk-action(string /* id */); } // ─────────────────────── Discography Builder (spec 13) ─────────────────── // The full-page flow that assembles an artist's complete discography into a // saved `kind='artist_collection'` collection. It fetches every release for // the artist from three sources (Qobuz artist page + local library + Plex // cache), dedupes them into GROUPS (one logical album = a chosen "primary" + // "alternates"), lets the user check which to include, reorder (by sort), // rename, and correct release-type mislabels, then persists the result. // // One candidate row (a primary OR an alternate). Every display string is // precomputed in Rust (crate::myqbz_builder) so the view does no lookups. export struct DiscoCandidate { // `${source}|${source_item_id}` — the per-candidate selection key. key: string, // "qobuz" | "local" | "plex" — drives the source indicator icon. source: string, source-item-id: string, title: string, artist: string, // "2019" or "—" (em-dash placeholder). year: string, // "12" or "—". tracks: string, artwork-url: string, // Effective release type AFTER the user's override is applied: // "album" | "ep" | "single" | "live" | "compilation". release-type: string, // True when a user override is active for this candidate (drives the // dotted-underline marker on the type button). is-overridden: bool, // QualityBadgeFull inputs. quality-tier: string, quality-detail: string, // Whether THIS candidate is currently checked (included on save). checked: bool, } // One dedupe GROUP: a primary candidate plus zero or more alternates. export struct DiscoGroup { // group_key (`${normalizedTitle}|${year}`) — stable row key. key: string, title: string, // "2019" / "—". year: string, primary: DiscoCandidate, alternates: [DiscoCandidate], // ALL candidates are compilations → the group is treated as a comp // (dimmed primary row, ignored by select-all). is-compilation: bool, } export global DiscoBuilderState { // The artist being built (id + resolved display name + portrait URL). in property artist-id; in property artist-name; in property artist-avatar-url; in property artist-avatar; // Render states (spec 13 §4). `loading` true initially + during load; // `load-error` non-empty shows the raw error message; otherwise the // groups table renders (empty groups -> "No results found"). in property loading: true; in property load-error: ""; // The dedupe groups in CURRENT sort order (Rust re-sorts on order change). in property <[DiscoGroup]> groups: []; // The collection-name input (defaulted to " — Complete Discography"). in-out property collection-name: ""; // Whether the name is empty AFTER trimming whitespace (Slint can't trim, so // the controller computes this on every name change). Gates the Create // button so a whitespace-only name disables it (spec 13 §2.13 / §10.19: // disabled = creating || selectedCount===0 || !collectionName.trim()). in property name-trimmed-empty: true; // Order segmented control: "release_date" | "title" | "manual". in-out property order-by: "release_date"; // Select-all header checkbox state (tri-state via the two flags). in property all-checked: false; in property some-checked: false; // Footer: total checked candidate count + total group count. in property selected-count: 0; in property group-count: 0; // Disables the Create button while a save is in flight. in property creating: false; // The per-row type-override popover open key (`${source}|${source_item_id}`), // or "" when none is open. One menu open at a time. in-out property open-type-menu-key: ""; } export global DiscoBuilderActions { // Back to the artist page (clears builder state). callback back(); // Collection-name input edited. callback name-changed(string); // Order segmented control: "release_date" | "title" | "manual". callback set-order(string); // Toggle one candidate's checked state (group-key + candidate-key). callback toggle-checked(string /* group-key */, string /* candidate-key */); // Header tri-state select-all toggle (non-compilation primaries only). callback toggle-all(); // Open an album (Qobuz album view, or local/Plex local-album view). callback open-album(string /* source */, string /* source-item-id */); // Apply a release-type override to a candidate (writes the sidecar). callback set-type-override(string /* source */, string /* source-item-id */, string /* type */); // Clear a candidate's override (revert to auto-detected). callback reset-type-override(string /* source */, string /* source-item-id */); // Save: create the artist_collection + bulk-add the checked groups. callback create(); } // ──────────────────────────── DJ-mix modal ──────────────────────────── // The "Random queue" sampler (TrackMixModal, spec 21 §C / spec 12 §12). // Opened from the hero DJ-mix CTA. On open the Rust controller resolves the // collection in-order + computes shuffle::unique_track_count (the slider max); // the slider snaps to the discrete buildSizeOptions set (50,100,150,…,All). // On confirm it runs dedup_by_similarity + hybrid_sample (thread RNG, sync // scope) -> set_queue + play -> stamp queue-source + touch_play, toasting // "Playing N of M" when the sampled count is below the requested size. export global MyQbzMixState { in property open: false; // True while resolving + counting unique tracks ("Loading…" state). in property loading: false; // Deduped unique-track count = the slider MAX (and the "All" size). in property unique-count: 0; // The discrete size options (buildSizeOptions): the slider index maps into // this list. e.g. [50,100,150,All(187)]. Empty while loading / for an // empty collection. in property <[int]> size-options: []; // Current slider index into `size-options` (0-based). in-out property selected-index: 0; // The size at `selected-index` (the value handed to the sampler). in property selected-size: 0; // True when the selected option is the trailing "All (N)" entry. in property selected-is-all: false; // Disables Shuffle while a sample + play is in flight. in property busy: false; } export global MyQbzMixActions { callback close(); // Slider moved: the new index into size-options (Rust re-derives the size). callback set-index(int /* index */); // Confirm -> run the sample + play. callback shuffle(); } // ──────────────────────────── MyQBZ edit modals ─────────────────────── // The Rename / Description / Delete-confirm modals (spec 12 §11), driven // from the hero overflow (⋯) menu. One global backs all three (only one is // ever open at a time). `busy` disables submit while the mutation runs. export global MyQbzEditState { // "" = closed; "rename" | "description" | "delete". in-out property mode: ""; in property open: false; // The collection name (delete-confirm body interpolation + rename prefill). in property name; // Rename: prefilled with the collection name; submit trims. in-out property draft-name; // Description: prefilled with the current description ("" clears to null). in-out property draft-description; // Disables submit while a mutation + reload is in flight. in-out property busy: false; } export global MyQbzEditActions { callback close(); callback submit-rename(); // trim; rename_collection -> reload -> close. callback submit-description(); // empty -> null; set_description -> reload. callback confirm-delete(); // delete_collection -> nav back + cleanup. } // 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; // --- OFFLINE MODE -------------------------------------------------------- // Settings > Offline, MODE group (the app operating without Qobuz — NOT // the offline cache). Backed by the shared engine's per-user store, not // the audio/playback stores: seeded by OfflineModeActions.load() on panel // mount; persisted through settings-bool keys "offline-mode-enabled" / // "offline-show-network-folders". in-out property offline-mode-enabled: false; in-out property offline-show-network-folders: false; // "Check now" re-probe in flight — flips the status row's button label. // Cleared by Rust on the next status broadcast, or after a short timeout // when the verdict comes back unchanged (the actor only broadcasts flips). in-out property offline-checking: false; } // 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 ------------------------------------------------------- // `is-macos` is set once from Rust (cfg!(target_os = "macos")) so the tray // section can swap the header to "Menu Bar" and show macOS-only controls. in-out property is-macos: false; in-out property tray-enable: true; in-out property tray-minimize-to-tray: false; in-out property tray-close-to-tray: false; // macOS-only: hide the Dock icon when closed to the menu bar (.accessory // activation policy). Off = Spotify-style (keep the Dock icon). in-out property tray-mac-hide-dock: 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 */); } // "My QBZ" navigation branding (custom label + icon), surfaced in // Settings > Appearance and reflected on the sidebar "My QBZ" row. // // Mirrors the Tauri `myQbzNavStore` contract (label default "My QBZ", // icon default the branded /my-qbz.svg). Persistence is per-user JSON // (see crate::myqbz_prefs); these properties are the live UI mirror and // the callbacks are the action surface (the Rust side persists + re-seeds). // // - `label` : the current label (never empty — blank input coerces // back to the default "My QBZ" string on the Rust side). // - `custom-icon` : the user's image, loaded straight from the picked file // path and rendered as-is (no tint). Only meaningful when // `has-custom-icon` is true; otherwise the consumer falls // back to the default branded glyph in markup (mask-tinted // via QbzIcon on the sidebar). The default glyph stays a // compile-time `@image-url` — the Rust side never decodes it. // - `has-custom-icon` : true when a usable user image is set. A stale / // deleted custom path falls back to the default on the Rust // side (the seed loads the file; a load failure clears the // flag without mutating the store), so this flag tracks "a // usable custom icon is present". export global MyQbzBrandingState { in-out property label: "My QBZ"; in-out property custom-icon; in-out property has-custom-icon: false; // Commit a new label (trimmed-empty coerces to the default on the Rust // side, which also re-seeds `label`). Fired on the text input's edit. callback set-label(string /* label */); // Open the native image picker; on pick, persist the path, load it, and // re-seed `icon` + `has-custom-icon`. No-op on cancel. callback pick-icon(); // Reset the icon to the default branded glyph (clears the persisted // custom path). The label is reset separately by clearing the input. callback reset-icon(); } // 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(); // ---- Scroll-position restore (browser-like, per history entry) ---- // The mounted scroll container reports its live viewport-y here on every // `changed viewport-y`; the Rust nav module stamps it onto the outgoing // entry so back/forward can put it back. callback report-scroll(length /* viewport-y */); // Armed by Rust right before a back/forward view switch: `restore-scope` // names which scroll container should restore (a stable per-list id, see // nav::scope_for), and `scroll-restore` is the viewport-y to apply. A // container restores only when its own scope matches, exactly once per // mount, and only after its content has laid out (viewport overflows). in-out property restore-scope: ""; in-out property scroll-restore: 0; } // Artist detail screen state. // One label row in the artist network sidebar's Labels section. export struct LabelEntry { id: string, name: string, } // One similar-artist row in the artist network sidebar's Similar // Artists section. export struct SimilarEntry { id: string, name: string, } export global ArtistState { in property 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); } // Right-side panels — independent now: Queue and Lyrics can BOTH be open // at once, stacked vertically in a shared right column (Feishin-style). Each // is toggled from its bar button and closed from its own X. The column is // visible when either is open. (Replaces the old exclusive `right-panel` int // where lyrics closed queue and vice-versa.) in-out property queue-open: false; in-out property lyrics-open: false; // Section navigation placement: the sections (Discover/Library/Local // Library/My QBZ) now live in the sidebar and open right-flyout menus. in-out property nav-in-sidebar: true; // Now-playing bar layout mode: 0 = New (default), 1 = Classic. Persisted // in ui_prefs.json (`npb_mode` "new"|"classic"); set on startup from the // store and on the player bar's "Now Playing view" menu select. in-out property npb-mode: 0; } // 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, // From an ephemeral folder — gates persistence actions in the row menu. is-ephemeral: bool, } // Now-playing / transport state. The Rust playback poll loop pushes the // live values here; the PlayerBar and SeekBar bind to it directly. export global NowPlayingState { in property 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; // True when the playing track is from an ephemeral folder (synthetic id): // it has no DB row, so favorite / add-to-playlist / track-info are gated off. in property is-ephemeral: false; in property playing: false; // Fetch/resolve in flight for the track being started — no audio yet. // Set the instant a play is requested (before the Plex/Qobuz/local // resolve) and cleared once the poll loop sees the audio advancing. // Drives the fetch spinner on the bar, the active track row, and the // album play button. in property loading: 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; // Whether THIS device currently has a Qobuz Connect session running (drives // the connect flyout's Connect/Disconnect button). Distinct from `is-remote`, // which means a PEER renderer owns playback. Set by the connect callback. in property qconnect-connected: false; // Name of the active Qobuz Connect renderer (shown under the cast button // when remote). Empty when local. Populated by the QConnect epic. in property 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(); // Qobuz Connect — toggle the session on/off from the bar's connect flyout. // Wired in main.rs to SlintQconnectService::connect/disconnect. callback qconnect-toggle(); } // One row in the QConnect device picker (a session renderer). Populated by the // event sink from `session.renderers`. export struct QconnectDevice { renderer-id: int, name: string, is-local: bool, // this is QBZ itself ("Play here") is-active: bool, // currently the active renderer // Device-type icon key (mirrors Qobuz Connect): "computer" | "web" | "mobile" | "speaker". icon: string, } // Qobuz Connect DEV diagnostics — live session topology + a rolling event log, // rendered by QconnectDevModal (opened from the connect flyout). Lets us inspect // QC behavior at runtime WITHOUT a rebuild (Slint builds are slow). `open` is // in-out so the flyout button + the modal backdrop toggle it directly. // // Also hosts the device-picker model (CONTROLLER mode): the available session // renderers + the active renderer id, plus the set-active callback the flyout // rows invoke to switch playback to another renderer (or back to "Play here"). export global QconnectDevState { in-out property open: false; in property status; // Joined log text (newest first, one entry per line). A single string so the // modal can render it in a read-only TextEdit (selectable + copyable). in property log-text; callback clear(); // Device picker. in property <[QconnectDevice]> devices: []; in property active-renderer-id: -1; callback set-active(int); // renderer-id; the local id = "Play here" } // Single global hover-tooltip channel. Each hoverable control writes its text // + absolute anchor (window coords: centre-x and top-y of the control) here on // hover; one TooltipOverlay (mounted last, on top of everything) renders the // bubble, clamped to the window so it never clips or hides behind a neighbour. export global TooltipState { in-out property 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); // Reorder the upcoming list: move the page-local row `from` to the insertion // slot `to` (0 = before the first upcoming row, N = after the last). The Rust // controller resolves the page-local indices to queue-wide upcoming indices // and routes to the cloud when connected (WS-authoritative). callback reorder(int /* from */, int /* to */); // Empty the queue. callback clear-queue(); // Toggle the favorite state of the now-playing track. callback toggle-now-playing-favorite(); // Save the current queue as a new playlist. callback save-as-playlist(); // Toggle infinite-play (auto-refill the queue with similar tracks). callback toggle-infinite-play(); // Re-filter the upcoming list after `search-query` changed. callback search-changed(); // Move to the previous / next paginator page. callback prev-page(); callback next-page(); // Switch the active tab (0 = Queue, 1 = History). callback set-tab(int); // Fired when the sidebar becomes visible, so the Rust controller can // pull a fresh queue snapshot (the queue may have changed while the // panel was closed, e.g. after a session restore). callback panel-opened(); // Reflects whether infinite-play is currently enabled. in property 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); } // ============================ Offline MODE ================================ // App-wide offline MODE status (the app operating without Qobuz — NOT the // offline CACHE/downloads). Mirrors `qbz_app::offline_mode::OfflineStatus`; // pushed by crate::offline_mode::start_ui_forwarder on every engine // broadcast. Read by the login screen (offline-boot affordances) and the // shell (D2 recovery banner); the gating slice adds more consumers. export global OfflineState { // Any offline flavor active (mode != online). in property offline: false; // 0 = online, 1 = real offline, 2 = induced offline. in property mode: 0; // Raw connectivity, independent of the mode: 0 unknown, 1 up, 2 down. in property connectivity: 0; // A probe was answered by a redirect — typical captive portal. in property captive-portal: false; // D2: unauthenticated offline session while connectivity is back up — // the shell shows the one-click "Sign in" banner. in property show-recovery-banner: false; // A previous login exists (last_user_id on disk). Gates the login // screen's "Start offline" affordance: offline mode needs a prior // user's data, never the empty user-0 profile. in property has-previous-session: false; // Startup session-restore failure detail, shown on the login screen // ("" = none). Set by the restore task's error arm; cleared on any // successful shell entry. in property login-error: ""; } // Settings > Offline, MODE group — Rust-wired actions (crate::offline_mode). export global OfflineModeActions { // (Re)seed the MODE-group toggle states from the engine store. Fired by // the panel's init, so every mount of Settings > Offline re-reads them. callback load(); // Force an immediate connectivity re-probe (the status row's "Check // now"). The status text updates by itself via the engine forwarder. callback check-now(); }