// 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 } // Re-export the hotkey text-input guard from its leaf file so existing // `import { UiFocusState } from "state.slint"` sites keep working, while // low-level primitives import it directly from "focus-state.slint" (avoids a // recursive import — state.slint imports JumpNavBar's JumpNavTab). export { UiFocusState } from "focus-state.slint"; 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) selected: bool, // checkbox state in a multi-select grid (LocalLibrary) // --- 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 text — now the hover tooltip icon: image, // the button glyph (the bar is icon-only) danger: bool, // red-bordered destructive button (e.g. Remove) needs-selection: bool, // dimmed + disabled while nothing is selected } // One titled discography bucket on the artist page (Albums, EPs & Singles, // Live, …). Distinct from DiscoverSection so it can carry the raw server // release_type key, the per-section sort, and the load-more flag without // bleeding artist-only fields into Discover. export struct ArtistReleaseSection { release-type: string, // raw server key (album/epSingle/live/…) title: string, albums: [AlbumCardItem], has-more: bool, // gates the per-section "Load more" affordance sort-by: string, // default/newest/oldest/title-asc/title-desc } 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, } // One editorial award on an album — the sidebar AWARDS list. Carries the // award id (may be "" — some /album/get entries omit it; the controller // then resolves it by name) so the laurel card can navigate to the award // page. Mirrors Tauri's `AlbumAward { id?, name }`. export struct AwardEntry { id: string, name: string, } // 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, // First tag name (uppercased accent subtag under the title; "" = none). // Only the single-cover Discover playlist cards populate it. category: string, // Dominant colour of the single cover, filling the contain-fit letterbox // on Discover playlist cards (default neutral dark; replaced once the // cover decodes). Mosaic cards ignore it. dominant-color: color, } // One selectable category tag for the Qobuz Playlists multi-select filter. // `name` is pre-localized by the discover-index response. export struct PlaylistTagItem { slug: string, name: string, selected: bool, } // One row in the search cortinilla (the live as-you-type dropdown under the // header search box). Frontend-agnostic: a flat row carrying everything the // SlimCard-style render needs. `source` is "qobuz" or "local" — it selects // which controller seam fires on click (Qobuz media-action vs local play). // `kind` is the navigable category ("album" | "artist" | "track" | // "playlist" | "localTrack"). Declared before HomeState so the cortinilla // overlay (a separate component file) can import it from state.slint. export struct CortinillaRow { kind: string, // "album" | "artist" | "track" | "playlist" | "localTrack" id: string, source: string, // "qobuz" | "local" title: string, subtitle: string, artwork-url: string, // raw URL/fs path — drives the controller artwork job artwork: image, // empty until the artwork pipeline resolves it // Stable flat selection index across ALL cortinilla rows: 0 = top-result, // then 1.. enumerated section by section (declaration order). Set by the // controller so the overlay needs no cross-loop accumulator (Slint has // none). Used for click dispatch AND keyboard/hover highlight. flat-index: int, } // One labelled section in the cortinilla (e.g. "Albums", "Tracks", // "From your library"). `kind` is the category these rows belong to (drives // `cortinilla-view-more`). `has-more` toggles the trailing "View more" row. export struct CortinillaSection { title: string, kind: string, rows: [CortinillaRow], has-more: bool, } // Discover / Home screen state. export global HomeState { in property <[DiscoverSection]> sections: []; in property <[SlimItem]> popular: []; in property <[SlimItem]> recent: []; in property <[AlbumCardItem]> recent-albums: []; // The #566 ported rails — same data pipelines as their ForYouState twins // but separate lifecycles (Home loads them eagerly with the discover // index; For You lazily on first open). The HomeView arms self-hide // while empty. Titles: favorite-albums / release-watch are set by Rust; // top-artists' title is @tr in the view arm (bare list, like For You). // (qobuzMixes, the fourth ported section, is static tiles — no state.) in property favorite-albums; in property release-watch; in property <[SlimItem]> top-artists: []; // Qobuz Playlists row (Home + Editor's Picks). Single-cover cards; the // active tab's set is swapped in by crate::home::select_tab. in property <[SearchPlaylistItem]> playlists: []; // Category tags for the Qobuz Playlists multi-select filter (from the // discover-index `playlists_tags`). Empty when the response carries none. in property <[PlaylistTagItem]> playlist-tags: []; // Number of currently-selected category tags (0 = show all). in property playlist-tag-count: 0; 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 + the Qobuz Playlists category filter. Wired in // main.rs to crate::home. export global HomeActions { callback select-tab(string); // Toggle one category tag (by slug) in/out of the playlists filter — // client-side, re-filters the cached row. Empty selection = show all. callback toggle-playlist-tag(string); // Clear all selected category tags (back to showing every playlist). callback clear-playlist-tags(); // "View all" on the Recently Played Albums rail -> the full-page listing // (RecentAlbumsView), fed from the local play-history store. callback open-recent-albums(); } // RecentAlbumsView — the full "Recently Played Albums" page opened from the // Home rail's "View all". Fed from the LOCAL play-history store (capped at // 24, see crate::recently), so it needs no paging/search state; the loader // repopulates `albums` on every navigation. export global RecentAlbumsState { in property <[AlbumCardItem]> albums: []; in property loading: false; } // --------------------------------------------------------------------------- // Discover section configurator (Slice 5) — prefs-driven render loop + modal. // --------------------------------------------------------------------------- // One Discover section as a render-loop descriptor. Rust emits the ordered // ENABLED list per tab (DiscoverState.home-sections / editor-sections / // foryou-sections); each surface iterates and dispatches on `id` // (DiscoverySectionId::as_str). `kind` is a coarse render family used by the // one tab-dependent arm (mostStreamed renders as a slim grid on Home, an album // carousel on Editor's Picks). `section` carries the album-carousel payload for // Home arms that share the Carousel component but have no dedicated HomeState // field (newReleases / pressAwards / idealDiscography / editorPicks / // qobuzissimes / mostStreamed-album / recentlyPlayedAlbums); For You arms read // the typed ForYouState fields by id and ignore `section`. export struct SectionDescriptor { id: string, // "newReleases" | "qobuzPlaylists" | "qobuzMixes" | ... kind: string, // "albumCarousel" | "slimGrid" | "playlistCarousel" | ... section: DiscoverSection, } // One row in the configurator modal: a section in the active tab's FULL ordered // list (enabled AND disabled — reorder applies to disabled rows too). `label` // is resolved in Rust from the id->key map (Slint @tr cannot take a runtime // key), returning the English string until the Slint gettext pipeline lands. export struct ConfigRow { id: string, label: string, enabled: bool, } // Single owner of the configurator render-driver lists AND the modal payload. // `active-tab` lives here once (render loop + modal target read it); the Slice-3 // tab pill keeps writing HomeState.active-tab and crate::home::select_tab mirrors // it into here (one extra set, no second tab-switch path). export global DiscoverState { // "home" | "editorPicks" | "forYou". home::select_tab writes this. in-out property active-tab: "home"; // Ordered ENABLED descriptors per tab (Rust recomputes on every mutation + // on tab switch). Per-tab so a tab flip is a pure binding read. When // active-tab is "forYou" the home/editor lists are pushed empty so the Home // repeater renders nothing for that tab (unconditional-repeater form). in property <[SectionDescriptor]> home-sections: []; in property <[SectionDescriptor]> editor-sections: []; in property <[SectionDescriptor]> foryou-sections: []; // Configurator modal payload (for the modal's current target = active-tab). in-out property configurator-open: false; in property <[ConfigRow]> config-rows: []; // FULL list (enabled+disabled). in property enabled-count: 0; in property total-count: 0; } export global DiscoverActions { callback open-configurator(); // gear -> opens for active-tab callback close-configurator(); callback toggle-section(/* tab */ string, /* id */ string); callback move-section(/* tab */ string, /* id */ string, /* dir -1|+1 */ int); callback reset-tab(/* 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; // Favorite Albums — the user's favorited albums (For You #6). in property favorite-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; } // Discover > Recommendations (the 4th tab). External-recommendations engine // (Last.fm + ListenBrainz -> Qobuz) carousels, plus the Qobuz-editorial // cold-start fallback. Mirrors ForYouState's lazy-load latch + self-hiding // rows (each row's `length > 0` gate). Populated by crate::external_reco. export global ExternalRecoState { in property loading: false; in-out property loaded: false; // Per-row "still loading" flags. The controller sets the rows it is about // to build true at spawn and clears each as its builder resolves, so a SLOW // row (the Weekly ListenBrainz playlists resolve a few seconds after the // fast rows) shows its own skeleton instead of silently missing — the UI now // signals "more is still loading" during the progressive paint. in property pending-rec-artists-common: false; in property pending-rec-artists-recent: false; in property pending-rec-albums: false; in property pending-fresh-releases: false; in property pending-weekly-exploration: false; in property pending-weekly-jams: false; in property pending-deep-cut-albums: false; in property pending-top-albums: false; in property pending-top-artists: false; // True when no external source is connected: the editorial fallback // (top-albums + top-artists) shows instead of the personalized rows. in property editorial-fallback: false; // Recommended artists from your COMMON taste (overall top -> similar). in property <[SlimItem]> rec-artists-common: []; // Recommended artists from your RECENT taste (1-month top -> similar). in property <[SlimItem]> rec-artists-recent: []; // Recommended albums (Last.fm artist top-albums, not scrobbled). in property rec-albums; // Fresh releases (ListenBrainz, from artists you follow). in property fresh-releases; // Weekly Exploration (ListenBrainz discovery) tracks. in property <[SlimItem]> weekly-exploration: []; // Weekly Jams (ListenBrainz familiar) tracks. in property <[SlimItem]> weekly-jams: []; // Deep-cut albums from artists you know. in property deep-cut-albums; // Cold-start fallback: Qobuz editorial top albums. in property top-albums; // Cold-start fallback: Qobuz editorial top artists. in property <[SlimItem]> top-artists: []; // Results-cache freshness window selector index for the Recommendations tab: // 0=24h 1=36h 2=48h 3=72h. Seeded from discover_prefs at startup; default 48h. in-out property cache-ttl-index: 2; } // Recommendations-tab-only actions: cache-window select + force refresh. Wired to // discover_prefs (persist) + external_reco (refetch). Other Discover tabs unaffected. export global ExternalRecoActions { callback set-cache-ttl(int); // index 0..3 -> persist hours + reseed callback refresh-now(); // force-invalidate the results cache + refetch } // 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, // Artist-blacklist greyout: when true the row renders dimmed + inert // (no play/drag/favorite/download, nav-only menu) with a blacklist // glyph in the number/play cell. Stamped per-row by the controllers // (Task 6) from the user's blacklisted-artist set; never set on local // / Plex rows. Defaults false (Slint struct fields zero-init). is-blacklisted: bool, // 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, // Multi-disc grouping: the disc number whose "Disc N" header should be // rendered ABOVE this row. Set (> 0) only on the first track of each // disc, and only when the album spans more than one disc; 0 otherwise // (single-disc albums and every non-leading track render flat, with no // header). The "Disc" word is translated in the delegate via @tr so it // flows through the gettext pipeline, matching the Tauri markup // `{$t('album.disc')} {discNum}` (groupByDisc + isMultiDisc). disc-header-number: int, // Classical "work" grouping: the work-section header (e.g. "Symphony No. 9 // (Beethoven)") rendered ABOVE this row. Set (non-empty) only on the first // track of each consecutive same-work run, and only on albums that carry // work metadata; "" otherwise (non-classical albums and every non-leading // track render flat, with no header). Mirrors the official Qobuz player's // per-work sections (PR #536); the album view stamps it, all other track // surfaces leave it "" so the row stays flat. // // E3: `work-header` now holds the work TITLE only; the composer is split // out below so its name can be a clickable link to the artist page. work-header: string, // Classical work composer (E3): rendered as a clickable artist link inside // the work header's parentheses. work-composer-id "" => plain (no link), // and an empty name => no parenthetical at all. Set only on the work // header's leading row, by the album view; "" everywhere else. work-composer-name: string, work-composer-id: string, } // Album detail screen state. // One credited album artist for the header credit line (E1: full // "Artist • Pianist • Composer (compositor)" list, not just the interpreter). export struct ArtistCredit { // Qobuz artist id as a string (clickable → artist page). "" = not clickable. id: string, name: string, // Localized role suffix shown in parens (e.g. "compositor"); "" for the // main artist(s). role: string, } export global AlbumState { in property id; in property title; // Primary interpreter (kept for back-compat: now-playing, fallbacks). in property artist; in property artist-id; // Full credited-artist list with roles for the header credit line. in property <[ArtistCredit]> artists: []; // 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; // Meta-line split around the label so the header can render the label // as a clickable link: meta-pre (year) + [label] + meta-post // (genre • N tracks • duration). info-line stays the fallback. in property meta-pre; in property meta-post; 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; // Half-length truncation used when the content area is space-constrained // (ShellState.content-constrained) so the header stays compact and the // tracks keep priority. Falls back to the full short text when there's room. in property description-shorter; in property artwork; // Static Immersive-style blurred artwork field for the header backdrop. in property header-atmosphere; // Artwork-derived tint for the header fallback 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 <[AwardEntry]> awards: []; // True when the album ships a downloadable booklet/liner-notes PDF // (Qobuz goodies). Gates the booklet button in the header — not every // album has one. in property has-booklet: false; // True when EVERY track of the album is cached offline (cache-status 3). // Swaps the header menu's "Make available offline" for "Refresh offline // copy" (Tauri's isAlbumFullyDownloaded). Kept live as downloads land. in property album-fully-cached: false; // Whether the album is in the user's favorites (library). Drives the // header heart (filled accent when on). Seeded from fav_cache on open, // flipped optimistically on toggle. in property is-favorite: false; // True while a favorite toggle is in flight (spinner on the heart). in property favorite-loading: false; // Whether THIS album is blocked (album blacklist). Drives the header menu // toggle ("Block this album" / "Unblock album"). Seeded on open. in property is-album-blocked: false; // Multi-select (track selection) mode for the track list. in-out: the // toolbar toggle flips it from the UI; Rust resets it on album change. in-out property multi-select: false; // Number of selected rows (drives the bulk bar count + select-all label). in property selected-count: 0; 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; // ---- Bottom carousels (webplayer parity) + external links ----------- // "More from this artist" carousel ("Del mismo artista..." on the // webplayer / "By the same artist" in Tauri). Populated after the album // renders from /artist/getReleasesGrid. `endpoint` non-empty on the // section shows the "View all" link (-> artist discography). in property more-from-artist; in property show-more-from-artist: false; // "Listening suggestions" carousel ("Sugerencias de escucha" on the // webplayer) from /album/suggest. Album cards -> open album. in property suggestions-section; in property show-suggestions: false; // Second suggestions row, sourced from Last.fm similar artists (one album // each), deduped against the Qobuz row. Sits under the same heading, gated // on Last.fm being connected. Empty -> hidden. in property lastfm-suggestions-section; in property show-lastfm-suggestions: false; // External links sidebar block (Last.fm / Discogs / MusicBrainz), // Feishin-style. URLs are prebuilt by the controller from artist + title; // each row opens its url via AlbumActions.open-external-link. Hidden for // local-only albums (no Qobuz artist/title to search). in property show-external-links: false; in property lastfm-url; in property discogs-url; in property musicbrainz-url; } // --------------------------------------------------------------------------- // Track Info + Album Info (Credits/Review) modals — 1:1 port of Tauri's // TrackInfoModal.svelte + AlbumCreditsModal.svelte. Data is fetched fresh by // the Rust controller (crate::info_modals) via core.get_track/get_album and // mapped to these plain structs. Roles are parsed + grouped + localized in // qbz_qobuz::performers (frontend-agnostic). // --------------------------------------------------------------------------- // One grouped-by-role credit cell in the Track Info grid. `role` is the // already-localized, UPPER-CASED label; each name is clickable -> musician. export struct InfoCreditRow { role: string, // Original (non-localized) role string, passed to the musician-nav hint. role-raw: string, names: [string], // Names pre-joined with ", " — rendered as a single wrapping Text so a long // credit list (e.g. 5 producers) wraps instead of clipping at the column edge. names-line: string, } // A row in the Track Info 2-column credits grid. The Rust side pairs the // ordered cells (left + optional right) so the modal needs no dynamic grid // placement. `has-right` is false on a trailing odd cell. export struct InfoCreditPair { left: InfoCreditRow, right: InfoCreditRow, has-right: bool, } // One performer line inside an expanded Album-Credits track. `roles` is the // raw ", Role1, Role2" suffix (empty when the performer has no roles). export struct AlbumCreditPerformer { name: string, roles: string, // First role (clean) — the musician-nav role hint (Tauri roles[0]). primary-role: string, } // One track row in the Album-Credits "Credits" tab. export struct AlbumCreditTrack { id: string, number: string, title: string, artist: string, // True when the row has performers or a copyright line (expandable). has-credits: bool, performers: [AlbumCreditPerformer], copyright: string, } // Track Info modal — opened from the NPB (i) button / song-card title / // TrackRow context menu via media-action("track", id, "track-info"). export global TrackInfoState { in-out property open: false; in property loading: false; in property error: ""; in property title; in property album; in property artist; // "" -> artist rendered as plain text (not a link). in property artist-id; in property duration; in property quality; // "" -> the row is hidden. in property isrc; in property label; in property label-id; // Paired (2-column) ordered credit cells. in property <[InfoCreditPair]> credits: []; // Credits split into two independent columns (even-index -> left, // odd-index -> right) so the modal renders two natural vertical stacks // instead of height-coupled paired rows (which left vertical gaps). in property <[InfoCreditRow]> credits-left: []; in property <[InfoCreditRow]> credits-right: []; in property copyright; } export global TrackInfoActions { callback close(); callback open-artist(string /* artist-id */); callback open-label(string /* label-id */); callback open-musician(string /* name */, string /* role */); // Populate TrackInfoState for the given track WITHOUT opening the floating // modal — used by the immersive split Track Info panel, which renders the // same data inline. Fetch+map+apply are identical to open-track-info; the // ONLY difference is `open` stays false so the overlay modal never appears. callback load-inline(string /* track-id */); } // One card in the immersive Suggestions panel (data-panels.md §6): either a // curated artist PLAYLIST (`kind == "playlist"`, book-collage of up to 3 album // covers) or the seed-track SONG RADIO (`kind == "radio"`, diamond-collage of // up to 4 covers). `cover-urls` is the artwork pipeline's input (slot-indexed); // `covers` holds the decoded images, applied per-slot by the controller. Slots // 0-3 are addressed via the parallel cover0..cover3 fields (Slint structs can't // have per-element image arrays painted in place, so flat slots are used — same // pattern as the search/favorites playlist collages). export struct SuggestionCard { // "playlist" | "radio". kind: string, title: string, subtitle: string, // Remote URLs for the (up to 4) collage slots — the artwork-job input. cover-urls: [string], // Decoded collage covers, painted per-slot by the artwork pipeline. cover0: image, cover1: image, cover2: image, cover3: image, // Playlist-card target id ("" for the radio card). playlist-id: string, // Radio-card seed (empty for playlist cards). seed-track-id: string, seed-track-name: string, seed-artist-id: string, // "qobuz" (playlist) | "qbz" (radio) — drives the corner badge glyph. badge: string, // Radio card: true while the Song Radio session is building (spinner). loading: bool, } // Immersive Suggestions panel state (split-only, split-panel == 2). Populated // by crate::suggestions::apply_suggestions; the recommended-track list reuses // the shared TrackItem struct. export global SuggestionsState { in property loading: false; in property error: ""; // The artist whose suggestions are loaded (dedup guard mirror). in property artist-id: ""; // The seed track these suggestions were built for. in property seed-track-id: ""; in property <[SuggestionCard]> cards: []; in property <[TrackItem]> tracks: []; } export global SuggestionsActions { // Play / queue / play-next a curated artist playlist by id. callback play-playlist(string /* playlist-id */); callback queue-playlist(string /* playlist-id */); callback play-next-playlist(string /* playlist-id */); // Start the seed-track Song Radio: (seed-track-id, seed-track-name, seed-artist-id). callback start-radio(string, string, string); // Play a single recommended track by id. callback play-track(string /* track-id */); // Load suggestions for the given track id (the Rust side reads the // artist-id + name off NowPlayingState). Fired on panel entry and on // now-playing track change while the panel is open. A "" id resets to the // empty (no-track) state. callback load(string /* track-id */); } // Playlist "Suggested Songs" section (T8). 1:1 port of the Svelte // `PlaylistSuggestions` component: a fixed page of suggested tracks (6 // visible) with per-row add / play / info / dismiss + play-on-hover, // driven by a Rust-held pool + pagination (crate::playlist_suggestions). // DISTINCT from the immersive SuggestionsState/Actions above (different // surface, different controller) — the name split is deliberate. export struct PlaylistSuggestionRow { // Qobuz ids carried as STRINGS (parsed to u64 in Rust) — matches the // shared TrackItem id convention; Qobuz catalog ids can exceed Slint's // 32-bit `int`, and album ids are alphanumeric. track-id: string, title: string, artist-name: string, artist-id: string, album-title: string, album-id: string, // Remote cover URL (artwork-job input) + the decoded thumbnail. artwork-url: string, artwork: image, duration-label: string, // Dev-mode "why suggested" string (empty in production — include_reasons=false). reason: string, // True while this row's add call is in flight; flips to `added` (a brief // check state) on success, just before the row leaves the pool. adding: bool, added: bool, } // Playlist suggestions section state. Populated by // crate::playlist_suggestions::*; the row list reuses PlaylistSuggestionRow. export global PlaylistSuggestionsState { // The user has launched suggestions for the open playlist (manual // activation gate — mirrors the Svelte `suggestionsActivated`). Until // then the section shows only the "Suggest Songs" CTA. in-out property activated: false; in property loading: false; // A background pool expansion (load-more / auto-variety) is in flight. in property loading-more: false; // More pages are available beyond the visible 6 (drives the refresh hint). in property has-more: false; // The filtered pool is exhausted (nothing left to suggest) — the loaded // section hides itself, matching the Svelte `isEmpty` branch. in property is-empty: false; in property error: ""; in property <[PlaylistSuggestionRow]> rows: []; } export global PlaylistSuggestionsActions { // Launch suggestions for the open playlist: gather seed artists + // exclude ids off the loaded Qobuz tracks, then fetch the first pool page. callback activate(); // Advance the visible page (wraps + expands the pool on cycle — Svelte parity). callback refresh(); // Add a suggested track to the open playlist, then drop it from the pool. callback add-track(string /* track-id */); // Preview / play a single suggested track now. callback play-track(string /* track-id */); // Dismiss a suggestion (sticky per-playlist) + drop it from the pool. callback dismiss-track(string /* track-id */); // Open the Track Info modal for a suggestion. callback show-info(string /* track-id */); // Navigate to a suggestion's album / artist. callback go-album(string /* album-id */); callback go-artist(string /* artist-id */); } // Album Info modal — opened from the AlbumPageView header (i) button via // media-action("album", id, "info"). export global AlbumInfoState { in-out property open: false; in property loading: false; in property error: ""; // "credits" | "review". The Review tab only exists when has-review. in-out property active-tab: "credits"; in property has-review: false; in property artwork; in property title; in property artist; in property label; in property label-id; // Localized "September 2, 2021" ("" hides the date span). in property release-date; // "Hard Rock · 10 tracks · 1h 21m". in property meta-line; // "24-Bit / 96 kHz" ("" hides). in property quality; in property <[AlbumCreditTrack]> tracks: []; // HTML-stripped editorial review ("" -> no Review tab). in property review; } export global AlbumInfoActions { callback close(); callback set-tab(string /* "credits" | "review" */); callback play-track(string /* track-id */); callback open-label(string /* label-id */); callback open-musician(string /* name */, string /* role */); } // Album-view actions wired to Rust callbacks. export global AlbumActions { // Filter the visible track list by a query (client-side). callback search(string); // Toggle multi-select (track selection) mode on the album track list. callback toggle-multi-select(); // Bulk-bar action over the selected rows (id = "select-all" | "play-next" // | "queue" | "add-to-playlist" | "add-to-favorites" | "make-offline" | // "clear"). callback bulk-action(string); // Per-disc context-menu action on a multi-disc album's "Disc N" header — // scoped to that disc's tracks only (action = "play" | "shuffle" | "queue" // | "play-next"). The whole-album header buttons are unaffected. callback disc-action(int /* disc number */, string /* action */); // Open an external link (Last.fm / Discogs / MusicBrainz) in the system // browser. The url is prebuilt by the controller (AlbumState.*-url). callback open-external-link(string /* url */); } // --------------------------------------------------------------------------- // Booklet (digital liner-notes PDF) reader — 1:1 port of Tauri's // BookletViewer.svelte. The Rust controller (crate::booklet) downloads the // album goody PDF, rasterizes pages via the qbz-booklet crate (MuPDF), and // pushes the decoded page image here. Mounted as a top overlay in AppShell // (ADR-009 z-index >= 3000). // --------------------------------------------------------------------------- export global BookletState { // Modal open / lifecycle. in-out property open: false; // True while the PDF downloads or a page renders (spinner). in property loading: false; in property error: false; in property error-text; // Album title for the modal header. in property title; // The current rendered page image + its pixel size (for sizing/scroll). in property page-image; in property page-pixel-width: 0; in property page-pixel-height: 0; // 1-based current page + total page count. in property current-page: 1; in property num-pages: 0; // Zoom factor (1.0 = fit-width baseline) + cumulative rotation in degrees. in property zoom: 1.0; in property rotation: 0; } export global BookletActions { callback close(); callback next-page(); callback prev-page(); callback zoom-in(); callback zoom-out(); callback fit-width(); callback rotate(); // Save the open booklet PDF to a user-chosen location. callback download(); } // 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, artist-releases, settings, search, musician, label, label-releases, award, award-albums, location, favorites, mix, playlist, discover-browse, recent-albums, playlist-manager, offline-manager, blacklist-manager, local-library, local-album, mixtapes, collections, mixtape-detail, discography-builder, purchases, purchase-album, now-playing } // 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 in-out property albums-multi-select: false; // album checkboxes on in property albums-selected-count: 0; // bar count + select-all // --- 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: ""; // SQL-side sort (defines the pagination order): "default" | "artist-asc" // | "artist-desc" | "title-asc" | "title-desc" | "year-desc" // | "year-asc" | "added-desc" in-out property tracks-sort: "default"; // 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 / toggle-select). The Albums // grid honors the shared AlbumCollectionView contract. callback album-action(string /* album id */, string /* action */); // Toggle the Albums-grid multi-select mode. callback albums-toggle-multi-select(); // Albums bulk bar action: select-all | clear | queue | play-next | // add-to-playlist | add-to-mixtape. callback albums-bulk-action(string /* action */); // Windowed grid: the visible row band changed (item indices into // albums-visible, prefetch margin included) — dispatch artwork for it. callback albums-window-changed(int /* first */, int /* last */); // --- 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 */); // SQL sort key (default|artist-asc|...|added-desc), persisted; page-1 re-query. callback tracks-set-sort(string /* key */); // 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 */); // Per-row context-menu actions beyond "play" (play-next / queue / // add-to-playlist) — resolved against the open version's track cache. callback track-menu-action(string /* track id */, string /* action */); callback select-version(int); // Client-side track filter (mirrors AlbumActions.search), applied over the // current version's tracks. callback search(string /* query */); // Per-disc context-menu action on a multi-disc local album's "Disc N" // header — scoped to that disc's tracks only (action = "play" | "shuffle" | // "queue" | "play-next"). The whole-album header buttons are unaffected. callback disc-action(int /* disc number */, string /* action */); } // ============================ 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 } // ============================ Scrobblers ================================== // Settings > Integrations — source-agnostic Last.fm + ListenBrainz scrobbling. // State mirrors `scrobbler_settings.db`; actions delegate to the Rust // `scrobble` controller (auth flows + the now-playing/scrobble fire live // there). ADR-008: no pills. // Discord Rich Presence (Settings > Integrations). Opt-in, default OFF; the // `enabled` flag is seeded from ui_prefs and `set-enabled` persists + applies it. export global DiscordState { in-out property enabled: false; callback set-enabled(bool); } export global ScrobbleState { // --- Master section toggles ------------------------------------------- in-out property enabled: false; // master toggle (default OFF) in-out property ui-collapsed: false; // collapse chevron // --- Last.fm ---------------------------------------------------------- in-out property lastfm-enabled: false; // per-service scrobble flag in property lastfm-authed: false; // session key present in property lastfm-username: ""; // "Signed in as ..." in property lastfm-auth-url: ""; // non-empty after connect in property lastfm-busy: false; // get_token / get_session in flight // --- ListenBrainz ----------------------------------------------------- in-out property listenbrainz-enabled: false; in property listenbrainz-authed: false; in property listenbrainz-username: ""; in-out property listenbrainz-token-input: ""; // token field buffer in property listenbrainz-busy: false; // set_token in flight // --- Shared status line (0 none, 1 info, 2 ok, 3 error) --------------- in property status-text: ""; in property status-kind: 0; } export global ScrobbleActions { callback load(); // panel init: seed from store callback enable-toggle(bool); callback collapse-toggle(bool); // Last.fm. callback lastfm-enable-toggle(bool); callback lastfm-connect(); // get_token + open browser callback lastfm-open-auth-url(); // re-open authorize URL callback lastfm-confirm(); // get_session after approve callback lastfm-disconnect(); // ListenBrainz. callback listenbrainz-enable-toggle(bool); callback listenbrainz-set-token(string); callback listenbrainz-disconnect(); } // ============================ 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 "") // "" (Qobuz) | "local" | "offline" — local playlists (library.db, // ids "local:") show a hard-drive glyph instead of list-music. local-kind: string, 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(); // Open the playlist importer modal (sidebar "..." menu). callback import-playlist(); } // "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; // D8 "Offline-only playlist" opt-in: ON -> created as a LOCAL entity // (library.db, id "local:") that never reaches Qobuz. in-out property offline-only: false; // True while the app is offline: creation always produces a local // playlist, so the toggle shows ON and locked with a hint. in property offline-locked: false; } 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; // Followed (subscribed) on Qobuz — an account-level follow that syncs // across Qobuz clients, distinct from the internal favorite heart. in property is-following: false; // Already copied into the user's own playlists this session (hides the // Copy action so it isn't duplicated). in property is-copied: false; // LOCAL playlist (id "local:", library.db entity — never on // Qobuz). Qobuz-bound header actions (favorite / cache) hide; the // "Upload to Qobuz" action appears when not offline-only. in property is-local: false; // D8: never offered for upload, never reaches Qobuz/QConnect. in property offline-only: false; // D11.a: a MIXED (Qobuz-id) playlist rendered OFFLINE from its local // sidecar rows only (the Qobuz membership is not enumerable offline). // Lets the AppShell mount the detail instead of the OfflinePlaceholder. in property offline-subset: 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; // LOCAL playlist (id "local:") — edits route to library.db and // the offline-only toggle below shows. in property is-local: false; // D8 flag, editable so an offline-created playlist can be unmarked // later and gain the "Upload to Qobuz" action. in-out property offline-only: 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, // LOCAL playlist entity (id "local:", library.db) — the card / // list-row shows a hard-drive marker; offline-only flavors label it. is-local-playlist: bool, offline-only: 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 */); } // === Artist Blacklist Manager (Tauri's BlacklistManagerView) ============ // The standalone manage-blacklist surface reached from the Settings content- // filtering row. One blacklisted-artist row carries everything the view needs; // the controller (crate::blacklist_manager) precomputes the date string + the // has-notes flag so the .slint does no formatting. Search filtering is done // controller-side: `items` already holds only the filtered rows, while `count` // stays the FULL list count (drives the badge + the empty/no-results split). export struct BlacklistedArtistItem { // Numeric Qobuz artist id (fits an int for display + passing back on click // / remove). Slint has no u64/i64; the controller passes it through as int. artist-id: int, artist-name: string, // Unix seconds (kept for reference; the view renders `added-display`). added-at: int, // Controller-formatted "MMM D, YYYY" (English; gettext for Rust is unwired). added-display: string, notes: string, has-notes: bool, } export struct BlacklistedAlbumItem { // Qobuz album id is a STRING (alphanumeric), unlike the artist int id. album-id: string, album-title: string, artist-name: string, // Network cover URL; resolved to `cover` by the artwork pipeline. cover-url: string, cover: image, added-at: int, added-display: string, notes: string, has-notes: bool, } export global BlacklistState { // The CURRENTLY-VISIBLE rows (already search-filtered by the controller). in property <[BlacklistedArtistItem]> items: []; // Global enable flag (default-enabled, mirrors the backend default). // Shared by both axes (artists + albums). in property enabled: true; in property loading: true; // FULL list count (NOT the filtered length) — drives the count badge and // the empty-vs-no-results branch (count==0 = empty; count>0 && items empty // = no search results). in property count: 0; // Echoed back so the search input reflects the controller's query (and the // no-results message can interpolate it). in property search-query: ""; // --- Album axis (parallel String-keyed list, shown under the Albums tab) --- in property <[BlacklistedAlbumItem]> album-items: []; // FULL blocked-album count (drives the Albums tab badge + empty state). in property album-count: 0; // Active manager tab: 0 = Artists, 1 = Albums. in property active-tab: 0; } export global BlacklistActions { // Forward-open seam: the Settings content-filtering row (T10) calls this to // navigate INTO the manager (records the nav entry, swaps the view, loads). // Mirrors OfflineManagerActions.open — the back chrome is NavButtons. callback open(); callback back(); callback artist-select(int /* artist id */); callback toggle-enabled(); callback remove(int /* artist id */); callback clear-all(); callback search-changed(string /* query */); // --- Album axis --- callback set-tab(int /* 0=artists, 1=albums */); callback album-select(string /* album id */); callback remove-album(string /* album id */); callback clear-all-albums(); // Block an album from a context menu (id + display fields for the manager). callback block-album(string /* id */, string /* title */, string /* artist */, string /* cover-url */); } // === 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, // Numeric Qobuz artist id resolved by the resolveItems pass ("" until it // lands / for local-Plex items). Routes the artist link to the QOBUZ // artist page; without it the link can only fall back to the // LocalLibrary Artists tab (name-based). artist-id: string, // 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. Qobuz items route by the resolved numeric // artist-id to the Qobuz artist page; local/Plex items (empty id) route // by NAME to the LocalLibrary Artists tab. callback open-artist(string /* source */, string /* artist-name */, string /* artist-id */); // --- 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; // DSD delivery mode (DSD plan Phases 2-3): Convert to PCM / DoP / // Native DSD. Only meaningful on the ALSA direct backend. in-out property <[string]> dsd-modes: []; in-out property dsd-mode-index: 0; 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 backend-is-jack: 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; // Discover — opt-out: show the external "Recommendations" tab. Default on. // Seeded from persisted discover prefs at session start (crate::discover_prefs). in-out property show-recommendations: true; // MusicBrainz metadata — opt-out (default on). Drives the core client's // enabled flag, the artist Network/Scene sidebar (mb-available), and the // playlist Suggested-Songs CTA. Seeded from ui_prefs at session start // (crate::discover_prefs::seed); persisted via the "musicbrainz" key. in-out property musicbrainz-enabled: true; 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; // Playback — "Auto-connect Qobuz Connect on startup" dropdown // (remember-last / on / off). Backed by the QConnect settings DB via the // Rust controller, which owns the index -> mode mapping. in-out property <[string]> qconnect-startup-modes: []; in-out property qconnect-startup-index: 0; // Playback — Qobuz Connect device name. The persisted custom override // ("" = unset) plus the effective default name shown as the input // placeholder ("Qbz - {hostname}"). Backed by the QConnect settings DB // via the settings-string callback; applies on the next connect. in-out property qconnect-device-name: ""; in-out property qconnect-device-name-default: ""; // --- 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 the settings-bool key "offline-mode-enabled". in-out property offline-mode-enabled: 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 ------------------------------------------------------------- // Seeded at startup by Rust from the qbz-theme registry // (theme::dropdown_labels), in registry display order. The list below is a // standalone-preview fallback only; the index is DERIVED from the persisted // theme slug (theme::index_for_id), never assumed. 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; // True while the "System" theme is selected (native OS-palette follow). in-out property theme-is-system: false; // Auto-Theme (shown when the appended "Auto (dynamic)" theme is selected). // Source: 0 = System Colors, 1 = Wallpaper Sync, 2 = Custom Image. in-out property theme-is-auto: false; in-out property <[string]> auto-theme-sources: [@tr("System Colors"), @tr("Wallpaper Sync"), @tr("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: ""; // --- Custom theme editor (shown when the appended "Custom" theme is // selected). The editable base is 12 semantic tokens; Rust seeds these // swatch colors from the persisted custom_theme.json and re-derives the // whole palette live on every edit. `custom-open-token` holds the key of the // single token whose inline ColorPicker is expanded ("" = none). in-out property theme-is-custom: false; // Swatch colors for each editable base token, seeded from Rust. in-out property custom-surface-main: #000000; in-out property custom-surface-card: #0a0a0a; in-out property custom-surface-elevated: #1a1a1a; in-out property custom-text-primary: #ffffff; in-out property custom-text-secondary: #cccccc; in-out property custom-accent: #4285f4; in-out property custom-danger: #ef4444; in-out property custom-warning: #fbbf24; in-out property custom-success: #3fae6a; in-out property custom-border: #2a2a2a; in-out property custom-favorite: #ef4444; in-out property custom-is-dark: true; // Key of the token whose inline picker is open ("" = all collapsed). in-out property custom-open-token: ""; // A single token's color changed (drag-live). key = token id, color = value. callback custom-set-token(string /* key */, color /* value */); // A single token committed from the HEX field. key = token id, hex = #rrggbb. callback custom-set-token-hex(string /* key */, string /* hex */); // Re-seed the base from the currently active theme ("Start from current"). callback custom-seed-from-current(); // Flip the custom theme polarity (dark/light) and re-derive. callback custom-toggle-dark(bool /* is-dark */); // --- TYPOGRAPHY / SCALE / LANGUAGE ------------------------------------- // NOTE: the old Tauri-style percentage `ui-scales` placeholder properties // that lived here were superseded by the working interface-size presets // (`ui-scale-options` / `ui-scale-index` in the INTERFACE SIZE block // further down, wired to SLINT_SCALE_FACTOR in main.rs). in-out property <[string]> languages: [@tr("Auto"), "English", "Español", "Français", "Deutsch", "Português", "Русский", "日本語", "Nederlands"]; 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; // Immersive search action — what selecting a result in the Immersive search // cortinilla does (index: 0=Disabled, 1=Replace queue, 2=Play next, // 3=Add to queue). Persisted via appearance-select("immersive-search-action"). in-out property <[string]> immersive-search-actions: [@tr("Disabled"), @tr("Replace current queue"), @tr("Play next"), @tr("Add to queue")]; in-out property immersive-search-action-index: 1; // Immersive default view — which immersive view opens by default. // 0 = Remember last (restore the last view that was open), // 1 = Album Reactive, 2 = Static, 3 = Coverflow, 4 = Spectrum, // 5 = Lyrics, 6 = Queue (all 1-6 pin a FOCUS view-mode foreground). // Persisted via appearance-select("immersive-default-view"). in-out property <[string]> immersive-default-views: [@tr("Remember last"), @tr("Album Reactive"), @tr("Static"), @tr("Coverflow"), @tr("Spectrum"), @tr("Lyrics"), @tr("Queue")]; in-out property immersive-default-view-index: 0; // --- 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; // Intelligent search: smart cache, ranking, and the search preview dropdown. in-out property intelligent-search: true; // 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 ----------------------------------------- // The header IS the title bar (merged model, owner decision 2026-07-03): // it is always present; custom chrome (use-system-title-bar off) makes it // gain the window controls + drag and drops the system decorations. // Defaults are per-OS, seeded from ui_prefs at startup BEFORE the first // show (decorations negotiate at surface creation): Linux = system chrome, // macOS = overlay (native traffic lights over the header). The .slint // default is the safe Linux one. in-out property use-system-title-bar: true; // APPLIED chrome state (what the window was actually created with, vs the // persisted pref above which the settings toggle edits). All chrome // bindings (no-frame, header drag/inset/controls, Purchases placement) // read THIS one. Rust decides per platform how the pref propagates: // Linux mirrors the pref here live (hot apply, today's behavior); macOS // leaves it at the startup value (the overlay attributes are fixed at // window creation — restart to apply). in-out property system-title-bar-active: true; // Tiling-WM variant: frameless without drawn controls / header drag. in-out property hide-title-bar: false; // Phase 2 (deferred): rounded corners matching the desktop decoration. in-out property match-system-chrome: false; // True when GPU acceleration is on (gates match-system-chrome). in-out property hardware-accel-enabled: true; // Purchases nav entry placement (the only per-item relocation kept — // the old search-in-title-bar / nav-tb-* toggles were dropped, owner // decision 2026-07-03). in-out property nav-tb-purchases: false; // Window controls group. in-out property <[string]> wc-positions: [@tr("Left"), @tr("Right")]; in-out property wc-position-index: 1; in-out property <[string]> wc-styles: [@tr("Rectangular"), @tr("Full-height rounded"), @tr("Circular"), @tr("Square")]; in-out property wc-style-index: 0; in-out property <[string]> wc-sizes: [@tr("Small"), @tr("Normal"), @tr("Large")]; in-out property wc-size-index: 1; in-out property <[string]> wc-color-presets: [@tr("Default"), "macOS", "Adwaita", @tr("Monochrome"), @tr("Custom")]; in-out property wc-color-preset-index: 0; in-out property show-window-controls: true; // --- MINIPLAYER -------------------------------------------------------- in-out property <[string]> miniplayer-views: [@tr("Remember last used"), @tr("Micro"), @tr("Compact"), @tr("Artwork"), @tr("Queue"), @tr("Lyrics")]; in-out property miniplayer-view-index: 0; // --- STARTUP ----------------------------------------------------------- in-out property <[string]> startup-pages: [@tr("Home"), @tr("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: [@tr("Auto"), @tr("Mono light"), @tr("Mono dark"), @tr("Color")]; in-out property tray-icon-theme-index: 0; // --- RENDERER ---------------------------------------------------------- // Linux-only: the renderer tier (wgpu / GL / software) is a startup-time // choice there. Hidden by default; Rust flips this on for Linux. macOS is // always Skia/Metal, so the row never shows. in-out property renderer-setting-visible: false; in-out property <[string]> renderer-options: [@tr("Auto (recommended)"), @tr("GPU"), @tr("GPU (compatibility)"), @tr("Software")]; in-out property renderer-index: 0; // --- INTERFACE SIZE ---------------------------------------------------- // Window scale-factor preset (XS 0.8 / Small 0.9 / Default 1.0 / // Large 1.2 / XL 1.5) — startup-time choice, restart to apply. // Index 2 = Default. in-out property <[string]> ui-scale-options: [@tr("Extra small"), @tr("Small"), @tr("Default"), @tr("Large"), @tr("Extra large")]; in-out property ui-scale-index: 2; // --- IMMERSIVE --------------------------------------------------------- in-out property <[string]> immersive-views: [ @tr("Remember last used"), @tr("Coverflow"), @tr("Static"), @tr("Vinyl"), @tr("Visualizer"), @tr("Neon Flow"), @tr("Tunnel"), @tr("Comet"), @tr("Lyrics"), @tr("Queue"), @tr("Split: Lyrics"), @tr("Split: Track Info"), @tr("Split: Suggestions"), @tr("Split: Queue") ]; in-out property immersive-view-index: 0; in-out property <[string]> immersive-background-modes: [@tr("Full"), @tr("Lite"), @tr("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, } // One Magazine/Stories teaser (from /artist/story) shown in the sidebar. export struct StoryItem { title: string, author: string, excerpt: string, url: string, // https://play.qobuz.com/magazine/story/{id} image-url: string, // ready-to-use arc-cdn thumbnail URL image: image, // decoded by the artwork pipeline } export global ArtistState { in property id; in property name; // Whether the user follows (Qobuz-favorites) this artist — drives the // header heart fill; seeded on artist open, toggled by the follow action. in property is-following: false; // 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; // Static Immersive-style blurred artwork field for the header backdrop. in property header-atmosphere; // Artwork-derived tint for the header fallback gradient. in property header-color: #181820; in property <[TrackItem]> top-tracks: []; // "Novedad más reciente" — the single latest-release highlight (last_release). // `has-last-release` gates the block; nullable on the API (hidden for artists // with no recent release, e.g. those with an empty last_release). in property has-last-release: false; in property last-release; // "Appears On" (tracks_appears_on) — flat track section of guest appearances. in property <[TrackItem]> appears-on: []; // Magazine / Stories teasers (from /artist/story) — sidebar section. in property <[StoryItem]> stories: []; in property stories-loading: false; // Popular Tracks multi-select (track selection) mode + selected count — // mirrors AlbumState. in-out: the section toggle flips it from the UI; // Rust resets it on artist change. in-out property top-tracks-multi-select: false; in property top-tracks-selected-count: 0; // Releases grouped into titled sections (Albums, EPs & Singles, ...). in property <[ArtistReleaseSection]> 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; // Whether the CURRENTLY displayed artist is blacklisted. Drives the // overflow-menu Blacklist/Show toggle label + the hidden-artist // banner. Set in navigate_artist from artist_blacklist::is_blacklisted // (which auto-gates on the enabled flag — reads false when the // feature is disabled, which is acceptable here). in-out property is-blacklisted: 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); // Toggle Popular Tracks multi-select mode. callback toggle-top-tracks-select(); // Bulk-bar action over the selected Popular Tracks (id = "select-all" | // "play-next" | "queue" | "add-to-playlist" | "add-to-favorites" | // "clear"). callback top-tracks-bulk-action(string); // Popular Tracks section "more" menu (id = "next-all" | "queue-all" | // "shuffle-all" | "playlist-all"). callback top-tracks-menu-action(string); // Change the persisted sort for one release bucket (release-type, sort). // sort = default | newest | oldest | title-asc | title-desc. callback set-section-sort(string, string); // Load one more page of releases for a bucket (release-type). Capped to // 4 pages on the index; the full list lives on the dedicated page. callback load-more-section(string); // Open the dedicated discography page pre-filtered to a release-type // ("all" for the whole discography). callback open-releases(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; // Active sidebar tab: "network" (Artist Network) | "magazine" (Stories). // Lives here (not ArtistState) so it survives sidebar close/reopen; reset // to "network" on each new artist. in-out property active-tab: "network"; // 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: []; // Multi-select (mirrors AlbumState): mode flag + count. Per-row toggle // routes through the shared media-action("track",...) arm. in-out property multi-select: false; in property selected-count: 0; // 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 */); } // Award pages (AwardView landing + AwardAlbumsView listing) — mirrors // Tauri's AwardView.svelte / AwardAlbumsView.svelte. One global serves // both, like LabelState: the landing renders the hero + `albums` preview // (first /award/getAlbums page) + `other-awards`; the listing renders the // full paginated `albums` with client-side search (`visible`). The grid is // always sourced from /award/getAlbums — never `/award/page.releases` // (user-scoped, hero-only). export global AwardState { in property id; in property name; // Hero image URL (award image, falling back to magazine image) + the // decoded image. Empty image-url => laurel/gold placeholder. in property image-url; in property image; // Magazine name shown under the award name (may be ""). in property magazine-name; // Header follow heart — three states: not-following / following / // toggling (disabled while the API call is in flight). in property is-following: false; in property following-toggling: false; // Loaded album catalog (landing preview OR full listing) + the artwork // pipeline target (stable indices). `visible` is the search-filtered // subset the listing renders (shares `albums` when the query is empty, // so artwork stays live — the DiscoverBrowse/Label pattern). in property <[AlbumCardItem]> albums: []; in property <[AlbumCardItem]> visible: []; in property total: 0; in property has-more: false; in property loading: false; in property load-more-loading: false; // True when the releases load failed (drives the error/retry branch). in property load-error: false; // "Other awards" carousel (from /award/explore, current award excluded). // Reuses SlimItem: title = award name, subtitle = magazine. in property <[SlimItem]> other-awards: []; // True once the landing page-data load resolves (gates the spinner). in property page-loaded: false; // Listing in-view search query + the count actually shown after filter. in-out property search-query: ""; in property shown: 0; } export global AwardActions { // Header: toggle follow on the open award. callback toggle-follow(string /* award id */); // Landing "See all" -> open the full albums listing. callback open-albums(); // "Other awards" card click -> open that award's landing. callback open-award(string /* award id */, string /* award name */); // Listing: fetch the next album page. callback load-more(); // Listing: re-filter `visible` after `search-query` changed (UI thread). callback search(string /* query */); // Releases error branch: retry the failed load. callback retry(); } // Dedicated artist discography page (one release bucket, fully paginated) // reached via "See discography" on the artist page. export global ArtistReleasesState { in property id; // artist id in property name; // artist name (header) in property release-type; // raw server key (album/epSingle/…) in property title; // display title (header) in property <[AlbumCardItem]> albums: []; in property has-more: false; in property loading: false; in property load-more-loading: false; in property load-error: false; in-out property sort-by: "default"; } export global ArtistReleasesActions { // Fetch the next page (infinite scroll / Load more). callback load-more(); // Change the sort (default/newest/oldest/title-asc/title-desc). callback set-sort(string); // Retry after a failed load. callback retry(); } // 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(); } // Interface-size preset (UI scale). Seeded once from Rust before the shell // renders. The actual scaling happens through the window scale factor // (SLINT_SCALE_FACTOR set in main.rs) — this global only lets bindings that // must stay PHYSICALLY constant divide the preset back out (e.g. the window // minimums, so an XL window still fits a 1366x768 screen). export global UiScale { in-out property factor: 1.0; } // 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 { // Kiosk profile active (2.0.2 frente #3). Seeded from Rust at startup and // flipped by the NPB "Kiosk/Desktop mode" toggle; drives that toggle's label. in-out property kiosk-profile: false; // --- Reduce motion (weak renderer tiers) ------------------------------- // Seeded from Rust: true on the femtovg-GL / software tiers, where every // animation frame is a full-window repaint on a weak GPU. When set, // loading indicators / eq bars step at ~8fps from `coarse-tick-ms` // (advanced by one Timer in AppShell) instead of the display-rate // animation-tick(). Same phase math, 60fps -> 8fps repaints. in-out property reduce-motion: false; in-out property coarse-tick-ms: 0; // 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. Large NPB (mode 3) is gated // on the OPEN sidebar (it docks the cover + spectrum at the sidebar bottom); // so when the cycle leaves the open state while Large is active, the bar // reverts LIVE to New (0). The persisted ui_prefs key stays "large", so a // later relaunch (sidebar defaults open) restores Large — the revert is a // compatibility fallback, not a preference change. // Persist the sidebar state / nav placement (wired to ui_prefs in Rust). callback persist-sidebar-state(int); callback persist-nav(bool); callback persist-nav-compact(bool); // Persist the current top-level view for "Startup page = where you left off" // (Rust reads NavState.view and stores only SAFE, id-free views). callback persist-view(); public function cycle-sidebar() { root.sidebar-state = Math.mod(root.sidebar-state + 1, 3); if (root.sidebar-state != 0 && root.npb-mode == 3) { root.npb-mode = 0; } root.persist-sidebar-state(root.sidebar-state); } // 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; // Live WINDOW width (set from the AppWindow root). Views read it to make // content-area responsiveness decisions independent of their own // (sidebar-shrunk) width — e.g. the album Label/Awards sidebar collapses // to an icon rail at the now-playing-bar breakpoint (1366px) WHEN a right // panel is open. Defaults to the preferred window width. in-out property window-width: 1180px; // True when the content area is space-constrained: window at/under the // NPB responsive breakpoint AND a right panel (Queue/Lyrics) is open. out property content-constrained: root.window-width < 1366px && (root.queue-open || root.lyrics-open); // Global pointer position in window coords, fed from winit. Used by // hover-only chrome that must not install a TouchArea over interactive // content (for example auto-hide scrollbars over album cards). in-out property pointer-in-window: false; in-out property pointer-x: -1px; in-out property pointer-y: -1px; // 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; // Header-nav flavor while nav-in-sidebar is OFF: when true, the header // always uses the icon-only compact nav (the sidebar-fully-closed look) // instead of the full text tabs, regardless of the sidebar state. No // effect while nav-in-sidebar is ON. Persisted (`nav_header_compact`). in-out property nav-header-compact: false; // Now-playing bar layout mode: 0 = New (default), 1 = Classic, 2 = Small, // 3 = Large. Persisted in ui_prefs.json (`npb_mode` // "new"|"classic"|"small"|"large"); set on startup from the store and on the // player bar's "Now Playing view" menu select. in-out property npb-mode: 0; // Large NPB (mode 3) is only valid while the sidebar is OPEN (state 0): the // cover + spectrum dock lives at the bottom of the open sidebar. Views read // this to mount the dock and to drive the FFT visualizer tap. out property large-active: root.npb-mode == 3 && root.sidebar-state == 0; // Large dock column = open sidebar (240) minus its VL 16px*2 padding => 208px. // Equal L/R gutters fall out: 16 + 208 + 16 = 240. (16px == Spacing.md; kept a // literal here so state.slint stays free of a foundation import — if Spacing.md // or the 240 open width ever change, update this and AppShell's `x: 16px`.) out property large-dock-width: root.sidebar-rendered-width - 2 * 16px; // Total bottom band the dock occupies — MUST byte-for-byte equal // SidebarNowPlayingDock's self-derived `height` in BOTH visualizer states // (AppShell pins the overlay via `y: root.height - large-dock-height`, so a // mismatch un-flushes the cover). ON = spectrum 46 + 10 gap + square cover + // 9 top pad; OFF = cover + 9 (spectrum band reclaimed by the sidebar list). out property large-dock-height: root.large-visualizer-on ? (46px + 10px + root.large-dock-width + 9px) : (root.large-dock-width + 9px); // Large dock spectrum visualization (the click-cycle is now 3-way; the old // "Off" state is handled by the toggle below): 0 = Bars (mirrored), 1 = // Waveform, 2 = Energy blocks. PERSISTED in ui_prefs (`large_spectrum_mode`) // via the Rust ("npb-large", _, "spectrum-cycle") media-action — Slint never // mutates it directly, so the choice survives restarts. in-out property large-spectrum-mode: 0; // Large dock visualizer on/off (the top-right eye toggle on the cover). // PERSISTED in ui_prefs (`large_visualizer`) via ("npb-large", _, // "viz-toggle"). When off, the spectrum band collapses and the sidebar // content reclaims the space; the FFT tap idles (AppShell viz-should-run). in-out property large-visualizer-on: true; } // Authenticated user, for the header user element. export global SessionState { in property user-name: ""; in property subscription: ""; } // One entry in the playback queue, for the Queue sidebar list. export struct QueueItem { id: string, title: string, artist: string, artwork: image, playing: bool, duration: string, // "M:SS" explicit: bool, // From an ephemeral folder — gates persistence actions in the row menu. is-ephemeral: bool, } // ImmersiveView audio visualizers. Driven at ~30fps by the Rust drain // (crate::visualizer) from the frontend-agnostic FFT producer in qbz-audio. // Magnitudes are normalized 0..1. The tap stays disabled (no capture / no FFT // cost) until the immersive view calls set-enabled(true) on open. export global VisualizerState { in property enabled: false; in property <[float]> bars; // 16 log-spaced FFT bars (viz:data) in property <[float]> spectral; // 512 spectral-ribbon bands (viz:spectral) in property <[float]> energy; // 5 energy bands: sub-bass..air (viz:energy) in property <[float]> waveform; // 256 L then 256 R samples (viz:waveform) in property transient: 0; // last transient intensity; UI decays it callback set-enabled(bool); } // Full-window immersive now-playing overlay. The first Slint slice ships the // Album Reactive focus view only; broader panels can hang off this same state. export global ImmersiveState { in-out property open: false; in property bg-image; in property glow-color: #6464ff59; // View mode — Tauri's focus/split duality (ImmersivePlayer.svelte focus // vs split layout, recon data-panels.md §"two layout contexts"): // 0 = FOCUS — full-bleed single panel (the visualizer modes + Lyrics-focus). // 1 = SPLIT — artwork + metadata on the LEFT, the active data panel on the // RIGHT. The leading header icon (was inert) toggles this 0<->1. // Default 0 (focus) so opening lands on a real full-bleed foreground. in-out property view-mode: 0; // FOCUS-mode foreground for the conditional-mounted panel (read only while // view-mode == 0): // 0 = Album Reactive, 1 = Static, 2 = Coverflow, 3 = Spectrum, // 4 = Lyrics-focus (one giant centered current line), // 5 = Queue-focus (full-screen Queue + History panel; reuses QueueState, // no extra state field — same component as the split Queue panel). // SESSION default 0 (Album Reactive) so opening shows a real foreground. // NOTE: this compact enum is a Slint-scaffold contract; it deliberately // does NOT match Tauri's focus-tab string order (coverflow first). Product // default + persistence ("remember last view") is a later (header) session. in-out property mode: 0; // SPLIT-mode active panel (read only while view-mode == 1): // 0 = Lyrics, 1 = Track Info, 2 = Suggestions, 3 = Queue. // Checkpoint A ships only Lyrics (0); the other panels land in their own // checkpoints. Default 0. in-out property split-panel: 0; // Spectrum bar gradient, derived from the album art (contrast colors; // negative fallback for dark covers so the bars read against the black bg). // Defaults = the Svelte cyan -> purple until the art resolves. in property spectrum-primary: #00dcc8; in property spectrum-secondary: #9632ff; // Lyrics-focus single-line accent: derived from the album art but rotated // to the COMPLEMENT of the cover's dominant hue, so it contrasts with the // atmosphere (which is built from the same cover) instead of blending into // it. Default high-contrast teal until the art resolves / for B&W covers. in property lyrics-accent: #3fd9c8; // GPU fragment-shader underlay: a WGSL shader rendered into a wgpu texture by // src/shader_underlay.rs and handed back here each drain tick. `shader-mode` // selects the scene: // 0 = OFF — use the existing atmosphere + visualizer panels. // 1 = Plasma — classic sin-field plasma (ui/shaders/plasma.wgsl). // 2 = Tunnel — neon polar-warped tunnel (ui/shaders/tunnel.wgsl). // 3 = Aurora — domain-warped aurora curtains (ui/shaders/aurora.wgsl). // Each scene maps to shader_underlay pipeline index (shader-mode - 1). When // shader-mode > 0 the atmosphere + the mode panels are hidden so the shader // is the visible background; the auto-hide chrome (scene picker + compact // player) stays on top. Pick a scene via the header shader picker, or cycle // 0->1->2->3->0 with the 'g' key. in property shader-texture; in-out property shader-mode: 0; // False on the femtovg-GL / software renderer tiers (no wgpu underlay → // shader scenes would render black). Seeded from Rust at startup; gates // the picker rows and the `g` cycle in ImmersiveView. in-out property shader-scenes-available: true; // --- Immersive search cortinilla (parallel to SearchState.cortinilla-*) --- // Albums/Artists/Playlists only; selecting ACTS ON PLAYBACK (per the Settings // "search action") instead of navigating, since immersive has no nav. While // search-open is true the HUD must NOT auto-hide. See the immersive-search // design spec (qbz-nix-docs/specs/2026-06-20-immersive-search-design.md). in-out property search-open: false; // The TRIMMED query the cortinilla reflects (controller-set, display only). in property search-query; // The raw immersive search TextInput text, two-way bound to the field (in-out) // so the controller can CLEAR it after the user activates a result — // consistency with the main header cortinilla. Kept SEPARATE from the trimmed // `search-query` so the controller writing the trimmed value never fights the // user's in-progress text (e.g. trailing spaces). in-out property search-input-text: ""; in property search-loading: false; in property <[CortinillaSection]> search-sections: []; in-out property search-selected-index: -1; in property search-scroll-y: 0px; } // Callbacks for the immersive search cortinilla — parallel to SearchActions but // scoped to the immersive overlay (load on type, keyboard nav, select-acts-on- // playback, dismiss). Wired in main.rs; active only while ImmersiveState.open. export global ImmersiveSearchActions { callback live(string /* query */); callback row-clicked(int /* flat-index */); callback move-selection(int /* delta: -1 up / +1 down */); callback dismiss(); } // Lifecycle hooks for the immersive overlay, wired in main.rs. The ImmersiveView // component is conditionally mounted (it exists only while the overlay is OPEN), // so its `init` == the overlay opening. `opened()` lets Rust apply the configured // default view (pinned, or restore-last); `view-changed()` lets Rust persist the // current view when the configured default is "remember last". export global ImmersiveActions { callback opened(); callback view-changed(); } // Native window-control actions for the MAIN AppWindow, exposed to the // immersive overlay header cluster. The AppWindow uses native OS decorations, // so the only seam to manipulate it is the winit backend (see // src/main.rs wire_window_controls): every callback below reaches the MAIN // window's winit handle via WinitWindowAccessor::with_winit_window. The two // bool props drive the glyph swap on the maximize/fullscreen buttons; they are // only updated on OUR OWN toggles, so they can go stale if the user // maximizes/fullscreens via the OS chrome (acceptable for v1). export global WindowControlActions { in property is-maximized: false; in property is-fullscreen: false; callback minimize(); callback toggle-maximize(); callback toggle-fullscreen(); callback close-app(); callback drag-move(); } // 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; // Whether the playing track's ALBUM is a library favorite. Seeded from // fav_cache in refresh_now_playing_meta (playback.rs) on every track-meta // push, and flipped live by set_album_row_favorite (main.rs) when a // favorite toggle from ANY surface matches album-id. Drives the "+" // flyout's add/remove-album-to-collection entry (TransportControls). in property album-favorite: false; in property artist-id; in property artwork; // Higher-res cover (~300px) for the hover preview that floats above the // now-playing bar's small art, so the ~220px popup is crisp instead of an // upscale of the 160px bar art. Loaded alongside `artwork`; cleared on // track change. Falls back to `artwork` in the UI until it resolves. in property artwork-large; 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; // Playing track's source: "qobuz" | "local" | "plex" | "ephemeral". Track // info is a Qobuz-only feature, so its trigger is gated on `source == // "qobuz"` (local/Plex/ephemeral tracks have no Qobuz metadata page). in property source; // Parental-warning / explicit flag for the playing track. Drives the // explicit badge in the miniplayer compact + artwork surfaces (Tauri parity). in property explicit: 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" // Numeric stream params for the Spectral Ribbon overlay (header + freq axis // Nyquist). 0 = unknown. in property sample-rate-hz: 0; in property bit-depth: 0; // Output backend + mode LEDs (Mode C "Small" bar reads these). Mirror of // the same four on SettingsState (:2154-2157) — surfaced here so the // Small now-playing bar has ONE source for the song card + DAC/EXC // cluster. Pushed on every audio snapshot-apply (src/settings.rs). in property output-backend-label: "SYST"; // PIPE|ALSA|JACK|PULS|SYST in property output-mode-label: "DEFAULT"; // DACPASS|BITPERF|EXCL|DIRECT|LOCKED|ROUTED|SHARED|DEFAULT in property output-backend-active: false; in property output-mode-active: false; // 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 — live (fires on every drag tick) callback persist-volume(float); // 0..1 — drag-end only, persists to ui_prefs 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(); } // Miniplayer-specific state. The miniplayer is a SECOND Slint Window with its // OWN global instances (Slint globals are per-component-tree, NOT shared with // AppWindow), so the Rust layer mirrors NowPlayingState/LyricsState into the // mini window's instances and drives THIS global for mini-only concerns: // the active surface, always-on-top, hover (capsule reveal), the AOT warning // chip, and a SELF-CONTAINED navigable queue model (current + full upcoming, // NOT QueueState.upcoming-page which is the 40-row sidebar slice). export global MiniPlayerState { // 0 micro · 1 compact · 2 artwork · 3 queue · 4 lyrics in-out property surface: 2; // Static artwork-derived background for the mini surfaces. in-out property background-blur: false; // Whole-window hover — reveals the window-controls capsule. Fed from the // mini window's winit CursorMoved/CursorLeft (or a root TouchArea). in-out property window-hovered: false; // Self-contained navigable queue: current track first, then the FULL // de-duplicated upcoming list (no 20-cap). Scrollable in the surface. in property <[QueueItem]> queue-tracks: []; in property current-track-id; // Surface navigation + window actions (wired in main.rs to crate::miniplayer). callback surface-change(int); // 0..4 → apply geometry + persist callback expand(); // exit mini, show main app callback close-app(); // close the app from the mini callback toggle-background-blur(); // toggle static artwork backdrop callback start-drag(); // begin a borderless window drag // Per-row play — index into queue-tracks. Routes remote-first (REQ-2): // play_track_if_remote, bail before mutating local; current row resumes. callback queue-play(int); } // One rendered lyrics line. `time-ms`/`end-ms` are milliseconds from track // start (-1 = unknown / plain line). `has-words` flags native Qobuz word // timing held Rust-side (`crate::lyrics::CURRENT_DOC`) — the S4 sync engine // reads it for the word-anchored karaoke fill; word data itself never // crosses into the UI model. export struct LyricsLineItem { text: string, time-ms: int, end-ms: int, has-words: bool, } // One VISUAL line of the ACTIVE logical line, for the per-visual-line karaoke // highlight. The engine (`lyrics_measure` + `lyrics_sync`) measures the active // line's word-wrap against the current content width + font + size, and pushes // one segment per visual row so the highlight fills row 1 to 100% BEFORE row 2 // starts (matching Tauri) instead of lighting every wrapped row at once. // // `start-weight` = cumulative width fraction (0..1) of all PRIOR segments; // `width-weight` = this segment's width / total line width. The row re-maps the // single global `line-progress` to a per-segment local fraction: // local = clamp((line-progress - start-weight) / width-weight, 0, 1) export struct LyricsSegment { text: string, start-weight: float, width-weight: float, } // Lyrics panel state. The Rust glue (`crate::lyrics`) pushes fetch results // here on the de-duped track-change edge; the sidebar binds directly. The // S4 sync engine drives `active-index` (placeholder -1 until then). export global LyricsState { // 0 idle · 1 loading · 2 ready · 3 no lyrics · 4 error · 5 offline-unavailable in property status: 0; in property <[LyricsLineItem]> lines: []; in property synced: false; // Provider id ("qobuz" | "lrclib" | "ovh"); `provider-label` is the // attribution display name — empty for first-party Qobuz (no attribution // needed, spec §3.5). in property provider; in property provider-label; // Playing-track meta for the header ("{title} - {artist}"). in property track-title; in property track-artist; // Surfaced backend error text (status == 4) — dynamic content, not chrome. in property error; // Active line index; -1 = none. The S4 sync engine drives it. in property active-index: -1; // Karaoke clip fraction 0..1 of the ACTIVE line's rendered width — the // S4 engine pushes it at ~30Hz while playing: word-anchored when the // current doc has native Qobuz wsync words, line-proportional for LRC // sources (Q2). Drives the clip-rect width in LyricsLinesView. in property line-progress: 0.0; // Clip-width animation duration (ms). Slightly above the engine tick // while progress is continuous so the fill sweeps smoothly between // pushes; the engine sets 0 across discontinuities (line change / // backward seek) so the fill first-paints exactly at the reported // progress (Tauri parity, LyricsLines.svelte:114-138). in property fill-anim-ms: 0; // Per-visual-line segmentation of the ACTIVE line, for the karaoke // highlight on wrapped lines. The engine recomputes it when the active // index, content-width, font-index or size-index changes (low frequency) // and pushes one entry per visual row. Empty until the first active line. in property <[LyricsSegment]> active-segments: []; // The lyrics view's current text content width (column width minus side // padding). The view pushes it from its root `changed width`; the engine // uses it as the wrap budget when segmenting the active line. in property content-width: 0px; // Fired by the sidebar's `init` (conditional mount, ADR-010): re-requests // lyrics for the current track (cache-served when already fetched). callback panel-opened(); // ---- Display prefs (S5) ------------------------------------------------- // 1:1 to Tauri's lyricsDisplayStore (defaults lyricsDisplayStore.ts:37-44), // persisted per-user as JSON by `crate::lyrics_prefs` and seeded on every // session activation. The flyout mutates these in-out props directly and // fires `prefs-changed()`; Rust reads them back and persists. in-out property auto-follow: true; // 0 System · 1 LINE Seed JP · 2 Montserrat · 3 Noto Sans · 4 Source Sans 3 // (Q6 SHIPPED: the four families are bundled TTFs embedded at compile time // in LyricsLinesView.slint; "System" = empty font-family -> window default). in-out property font-index: 0; // 0 S · 1 M · 2 L · 3 XL — Tauri default 'medium'. Px mapping (the // 280px-adapted scale, Q3) lives in LyricsSidebar. in-out property size-index: 1; // 0 off · 1 soft · 2 strong (Tauri default 'strong'). in-out property dimming-mode: 2; in-out property uppercase: false; // Active (karaoke fill) color. `use-custom-color: false` = follow the theme // accent (Tauri activeColor: ''). The palette swatches set both (deviation // D6: swatches instead of a native color input — Slint has none). in-out property use-custom-color: false; in-out property custom-color: #8b5cf6; // Fired by the flyout after ANY pref mutation -> Rust persists the set. callback prefs-changed(); // Footer actions: join current lines -> clipboard (+ toast) / restore the // Tauri defaults (+ persist + re-seed). callback copy-lyrics(); callback reset-prefs(); // ---- Lyrics cache row (Settings > Offline, S5) -------------------------- // Stats read from the REAL per-user lyrics.db via qbz_lyrics cache_stats // (fix-forward F1 — Tauri measured a stale global path). Pre-formatted in // Rust; `cache-loaded` gates the description line. in property cache-loaded: false; in property cache-entries-text; in property cache-size; callback cache-refresh(); callback cache-clear(); } // 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" } // Cast (Chromecast / DLNA) — distinct from Qobuz Connect above. A discovered // renderer for the cast picker. Modeled as a tagged record (the `protocol` // field) so Chromecast and DLNA share one list; DLNA carries capability flags. export struct CastDevice { id: string, name: string, ip: string, protocol: string, // "chromecast" | "dlna" model: string, can-play: bool, // DLNA has-av-transport (Chromecast: true) can-set-volume: bool, // DLNA has-rendering-control (Chromecast: true) } // Cast picker + connection + transport-mirror state. Populated by // `cast_service.rs`; read by CastPicker and AudioOutputBadges. There is NO // error/connecting sub-state in the Tauri original beyond a raw error string. export global CastState { in-out property picker-open: false; in-out property selected-tab: "chromecast"; // "chromecast" | "dlna" in property scanning: false; // 15s scan-spinner window in property <[CastDevice]> devices: []; in property chromecast-count: 0; in property dlna-count: 0; // Connection. in property connected: false; in property protocol; // "chromecast" | "dlna" | "" in property device-name; // connected device label // Transport mirror (cast owns transport while connected). in property is-playing: false; in property position-secs: 0; in property duration-secs: 0; // Delivered quality (D3) — surfaced in the badge tooltip / picker banner. in property quality-label; // coarse, e.g. "FLAC 24-bit/>96kHz" in property quality-detail; // precise, e.g. "192 kHz / 24-bit" // Raw last-error for the picker error state (empty = none). in property error; } export global CastActions { callback open(); // open picker + start discovery callback close(); // close picker + stop discovery callback refresh(); // re-arm discovery scan callback connect(string, string); // device-id, protocol callback disconnect(); } // 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; } // Floating album-art preview (now-playing cover hover). Overlay-rendered like // the tooltip so it NEVER steals hover from the trigger — a PopupWindow DID, // which flipped the trigger's has-hover and made the preview flicker. The // overlay reads the cover straight from NowPlayingState; this only carries the // anchor + visibility. export global ArtPreviewState { in-out property anchor-x; // art centre, window coords in-out property anchor-y; // art 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: []; // --- Immersive coverflow (mode 2) ------------------------------------ // ONE STABLE flat ordered model of the whole queue, oldest-first: // [history.reversed (oldest..newest), NOW-PLAYING, upcoming...] // This list and its decoded cover handles do NOT change on a pure track // advance/jump — only `coverflow-index` (the flat position of NOW) moves, // and the .slint `scroll` float animates to it. The model is rebuilt by the // Rust controller ONLY when the queue CONTENTS change (different id-sequence: // new queue / shuffle / add / remove), gated by `coverflow-seq-hash`. On a // pure advance the model setter is SKIPPED entirely (no Repeater rebuild, no // Image churn), so visible covers never re-decode. Windowed (±4) lazy decode // keeps at most one new cover decoding per advance. in property <[QueueItem]> coverflow-tracks: []; // The SAME flat model, REVERSED (last..first). Rebuilt under the same // id-sequence gate as `coverflow-tracks` (so equally stable — never touched // on a pure advance). The RIGHT (upcoming) Repeater iterates THIS list so it // paints far-upcoming -> near-upcoming: a Slint Repeater paints in model // order and there is no per-element z-index in Slint 1.16, so reversing the // model is how we get the nearer (upcoming) cover to paint ON TOP of farther // ones — symmetric with the LEFT side, which already paints far->near in // ascending index order. A track's element holds the SAME decoded image on // both lists (the reversed list is built by cloning + reversing the forward // QueueItems), so no extra decode and offsets computed from the original // index match the forward side exactly during the ±0.5 slide hand-off. in property <[QueueItem]> coverflow-tracks-rev: []; // Flat index of the NOW-PLAYING cover within `coverflow-tracks` // (= history.len() when a current track exists, else 0). The .slint panel // binds an animated `scroll` float to this int so a change eases the fan. in property coverflow-index: 0; // Order-sensitive rolling fingerprint of the flat id-sequence. The Rust // controller compares this to the value it last pushed; equal => pure // advance => skip the model rebuild and only update `coverflow-index`. in property coverflow-seq-hash: 0; // --- 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 an upcoming entry by its QUEUE-WIDE (unfiltered) index. Used by the // immersive coverflow: a side card at flat index `i > coverflow-index` maps // to up_index = i - coverflow-index - 1 and dispatches here (page/search- // independent) — NOT the paged play-upcoming. callback play-coverflow-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); // Remove every upcoming entry after the page-local `index` (that row is // kept). The Rust controller resolves it to a queue-wide upcoming index. callback remove-all-after(int); // Open the Add-to-Playlist picker seeded with the upcoming row at the // page-local `index`. callback add-to-playlist(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; // "Stop after this song" marker: the decimal-string id of the marked track, // or "" for none. Each queue row compares its item.id to this to show the // CircleStop glyph. `toggle-stop-after` flips the marker for a given track id. callback toggle-stop-after(string /* track-id */); in property stop-after-id: ""; } // Sleep timer (queue footer). Frontend-only: a Rust tokio task owns the monotonic // deadline + 1 Hz countdown and calls core().pause() at expiry. Non-persistent. export global SleepTimerState { in property active: false; in property remaining: ""; // pre-formatted by Rust (single source of truth) in-out property popover-open: false; in-out property selected-index: 0; // preset index; last entry = Custom in-out property custom-minutes: 60; } export global SleepTimerActions { callback set(int /* minutes */); callback cancel(); } // In-app log viewer (Settings > Developer). One row of the [LogRow] model. export struct LogRow { ts: string, level: string, target: string, message: string, } export global LogViewerState { in-out property open: false; in property <[LogRow]> rows: []; // already filtered + capped by Rust in property total: 0; // ring size in property shown: 0; // after filter + cap in-out property filter-level: "all"; // all|error|warn|info|debug|trace in-out property search: ""; in-out property auto-tail: false; in property uploading: false; in property uploaded-url: ""; in property copied: false; callback refresh(); callback clear(); callback set-level(string); callback set-search(string); callback copy-all(); callback copy-bundle(); callback upload(); // public paste (already redacted) -> uploaded-url callback open-log-file(); callback toggle-auto-tail(bool); callback copy-url(); // copy the uploaded paste URL to the clipboard } // Report-an-issue modal (hamburger menu). Mirrors the Tauri ReportIssueModal: // explains the manual, redacted log-sharing flow, then offers "Go to logs" // (opens the log viewer) and "Create issue" (opens the GitHub new-issue page). export global ReportIssueState { in-out property open: false; } export global ReportIssueActions { callback create-issue(); // opens the GitHub new-issue URL (Rust: open::that) } // Diagnostics panel (Settings > Developer). One saved-vs-runtime table row. // status: 0 info | 1 match | 2 mismatch. export struct DiagRow { label: string, saved: string, runtime: string, status: int, } export global DiagnosticsState { in property loaded: false; in property loading: false; in property error: ""; in property app-version: ""; in property copied: false; in property cast-scanning: false; in property <[DiagRow]> system-rows: []; in property <[DiagRow]> playback-rows: []; in property <[DiagRow]> qconnect-rows: []; in property <[DiagRow]> cast-rows: []; in property <[DiagRow]> audio-rows: []; in property <[DiagRow]> graphics-rows: []; in property <[DiagRow]> env-rows: []; callback refresh(); callback export-clipboard(); callback cast-scan(); } // One row in the "Add to playlist" picker. export struct PlaylistPickItem { id: string, name: string, tracks-line: string, // "N tracks" // LOCAL playlist (library.db entity, id "local:") — the row // shows a hard-drive glyph; adds write the local repo (works offline). is-local: bool, // Client-side filter result — recomputed Rust-side on every filter // keystroke (Slint 1.16 has no string `.contains`, so the substring // match lives in Rust). Match ordinal among the filtered rows; // -1 = filtered out. Rows ranked >= 8 hide behind the overflow row. filter-rank: int, } // "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; // Client-side filter: case-insensitive substring over playlist names. // Pure frontend, no backend call. The list filters live as the user types. in-out property filter: ""; // Total rows matching the current filter — drives the dropdown's // no-results/overflow rows and its explicit height. in property filter-matches: 0; // Inline "Create new playlist" affordance (mirrors Tauri's // CREATE_NEW_PLAYLIST inline name field). When `creating-open` is true the // name input is revealed; `create-name` holds the typed name; `creating` // gates the confirm while the create-and-add runs. in-out property creating-open: false; in-out property create-name: ""; in-out property creating: false; } export global PlaylistPickerActions { callback pick(string /* playlist id */); callback close(); // Inline create-and-add: creates a new playlist (Qobuz online / local // offline per D8) and adds the carried tracks to it. callback create-and-add(); // Client-side filter changed — Rust recomputes each row's filter-rank // plus filter-matches (case-insensitive substring) since Slint 1.16 // lacks string `.contains`. callback filter-changed(string); } // "Duplicate tracks" confirm sub-modal — shown when a Qobuz-tracks → Qobuz- // playlist add finds tracks already in the target. The track-id Vec and // the duplicate id set live Rust-side in a stash cell (Slint never holds // them); this global only carries the counts + busy flag for rendering. export global DuplicateConfirmState { in-out property open: false; in property duplicate-count: 0; in property total-count: 0; in property busy: false; in property playlist-name; } export global DuplicateConfirmActions { callback add-all(); callback add-new-only(); callback cancel(); } // 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(); // Windowed grid: the visible row band changed (item indices into // albums-visible, prefetch margin included) — dispatch artwork for it. callback albums-window-changed(int /* first */, int /* last */); callback unfavorite-track(string /* track id */); callback unfavorite-album(string /* album id */); callback retry-load(); } // ── Purchases (My-Purchases library surface, source-of-truth §2.1) ── // // A purchased album row/card. `quality-label` is the grid card's kHz form // ("24/96 kHz"); `quality-list` is the album-list row's formatQuality form // ("24bit/96kHz" or "CD Quality"). `purchase-date` is the SHORT-month date. // `downloaded` only drives the hide-downloaded filter (the list view never // shows a per-album download badge). The Rust side fills `artwork` once the // pipeline resolves the cover. export struct PurchaseAlbumItem { id: string, title: string, artist: string, artist-id: string, artwork-url: string, artwork: image, quality-label: string, // grid card: "{bd}/{sr} kHz" ("" if either missing) quality-list: string, // list row: "24bit/96kHz" | "CD Quality" purchase-date: string, // SHORT month "Nov 6, 2025" downloadable: bool, downloaded: bool, } // A purchased track row. `quality` is the BARE "{bit}/{rate}" string (no kHz, // §A.6) — "" when either field is missing. `dl-status` is the flattened // download-store status for the row's 4-state control: // "" | "downloading" | "complete" | "failed". `downloaded` (server registry) // OR dl-status=="complete" both render the green redownload check. export struct PurchaseTrackItem { id: string, title: string, artist: string, artist-id: string, album-title: string, album-id: string, artwork-url: string, artwork: image, quality: string, // BARE "{bit}/{rate}" (§A.6); "" if either missing duration: string, // "m:ss" purchase-date: string, // SHORT month streamable: bool, downloaded: bool, // server registry downloaded flag dl-status: string, // "" | downloading | complete | failed } // A group-by section for the albums tab (alpha / artist grouping). export struct PurchaseAlbumGroup { key: string, title: string, albums: [PurchaseAlbumItem], } // A group-by section for the tracks tab (name / artist / album grouping). export struct PurchaseTrackGroup { key: string, title: string, tracks: [PurchaseTrackItem], } // One option in the anchored format-picker popup (tracks-tab per-track // download). `detail` is the "{bd}/{sr}" line shown after the label when both // are present. export struct PurchaseFormatItem { id: int, label: string, detail: string, } export global PurchasesState { // "albums" | "tracks" in-out property active-tab: "albums"; // Region-notice banner (above the nav). Shown until dismissed; the // persisted "seen" flag seeds the initial value from Rust (Slice 12). // Default true so a fresh session shows it, matching Tauri's // `getUserItem('qbz-purchases-region-notice-seen') !== 'true'`. in property show-region-notice: true; // Search (controller-debounced 300ms). `search-query` is the live input; // the Rust side debounces and refetches/filters. in-out property search-query: ""; in property search-active: false; // query.trim().length > 0 // Albums toolbar. in-out property album-view-mode: "grid"; // "grid" | "list" in-out property album-grouping-enabled: false; in-out property album-group-mode: "alpha"; // "alpha" | "artist" in-out property album-sort-by: "date"; // date|artist|album|quality in-out property album-sort-direction: "desc"; // "asc" | "desc" // Tracks toolbar. in-out property track-grouping-enabled: false; in-out property track-group-mode: "artist"; // artist|album|name // Filter panel (shared albums + tracks). Persisted per-user. in-out property filter-hide-unavailable: false; in-out property filter-quality: "all"; // all|hires|cd|lossy in-out property filter-hide-downloaded: false; // Mutually-exclusive open menu: "" | albumGroup | albumSort | filter | trackGroup in-out property open-menu: ""; // Derived flags (computed in Rust, pushed here). in property has-active-filters: false; in property active-filter-count: 0; // Tab counts (server total unless search/filter active → filtered length). in property album-tab-count: 0; in property track-tab-count: 0; // Rendered collections (already filtered/sorted/grouped in Rust). Flat // arrays drive the non-grouped render; the *-grouped arrays drive the // group-by render. in property <[PurchaseAlbumItem]> albums: []; in property <[PurchaseAlbumGroup]> albums-grouped: []; in property <[PurchaseTrackItem]> tracks: []; in property <[PurchaseTrackGroup]> tracks-grouped: []; // Stable FULL artwork-target models (every loaded item, in load order). // The artwork pipeline writes decoded covers here by index, then dual-sets // into the rendered (filtered/sorted/grouped) models by id (the favorites // pattern — a sort/filter/group makes the rendered models NOT share these). // Not bound by the view; targets only. in-out property <[PurchaseAlbumItem]> albums-full: []; in-out property <[PurchaseTrackItem]> tracks-full: []; // Content state. in property loading: true; in property load-error: ""; // i18n key (purchases.*) OR raw string // Format-picker popup (anchored). `picker-track-id` non-empty => shown. in property <[PurchaseFormatItem]> picker-formats: []; in property picker-track-id: ""; in property picker-anchor-x: 0px; in property picker-anchor-y: 0px; // Active/playing track (mirror props) for row highlight. in property active-track-id: ""; in property playback-active: false; } export global PurchasesActions { // Tabs / search. callback select-tab(string /* albums | tracks */); callback search(string /* query */); callback clear-search(); // Albums toolbar. callback set-album-group(string /* off | alpha | artist */); callback select-album-sort(string /* date|artist|album|quality */); callback set-album-view-mode(string /* grid | list */); // Tracks toolbar. callback set-track-group(string /* off | album | artist | name */); // Filter panel. callback set-filter-hide-unavailable(bool); callback set-filter-quality(string /* all|hires|cd|lossy */); callback set-filter-hide-downloaded(bool); callback clear-all-filters(); // Menu open/close (mutually exclusive). "" closes. callback set-open-menu(string); // Region notice dismiss. callback dismiss-region-notice(); // Navigation / play. callback open-album(string /* album id */); callback open-artist(string /* artist id */); callback play-track(string /* track id */); // Per-track download flow. `anchor-x`/`anchor-y` position the format // picker; the Rust side decides 1-format (no picker) vs >1 (picker). callback download-track(string /* track id */, length /* anchor-x */, length /* anchor-y */); // Format-picker: a format chosen → execute the download. callback pick-format(string /* track id */, int /* format id */); callback close-format-picker(); // Error branch retry (re-fetch the active tab). INTENTIONALLY UNWIRED: the // Svelte error state (PurchasesView.svelte:851-855) renders ShoppingBag + // text with NO retry trigger, so the 1:1 EmptyBlock has no clickable retry. // The handler in main.rs is kept as scaffolding (it would force-reload the // active tab) so a future non-1:1 retry affordance has a seam to bind to. callback retry(); } // ── Purchase detail / download view (source-of-truth §2.2) ────────────────── // One row in the detail track table. Mirrors the bespoke Tauri detail row // (§2.2.5): bare "{bit}/{rate}" quality (§A.6), a 4-state download control, and // disc grouping. `disc-header-number` > 0 marks the FIRST track of a disc on a // multi-disc album so a "Disc N" divider renders above it (the AlbumPageView // disc-header-number mechanism); 0 otherwise (single-disc → flat). // // `dl-status` is the format-scoped per-track status ("" | downloading | // complete | failed) from `track_status_scoped`. `is-downloaded` is the row's // derived green-check state (server `downloaded_format_ids` for the selected // format OR a store `complete`). `show-download` gates whether ANY download // control renders (album downloadable OR already downloaded, §2.2.5). export struct PurchaseDetailTrack { id: string, track-number: int, title: string, performer: string, // shown on a 2nd line when ≠ album artist show-performer: bool, duration: string, // "m:ss" quality: string, // BARE "{bit}/{rate}" (§A.6); "" if either missing streamable: bool, dl-status: string, // "" | downloading | complete | failed (format-scoped) is-downloaded: bool, // green redownload check show-download: bool, // album.downloadable || is-downloaded disc-header-number: int, // > 0 → "Disc N" header above this row (multi-disc) } export global PurchaseDetailState { // Bound album id. The view watches this and fires `load` on change so a // navigation between two different purchase albums RE-FETCHES (§A.3); the // controller seeds it before navigating. Empty → nothing to render. in-out property album-id: ""; // Content state. `load-error` is the RAW error string (NOT i18n-mapped, // unlike the list view — §2.2.4 / §8.4). in property loading: true; in property load-error: ""; in property loaded: false; // Header. in property title: ""; in property artist: ""; in property artist-id: ""; // "" → artist is plain text (no link) in property artwork; in property downloadable: true; // !downloadable → header overlay + no actions in property purchased-on: ""; // LONG-month "November 6, 2025" ("" hides) in property label-name: ""; // "" hides the "· label" segment in property genre-name: ""; // "" hides the "· genre" segment in property quality: ""; // formatQuality "24bit/96kHz" | "CD Quality" in property total-tracks: 0; in property total-duration: ""; // "{h}h {m}m" | "{m}m" // Format dropdown. `format-labels` are the backend fmt.label strings (NOT // an i18n key); `format-index` is the currently-selected option (default 0). in property <[string]> format-labels: []; in property format-index: 0; in property has-formats: false; // Download progress (source-of-truth §2.2.1 / §A.4). `all-complete` IS // format-scoped; `completed-count` / `progress-percent` / `was-cancelled` // are NOT (count ALL statuses regardless of selected format). in property is-downloading-all: false; in property all-complete: false; // format-scoped in property was-cancelled: false; // NOT format-scoped in property completed-count: 0; // NOT format-scoped in property progress-percent: 0; // completed/total*100, NOT format-scoped // Whether the progress section renders at all (downloadingAll || allComplete // || wasCancelled) — precomputed in Rust to keep the .slint declarative. in property show-progress: false; // Add-to-Library block (only when all-complete && a destination is set). in property show-add-to-library: false; in property adding-to-library: false; // Download-all circle gating: disabled while downloading-all OR no format. in property can-download-all: false; // Whether the Play-album circle renders (onAlbumPlay present — always true // in Slint since play routing exists; kept as a flag for parity). in property show-play: true; // Active/playing track for row highlight + playing-bars. in property active-track-id: ""; in property playback-active: false; // The disc-grouped track rows (already flattened with disc-header markers in // Rust). The view iterates this once. in property <[PurchaseDetailTrack]> tracks: []; } export global PurchaseDetailActions { // Reactive reload: the view fires this whenever the bound `album-id` // changes (and on first mount) so a fresh load runs (§A.3). callback load(string /* album id */); // Header / navigation. callback open-artist(string /* artist id */); callback play-album(); // Play-album circle (§A.1, routing in Slice 10) callback play-track(string /* track id */); // per-track row play (§A.2) // Format dropdown — a new option chosen (index into format-labels). callback select-format(int /* index */); // Download-all circle → folder picker → startAlbumDownload. callback download-all(); // Per-track download → folder picker → startTrackDownload. callback download-track(string /* track id */); // Progress section cancel button. callback cancel-download(); // Add-to-Library block button. callback add-to-library(); } // 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; // Keyboard/checkbox multi-select (mirrors AlbumState): mode flag + count. // Per-row toggle routes through the shared media-action("track",...) arm. in-out property multi-select: false; in property selected-count: 0; } // 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; // The live header search-box text, two-way bound to the TextInput in // HeaderBar. Owned here (not internal to the TextInput) so the controller // can CLEAR it after the user activates a cortinilla element (row click / // "View more" / Enter) — lingering text would otherwise re-invoke the // dropdown when focus bounces back to the field after any UI interaction. in-out property header-search-text: ""; // ---- Cortinilla (live as-you-type dropdown under the header search) ---- // Whether the cortinilla overlay is currently shown. The overlay is a // LAST child of AppShell, visibility-gated on this flag. in-out property cortinilla-open: false; // The query the cortinilla currently reflects (the live header text). in property cortinilla-query; // A live cortinilla fetch is in flight (spinner / shimmer affordance). in property cortinilla-loading: false; // The single best match shown at the very top of the cortinilla. Its // flat index is always 0 (see the flat-index convention on // SearchActions.cortinilla-row-clicked). `kind == ""` = no top result. in property top-result; // The labelled result sections (Albums / Tracks / Artists / Playlists / // library), each with its own rows + "View more" affordance. in property <[CortinillaSection]> sections: []; // Keyboard-nav highlight into the FLAT navigable list (top-result = 0, // then each section's rows in declaration order). -1 = nothing selected, // so Enter falls through to a full search-all instead of opening a row. in-out property selected-index: -1; // Content-space top-y (px) of the currently keyboard-selected row, set by // the controller on arrow nav so the overlay can scroll it into view. 0 when // nothing is selected (scroll to top). in property cortinilla-scroll-y: 0px; } // 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); // ---- Cortinilla (live as-you-type dropdown) ---- // A cortinilla row was activated (click or Enter on a highlighted row). // The argument is a FLAT index into the navigable list, where: // 0 = top-result (SearchState.top-result) // 1.. = section rows, in section declaration order, each // section's rows enumerated in order before the next. // The controller resolves the flat index back to a CortinillaRow and // dispatches on its `source`/`kind` (Qobuz media-action vs local play). callback cortinilla-row-clicked(int /* flat-index */); // "View more" pressed on a section — open the full results page (or the // LocalLibrary Tracks tab for the local section) for that category. callback cortinilla-view-more(string /* kind */); // Enter with nothing highlighted (selected-index == -1) — run a full // search-all and navigate to the results page. callback cortinilla-search-all(); // Click-outside / Escape — close the cortinilla without acting. callback cortinilla-dismiss(); // Arrow key moved the keyboard highlight: delta -1 = up, +1 = down. // Wraps/clamps against the flat navigable list in the controller. callback cortinilla-move-selection(int /* delta: -1 up / +1 down */); } // ============================ 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 — // drives the header badge's "Sign in" affordance (formerly a banner). in property show-recovery-banner: false; // Session started without Qobuz auth ("Start offline" on the login // screen). Unlike show-recovery-banner this is true regardless of // connectivity — the badge's "Logged out" state needs it even while // the link is down. in property offline-session: 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: ""; // Header status-badge state, derived: 0 = hidden (online + authenticated), // 1 = hard offline (no connectivity), 2 = manual offline (induced), // 3 = logged out (offline session — the actionable fact, so it takes // precedence over the other two). out property badge-state: self.offline-session ? 3 : self.mode == 2 ? 2 : self.mode == 1 ? 1 : 0; } // ============================ Login flow ================================== // Browser sign-in progress, driven by the Rust auth flow (crate::auth via // main.rs). The app has no embedded webview, so the login screen must // narrate the system-browser OAuth — the browser may open in the background // without stealing focus, and the code exchange takes a few seconds. export global LoginState { // 0 = idle · 1 = waiting for the user to finish in the browser · // 2 = authorization code received, exchanging it for a session. in property phase: 0; // Last sign-in failure ("" = none). Cleared when a new attempt starts // and on successful shell entry. in property 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(); // Flip induced offline from anywhere outside the Settings panel (the // header badge flyout's quick toggle). Routes through the same // persistence + #279 snapshot path as the Settings toggle. callback set-offline(bool); } // B9 — offline Favorites "playable favorites" rail. While OFFLINE the // Favorites view is replaced by the OfflinePlaceholder; this rail (mounted // as the placeholder's children on the Favorites mount only) lists the // favorite tracks that are still playable — offline-cache READY copies and // library qobuz_download rows — so the view isn't a dead end. export global OfflineFavoritesState { // The playable favorite tracks, display order = play order (the Rust // side keeps a matching queue; a row click plays the rail from there). in property <[SlimItem]> tracks: []; } export global OfflineFavoritesActions { // (Re)build the rail from the local stores — fired by the rail's init // on every mount of the Favorites offline placeholder. callback load(); // Play the rail starting at the clicked track id (offline-capable // queue path; the cached copies play through the offline tier). callback play(string); } // ================= Playlist Importer (public playlists) ================== // "Import Playlist" modal state — 1:1 port of Tauri's // PlaylistImportModal.svelte. All interpolated strings (log lines, status // line, summary block) arrive pre-formatted from the Rust controller // (crate::playlist_import); the UI renders them verbatim. Provider // detection lives Rust-side too (Slint 1.16 has no string `.contains`) — // every URL keystroke round-trips through `url-edited`. // One append-only line in the conversion log. export struct ImportLogEntry { // Final display string (pre-formatted Rust-side). message: string, // "info" | "success" | "error" — drives the row color. status: string, } export global PlaylistImportState { in-out property open: false; in-out property url; // Rename field — prefilled with the fetched playlist name. in-out property custom-name; // True during preview fetch AND import execute. in property loading: false; // "" = none; shown as the red banner at the top of the body. in property error: ""; // "" | "spotify" | "apple" | "tidal" | "deezer" — the locked-or- // detected provider; its source logo renders full-opacity. in property active-provider: ""; // Rust-computed: provider detected && !offline. in property can-fetch: false; // Rust-computed: a preview exists AND the URL hasn't been edited // since the fetch — flips the modal into step B (rename + Import). in property show-preview: false; in property import-completed: false; // The "Conversion progress" panel (any log lines, or loading). in property progress-visible: false; // Bar + status/current-track lines visible. in property has-progress: false; in property progress: 0.0; // 0..1 in property status-line: ""; in property current-track: ""; in property <[ImportLogEntry]> log: []; // Folder dropdown options — exact CreatePlaylistState pattern: index // 0 is always "No folder" (id ""), the rest are the user's folder // names parallel to `folder-ids`. in property <[string]> folder-options: ["No folder"]; in property <[string]> folder-ids: [""]; in-out property folder-index: 0; // Summary block, pre-formatted lines ("" = hidden). in property summary-playlist: ""; in property summary-matched: ""; in property summary-skipped: ""; in property summary-parts: ""; } export global PlaylistImportActions { callback close(); // Fired on every URL keystroke — Rust recomputes active-provider / // can-fetch / show-preview (and the post-completion reset path). callback url-edited(string); // Keeps Rust's custom_name mirror fresh. callback name-edited(string); // Step A: preview_public_playlist(url). callback fetch(); // Step B: import_public_playlist(...) with rename + folder choice. callback execute(); } // ── HiFi Wizard (DAC setup) ───────────────────────────────────────────── // Native-Slint port + redesign of the Tauri DACSetupWizard. 6-step adaptive // flow: welcome → check → select-dacs → review-and-apply → test → done. // READ-ONLY by contract: the wizard never writes a system audio config — it // auto-detects DACs (pw-dump, Slice 0), generates copy-paste config, and runs // an in-app playback test reading the real negotiated rate (N6, Slice 8b). // A "you're missing X" fix for the check step: a caption + a copy-paste command. export struct RemediationRow { caption: string, command: string, } // One enumerated sink the user can pick to configure. export struct DacCandidateRow { id: string, // PipeWire node.name (the value the user used to paste) description: string, // pretty name, e.g. "DacMagic Plus Analog Stereo" bus: string, // "usb" | "pci" | "bluetooth" | "" is-default: bool, // current system default sink looks-like-dac: bool, // hardware && bus ∈ {usb,pci} → pre-selected checked: bool, // user selection rates-label: string, // pre-filled supported rates, e.g. "44.1 / 96 / 192 kHz" } // One selected DAC's generated copy-paste config (Slice 10, read-only). export struct DacConfigRow { name: string, node-name: string, pipewire-conf: string, pulse-conf: string, wireplumber-conf: string, expanded: bool, // accordion state } export global DacWizardState { in-out property open: false; // 0 welcome · 1 check · 2 select-dacs · 3 review · 4 test · 5 done in-out property step: 0; // welcome in-out property welcome-confirmed: false; // check (Slice 6): distro + init both auto-detected, both overridable // (distro → package manager, init → service commands; they're orthogonal). in-out property <[string]> distro-options: []; in-out property distro-index: 0; in-out property <[string]> init-options: []; in-out property init-index: 0; in-out property health-ok: false; in-out property health-summary: ""; in-out property <[RemediationRow]> remediations: []; // In a Flatpak/Snap sandbox QBZ can't inspect the host stack, so it shows // reference setup commands + an info notice instead of a health verdict. in-out property sandboxed: false; in-out property sandbox-name: ""; // select-dacs (Slice 7) in-out property <[DacCandidateRow]> candidates: []; in-out property detecting: false; // enumeration in flight in-out property has-enumeration: true; // false → manual escape hatch in-out property any-dac-selected: false; // gates Next on the DACs step in-out property manual-node-name: ""; in-out property manual-valid: false; in-out property manual-dac-type: ""; // usb|pci|bluetooth|virtual|unknown // test (Slice 9): live read-back lines, set by the controller in-out property test-requested-label: ""; // "QBZ requesting 96000 Hz · 24-bit" in-out property test-negotiated-label: ""; // "DAC running at 96000 Hz" (N6) in-out property test-rate-matched: false; in-out property test-playing: false; // review-and-apply (Slice 10): generated copy-paste config per selected DAC. in-out property <[DacConfigRow]> dac-configs: []; in-out property backup-cmd: ""; in-out property restart-cmd: ""; in-out property <[string]> created-paths: []; // done step // Progress checkboxes (Tauri parity) — help the user track what they ran. in-out property review-backup-done: false; in-out property review-config-done: false; in-out property review-restart-done: false; } export global DacWizardActions { // Wired in Rust (crate::dac_wizard); plain step nav (back/close) stays // inline in the modal via state mutation. callback open(); // open + detect distro/init + run health probe callback set-distro(int); // recompute remediations for the chosen distro callback set-init(int); // recompute service commands for the chosen init callback copy-command(string); // → crate::share::copy_to_clipboard callback run-detect(); // enumerate sinks → candidates (Slice 7) callback toggle-dac(int); // flip one candidate's checked state callback validate-manual(string);// validate a pasted node.name (escape hatch) callback start-test(); // queue + play the 4 test tracks (Slice 9) callback verify-own(); // start the read-back using the user's own music callback poll-test(); // refresh the requested-vs-negotiated read-back callback test-play-index(int); // jump straight to one of the 4 test tracks callback stop-test(); callback gen-configs(); // build the per-DAC copy-paste config (Slice 10) callback toggle-config(int); // collapse/expand one DAC's config accordion } // ============================================================================ // Keyboard shortcuts (hotkeys) — port of the Tauri keybindings system. // ============================================================================ // One action row, prebuilt in Rust (crate::keybindings) with its display // shortcut already formatted ("Ctrl + →"). Shared by the read-only cheatsheet // and the editable customize editor. export struct KeybindingRow { id: string, // stable action id, e.g. "playback.toggle" label: string, // localized action label shortcut: string, // formatted display, e.g. "Ctrl + →" / "Space" modified: bool, // differs from the default binding contextual: bool, // only fires in a context (immersive / mini) } // A category group (Playback / Navigation / Interface / Immersive / Mini Player) // with its localized header label and its ordered rows. export struct KeybindingCategoryGroup { label: string, // localized category header rows: [KeybindingRow], } export global KeybindingsState { // Ordered category groups, rebuilt in Rust whenever a binding changes. in property <[KeybindingCategoryGroup]> groups: []; // Same groups split round-robin into three columns, for the read-only // cheatsheet's 3-column layout (the editor still uses the full `groups`). in property <[KeybindingCategoryGroup]> groups-col1: []; in property <[KeybindingCategoryGroup]> groups-col2: []; in property <[KeybindingCategoryGroup]> groups-col3: []; // Count of actions whose binding differs from default ("N modified"). in property modified-count: 0; // --- Customize editor capture state ------------------------------------- // Action id currently being recorded ("" = not recording). The winit key // handler reads this; while non-empty it captures the next combo instead // of dispatching a hotkey. in-out property recording-id: ""; // Live formatted display of the combo being pressed while recording. in property pending-display: ""; // Localized label of the action that already owns the pressed combo // ("" = no conflict). When set, the capture is NOT applied. in property conflict-label: ""; } export global KeybindingsActions { callback start-record(string); // begin capturing for an action id callback cancel-record(); // stop capturing (Escape / click-away) callback reset-one(string); // reset one action to its default callback reset-all(); // reset every action to defaults } // Open flags for the two keybindings modals. The cheatsheet (read-only, `?`) // and the editable customize editor. Self-gated mounts in AppShell (ADR-010). export global KeyboardShortcutsState { in-out property open: false; // read-only cheatsheet in-out property customize-open: false; // editable customize editor } // ============================================================================ // Open Qobuz Link (Ctrl+L) — cross-platform link resolver modal. // ============================================================================ export global LinkResolverState { in-out property open: false; // Two-way bound to the input field. in-out property url: ""; // Detected platform id for the leading icon: "qobuz" | "spotify" | "apple" // | "tidal" | "deezer" | "songlink" | "" (none/unknown). Set by Rust on // url-changed; reset to "" from the hamburger menu on open (hence in-out). in-out property platform: ""; in-out property resolving: false; // Localized error message ("" = none). in-out property error: ""; // Playlist banner: the link is a playlist → offer the Playlist Importer. in-out property playlist-detected: false; in property playlist-provider: ""; } export global LinkResolverActions { callback submit(string); // resolve + navigate callback close(); callback url-changed(string); // live platform detection for the icon callback open-importer(); // playlist banner → open Playlist Importer } // UiFocusState (the hotkey text-input guard) lives in the leaf file // `focus-state.slint` and is re-exported at the top of this file. // =================================================================== // About modal (hamburger menu → "About QBZ"). // =================================================================== // One author/contributor row: GitHub handle + the profile URL opened on click, // plus the GitHub avatar (fetched async by crate::about — default/blank until it // arrives). export struct AboutContributorRow { name: string, url: string, avatar: image, } // One wrap row of contributor chips. Slint has no flex-wrap, so crate::about // pre-groups the flat contributor list into fixed-width rows (4 per row) and the // modal renders an outer VerticalLayout over the groups + an inner // HorizontalLayout over each group's items. export struct AboutContributorGroup { items: [AboutContributorRow], } export global AboutState { in-out property open: false; // Seeded by crate::about at install time (and on language change for the // attributions descriptions). Static for the lifetime of the app. in property version: ""; in property platform-label: ""; // e.g. "Linux (Slint)" in property build-date: ""; // YYYY-MM-DD ("" hides it) in property build-commit: ""; // short hash ("" hides it) in property release-url: "https://github.com/vicrodh/qbz/releases"; // Contributors pre-grouped into wrap rows (see AboutContributorGroup). in property <[AboutContributorGroup]> contributor-rows: []; // The single author row (vicrodh) — kept as discrete fields since it // renders differently (slightly larger) from the contributor list. in property author-name: "vicrodh"; in property author-url: "https://github.com/vicrodh"; in property author-avatar; } export global AboutActions { // Open an external URL in the system browser (Rust: open::that). callback open-url(string); } // =================================================================== // What's New modal (hamburger menu → "What's New"). // =================================================================== // One rendered block of the release-notes body. `kind`: 0 = section heading, // 1 = bullet, 2 = paragraph. `level` is the bullet nesting depth (1, 2, …). // `id` is the slug for section blocks (TOC anchor; empty for bullets/paragraphs). export struct WhatsNewBlock { kind: int, level: int, text: string, id: string, } // One table-of-contents entry (level-0 section headings only). export struct WhatsNewTocEntry { id: string, label: string, } export global WhatsNewState { in-out property open: false; in property loading: false; in property version: ""; in property date: ""; // formatted "Mon D, YYYY" in property has-body: false; // false → show the empty state in property <[WhatsNewBlock]> blocks: []; in property <[WhatsNewTocEntry]> toc: []; } export global WhatsNewActions { // Open the modal AND kick off the GitHub release fetch (Rust). callback open(); callback close(); }