// Shared UI state — structs and globals the Rust controllers populate. // // The Rust side builds these models on a worker thread, then sets them // on the global from the Slint event loop (see src/home.rs, src/main.rs). import { JumpNavTab } from "primitives/JumpNavBar.slint"; export { JumpNavTab } export struct AlbumCardItem { id: string, title: string, artist: string, genre: string, // shown in the hover overlay year: string, // 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 } export struct DiscoverSection { title: 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, } // Discover / Home screen state. export global HomeState { in property <[DiscoverSection]> sections: []; in property <[SlimItem]> popular: []; in property <[SlimItem]> recent: []; in property <[AlbumCardItem]> recent-albums: []; in property loading: false; } export struct AlbumTrackItem { id: string, number: string, title: string, artist: string, duration: string, quality-tier: string, explicit: bool, // Multi-select state — set true when the row is in the user's // selection. First used by the artist page's Popular Tracks. selected: bool, } // Album detail screen state. export global AlbumState { in property id; in property title; in property artist; in property artist-id; // Network URL of the cover — used by the artwork right-click // menu for Open-in-browser and Save-as actions. in property artwork-url; in property has-custom-cover: false; in property info-line; in property quality-tier; // "24-bit / 96 kHz" — quality-badge detail line. in property quality-detail; // Editorial description / review (HTML stripped). May be empty. in property description; // Truncated description for the header; full text lives in the modal. in property description-short; in property artwork; // Artwork-derived tint for the header gradient. in property header-color: #181820; in property label; in property <[string]> awards: []; in property <[AlbumTrackItem]> tracks: []; in property loading: false; } // Album-view actions wired to Rust callbacks. export global AlbumActions { // Filter the visible track list by a query (client-side). callback search(string); } // Which content view the shell shows. export enum ContentView { home, album, artist, settings, search, musician, label } // Settings screen state. Audio and Playback preferences are loaded from // the persistence stores by the Rust settings controller and pushed here; // changes flow back through the settings-bool / settings-select / // settings-slider callbacks. export global SettingsState { // Sub-section: 0 = Audio, 1 = Playback. in-out property section: 0; in property loading: true; // --- AUDIO ------------------------------------------------------------- // Audio — dropdowns. Each list is paired with a selected index; the // Rust controller owns the index -> value mapping. in-out property <[string]> streaming-qualities: []; in-out property streaming-quality-index: 0; in-out property <[string]> sample-rates: []; in-out property sample-rate-index: 0; in-out property <[string]> backends: []; in-out property backend-index: 0; in-out property <[string]> devices: []; // Parallel to `devices`: true marks a bit-perfect-capable device. in-out property <[bool]> device-bp: []; // Parallel to `devices`: a non-empty entry is a section-header label // shown above that device row (empty = no header). in-out property <[string]> device-groups: []; in-out property device-index: 0; in-out property <[string]> alsa-plugins: []; in-out property alsa-plugin-index: 0; // Audio — toggles. in-out property limit-quality-to-device: false; in-out property alsa-hardware-volume: false; in-out property exclusive-mode: false; in-out property reserve-dac: false; in-out property dac-passthrough: false; in-out property pw-force-bitperfect: false; in-out property allow-quality-fallback: false; in-out property sync-audio-on-startup: false; in-out property skip-sink-switch: false; // Audio — Rust-computed conditional flags driving `if`-gated rows. in-out property backend-is-alsa: false; in-out property backend-is-pipewire: false; in-out property alsa-plugin-is-hw: false; // --- PLAYBACK ---------------------------------------------------------- // Playback — toggles. `continue-playback` maps to the autoplay mode // (on = ContinueWithinSource, off = PlayTrackOnly). in-out property continue-playback: true; in-out property show-context-icon: true; in-out property persist-session: false; in-out property resume-position: false; in-out property gapless: true; in-out property stream-uncached: false; in-out property streaming-only: false; // 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; } // One entry of a nav-tab dropdown. export struct NavMenuEntry { label: string, icon: image, has-glyph: bool, } // 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; // The open tab's menu rows. in-out property <[NavMenuEntry]> items: []; // Index of the row wired to Home (-1 = none); only Discover uses it. in-out property home-item: -1; } export global NavState { in-out property view: ContentView.home; // Browser-like history flags, driven by the Rust nav module. in property can-back: false; in property can-forward: false; // Navigation requests routed to Rust so history is recorded. callback request-back(); callback request-forward(); callback request-settings(); } // Artist detail screen state. // One label row in the artist network sidebar's Labels section. export struct LabelEntry { id: string, name: string, } // One similar-artist row in the artist network sidebar's Similar // Artists section. export struct SimilarEntry { id: string, name: string, } export global ArtistState { in property id; in property name; // Network URL of the portrait — used by the artwork right-click // menu for Open-in-browser and Save-as actions. in property artwork-url; // True when the user has registered a local image to override // the Qobuz-supplied portrait. Drives the menu's // "Add image" -> "Change image / Remove image" toggle. in property has-custom-image: false; in property bio; // Truncated bio (word-boundary cut + ellipsis) for the at-rest view; // the full bio in `bio` is shown when the Read-more toggle expands. in property bio-short; in property bio-truncated: false; // Editorial source for the biography ("TiVo" etc); shown as a small // "Source: " line under the Read more toggle when present. in property bio-source; in property artwork; // Artwork-derived tint for the header gradient. in property header-color: #181820; in property <[AlbumTrackItem]> top-tracks: []; // Releases grouped into titled sections (Albums, EPs & Singles, ...). in property <[DiscoverSection]> release-sections: []; // Labels collected from the artist's own album releases (deduped by // id, alphabetically sorted) — feed the Labels section of the // network sidebar. in property <[LabelEntry]> labels: []; // Similar artists from the /artist/page response — feed the Similar // Artists section of the network sidebar. in property <[SimilarEntry]> similar-artists: []; // Filtered JUMP TO tabs for this artist — only the sections // that actually have content. Built in artist::apply_artist // from the page response. anchor-y is the page-local y the // ArtistPageView will scroll to when the tab is clicked. in property <[JumpNavTab]> jump-tabs: []; in property loading: false; } // Artwork right-click actions — shared by ArtistPageView and // AlbumPageView for their respective headers. `kind` is "artist" or // "album"; `key` is the artist name or album id; `url` is the // current network URL (for open-in-browser and save-as). export global ArtworkActions { callback open-in-browser(string /* url */); callback save-as(string /* url */, string /* default file name */); // Opens the system file picker and registers the chosen image // as the custom override for this artist/album. callback add-custom(string /* kind */, string /* key */); callback remove-custom(string /* kind */, string /* key */); } // Artist-view actions wired to Rust callbacks. export global ArtistActions { // Filter the visible Popular Tracks and release-section albums by // a query string (client-side substring match, mirrors the album // track search in AlbumActions). callback search(string); } // Artist network sidebar — Origin / Labels / Similar / Relationships / // Discovery. The first three lists come from artist state already; this // global covers the MusicBrainz-driven sections plus the open/closed // flag the network button toggles. // One row of the Origin section. Empty strings mark absent fields so the // view can hide individual rows without an Option wrapper. export struct MbOriginData { // True when the artist is a person (vs group/orchestra/etc). Drives // the row labels: "Born / Born in / Died" vs "Founded / Founded in / // Disbanded". is-person: bool, // Begin date — "1976" / "May 1976" / "May 14 1976". Empty when MB // has no life_span.begin. begin-date: string, // End date — same shape as begin. Empty when life_span.end is // missing (or the artist is still active). end-date: string, // Location display string ("Berlin, Germany" / "United Kingdom"). // Empty when MB has no location. location-display: string, // True when the location is precise enough to drill in (city/state // precision, or precision==country but a city is present somehow). // When false, the row renders as plain text. location-clickable: bool, } // One member, group, or collaborator row. export struct MbRelationship { mbid: string, name: string, // Primary role string for the musician-click callback ("Band Member", // "Guitar", "Producer", ...). Always populated — defaults to "Band // Member" / "Band" / "Collaborator" by section when MB has no // attributes for the relation. role: string, // Tooltip — roles joined with ", " plus the period in parens when // present. Falls back to the name. tooltip: string, } // All MB relationship rows for the sidebar. Each list is independently // hideable. export struct MbRelationshipsData { members: [MbRelationship], groups: [MbRelationship], collaborators: [MbRelationship], // Convenience flag — true when any of the three lists has at least // one row, so the section header can hide on no-data. has-data: bool, } // One row of the "You may also like" discovery section. export struct DiscoveryArtist { // MB id — used by the dismiss callback to remember the rejection. mbid: string, name: string, // Qobuz artist id (string form) when the MB candidate matched a // Qobuz artist; empty when only available on MB. Drives whether the // row is clickable. qobuz-id: string, } export global NetworkSidebarState { // Open/closed. The header network button toggles this; persistence // is handled by the Rust side (`network_sidebar_prefs::set_open`). // Defaults to open — Rust overrides on startup from the persisted // value if any. in-out property open: true; // True until proven otherwise. Set to false when MB is disabled in // settings, or when the name → mbid resolution fails. Drives whether // the MB-sourced sections render at all (Origin, Relationships, // Discovery). in property mb-available: true; // Resolved MB id for the current artist; empty before resolve // completes or when the artist could not be matched. Used by the // Rust side as the key for relationship/discovery loads. in property mb-mbid; // Origin (MB metadata) section in property origin-loading: false; in property origin; // Relationships (MB) section in property relationships-loading: false; in property relationships; // Discovery (MB tag-based) section — populated in phase 8. in property discovery-loading: false; in property <[DiscoveryArtist]> discovery-artists: []; // Primary tag the current discovery batch was seeded with — // delivered as the first argument of the discovery-dismissed // callback so the Rust side can persist the dismiss under the // right key. in property discovery-tag; } // Callbacks the sidebar emits. Each delivers the minimum payload the // Rust controller needs; the controller reconstructs richer payloads // (location precision, affinity seeds) from cached metadata when it // has to call out to a target view. export global NetworkSidebarActions { // Origin location row clicked — opens ArtistsByLocation view. callback location-clicked(string /* current artist mbid */); // Label row clicked — opens LabelReleases view. callback label-clicked(string /* label id */, string /* label name */); // Similar / discovery / similar-row clicked — opens the Qobuz // artist page for `qobuz-id`. callback artist-clicked(string /* qobuz id */); // Relationship row clicked — opens MusicianPage with the role. callback musician-clicked(string /* name */, string /* role */); // Discovery dismiss — removes the row and remembers the rejection. // Carries both the MBID (model key for removal) and the name // (persisted into the per-tag dismiss store after normalization). callback discovery-dismissed(string /* mbid */, string /* name */); // Sidebar toggled — Rust persists the new open state. callback toggle(); } // One row of MusicianPageView's "Appears On" grid — same shape as // the album cards on Discover, plus a role label stamped onto each // album by the Qobuz search wrapping (e.g. "Drums" / "Guitar"). export struct MusicianAppearanceItem { album-id: string, album-title: string, artist-name: string, year: string, role-on-album: string, artwork-url: string, artwork: image, } // MusicianPageView state — opened by the artist network sidebar // when a relationship row is clicked and the musician has no // confident Qobuz artist match. Populated by `crate::musician` // from the resolve + appearances backend calls. export global MusicianState { in property name; in property role; // "confirmed" | "contextual" | "weak" | "none" — the sidebar // uses this to skip MusicianPageView entirely on Confirmed and // navigate to the matched artist instead. in property confidence; // Bands / projects from MusicBrainz; not yet populated but // kept in the data shape so the section renders cleanly when // wired. in property <[string]> bands: []; in property <[MusicianAppearanceItem]> appearances: []; in property total: 0; in property loading: false; in property load-more-loading: false; } export global MusicianActions { callback load-more(); } // LabelReleasesView state — opened by the artist network sidebar's // Labels section. Header (name + image) from /label/page, the // album grid from /label/getAlbums. Populated by `crate::label`. export global LabelState { in property id; in property name; in property image-url; in property image; in property <[AlbumCardItem]> albums: []; in property total: 0; in property loading: false; in property load-more-loading: false; } export global LabelActions { callback load-more(); } // Shell chrome state — collapsible left sidebar and right-side panels. // Globals are imported directly by the shell components, so toggles // need no callback plumbing. export global ShellState { in-out property sidebar-collapsed: false; in-out property sidebar-width: 280px; // 0 = none, 1 = queue, 2 = lyrics in-out property right-panel: 0; } // Authenticated user, for the header user element. export global SessionState { in property user-name: ""; in property subscription: ""; } // One entry in the playback queue, for the Queue sidebar list. export struct QueueItem { id: string, title: string, artist: string, artwork: image, playing: bool, duration: string, // "M:SS" explicit: bool, } // Now-playing / transport state. The Rust playback poll loop pushes the // live values here; the PlayerBar and SeekBar bind to it directly. export global NowPlayingState { in property has-track: false; in property title; in property artist; in property album; in property album-id; in property artist-id; in property artwork; in property track-id; in property playing: false; in property position-secs: 0; in property duration-secs: 0; in property progress: 0.0; // 0..1 in property cache: 0.0; // 0..1 buffered 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 // Transport callbacks, wired to the Rust playback controller. callback toggle-play(); callback next(); callback previous(); callback seek(float); // 0..1 callback set-volume(float); // 0..1 callback toggle-mute(); callback toggle-shuffle(); callback cycle-repeat(); } // Playback queue model, for the Queue sidebar. // // The sidebar shows two tabs. The Queue tab renders `now-playing` plus a // paginated slice of `upcoming` (40 per page). The History tab renders // `history` (most-recent first). The Rust queue controller owns the full // upcoming/history lists and the pagination maths; it pushes the current // page's slice into `upcoming-page` and the counters that label it. export global QueueState { // --- Queue tab ------------------------------------------------------- // Current track card. `has-current` gates the NOW PLAYING section. in property has-current: false; in property now-playing; // Whether the now-playing track is in the user's favorites. in property now-playing-favorite: false; // The slice of upcoming tracks for the current page (<= 40 entries). in property <[QueueItem]> upcoming-page: []; // Total upcoming-track count (post-filter when a search is active). in property upcoming-total: 0; // Tracks remaining after the current one (total - played - 1). in property upcoming-remaining: 0; // Zero-based page index and total page count for the paginator. in-out property page: 0; in property page-count: 1; // Index of the first upcoming entry shown, 1-based, for the counter. in property page-start: 0; // Index of the last upcoming entry shown, 1-based, for the counter. in property page-end: 0; // --- History tab ----------------------------------------------------- in property <[QueueItem]> history: []; // --- Shared ---------------------------------------------------------- // Active tab: 0 = Queue, 1 = History. in-out property tab: 0; // Live search query; the Rust controller re-filters `upcoming-page`. in-out property search-query: ""; // --- Callbacks (wired to the Rust queue controller) ------------------ // Play the upcoming entry at `index` within the current page slice. callback play-upcoming(int); // Play a history entry by its `index` in the `history` list. callback play-history(int); // Remove the upcoming entry at `index` within the current page slice. callback remove-upcoming(int); // Empty the queue. callback clear-queue(); // Toggle the favorite state of the now-playing track. callback toggle-now-playing-favorite(); // Save the current queue as a new playlist. callback save-as-playlist(); // Toggle infinite-play (auto-refill the queue with similar tracks). callback toggle-infinite-play(); // Re-filter the upcoming list after `search-query` changed. callback search-changed(); // Move to the previous / next paginator page. callback prev-page(); callback next-page(); // Switch the active tab (0 = Queue, 1 = History). callback set-tab(int); // Fired when the sidebar becomes visible, so the Rust controller can // pull a fresh queue snapshot (the queue may have changed while the // panel was closed, e.g. after a session restore). callback panel-opened(); // Reflects whether infinite-play is currently enabled. in property infinite-play: false; } // A track result row in search (cover thumbnail, unlike album tracks). export struct SearchTrackItem { id: string, title: string, artist: string, duration: string, quality-tier: string, explicit: bool, artwork-url: string, artwork: image, } // A playlist result in search. Qobuz returns up to four cover URLs per // playlist; the card renders them as a count-based collage. 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, } // 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 <[SearchTrackItem]> 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; // Active searchType filter, as a dropdown index (0 = no filter). // The controller owns the index -> API-value mapping. in property filter-index: 0; } // Search actions wired to the Rust search controller. export global SearchActions { // Enter / explicit submit — navigate to the full results page. callback submit(string); // Live (debounced) search as the user types. callback live(string); // User picked a results tab (0..4). callback tab-changed(int); // searchType filter changed — the new dropdown index. callback filter-changed(int); // Request the next page for the given tab. callback load-more(int); }