Files
qbz/crates/qbz-slint/ui/state.slint
T
vicrodh da7758b9d6 feat(slint): new now-playing bar layout
Land the "New" now-playing bar as the first of the four switchable
layouts. A dedicated SongCard component holds the cover (or an idle
sleeping cat), the elidable title, the artist·album links, and the
vertical audio stamp (compact quality badge over the backend/mode LEDs).
A full-width seek bar sits on top with a buffer-driven stream lock.

The controls row uses responsive symmetric column zones (39/30/25% by
window width); the side columns share one fraction so the transport stays
absolutely centred and PLAY lands on the window centre. The song-card /
controls divider is a hard wall: the text column stretches to push the
audio stamp to its right edge, the album elides first (and never collapses
to zero) so the stamp is always visible and never crosses the wall.

Adds QualityBadgeFull compact mode, backend/mode output LED labels from
settings, a global TooltipOverlay so clipped columns keep their tooltips,
and the supporting icons.

cc
2026-06-02 06:48:36 -06:00

2265 lines
101 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
is-favorite: bool, // filled accent heart on the card when favorited
removing: bool, // fading out (un-favorited in the favorites view)
// --- List-row extras (AlbumListRow; ignored by the grid AlbumCard) ----
release-type: string, // "Album" | "EP" | "Single" | "Live" | "Compilation"
source: string, // "qobuz" | "local" | "plex" | "" — hideable column
quality-detail: string,// "24-bit / 96 kHz" — bare detail for QualityBadgeFull
track-count: string, // "12" — TRACKS column
plain-year: string, // "1973" — bare year for the list-row YEAR column
}
// One entry in an A-Z jump strip (favorites tracks name-group / albums
// A-Z group). `index` is the first row index (tracks); albums jump
// proportionally by the entry's ordinal so it needs no exact offset.
export struct AlphaJump {
letter: string,
index: int,
}
// One button in the shared multi-select bulk bar (MultiSelectBar).
export struct BulkAction {
id: string, // emitted verbatim via MultiSelectBar.action(id)
label: string, // already-translated button label
danger: bool, // red-bordered destructive button (e.g. Remove)
needs-selection: bool, // dimmed + disabled while nothing is selected
}
export struct DiscoverSection {
title: string,
// Discover endpoint path this section maps to (e.g. "/discover/newReleases").
// Empty for sections with no full-list page; non-empty shows the "View all"
// link and drives the DiscoverBrowse page.
endpoint: string,
albums: [AlbumCardItem],
}
// Compact "slim" card item — a small thumbnail with a title/subtitle,
// optionally rank-numbered (Popular albums, Recently Played).
export struct SlimItem {
id: string,
title: string,
subtitle: string,
rank: string, // "" = no rank
artwork-url: string,
artwork: image,
// Artist follow state — drives the ArtistCard Follow button.
following: bool,
}
// A playlist result in search (and the label landing's Playlists row).
// Qobuz returns up to four cover URLs per playlist; the card renders them
// as a count-based collage. Defined here (before LabelState) so the label
// landing can reuse it — Slint structs must be declared before first use.
export struct SearchPlaylistItem {
id: string,
title: string,
subtitle: string, // owner name + track count
cover-count: int, // number of cover images, 0-4
url1: string,
url2: string,
url3: string,
url4: string,
cover1: image,
cover2: image,
cover3: image,
cover4: image,
}
// Discover / Home screen state.
export global HomeState {
in property <[DiscoverSection]> sections: [];
in property <[SlimItem]> popular: [];
in property <[SlimItem]> recent: [];
in property <[AlbumCardItem]> recent-albums: [];
in property <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 {
// Set the context the popup edits ("discover" | "favorites"), called
// when a surface opens the shared popup.
callback set-context(string);
callback toggle(string /* genre id */);
callback toggle-expand(string /* genre id */);
callback search(string /* query */);
callback clear();
callback set-remember(bool);
callback set-advanced(bool);
}
// Small string helpers backed by Rust. Slint 1.16 has no `contains`
// builtin, so the searchable QbzSelect filter (long device lists) needs a
// native case-insensitive substring test. Pure so it can be called from
// property bindings; reusable by any searchable list.
export global TextUtil {
pure callback contains-ci(string /* haystack */, string /* needle */) -> bool;
}
// The single, shared track-row model. Superset of every track-list
// surface (album, artist popular, search, favorites, mix, playlist),
// rendered by the shared primitives/TrackRow component. Fields that a
// given surface doesn't use stay empty/false:
// - `number` explicit track number (album/playlist); rows fall
// back to their list index when empty.
// - `album` shown only by rows with show-album (mix/playlist).
// - `selected` multi-select state (artist Popular, playlist editor).
// - `artwork` the cover thumbnail for show-artwork rows.
export struct TrackItem {
id: string,
number: string,
title: string,
artist: string,
album: string,
duration: string,
quality-tier: string,
// Exact bit-depth / sample-rate line, e.g. "24-bit / 96 kHz" — feeds the
// track-row quality badge (empty for unknown / where only the tier shows).
quality-detail: string,
explicit: bool,
selected: bool,
artwork-url: string,
artwork: image,
// Favorite state — drives the heart fill and the optimistic toggle.
// Populated from the user's favorite-track set when each row is built.
is-favorite: bool,
// Ids for the clickable artist/album links in the row. Empty = the
// text is plain (not a link). artist-id -> artist view, album-id ->
// album view, both via the media-action bubble channel.
artist-id: string,
album-id: string,
// "qobuz" | "local" | "plex" | "" — drives the Local Library source
// indicator (shown instead of the favorite/offline columns).
source: string,
// Fading out (un-favorited in the favorites view) before removal.
removing: bool,
// Offline-cache status: 0 none, 1 queued, 2 downloading, 3 ready, 4 failed.
// Drives the per-row offline affordance (make-offline / ring / check / warn).
cache-status: int,
// Download progress 0.0..1.0 — meaningful only while cache-status == 2.
cache-progress: float,
// True while this track's offline CMAF bundle is being decrypted for
// playback — drives the padlock glyph (swaps the play/equalizer icon).
unlocking: bool,
}
// Album detail screen state.
export global AlbumState {
in property <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;
// Label id for the sidebar label card → label page navigation ("" = none).
in property <string> label-id;
in property <[string]> awards: [];
in property <[TrackItem]> tracks: [];
in property <bool> loading: false;
// True when this is a Local Library album (local files / offline copies).
// Routes the play actions to local playback + hides Qobuz-only chrome.
in property <bool> is-local: false;
// Local source ("user" | "qobuz_download" | "plex" | ""). Gates the
// tag-editor pencil (hidden for Plex; tag write isn't supported there).
in property <string> source;
}
// Album-view actions wired to Rust callbacks.
export global AlbumActions {
// Filter the visible track list by a query (client-side).
callback search(string);
}
// Track drag-and-drop onto sidebar playlists (a star QBZ feature).
// Slint has no HTML5 DnD, so this is pointer-based: the source row
// grabs the pointer and feeds global coords here; the floating ghost
// + each sidebar playlist row read this to self-detect the drop.
export global DragState {
in-out property <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, label-releases, location, favorites, mix, playlist, discover-browse, playlist-manager, offline-manager, local-library, local-album }
// In-app toast (transient notification). Mirrors Tauri's toastStore:
// a single toast at a time (a new one replaces the current), auto-hidden
// by the Rust layer per kind. `buffering` is persistent (no auto-hide).
export enum ToastKind { success, error, info, warning, buffering }
// Local Library (on-disk + Plex music). Greenfield Slint port — the view
// shows header stats + the browse tabs (Albums / Artists / Folders /
// Tracks). All management/maintenance/danger actions live under
// Settings > Local Library; the gear button routes there.
// One card in the Local Library Artists grid (name + "N albums · M tracks").
// Images + click-to-detail are a later slice.
export struct LocalArtistItem {
name: string, // raw/canonical key (used for selection + matching)
display-name: string, // canonical display name (after name-merge)
album-count: int,
track-count: int,
image-path: string, // local file path OR cached Qobuz URL ("" = none)
image: image, // decoded portrait (default-empty = show placeholder)
}
// One A-Z section of the local Artists left rail (master list grouped by the
// display name's first letter, "#" bucket for non-alphabetic). 1:1 with the
// Tauri Artists tab, which groups the compact rows under sticky letters.
export struct LocalArtistSection {
letter: string,
artists: [LocalArtistItem],
}
// One visible row in the Folders tree (flattened-model lazy tree — Slint has
// no native TreeView). Folders expand to fetch children lazily.
export struct FolderNode {
path: string, // absolute filesystem path (the node id)
segment: string, // last path component (display)
depth: int, // indent level
is-folder: bool, // folder vs. track row
expanded: bool, // folder open?
can-expand: bool, // has children (track_count_under > 0)
track-count: int, // recursive track count under a folder
}
export global LocalLibraryState {
// "albums" | "artists" | "folders" | "tracks"
in-out property <string> active-tab: "albums";
in property <bool> loading: false;
in property <string> load-error: "";
// Header stats / per-tab badge counts.
in property <int> album-count: 0;
in property <int> artist-count: 0;
in property <int> track-count: 0;
in property <int> folder-count: 0;
// --- Albums tab (server-paginated, metadata-grouped local albums) ----
// `albums` is the accumulated paged set (also the artwork target). Sort
// + search are pushed to SQL (the performant path); paging fetches the
// next chunk on scroll. List / group / A-Z / filters land with the next
// slice — this slice ships the chunked grid.
in property <[AlbumCardItem]> albums: [];
in property <int> albums-total: 0;
in property <bool> albums-loading: false; // initial page fetch
in property <bool> albums-loading-more: false; // subsequent pages
in property <bool> albums-has-more: false;
in property <bool> albums-load-failed: false;
in-out property <int> albums-next-offset: 0;
in-out property <string> albums-search: "";
// "artist-asc" | "artist-desc" | "title-asc" | "title-desc"
// | "year-desc" | "year-asc"
in-out property <string> albums-sort: "artist-asc";
// "grid" | "list" view toggle.
in-out property <string> albums-view-mode: "grid";
// `albums` is the full mapped set (artwork-index target). `albums-visible`
// / `albums-grouped` are the search+filter+sort+group-derived render sets.
in property <[AlbumCardItem]> albums-visible: [];
in property <[DiscoverSection]> albums-grouped: [];
in-out property <string> albums-group: "off"; // off | alpha | artist
in property <[AlphaJump]> albums-alpha: []; // A-Z jumps (alpha group)
in property <int> albums-shown: 0; // count after filter
// --- Tracks tab (server-paginated flat list — the 16K-freeze fix) ---
// Group-by / multi-select / per-row play land with the playback slice.
in property <[TrackItem]> tracks: [];
in property <int> tracks-total: 0;
in property <bool> tracks-loading: false;
in property <bool> tracks-loading-more: false;
in property <bool> tracks-has-more: false;
in property <bool> tracks-load-failed: false;
in-out property <int> tracks-next-offset: 0;
in-out property <string> tracks-search: "";
// Group-by + multi-select (this slice). `tracks-visible` is the
// group-ordered + search-filtered render model (the list renders THIS).
in property <[TrackItem]> tracks-visible: [];
in-out property <string> tracks-group-mode: "off"; // off | album | artist | name
in property <[AlphaJump]> tracks-alpha: []; // A-Z jumps (name grouping)
in-out property <bool> tracks-multi-select: false; // checkbox column on
in property <int> tracks-selected-count: 0; // bar count + select-all
// --- Folders tab ---------------------------------------------------
// flat = album grid grouped by directory; tree = two-pane lazy filesystem.
in-out property <string> folders-view-mode: "flat"; // "flat" | "tree"
// Flat mode: `folders` is the full set (artwork target); `folders-visible`
// / `folders-grouped` are the search/sort/group-derived render sets.
in property <[AlbumCardItem]> folders: [];
in property <bool> folders-loading: false;
in property <bool> folders-load-failed: false;
in property <[AlbumCardItem]> folders-visible: [];
in property <[DiscoverSection]> folders-grouped: [];
in-out property <string> folders-search: "";
in-out property <string> folders-sort: "title-asc"; // title-asc|title-desc|artist-asc|year-desc
in-out property <string> folders-group: "off"; // off | alpha | artist
in-out property <string> folders-grid-view: "grid"; // grid | list
// Tree mode: a flattened visible-node list + the selected folder's detail
// (its direct tracks; subfolders shown as a grid).
in property <[FolderNode]> folder-tree: [];
in property <bool> folder-tree-loading: false;
in-out property <string> folders-selected-path: "";
in property <string> folders-selected-name: "";
in property <[FolderNode]> folder-detail-subfolders: [];
in property <[TrackItem]> folder-detail-tracks: [];
in property <bool> folder-detail-loading: false;
// --- Artists tab — two-column master/detail (1:1 with Tauri) --------
// Left rail: the merged/deduped artist master list. `artists` is the full
// merged set (post name-merge); `artists-grouped` / `artists-alpha` are
// the search-and-group-derived render sets; `artists-shown` is the count
// after the search filter (drives the rail header).
in property <[LocalArtistItem]> artists: [];
in property <bool> artists-loading: false;
in property <bool> artists-load-failed: false;
in-out property <string> artists-search: "";
in property <[LocalArtistSection]> artists-grouped: [];
in property <[AlphaJump]> artists-alpha: [];
in property <int> artists-shown: 0;
// Right pane: the selected artist's albums (filtered in place from the
// loaded album set — no new backend call, mirroring Tauri).
in-out property <string> artists-selected-name: ""; // "" = none selected
in property <string> artists-selected-display: "";
in property <[AlbumCardItem]> artists-selected-albums: [];
in property <bool> artists-selected-loading: false;
// Artist-image background fetch (capped Qobuz portrait fill).
in-out property <bool> artists-fetch-images: true; // default ON (mirrors Tauri)
in property <bool> artists-images-fetching: false; // a fetch session is running
in property <int> artists-images-fetched: 0; // painted this session
}
export global LocalLibraryActions {
// Switch browse tab — records a per-tab history entry (mirrors Favorites
// / Discover) so back/forward moves between Local Library tabs.
callback select-tab(string /* "albums"|"artists"|"folders"|"tracks" */);
// Open Settings > Local Library (the gear button — management lives there).
callback open-settings();
// --- Albums tab ---
// Re-query page 1 with the current search text (server-side LIKE).
callback albums-search(string /* query */);
// Set the sort key + re-derive (in-memory, full-load).
callback albums-set-sort(string /* sort key */);
// Set group mode (off|alpha|artist) + re-derive.
callback albums-set-group(string /* mode */);
// Set grid|list view.
callback albums-set-view(string /* mode */);
// A quality/format/source filter toggle changed -> re-derive.
callback albums-filter-changed();
// Clear all quality/format/source filters.
callback albums-clear-filter();
// Retry after a load error.
callback albums-retry();
// Open a local album's detail view (lands with the album-detail slice).
callback open-album(string /* album id */);
// Open an artist (lands with the Artists slice).
callback open-artist(string /* artist id */);
// Per-card action (play / queue / play-next) — lands with the playback
// slice. Wired now so the shared AlbumCollectionView contract is honored.
callback album-action(string /* album id */, string /* action */);
// --- Tracks tab ---
callback tracks-search(string /* query */);
callback tracks-load-more();
callback tracks-retry();
// Per-row play/queue/toggle-select (action: play|play-next|queue|toggle-select).
callback track-action(string /* track id */, string /* action */);
// Group-by mode (off|album|artist|name), persisted.
callback tracks-set-group(string /* mode */);
// Toggle the multi-select checkbox column.
callback tracks-toggle-multi-select();
// Bulk bar action: select-all | clear | queue | play-next | add-to-playlist.
callback tracks-bulk-action(string /* action */);
// --- Folders tab ---
callback folders-search(string /* query */);
callback folders-set-sort(string /* sort key */);
callback folders-set-group(string /* off|alpha|artist */);
callback folders-set-mode(string /* flat|tree */);
// Tree: expand/collapse a folder node (lazy child fetch on expand).
callback folders-toggle-node(string /* path */, bool /* now-expanded */);
// Tree: select a folder -> load its detail (direct tracks + subfolders).
callback folders-select(string /* path */, string /* segment */);
// Tree: play everything under a folder (recursive).
callback folders-play-node(string /* path */);
// Tree: play one track from the selected folder's direct tracks (the
// folder's direct list becomes the queue, starting at this track).
callback folders-play-track(string /* track id */);
// --- Artists tab ---
// Filter the master list (two-way bound to artists-search; re-derive).
callback artists-search(string /* query */);
// Select an artist -> filter their albums into the right pane.
callback artists-select(string /* artist name */);
}
// Albums tab quality/format/source filter (1:1 with Tauri matchesQualityFilters:
// OR within each group, AND between groups; an empty group matches all).
export global LibAlbumFilterState {
in-out property <bool> open: false;
// Quality.
in-out property <bool> hires: false;
in-out property <bool> cd: false;
in-out property <bool> lossy: false;
// Format.
in-out property <bool> flac: false;
in-out property <bool> alac: false;
in-out property <bool> ape: false;
in-out property <bool> wav: false;
in-out property <bool> mp3: false;
in-out property <bool> aac: false;
in-out property <bool> other: false;
// Source.
in-out property <bool> local: false;
in-out property <bool> offline: false;
in-out property <bool> plex: false;
// Active-filter count (Rust-computed) for the toolbar badge.
in property <int> count: 0;
}
// Dedicated Local Library album detail (separate from the Qobuz AlbumView).
// Reuses TrackRow / NavButtons / QualityBadge internals, but ONLY local
// actions (play / shuffle / edit tags / add-to-playlist) — no Qobuz chrome.
// When an album has multiple physical versions (distinct source folders),
// `versions` drives a picker and the track list shows ONE version (no dups).
// One physical version of a local album (a distinct source folder). The
// picker shows the source icon + a quality/format label.
export struct LocalAlbumVersion {
label: string, // "24-bit / 96 kHz · FLAC"
source: string, // "user" | "qobuz_download" | "plex" | ""
}
export global LocalAlbumState {
in property <string> id; // metadata group key
in property <string> title;
in property <string> artist;
in property <string> info-line; // "12 tracks · 48 min"
in property <string> quality-tier; // hires | cd | ""
in property <string> quality-detail; // "24-bit / 96 kHz"
in property <string> cover-url;
in property <image> cover;
in property <bool> loading: false;
in property <[TrackItem]> tracks: [];
// Versions (distinct copies). >1 -> show the picker. Sorted best-quality
// first, so version 0 is the highest-quality copy.
in property <[LocalAlbumVersion]> versions: [];
in-out property <int> version-index: 0;
}
export global LocalAlbumActions {
callback play-all();
callback shuffle();
callback edit-tags();
callback add-to-playlist();
callback play-track(string /* track id */);
callback select-version(int);
}
// ============================ Local Library management ====================
// The folder list + scan + maintenance + danger zone live under
// Settings > Local Library (NOT the browse view). One row per registered
// library folder; `Option<T>` maps to a "" / 0 sentinel + a Rust-computed
// label where the raw value is ambiguous.
// One registered library folder. Mirrors `qbz_library::LibraryFolder`
// (8 DB columns) + a UI-owned `selected` checkbox + Rust-computed labels.
export struct LibraryFolderItem {
id: int, // i64 folder id (small)
path: string, // absolute path
alias: string, // "" when none
display-name: string, // alias if non-empty else path (Rust-computed)
enabled: bool,
is-network: bool,
network-fs-type: string, // "" when none (e.g. "nfs","cifs")
user-override-network: bool,
last-scan: int, // unix seconds; 0 = never
last-scan-label: string, // "Never" / localized date (Rust-computed)
accessible: bool, // from the accessibility check; true until resolved
selected: bool, // UI multi-select state
}
// The folder list + maintenance + danger-zone state (Settings > Local Library).
export global LibraryFoldersState {
in property <[LibraryFolderItem]> folders: [];
in property <bool> loading: false;
in-out property <string> filter: ""; // folder search (alias??path substring)
in property <int> selected-count: 0; // drives Edit(==1) / Remove(>0) enablement
in property <bool> cleaning-missing: false;
in property <string> cleanup-status: ""; // "Removed N of M" / "" (auto-clears, Rust-side)
in property <bool> clearing-library: false;
}
// Folder-settings modal state (separate from the playlist FolderEditState).
export global LibFolderEditState {
in-out property <bool> open: false;
in property <int> folder-id: 0;
in-out property <string> alias: "";
in property <string> path: ""; // current path (updated after change-path)
in-out property <bool> enabled: true;
in-out property <bool> is-network: false;
in-out property <bool> user-override-network: false;
// QbzSelect index: 0 auto,1 cifs,2 nfs,3 sshfs,4 rclone,5 webdav,6 glusterfs,7 ceph,8 other.
in-out property <int> fs-type-index: 0;
in property <bool> accessible: true;
in property <bool> checking-accessible: false;
in property <string> last-scan-label: ""; // "Never" / date
}
// Library scan progress — a single global poll target (scan is not per-row,
// unlike the offline cache). `progress` is computed Rust-side (total==0 guard).
export global LibraryScanState {
in property <bool> scanning: false; // status == Scanning
in property <int> scan-status: 0; // 0 Idle,1 Scanning,2 Complete,3 Cancelled,4 Error
in property <int> total-files: 0;
in property <int> processed-files: 0;
in property <float> progress: 0.0; // 0..1
in property <string> current-file: ""; // basename, or the cleanup label
in property <int> error-count: 0;
}
export global LibraryManageActions {
callback load(); // (re)load the folder list (panel init)
callback add-folder(); // native dir picker (Rust)
callback remove-folders(); // bulk-remove selected (confirm)
callback toggle-folder-select(int /* id */);
callback edit-folder(int /* id */); // open the settings modal
callback save-folder-settings(int /* id */, string /* alias */, bool /* enabled */,
bool /* is-network */, string /* fs-type */, bool /* user-override */);
callback change-folder-path(int /* id */); // picker -> update path
callback scan-all();
callback scan-folder(int /* id */);
callback stop-scan();
callback cleanup-missing();
callback clear-library(); // two-step confirm
callback set-filter(string /* query */);
}
// ============================ Tag editor ==================================
// Local-album metadata editor (Settings-independent modal launched from the
// local album detail). Sidecar (default) + opt-in direct file-write. Remote
// MusicBrainz/Discogs lookup lands as a follow-up sub-slice (needs the
// remote-metadata crate move).
// One editable track row. `cue_start_secs` is f64 (-1.0 = none); `has_cue`
// is precomputed (cue_file_path != "" || cue_start_secs >= 0) and disables
// direct file-write. disc/track 0 = unset sentinel (Option<u32>).
export struct TagTrackEdit {
id: int,
file_path: string,
cue_file_path: string,
cue_start_secs: float,
has_cue: bool,
title: string,
disc_number: string, // raw text (LineEdit); controller parses on save ("" = unset)
track_number: string, // raw text (LineEdit); controller parses on save ("" = unset)
}
// One remote (MusicBrainz/Discogs) search-result card. Option fields map to
// a 0/"" sentinel + a has_* companion where the zero value is valid.
export struct RemoteResultItem {
provider: string, // "musicbrainz" | "discogs"
provider_id: string, // selection key + open-in-browser
title: string,
artist: string,
year: int,
has_year: bool,
track_count: int,
has_track_count: bool,
country: string,
format: string,
label: string,
catalog_number: string,
}
export global TagEditorState {
in-out property <bool> open: false;
in property <string> album_group_key; // == LocalAlbum.id == directory_path
in-out property <string> album_title;
in-out property <string> album_artist;
in-out property <string> year_input; // raw text; controller parses
in-out property <string> genre;
in-out property <string> catalog_number;
in-out property <int> album_total_discs; // collected, NOT persisted (1:1)
in property <string> directory_path; // reference + is_dir gate
in property <bool> can_direct_write; // false if any track has_cue
in-out property <int> persistence_index: 0; // 0 = sidecar, 1 = direct
in-out property <[TagTrackEdit]> tracks: [];
in-out property <bool> saving: false;
in-out property <int> write_progress_current: 0;
in-out property <int> write_progress_total: 0;
// Remote lookup (MusicBrainz / Discogs).
in-out property <int> remote_provider_index: 0; // 0 musicbrainz, 1 discogs
in-out property <bool> remote_searching: false;
in-out property <bool> remote_loading: false; // apply (get full) in flight
in-out property <[RemoteResultItem]> remote_results: [];
in-out property <string> selected_result_id: ""; // "" = none
in-out property <bool> show_remote_panel: false;
in-out property <bool> has_searched: false;
}
export global TagEditorActions {
callback close();
callback save();
callback set-persistence(int); // QbzSelect index -> mode (controller maps)
callback set-provider(int); // provider QbzSelect index
callback search-remote();
callback select-result(string /* provider_id */);
callback apply-remote();
callback open-in-browser();
}
// Global toast state, driven by crate::toast. The Toast overlay (mounted
// at the app root) renders whatever is here. The Rust layer owns timing
// (show + auto-hide); the UI only sets `visible = false` on the close
// button. `enabled` gates non-error toasts (errors always show), to match
// Tauri's "in-app notifications" preference.
export global ToastState {
in-out property <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,
}
// Flyout shown when a folder is clicked while the sidebar is collapsed
// (mini): a popup to the right listing that folder's playlists. The list is
// populated from Rust because collapsed folders' children are NOT present in
// the flat `SidebarState.entries`.
export global SidebarFolderPopupState {
in-out property <bool> open: false;
in-out property <string> folder-id;
in-out property <string> folder-name;
in-out property <int> count: 0; // folder's playlist count (caps the height)
in-out property <length> anchor-x; // window x (sidebar right edge)
in-out property <length> anchor-y; // window y (folder row top)
in property <[SidebarEntry]> playlists: [];
}
// Collapsed (mini) sidebar hover tooltip — the row shows only an icon, so a
// hover bubble surfaces the name (folders also show their playlist count).
// Mirrors Tauri's native `title` on the collapsed folder/playlist buttons.
// Rendered at the AppShell level because the sidebar clips its overflow; the
// anchor is the row's true window position (the collapsed rows live inside a
// Flickable, so SidebarRow corrects the absolute-position double-count).
export global SidebarTooltipState {
in-out property <bool> open: false;
in-out property <string> id; // owning row id (race-safe leave)
in-out property <string> text; // "Folder (N)" or "Playlist name"
in-out property <length> anchor-x; // window x (sidebar right edge)
in-out property <length> anchor-y; // window y (row top)
}
// Closed-sidebar playlists flyout. When the sidebar is fully closed there is no
// playlist list on screen; a header button opens this popup over SidebarState's
// entries (folders + playlists), capped at six visible rows with the rest
// scrolling, plus an always-visible footer link to the Playlist Manager.
export global SidebarPlaylistsPopupState {
in-out property <bool> open: false;
in-out property <length> anchor-x; // window x (header button left)
in-out property <length> anchor-y; // window y (below the button)
}
// The sidebar's playlists + folders. Populated by crate::sidebar on
// shell entry and after create/delete/move.
export global SidebarState {
in property <[SidebarEntry]> entries: [];
// Flat folder list, for the move-to-folder menu.
in property <[SidebarPlaylistItem]> folders: [];
// Folder list filtered by the move-to-folder search box. Equals
// `folders` when the query is empty; a case-insensitive substring
// subset otherwise. Filled by the Rust side (Slint strings have no
// `contains`, so the substring filter can't live in markup). The
// context menu iterates this; the current-folder exclusion stays in
// markup via the per-row `folder-id`.
in property <[SidebarPlaylistItem]> menu-folders: [];
in property <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 */);
// Populate SidebarFolderPopupState.playlists for the collapsed-sidebar
// folder flyout (the folder's children aren't in the flat entries).
callback load-folder-popup(string /* folder id */);
callback create-playlist();
callback create-folder();
callback toggle-folder(string /* folder id */);
callback delete-folder(string /* folder id */);
// Move a playlist into a folder ("" folder id = root).
callback move-playlist(string /* playlist id */, string /* folder id */);
// Right-click context menu actions (folder + playlist rows).
callback edit-playlist(string /* id */);
callback edit-folder(string /* id */);
callback hide-playlist(string /* id */);
callback hide-folder(string /* id */);
// 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 */);
}
// === Offline Cache Manager =============================================
// The standalone manage-downloads surface (Tauri's OfflineCacheManagerView).
// Rows are precomputed in Rust (crate::offline_manager): an album-header row
// followed by its track rows, flattened (mirrors PmTreeRow).
export struct OfflineRow {
kind: string, // "album" | "track"
album-id: string, // album-header: the album id; track: its album id
track-id: string, // track rows
title: string, // album title (album) | track title (track)
subtitle: string, // artist
meta: string, // "8 tracks · 320 MB" (album) | "3:45" (track)
// 0 none | 2 downloading | 3 ready | 4 failed. Album = worst across tracks.
status: int,
progress: float, // track downloading 0..1
cover: image, // album-header cover (loaded from the on-disk cover.jpg)
number: string, // track position within its album ("" on album rows)
selected: bool, // multi-select checkbox
}
// One entry in the manager's left A-Z artist rail.
export struct OfflineArtist {
name: string,
meta: string, // "3 albums · 21 tracks"
selected: bool,
}
export global OfflineManagerState {
in property <[OfflineRow]> rows: [];
in property <[OfflineArtist]> artists: [];
in property <bool> loading: false;
// Header stats.
in property <int> tracks-count: 0;
in property <string> size-text: ""; // "1.2 GB"
in property <string> limit-text: ""; // "of 5 GB" | "Unlimited"
in property <float> usage: 0.0; // 0..1 for the bar (0 = unlimited)
in-out property <int> limit-gb: 5; // edit-limit input
// Toolbar — set by the controller from the Rust-side filter state so the
// dropdown selection / toggle / rail highlight reflect the current view.
in property <string> selected-artist: ""; // "" = all artists
in property <int> sort-index: 0; // 0 alpha / 1 recent / 2 largest / 3 smallest
in property <bool> show-only-failed: false;
// Multi-select: how many TRACK rows are checked (drives the bulk bar).
in property <int> selected-count: 0;
}
export global OfflineManagerActions {
// Navigate to the manager (from the Settings "Offline Playback" entry).
callback open();
callback refresh();
// Toolbar.
callback select-artist(string /* name, "" = all */);
callback set-sort(int /* index */);
callback toggle-failed();
// Multi-select.
callback toggle-select(string /* track id */);
callback select-all();
callback clear-selection();
callback bulk-redownload();
callback bulk-remove();
callback remove-track(string /* track id */);
callback remove-album(string /* album id */);
callback redownload-track(string /* track id */);
callback redownload-album(string /* album id */);
callback redownload-failed(string /* album id */);
callback set-limit(int /* gb */);
callback clear-all();
callback open-folder();
callback play-track(string /* track id */);
}
// 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;
// Volume normalization (loudness leveling). Applied in the shared player
// and bypassed automatically when bit-perfect is active. Surfaced in the
// now-playing bar's normalization group as well as Settings > Playback.
in-out property <bool> normalization: false;
// Audio-output indicators for the now-playing song card (computed in Rust
// from the real backend + bit-perfect constraints). `*-label` is the fixed
// short text; `*-active` colours the led when a deliberate / bit-perfect
// mode is engaged (vs the shared system default).
in-out property <string> output-backend-label: "SYST"; // PIPE|ALSA|JACK|PULS|SYST
in-out property <string> output-mode-label: "DEFAULT"; // DACPASS|BITPERF|EXCL|DIRECT|LOCKED|ROUTED|SHARED|DEFAULT
in-out property <bool> output-backend-active: false;
in-out property <bool> output-mode-active: 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 -------------------------------------------------------------
// Only the themes that are actually ported/wired. Order is load-bearing:
// the index maps 1:1 to ThemeState.mode (0 Dark, 1 OLED, 2 Tokyo, 3 System).
in-out property <[string]> themes: [
"Dark", "OLED Black", "Tokyo Night", "System"
];
in-out property <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;
// Player volume +/- stepper buttons — opt-in, hidden by default.
in-out property <bool> show-volume-steppers: 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;
// Window y-coordinate of the anchor — used for sidebar flyouts, which
// open to the RIGHT of the row rather than under the header.
in-out property <length> anchor-y: 0px;
// True when the open menu is a sidebar section flyout (opens right of
// the row) rather than a header dropdown (opens under the tab).
in-out property <bool> from-sidebar: false;
// Optional section-name header shown as the first row (with a hairline
// separator below it). Empty = no header row.
in-out property <string> title: "";
// True while the pointer rests on the TRIGGER (the header tab / compact
// nav icon / sidebar nav row). Pauses the idle-close like hovering the
// menu does, so resting on the trigger never lets it close.
in-out property <bool> trigger-hovered: false;
// The open tab's menu rows.
in-out property <[NavMenuEntry]> items: [];
// Index of the row wired to Home (-1 = none); only Discover uses it.
in-out property <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;
// Releases sub-view (LabelReleasesView) — the paginated album grid.
// `albums` is the full loaded catalog + the artwork-pipeline target
// (stable indices); `visible` is the sorted/filtered/searched subset
// the view actually renders (shares `albums` when no filter is active,
// so artwork stays live — mirrors DiscoverBrowseState).
in property <[AlbumCardItem]> albums: [];
in property <[AlbumCardItem]> visible: [];
in property <int> total: 0;
// Whether the catalog has more pages to fetch (drives Load more). The
// /label/getAlbums endpoint caps each page well below the full catalog,
// so this is the reliable signal — `total` is often unset/underset.
in property <bool> has-more: false;
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";
// --- Landing page (LabelPageView) -----------------------------------
// Header description (HTML-stripped) + truncated form + the flag that
// gates the READ MORE affordance (same pattern as ArtistState.bio).
in property <string> description;
in property <string> description-short;
in property <bool> description-truncated: false;
// Header follow state — true when the label is in the user's favorites.
in property <bool> is-following: false;
// Popular tracks (album-thumbnail rows, multi-select capable).
in property <[TrackItem]> top-tracks: [];
// Releases + Critics' Picks carousels (reuse Carousel).
in property <DiscoverSection> releases-section;
in property <DiscoverSection> critics-section;
// Playlists / Artists / More-labels carousels. More-labels reuses
// ArtistCarousel (SlimItem already carries `following`).
in property <[SearchPlaylistItem]> playlists: [];
in property <[SlimItem]> artists: [];
in property <[SlimItem]> more-labels: [];
// Sticky JUMP TO tabs — only the sections that have content.
in property <[JumpNavTab]> jump-tabs: [];
// True once the landing page-data load resolves (gates the spinner).
in property <bool> page-loaded: false;
// --- Releases sub-view toolbar (UI-owned, like Discover) ------------
// "newest" | "oldest" | "title-asc" | "title-desc" | "artist-asc".
in-out property <string> sort-by: "newest";
in-out property <bool> filter-hires: false;
in-out property <bool> group-by-artist: false;
// Releases sub-view in-view search query (drives the count line).
in-out property <string> search-query: "";
// Group-by-artist rendering — one section per artist (reuses the
// DiscoverSection + grid pattern). Empty unless group-by-artist is on.
in property <[DiscoverSection]> grouped: [];
// Count of albums actually shown after filter/search (for the count line).
in property <int> shown: 0;
// Count of Hi-Res albums in the loaded catalog (count line).
in property <int> hires-count: 0;
}
export global LabelActions {
// Releases sub-view: fetch the next album page.
callback load-more();
// Landing: toggle follow on the header label or a more-labels card.
callback toggle-follow(string /* label id */);
// Landing: play all popular tracks.
callback play-top();
// Landing: top-tracks header overflow menu (play-next / queue / shuffle).
callback top-tracks-menu();
// Landing: enter/leave multi-select for popular tracks (select-all).
callback select-all-top-tracks();
// Landing: open the full releases sub-view ("See all").
callback open-releases();
// Releases sub-view toolbar.
callback set-sort(string /* option */);
callback set-hires(bool);
callback set-group(bool);
callback search(string /* query */);
}
// One artist in the scene-discovery grid (artists from the same
// place as the source artist).
export struct LocationArtistItem {
qobuz-id: string,
name: string,
genres-line: string,
image-url: string,
image: image,
}
// ArtistsByLocationView state — opened from the Origin section's
// location link. Populated by `crate::location_view`.
export global LocationViewState {
in property <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 {
// Three-state sidebar: 0 = open (240px, labels), 1 = mini (64px, icons
// only, Tauri-style), 2 = closed (0px). The collapse toggle cycles
// open -> mini -> closed -> open.
in-out property <int> sidebar-state: 0;
out property <length> sidebar-rendered-width:
root.sidebar-state == 2 ? 0px
: root.sidebar-state == 1 ? 64px
: 240px;
// True while in the icons-only mini state — gates label/header rendering.
out property <bool> sidebar-mini: root.sidebar-state == 1;
// Advance the sidebar through its three states.
public function cycle-sidebar() {
root.sidebar-state = Math.mod(root.sidebar-state + 1, 3);
}
// 0 = none, 1 = queue, 2 = lyrics
in-out property <int> right-panel: 0;
// Section navigation placement: the sections (Discover/Library/Local
// Library/My QBZ) now live in the sidebar and open right-flyout menus.
in-out property <bool> nav-in-sidebar: true;
}
// 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
// Furthest seekable fraction. 1.0 for fully-available (local/cached)
// tracks; equal to the streamed buffer while a track is still downloading,
// so the user can't seek into not-yet-downloaded territory.
in property <float> seekable-max: 1.0;
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
// Quality of the playing track, for the song-card badge.
in property <string> quality-tier; // "hires" | "cd" | "mp3" | ""
in property <string> quality-detail; // "24-bit / 96 kHz"
// Cast / render-aware state (Qobuz Connect). `is-remote` = a peer
// renderer owns playback (controls route to it); `volume-locked` =
// the active renderer disallows remote volume control. Both default
// off and are wired by the QConnect epic; the bar respects them now.
in property <bool> is-remote: false;
in property <bool> volume-locked: false;
// Name of the active Qobuz Connect renderer (shown under the cast button
// when remote). Empty when local. Populated by the QConnect epic.
in property <string> cast-target;
// Cast (Chromecast / DLNA) — when casting, the two output leds fuse into a
// single purple CAST / DLNA led (mirroring Tauri). Populated by the Cast epic.
in property <bool> cast-active: false;
in property <string> cast-protocol; // "cast" | "dlna"
// Playback CONTEXT — the source the queue was launched from (album /
// playlist / artist / label / mix). Drives the song-card layers button
// (navigate back to that page). Set at play time, persists across track
// changes; falls back to the track's album when no richer source is known.
in property <string> context-kind; // "album" | "playlist" | "artist" | "label" | "mix"
in property <string> context-id;
// Transport callbacks, wired to the Rust playback controller.
callback toggle-play();
callback next();
callback previous();
callback seek(float); // 0..1
callback set-volume(float); // 0..1
callback toggle-mute();
callback toggle-shuffle();
callback cycle-repeat();
}
// Single global hover-tooltip channel. Each hoverable control writes its text
// + absolute anchor (window coords: centre-x and top-y of the control) here on
// hover; one TooltipOverlay (mounted last, on top of everything) renders the
// bubble, clamped to the window so it never clips or hides behind a neighbour.
export global TooltipState {
in-out property <string> text;
in-out property <length> anchor-x; // control centre, window coords
in-out property <length> anchor-y; // control top, window coords
in-out property <bool> show: false;
}
// Playback queue model, for the Queue sidebar.
//
// The sidebar shows two tabs. The Queue tab renders `now-playing` plus a
// paginated slice of `upcoming` (40 per page). The History tab renders
// `history` (most-recent first). The Rust queue controller owns the full
// upcoming/history lists and the pagination maths; it pushes the current
// page's slice into `upcoming-page` and the counters that label it.
export global QueueState {
// --- Queue tab -------------------------------------------------------
// Current track card. `has-current` gates the NOW PLAYING section.
in property <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 <[string]> track-ids: []; // bulk add (favorites multi-select)
in property <[PlaylistPickItem]> playlists: [];
in property <bool> loading: false;
// True when track-ids are LocalLibrary row ids (i64) -> route to
// add_local_track_to_playlist instead of the Qobuz endpoint.
in property <bool> local-mode: false;
}
export global PlaylistPickerActions {
callback pick(string /* playlist id */);
callback close();
}
// One artist tile in the Favorites > Artists grid.
export struct FavoriteArtistItem {
id: string,
name: string,
image-url: string,
image: image,
}
// An A-Z section of favorite artists (grid grouped mode + sidepanel left
// list). `key` is the bucket letter ("#", "A".."Z"); `title` the header.
export struct FavArtistSection {
key: string,
title: string,
artists: [FavoriteArtistItem],
}
// One label card in the Favorites > Labels grid.
export struct FavoriteLabelItem {
id: string,
name: string,
albums-line: string, // "N albums"
image-url: string, // resolved URL ("" -> gradient placeholder)
image: image, // decoded cover, filled by the artwork pipeline
}
// Library > Favorites state. The active tab decides which model is
// shown; all are populated lazily by `crate::favorites` the first
// time their tab is opened. Albums reuse AlbumCardItem, tracks reuse
// TrackItem (artwork-on), artists use FavoriteArtistItem.
export global FavoritesState {
// "tracks" | "albums" | "artists" | "playlists" | "labels"
in-out property <string> active-tab: "tracks";
// Tracks reuse TrackItem so the shared SearchTrackRow can
// render them (artwork-on, per-row cover thumbnail). `tracks` is the
// full fetched set (the artwork pipeline targets it); `tracks-visible`
// is what the list renders after the search filter (mirrors
// LabelState.albums vs .visible).
in property <[TrackItem]> tracks: [];
in property <[TrackItem]> tracks-visible: [];
in-out property <string> tracks-search: "";
in-out property <string> tracks-group-mode: "off"; // off|album|artist|name
in property <[AlphaJump]> tracks-alpha: []; // A-Z jumps (name grouping)
in-out property <bool> tracks-multi-select: false;
in property <int> tracks-selected-count: 0;
in property <[AlbumCardItem]> albums: [];
in property <[AlbumCardItem]> albums-visible: []; // after search + sort (flat render)
in property <[DiscoverSection]> albums-grouped: []; // group-by sections
in-out property <string> albums-view-mode: "grid"; // "grid" | "list"
in-out property <string> albums-search: "";
in-out property <string> albums-sort-by: "default"; // default|title-asc|title-desc|artist-asc
in-out property <string> albums-group-mode: "off"; // off|alpha|artist
in property <[AlphaJump]> albums-alpha: []; // A-Z jumps (alpha grouping)
in property <int> albums-shown: 0; // count after filter (grouped or flat)
in property <[FavoriteArtistItem]> artists: [];
// Playlists has two internal sub-tabs (mirror Tauri favoritePlaylists /
// followingPlaylists), both rendered with the rich SearchPlaylistItem
// (4-cover collage). `playlists-visible` is the current sub-tab after the
// search filter (what the grid/list renders).
in property <[SearchPlaylistItem]> playlists-favorites: []; // Library (locally favorited)
in property <[SearchPlaylistItem]> playlists-following: []; // Qobuz-followed, not owned
in property <[SearchPlaylistItem]> playlists-visible: [];
in-out property <string> playlists-sub-tab: "favorites"; // "favorites" | "following" (transient)
in-out property <string> playlists-search: "";
in-out property <string> playlists-view-mode: "grid"; // "grid" | "list" (persisted)
in property <int> playlists-following-count: 0; // Following sub-tab badge
in property <[FavoriteLabelItem]> labels: [];
in property <[FavoriteLabelItem]> labels-visible: []; // after search filter (rendered)
in-out property <string> labels-search: "";
// Artists — toolbar / grid view
in-out property <string> artists-search: "";
in-out property <string> artists-view-mode: "grid"; // grid | sidepanel
in-out property <bool> artists-group-enabled: false; // A-Z grouping (grid)
in property <[FavoriteArtistItem]> artists-visible: []; // after search (flat grid)
in property <[FavArtistSection]> artists-grouped: []; // A-Z sections (grouped grid + sidepanel list)
in property <[AlphaJump]> artists-alpha: []; // A-Z jump strip
in property <int> artists-shown: 0; // count after search
// Artists — sidepanel (the selected artist's albums)
in-out property <string> selected-artist-id: ""; // "" = none selected
in property <string> selected-artist-name: "";
in property <bool> selected-albums-loading: false;
in property <string> selected-albums-error: "";
in property <int> selected-albums-total: 0; // sum across sections
in property <[DiscoverSection]> selected-artist-sections: []; // release-type sections
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;
in property <string> load-error: ""; // non-empty -> show error + retry
}
export global FavoritesActions {
// Switch tab; the Rust side lazy-loads the tab's data if it
// hasn't been fetched yet.
callback select-tab(string);
callback open-album(string);
callback open-artist(string);
callback open-label(string /* label id */, string /* name */);
callback open-playlist(string /* playlist id */);
callback play-track(string /* track id */);
callback track-action(string /* track id */, string /* action */);
callback search-tracks(string /* query */);
callback tracks-set-group(string /* off | album | artist | name */);
callback play-all-tracks();
callback shuffle-tracks();
callback toggle-multi-select();
callback bulk-action(string /* select-all | remove-selected | clear */);
callback album-action(string /* album id */, string /* action */);
callback albums-search(string /* query */);
callback search-labels(string /* query */);
callback playlists-set-sub-tab(string /* favorites | following */);
callback playlists-random();
callback labels-random();
callback search-playlists(string /* query */);
callback playlists-set-view(string /* grid | list */);
callback playlist-action(string /* playlist id */, string /* play | play-next | queue | share | favorite */);
callback search-artists(string /* query */);
callback artists-set-view(string /* grid | sidepanel */);
callback artists-set-group(string /* off | alpha */);
callback artists-shuffle();
callback select-artist(string /* artist id */, string /* name */);
callback albums-set-view(string /* grid | list */);
callback albums-set-sort(string /* sort key */);
callback albums-set-group(string /* off | alpha | artist */);
callback albums-shuffle();
callback unfavorite-track(string /* track id */);
callback unfavorite-album(string /* album id */);
callback retry-load();
}
// A Qobuz mix detail page (DailyQ / WeeklyQ / FavQ / TopQ), opened
// from the For You Qobuz Mixes tiles. Populated by crate::mix. Rows
// reuse the shared SearchTrackRow (with show-album), so the track
// context menu works here too.
export global MixState {
in property <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);
}