Files
qbz/crates/qbz-slint/ui/state.slint
T
vicrodh 6d8a2e1fff fix(slint): spinner on home/album/artist + dedupe only in All-tab carousel
Home, Album and Artist views now show the rotating LoadingSpinner (same
as search) above a label while the initial load runs, replacing the
bare Loading… text.

The top-result-artist dedupe moved from map_search_all into apply_search
and replace_category: the full artists list is kept in SearchState.artists
(used by the Artists tab grid), and a separate artists-carousel drops the
duplicate next to the Most-popular hero on the All tab.

cc
2026-05-19 21:00:40 -06:00

407 lines
16 KiB
Plaintext

// Shared UI state — structs and globals the Rust controllers populate.
//
// The Rust side builds these models on a worker thread, then sets them
// on the global from the Slint event loop (see src/home.rs, src/main.rs).
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 <bool> loading: false;
}
export struct AlbumTrackItem {
id: string,
number: string,
title: string,
artist: string,
duration: string,
quality-tier: string,
explicit: bool,
}
// Album detail screen state.
export global AlbumState {
in property <string> id;
in property <string> title;
in property <string> artist;
in property <string> artist-id;
in property <string> info-line;
in property <string> quality-tier;
// "24-bit / 96 kHz" — quality-badge detail line.
in property <string> quality-detail;
// Editorial description / review (HTML stripped). May be empty.
in property <string> description;
// Truncated description for the header; full text lives in the modal.
in property <string> description-short;
in property <image> artwork;
// Artwork-derived tint for the header gradient.
in property <color> header-color: #181820;
in property <string> label;
in property <[string]> awards: [];
in property <[AlbumTrackItem]> tracks: [];
in property <bool> 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 }
// Settings screen state. Audio and Playback preferences are loaded from
// the persistence stores by the Rust settings controller and pushed here;
// changes flow back through the settings-bool / settings-select /
// settings-slider callbacks.
export global SettingsState {
// Sub-section: 0 = Audio, 1 = Playback.
in-out property <int> section: 0;
in property <bool> loading: true;
// --- AUDIO -------------------------------------------------------------
// Audio — dropdowns. Each list is paired with a selected index; the
// Rust controller owns the index -> value mapping.
in-out property <[string]> streaming-qualities: [];
in-out property <int> streaming-quality-index: 0;
in-out property <[string]> sample-rates: [];
in-out property <int> sample-rate-index: 0;
in-out property <[string]> backends: [];
in-out property <int> backend-index: 0;
in-out property <[string]> devices: [];
// Parallel to `devices`: true marks a bit-perfect-capable device.
in-out property <[bool]> device-bp: [];
// Parallel to `devices`: a non-empty entry is a section-header label
// shown above that device row (empty = no header).
in-out property <[string]> device-groups: [];
in-out property <int> device-index: 0;
in-out property <[string]> alsa-plugins: [];
in-out property <int> alsa-plugin-index: 0;
// Audio — toggles.
in-out property <bool> limit-quality-to-device: false;
in-out property <bool> alsa-hardware-volume: false;
in-out property <bool> exclusive-mode: false;
in-out property <bool> reserve-dac: false;
in-out property <bool> dac-passthrough: false;
in-out property <bool> pw-force-bitperfect: false;
in-out property <bool> allow-quality-fallback: false;
in-out property <bool> sync-audio-on-startup: false;
in-out property <bool> skip-sink-switch: false;
// Audio — Rust-computed conditional flags driving `if`-gated rows.
in-out property <bool> backend-is-alsa: false;
in-out property <bool> backend-is-pipewire: false;
in-out property <bool> alsa-plugin-is-hw: false;
// --- PLAYBACK ----------------------------------------------------------
// Playback — toggles. `continue-playback` maps to the autoplay mode
// (on = ContinueWithinSource, off = PlayTrackOnly).
in-out property <bool> continue-playback: true;
in-out property <bool> show-context-icon: true;
in-out property <bool> persist-session: false;
in-out property <bool> resume-position: false;
in-out property <bool> gapless: true;
in-out property <bool> stream-uncached: false;
in-out property <bool> streaming-only: false;
// Playback — Initial Buffer Size slider (seconds, 1-10).
in-out property <int> buffer-seconds: 3;
// Playback — "When quality retries fail" dropdown.
in-out property <[string]> retry-behaviors: [];
in-out property <int> retry-behavior-index: 0;
}
// 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 <int> open-index: -1;
// Header x-coordinate of the open tab, so the overlay aligns the
// menu panel under it.
in-out property <length> anchor-x: 0px;
// The open tab's menu rows.
in-out property <[NavMenuEntry]> items: [];
// Index of the row wired to Home (-1 = none); only Discover uses it.
in-out property <int> home-item: -1;
}
export global NavState {
in-out property <ContentView> view: ContentView.home;
// Browser-like history flags, driven by the Rust nav module.
in property <bool> can-back: false;
in property <bool> can-forward: false;
// Navigation requests routed to Rust so history is recorded.
callback request-back();
callback request-forward();
callback request-settings();
}
// Artist detail screen state.
export global ArtistState {
in property <string> name;
in property <string> bio;
in property <image> artwork;
// Artwork-derived tint for the header gradient.
in property <color> header-color: #181820;
in property <[AlbumTrackItem]> top-tracks: [];
// Releases grouped into titled sections (Albums, EPs & Singles, ...).
in property <[DiscoverSection]> release-sections: [];
in property <bool> loading: false;
}
// Shell chrome state — collapsible left sidebar and right-side panels.
// Globals are imported directly by the shell components, so toggles
// need no callback plumbing.
export global ShellState {
in-out property <bool> sidebar-collapsed: false;
in-out property <length> sidebar-width: 280px;
// 0 = none, 1 = queue, 2 = lyrics
in-out property <int> right-panel: 0;
}
// Authenticated user, for the header user element.
export global SessionState {
in property <string> user-name: "";
in property <string> subscription: "";
}
// One entry in the playback queue, for the Queue sidebar list.
export struct QueueItem {
id: string,
title: string,
artist: string,
artwork: image,
playing: bool,
duration: string, // "M:SS"
explicit: bool,
}
// Now-playing / transport state. The Rust playback poll loop pushes the
// live values here; the PlayerBar and SeekBar bind to it directly.
export global NowPlayingState {
in property <bool> has-track: false;
in property <string> title;
in property <string> artist;
in property <string> album;
in property <string> album-id;
in property <string> artist-id;
in property <image> artwork;
in property <string> track-id;
in property <bool> playing: false;
in property <int> position-secs: 0;
in property <int> duration-secs: 0;
in property <float> progress: 0.0; // 0..1
in property <float> cache: 0.0; // 0..1 buffered
in property <string> elapsed: "0:00";
in property <string> remaining: "0:00";
in property <float> volume: 0.7; // 0..1
in property <bool> muted: false;
in property <bool> shuffle: false;
in property <int> repeat-mode: 0; // 0 off, 1 all, 2 one
// Transport callbacks, wired to the Rust playback controller.
callback toggle-play();
callback next();
callback previous();
callback seek(float); // 0..1
callback set-volume(float); // 0..1
callback toggle-mute();
callback toggle-shuffle();
callback cycle-repeat();
}
// Playback queue model, for the Queue sidebar.
//
// The sidebar shows two tabs. The Queue tab renders `now-playing` plus a
// paginated slice of `upcoming` (40 per page). The History tab renders
// `history` (most-recent first). The Rust queue controller owns the full
// upcoming/history lists and the pagination maths; it pushes the current
// page's slice into `upcoming-page` and the counters that label it.
export global QueueState {
// --- Queue tab -------------------------------------------------------
// Current track card. `has-current` gates the NOW PLAYING section.
in property <bool> has-current: false;
in property <QueueItem> now-playing;
// Whether the now-playing track is in the user's favorites.
in property <bool> now-playing-favorite: false;
// The slice of upcoming tracks for the current page (<= 40 entries).
in property <[QueueItem]> upcoming-page: [];
// Total upcoming-track count (post-filter when a search is active).
in property <int> upcoming-total: 0;
// Tracks remaining after the current one (total - played - 1).
in property <int> upcoming-remaining: 0;
// Zero-based page index and total page count for the paginator.
in-out property <int> page: 0;
in property <int> page-count: 1;
// Index of the first upcoming entry shown, 1-based, for the counter.
in property <int> page-start: 0;
// Index of the last upcoming entry shown, 1-based, for the counter.
in property <int> page-end: 0;
// --- History tab -----------------------------------------------------
in property <[QueueItem]> history: [];
// --- Shared ----------------------------------------------------------
// Active tab: 0 = Queue, 1 = History.
in-out property <int> tab: 0;
// Live search query; the Rust controller re-filters `upcoming-page`.
in-out property <string> search-query: "";
// --- Callbacks (wired to the Rust queue controller) ------------------
// Play the upcoming entry at `index` within the current page slice.
callback play-upcoming(int);
// Play a history entry by its `index` in the `history` list.
callback play-history(int);
// Remove the upcoming entry at `index` within the current page slice.
callback remove-upcoming(int);
// Empty the queue.
callback clear-queue();
// Toggle the favorite state of the now-playing track.
callback toggle-now-playing-favorite();
// Save the current queue as a new playlist.
callback save-as-playlist();
// Toggle infinite-play (auto-refill the queue with similar tracks).
callback toggle-infinite-play();
// Re-filter the upcoming list after `search-query` changed.
callback search-changed();
// Move to the previous / next paginator page.
callback prev-page();
callback next-page();
// Switch the active tab (0 = Queue, 1 = History).
callback set-tab(int);
// Fired when the sidebar becomes visible, so the Rust controller can
// pull a fresh queue snapshot (the queue may have changed while the
// panel was closed, e.g. after a session restore).
callback panel-opened();
// Reflects whether infinite-play is currently enabled.
in property <bool> infinite-play: false;
}
// 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 <string> query;
// Active tab: 0 = All, 1 = Albums, 2 = Tracks, 3 = Artists, 4 = Playlists.
in property <int> tab: 0;
in property <bool> loading: false;
in property <[AlbumCardItem]> albums: [];
in property <[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 <int> albums-total: 0;
in property <int> tracks-total: 0;
in property <int> artists-total: 0;
in property <int> playlists-total: 0;
// Most-popular hero: `kind` selects which of the three items is shown.
in property <string> most-popular-kind; // "album" | "artist" | "track" | ""
in property <AlbumCardItem> most-popular-album;
in property <SlimItem> most-popular-artist;
in property <SearchTrackItem> most-popular-track;
// Active searchType filter, as a dropdown index (0 = no filter).
// The controller owns the index -> API-value mapping.
in property <int> filter-index: 0;
}
// Search actions wired to the Rust search controller.
export global SearchActions {
// Enter / explicit submit — navigate to the full results page.
callback submit(string);
// Live (debounced) search as the user types.
callback live(string);
// User picked a results tab (0..4).
callback tab-changed(int);
// searchType filter changed — the new dropdown index.
callback filter-changed(int);
// Request the next page for the given tab.
callback load-more(int);
}