Files
qbz/crates/qbz-slint/ui/state.slint
T
vicrodh b2004e6867 feat(slint): TrackRow favorites + artist/album links, PlaylistView, LabelView, scrollbar
CHECKPOINT — this batch is NOT yet acceptable; substantial feedback is pending. Committed so the work is preserved for rework next session, after a max-effort Tauri code review. Areas: TrackRow favorite toggle/state + clickable artist/album (TrackItem gains is-favorite/artist-id/album-id + fav_cache, populated across all builders); PlaylistView header buttons + cover-art hover overlay; LabelView full AlbumCard + grid/list toggle; vertical scrollbar moved to the right gutter across ~12 views.

cc
2026-05-26 16:36:02 -06:00

1462 lines
60 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).
import { JumpNavTab } from "primitives/JumpNavBar.slint";
export { JumpNavTab }
export struct AlbumCardItem {
id: string,
title: string,
artist: string,
artist-id: string, // for the clickable artist name -> artist page
genre: string, // shown in the hover overlay
year: string, // localized "MMM D, YYYY" — shown in the hover overlay
quality-tier: string, // "hires" | "cd" | "" — drives the badge icon
quality-label: string, // "Hi-Res: 24-bit / 96 kHz" — badge hover tooltip
ribbon: string,
ribbon-kind: string, // "press" | "qobuzissime" | "albumOfTheWeek" | ""
artwork-url: string,
artwork: image, // empty until the artwork pipeline resolves it
// --- 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
}
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,
}
// 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;
// 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 <string> active-tab: "home";
}
// Discover tab switching. Wired in main.rs to crate::home::select_tab.
export global HomeActions {
callback select-tab(string);
}
// Discover "View all" full-list page — one album module (new releases,
// qobuzissimes, ...) opened from a Carousel's See-All link. Paginated.
export global DiscoverBrowseState {
in-out property <string> title;
// Rust-managed: the /discover/<x> endpoint + the next page offset, so
// DiscoverBrowseActions.load-more knows what to fetch next.
in-out property <string> endpoint;
in-out property <int> 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 <bool> loading;
in-out property <bool> loading-more;
in-out property <bool> 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 <string> search-query;
// "grid" | "list" — the view toggle.
in-out property <string> 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 <DiscoverSection> release-watch;
in property <[RadioStationItem]> radio-stations: [];
in property <DiscoverSection> recent-albums;
in property <[SlimItem]> recent-tracks: [];
in property <[SlimItem]> top-artists: [];
in property <[SlimItem]> artists-to-follow: [];
in property <DiscoverSection> more-from-library;
in property <DiscoverSection> rediscover;
// Spotlight — one favorite artist highlighted with a hero + a row
// of their content (top tracks card, radio card, albums).
in property <bool> spotlight-visible: false;
in property <string> spotlight-artist-id;
in property <string> spotlight-name;
in property <string> spotlight-category;
in property <string> spotlight-image-url;
in property <image> spotlight-image;
in property <bool> spotlight-has-top-tracks: false;
in property <[AlbumCardItem]> spotlight-albums: [];
in property <bool> loading: false;
// True once a load has completed, so the tab does not refetch on
// every visit.
in-out property <bool> loaded: false;
}
// One genre in the Filter-by-genre popup's simple grid.
export struct GenreChip {
id: string,
name: string,
selected: bool,
}
// One row of the advanced-view genre tree, flattened (level 0/1/2)
// with only currently-visible rows emitted by the Rust side.
export struct GenreTreeRow {
id: string,
name: string,
level: int, // 0 parent, 1 child, 2 grandchild
selected: bool,
expanded: bool,
has-children: bool,
count: int, // loaded child count (0 = none / unknown)
}
// Filter-by-genre state. The selection is shared across the three
// Discover tabs (matching Tauri's single "home" genre context) — it
// re-fetches the discover index with the chosen genre ids. Populated
// by crate::genre_filter.
export global GenreFilterState {
in property <[GenreChip]> genres: [];
in property <[GenreTreeRow]> tree: [];
in property <int> selected-count: 0;
in-out property <bool> open: false;
in-out property <bool> remember: true;
in-out property <bool> advanced: false;
in-out property <string> search-query;
in property <bool> loading: false;
}
export global GenreFilterActions {
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);
}
// 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,
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,
}
// Album detail screen state.
export global AlbumState {
in property <string> id;
in property <string> title;
in property <string> artist;
in property <string> artist-id;
// Network URL of the cover — used by the artwork right-click
// menu for Open-in-browser and Save-as actions.
in property <string> artwork-url;
in property <bool> has-custom-cover: false;
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 <[TrackItem]> 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);
}
// 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 <bool> active: false;
// Dragged-track count + ghost text (count == 1 shows title +
// "artist · album"; count > 1 shows "N tracks").
in-out property <int> count: 0;
in-out property <string> ghost-title;
in-out property <string> ghost-subtitle;
// Global pointer position (window coords) while dragging.
in-out property <length> pointer-x;
in-out property <length> pointer-y;
// The sidebar playlist id currently under the pointer (drop target).
in-out property <string> over-playlist-id;
}
export global DragActions {
// Begin a track drag from a row. `track-id` is the row; the Rust
// side expands it to the multi-selection when applicable.
callback start(string /* track id */, string /* title */, string /* subtitle */, length, length);
callback move(length /* gx */, length /* gy */);
callback end();
}
// Which content view the shell shows.
export enum ContentView { home, album, artist, settings, search, musician, label, location, favorites, mix, playlist, discover-browse, playlist-manager }
// 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 }
// 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 <bool> visible: false;
in-out property <string> message;
in-out property <ToastKind> kind: ToastKind.info;
in-out property <bool> persistent: false;
in-out property <bool> enabled: true;
}
// One row in the sidebar playlists list (also used for the folder
// list in the move-to-folder menu).
export struct SidebarPlaylistItem {
id: string,
name: string,
}
// One flattened entry in the sidebar nav list — a folder header or a
// playlist row (indented when inside a folder).
//
// Playlist rows carry up to four cover-art URLs + decoded images so the
// row can render a micro-collage instead of the generic list-music glyph
// (mirrors Tauri's `sidebarPlaylistCollage`). The covers come from the
// same `get_user_playlists()` payload (images300 / images150 / images),
// so no extra per-playlist fetch is needed.
export struct SidebarEntry {
kind: string, // "folder" | "playlist"
id: string,
name: string,
expanded: bool, // folders
count: int, // folder playlist count
indent: bool, // playlist nested in a folder
folder-id: string, // playlist's current folder id ("" = root; folders "")
cover-count: int, // number of cover images, 0-4 (playlists)
url1: string,
url2: string,
url3: string,
url4: string,
cover1: image,
cover2: image,
cover3: image,
cover4: image,
}
// 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 <bool> loading: false;
// The currently-open playlist id, for the active-row highlight.
in property <string> active-id;
// PLAYLISTS section collapse (the folder+playlist list hides). Mirrors
// Tauri's `playlistsCollapsed`.
in-out property <bool> 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 <bool> search-open: false;
in-out property <string> search-query;
// Active sort option: "name" | "recent" | "tracks" | "playcount"
// | "custom" (mirrors Tauri's SortOption). Persisted by crate::sidebar.
in-out property <string> 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 <bool> playlist-collage: true;
}
export global SidebarActions {
callback open-playlist(string /* id */);
callback create-playlist();
callback create-folder();
callback toggle-folder(string /* folder id */);
callback delete-folder(string /* folder id */);
// Move a playlist into a folder ("" folder id = root).
callback move-playlist(string /* playlist id */, string /* folder id */);
// Right-click context menu actions (folder + playlist rows).
callback edit-playlist(string /* id */);
callback edit-folder(string /* id */);
callback hide-playlist(string /* id */);
callback hide-folder(string /* id */);
// Filter `menu-folders` by a case-insensitive substring of the
// move-to-folder search box (Rust does the substring match).
callback search-folders(string /* query */);
// Pick a sort option (re-sorts the list). Same five values as Tauri.
callback set-sort(string /* option */);
// Re-run the playlist-name filter (called as the search input edits).
callback search-changed(string /* query */);
// Re-fetch the playlist list from the network.
callback refresh-playlists();
// Open the full Playlist Manager view.
callback manage-playlists();
}
// "Create playlist" modal state. Mirrors Tauri's PlaylistModal (create
// mode): name, optional description, target folder, public flag.
export global CreatePlaylistState {
in-out property <bool> open: false;
in-out property <string> name;
in-out property <string> description;
in-out property <bool> is-public: false;
in property <bool> 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 <int> folder-index: 0;
}
export global CreatePlaylistActions {
callback submit();
callback close();
}
// "Create folder" modal state.
export global CreateFolderState {
in-out property <bool> open: false;
in-out property <string> name;
in property <bool> creating: false;
}
export global CreateFolderActions {
callback submit();
callback close();
}
// Playlist detail screen state. Rows reuse the shared TrackRow
// (show-artwork + show-album); populated by crate::playlist.
export global PlaylistState {
in property <string> id;
in property <string> name;
in property <string> owner;
in property <string> description;
// Truncated description for the header (~2 lines); full text in
// the Read-more modal. Equal to description when it fits.
in property <string> description-short;
in property <int> track-count: 0;
in property <string> total-duration;
in property <string> cover-url;
in property <image> cover;
// True when a user-set custom artwork is shown (overrides the
// track-cover collage).
in property <bool> has-custom: false;
in property <bool> is-favorite: false;
// True when the playlist is one of the user's own (editable).
in property <bool> is-owner: false;
// Track edit mode — rows show a selection checkbox and a bulk bar
// appears (remove selected). Only available to the owner.
in-out property <bool> multi-select-mode: false;
in property <int> selected-count: 0;
in property <[TrackItem]> tracks: [];
in property <bool> loading: false;
// Sort: "" / "default" = playlist order; else title|artist|album|
// duration. `sort-asc` flips on re-selecting the active field.
in property <string> sort-field: "default";
in property <bool> 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 <bool> open: false;
in property <string> id;
in-out property <string> name;
in-out property <string> description;
in property <bool> busy: false;
}
export global EditPlaylistActions {
callback save();
callback delete();
callback close();
}
// === Playlist Manager ===================================================
// A full playlist + folder organization surface (Tauri's
// PlaylistManagerView). Rows are precomputed in Rust (crate::playlist_manager)
// and pushed as ready-to-render models — no per-row map lookups in .slint.
// One folder card / chip / tree-folder. `icon-color` is a solid hex (or
// "" → use the accent). Gradient folder colors are not supported here
// (Slint brush limitation); a gradient string falls back to the accent.
export struct PmFolderItem {
id: string,
name: string,
count: int, // playlists in this folder
// "preset" or "custom".
icon-type: string,
// Lucide preset name ("heart" | "star" | "music" | "folder" | "disc"
// | "library" | "headphones"); drives which glyph the card shows.
icon-preset: string,
// Resolved solid color for the icon square (parsed from the stored
// hex by the Rust controller). When `has-color` is false the card
// uses the accent instead (gradient / empty / unparseable colors
// fall back to accent — Slint has no gradient-brush-in-struct).
icon-color: color,
has-color: bool,
is-hidden: bool,
// Decoded custom image (icon-type == "custom"); empty otherwise.
custom-image: image,
has-custom-image: bool,
}
// One playlist card / list-row / tree-row. All display strings are
// precomputed (track line, local line) so the view never does lookups.
export struct PmPlaylistItem {
id: string, // playlist id (may be negative for offline pending)
name: string,
// "12 tracks" — total (remote + local).
tracks-line: string,
// "1h 43m" — total playtime, "" when zero (list-row meta only).
duration-line: string,
// "(3 local)" — local-only suffix, "" when none.
local-line: string,
local-count: int,
total-count: int,
play-count: int,
// "" | "all_local" | "some_local" | "no" — availability badge.
local-status: string,
is-favorite: bool,
is-hidden: bool,
folder-id: string, // "" = root
// Micro-collage covers (same 0-4 scheme as the sidebar).
cover-count: int,
url1: string,
url2: string,
url3: string,
url4: string,
cover1: image,
cover2: image,
cover3: image,
cover4: image,
}
// One flattened row of the tree view — a folder header or a nested /
// root playlist. The Rust side emits only currently-visible rows
// (collapsed folders hide their children).
export struct PmTreeRow {
kind: string, // "folder" | "playlist"
expanded: bool, // folders
folder: PmFolderItem, // populated when kind == "folder"
playlist: PmPlaylistItem, // populated when kind == "playlist"
indent: bool, // playlist nested under a folder
}
export global PlaylistManagerState {
in property <[PmFolderItem]> folders: [];
// Filtered folder list for the list-row move-to-folder menu. Starts as
// the full folder set; the menu's search box narrows it via
// `search-folders` (Slint strings have no `contains`, so Rust filters).
in property <[PmFolderItem]> menu-folders: [];
in property <[PmPlaylistItem]> playlists: []; // the filtered+sorted grid/list set
in property <[PmTreeRow]> tree: [];
// Header counters: total folders / total playlists (post-filter for
// the visible playlist count).
in property <int> folder-count: 0;
in property <int> playlist-count: 0;
in property <bool> loading: false;
// Toolbar state (session-scoped; persisted per-user where cheap).
in-out property <string> search-query;
// "all" | "visible" | "hidden".
in-out property <string> filter: "all";
// "name" | "recent" | "playcount" | "tracks" | "custom".
in-out property <string> sort: "name";
// "grid" | "list" | "tree".
in-out property <string> view-mode: "grid";
// Folder mode on (folders shown + folder filtering) vs flat.
in-out property <bool> folder-mode: true;
// Folders section collapse.
in-out property <bool> folders-collapsed: false;
// Reorder is only allowed when sort == "custom".
in property <bool> 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 */);
}
// 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 <bool> open: false;
// Empty id = create mode; non-empty = editing that folder.
in property <string> id: "";
in-out property <string> name;
in-out property <string> icon-preset: "folder";
// Selected solid color hex; "" = accent.
in-out property <string> icon-color: "";
in-out property <bool> is-hidden: false;
// Custom image path (icon-type == "custom" when set) + decoded image.
in-out property <string> custom-image-path: "";
in property <image> custom-image;
in property <bool> 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 <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;
}
// 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 -------------------------------------------------------------
in-out property <[string]> themes: [
"Dark", "OLED Black", "Light", "System",
"Warm", "Nord", "Dracula", "Tokyo Night", "Catppuccin Mocha",
"Breeze Dark", "Adwaita Dark", "Alucard", "Aurora", "Ikari",
"Ayanami", "Iscariot", "Stratego", "Rumi", "Zoey", "Mira",
"Rose Pine Dawn", "Breeze Light", "Adwaita Light", "Duotone Snow",
"Snow Storm", "Frost", "Langley", "Kurosaki",
"WCAG Light", "WCAG Dark", "High Contrast", "Colorblind"
];
in-out property <int> 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 <int> theme-filter: 0;
in-out property <bool> album-header-gradient: true;
// Auto-Theme (shown when the "System" theme is selected). Source:
// 0 = System Colors, 1 = Wallpaper Sync, 2 = Custom Image.
in-out property <bool> theme-is-system: false;
in-out property <[string]> auto-theme-sources: ["System Colors", "Wallpaper Sync", "Custom Image"];
in-out property <int> auto-theme-source-index: 0;
in-out property <string> auto-theme-custom-path: "";
in-out property <bool> auto-theme-generating: false;
in-out property <string> auto-theme-detected-de: "";
// --- TYPOGRAPHY / SCALE / LANGUAGE -------------------------------------
in-out property <[string]> languages: ["Auto", "English", "Español", "Français", "Deutsch", "Português"];
in-out property <int> language-index: 0;
in-out property <[string]> fonts: ["LINE Seed JP", "Montserrat", "Noto Sans", "Source Sans 3", "System"];
in-out property <int> font-index: 0;
in-out property <[string]> ui-scales: ["80%", "85%", "90%", "95%", "100%", "110%", "125%", "150%", "175%"];
in-out property <int> ui-scale-index: 4;
// --- VISUAL EXTRAS -----------------------------------------------------
// sidebar-playlist-collage is wired to SidebarState.playlist-collage in
// the panel (the only already-functional flag here).
in-out property <bool> sidebar-playlist-collage: true;
in-out property <bool> local-library-track-artwork: false;
// --- NOTIFICATIONS -----------------------------------------------------
in-out property <bool> in-app-toasts: true;
in-out property <bool> system-notifications: true;
// --- WINDOW TITLE ------------------------------------------------------
in-out property <bool> window-title-show: false;
in-out property <string> window-title-template: "";
// --- TITLE BAR / WINDOW CHROME -----------------------------------------
in-out property <bool> use-system-title-bar: false;
in-out property <bool> hide-title-bar: false;
in-out property <bool> match-system-chrome: false;
// True when GPU acceleration is on (gates match-system-chrome). Default
// true so the row is interactive in the unwired replica.
in-out property <bool> hardware-accel-enabled: true;
in-out property <bool> search-in-title-bar: false;
// Navigation-in-title-bar group: the parent label row plus per-target
// toggles (Discover/Home, Favorites, Library, My QBZ, Purchases).
in-out property <bool> nav-tb-discover: false;
in-out property <bool> nav-tb-favorites: false;
in-out property <bool> nav-tb-library: false;
in-out property <bool> nav-tb-myqbz: false;
in-out property <bool> nav-tb-purchases: false;
// Nav buttons position: 0 = Auto, 1 = Left, 2 = Right.
in-out property <[string]> nav-tb-positions: ["Auto (opposite to window controls)", "Left", "Right"];
in-out property <int> nav-tb-position-index: 0;
// Window controls group.
in-out property <[string]> wc-positions: ["Left", "Right"];
in-out property <int> wc-position-index: 1;
in-out property <[string]> wc-styles: ["Rectangular", "Full-height rounded", "Circular", "Square"];
in-out property <int> wc-style-index: 0;
in-out property <[string]> wc-sizes: ["Small", "Normal", "Large"];
in-out property <int> wc-size-index: 1;
in-out property <[string]> wc-color-presets: ["Default", "macOS", "Adwaita", "Monochrome", "Custom"];
in-out property <int> wc-color-preset-index: 0;
in-out property <bool> show-window-controls: true;
// --- MINIPLAYER --------------------------------------------------------
in-out property <[string]> miniplayer-views: ["Remember last used", "Micro", "Compact", "Artwork", "Queue", "Lyrics"];
in-out property <int> miniplayer-view-index: 0;
// --- STARTUP -----------------------------------------------------------
in-out property <[string]> startup-pages: ["Home", "Where you left off"];
in-out property <int> startup-page-index: 0;
in-out property <bool> show-purchases: false;
// --- SYSTEM TRAY -------------------------------------------------------
in-out property <bool> tray-enable: true;
in-out property <bool> tray-minimize-to-tray: false;
in-out property <bool> tray-close-to-tray: false;
in-out property <[string]> tray-icon-themes: ["Auto", "Mono light", "Mono dark", "Color"];
in-out property <int> tray-icon-theme-index: 0;
// --- IMMERSIVE ---------------------------------------------------------
in-out property <[string]> immersive-views: [
"Remember last used", "Coverflow", "Static", "Vinyl", "Visualizer",
"Neon Flow", "Tunnel", "Comet", "Lyrics", "Queue",
"Split: Lyrics", "Split: Track Info", "Split: Suggestions", "Split: Queue"
];
in-out property <int> immersive-view-index: 0;
in-out property <[string]> immersive-background-modes: ["Full", "Lite", "Off"];
in-out property <int> immersive-background-mode-index: 0;
// Collapsible sub-panels (collapsed by default, matching Tauri).
in-out property <bool> immersive-panels-collapsed: true;
in-out property <bool> immersive-fps-collapsed: true;
// Callbacks. Unwired in this replica — the Rust side may bind them
// later. Kept generic (key + value) so the controller can dispatch.
callback appearance-bool(string /* key */, bool /* value */);
callback appearance-select(string /* key */, int /* index */);
callback appearance-text(string /* key */, string /* value */);
callback appearance-action(string /* key */);
}
// One entry of a nav-tab dropdown.
export struct NavMenuEntry {
label: string,
icon: image,
has-glyph: bool,
// Destination route fired when the row is clicked, e.g. "home",
// "favorites-tracks", "favorites-albums". Empty = no-op (the
// destination isn't implemented yet).
route: string,
}
// Header nav-tab dropdown state.
//
// The four header tabs (Discover / Library / Local Library / My QBZ) no
// longer own a PopupWindow each — a PopupWindow grabs the pointer while
// open, which blocks hover-switching between tabs. Instead a single
// in-window overlay (HeaderMenuOverlay, a late child of the AppShell
// root) draws the open tab's menu. Tabs write their identity into this
// global on hover/click; hovering a different tab overwrites it, giving
// an instant switch. `open-index` of -1 means no menu is open.
export global HeaderMenuState {
// Which tab's menu is open: -1 = none, 0..3 = the tab index.
in-out property <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.
// 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 <string> id;
in property <string> name;
// Network URL of the portrait — used by the artwork right-click
// menu for Open-in-browser and Save-as actions.
in property <string> 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 <bool> has-custom-image: false;
in property <string> 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 <string> bio-short;
in property <bool> bio-truncated: false;
// Editorial source for the biography ("TiVo" etc); shown as a small
// "Source: <name>" line under the Read more toggle when present.
in property <string> bio-source;
in property <image> artwork;
// Artwork-derived tint for the header gradient.
in property <color> header-color: #181820;
in property <[TrackItem]> top-tracks: [];
// Releases grouped into titled sections (Albums, EPs & Singles, ...).
in property <[DiscoverSection]> release-sections: [];
// Labels collected from the artist's own album releases (deduped by
// id, alphabetically sorted) — feed the Labels section of the
// network sidebar.
in property <[LabelEntry]> labels: [];
// Similar artists from the /artist/page response — feed the Similar
// Artists section of the network sidebar.
in property <[SimilarEntry]> similar-artists: [];
// Filtered JUMP TO tabs for this artist — only the sections
// that actually have content. Built in artist::apply_artist
// from the page response. anchor-y is the page-local y the
// ArtistPageView will scroll to when the tab is clicked.
in property <[JumpNavTab]> jump-tabs: [];
in property <bool> 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 <bool> 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 <bool> 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 <string> mb-mbid;
// Origin (MB metadata) section
in property <bool> origin-loading: false;
in property <MbOriginData> origin;
// Relationships (MB) section
in property <bool> relationships-loading: false;
in property <MbRelationshipsData> relationships;
// Discovery (MB tag-based) section — populated in phase 8.
in property <bool> 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 <string> 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 <string> name;
in property <string> role;
// "confirmed" | "contextual" | "weak" | "none" — the sidebar
// uses this to skip MusicianPageView entirely on Confirmed and
// navigate to the matched artist instead.
in property <string> 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 <int> total: 0;
in property <bool> loading: false;
in property <bool> 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 <string> id;
in property <string> name;
in property <string> image-url;
in property <image> image;
in property <[AlbumCardItem]> albums: [];
in property <int> total: 0;
in property <bool> loading: false;
in property <bool> load-more-loading: false;
// "grid" | "list" — the view toggle (mirrors DiscoverBrowseView).
// UI-owned, like Discover's; no Rust round-trip needed.
in-out property <string> view-mode: "grid";
}
export global LabelActions {
callback load-more();
}
// 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 <string> scene-label;
in property <string> genre-summary;
in property <[LocationArtistItem]> artists: [];
in property <int> total: 0;
in property <bool> loading: false;
in property <bool> load-more-loading: false;
}
export global LocationViewActions {
callback open-artist(string /* qobuz id */);
callback load-more();
}
// Shell chrome state — collapsible left sidebar and right-side panels.
// Globals are imported directly by the shell components, so toggles
// need no callback plumbing.
export global ShellState {
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;
}
// One row in the "Add to playlist" picker.
export struct PlaylistPickItem {
id: string,
name: string,
tracks-line: string, // "N tracks"
}
// "Add to playlist" picker — a global overlay opened from the track
// context menu. Holds the pending track id while the user picks a
// destination playlist. Populated by crate::playlist_picker.
export global PlaylistPickerState {
in-out property <bool> open: false;
in property <string> track-id;
in property <[PlaylistPickItem]> playlists: [];
in property <bool> loading: false;
}
export global PlaylistPickerActions {
callback pick(string /* playlist id */);
callback close();
}
// 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,
}
// One artist tile in the Favorites > Artists grid.
export struct FavoriteArtistItem {
id: string,
name: string,
albums-line: string,
image-url: string,
image: image,
}
// One playlist tile in the Favorites > Playlists grid.
export struct FavoritePlaylistItem {
id: string,
name: string,
owner: string,
tracks-line: string, // "N tracks"
cover-url: string,
cover: image,
}
// One label row in the Favorites > Labels list.
export struct FavoriteLabelItem {
id: string,
name: string,
albums-line: string, // "N releases"
}
// 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 <string> active-tab: "tracks";
// Tracks reuse TrackItem so the shared SearchTrackRow can
// render them (artwork-on, per-row cover thumbnail).
in property <[TrackItem]> tracks: [];
in property <[AlbumCardItem]> albums: [];
in property <[FavoriteArtistItem]> artists: [];
in property <[FavoritePlaylistItem]> playlists: [];
in property <[FavoriteLabelItem]> labels: [];
in property <int> tracks-total: 0;
in property <int> albums-total: 0;
in property <int> artists-total: 0;
in property <int> playlists-total: 0;
in property <int> labels-total: 0;
in property <bool> loading: false;
}
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 */);
}
// 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 <string> kind; // daily | weekly | fav | top
in property <string> title;
in property <string> subtitle;
in property <[TrackItem]> tracks: [];
in property <int> track-count: 0;
in property <string> total-duration;
in property <bool> loading: false;
}
// Search results screen state. The Rust search controller populates the
// four category lists, their totals, and the most-popular hero entry.
export global SearchState {
in property <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 <[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 <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 <TrackItem> 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 <string> most-popular-quality-label;
// 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);
}