New local_playlists + local_playlist_tracks schema in the shared per-user library.db (ids 'local:<uuid>', sources qobuz/local/plex, position-compacted, 8 repo tests in qbz-library). PlaylistRef routing at the navigation seam makes local ids unrepresentable in Qobuz playlist calls by construction (all take u64). Local playlists appear in sidebar, manager and picker (picker hides the Qobuz set while offline); detail view reuses the existing playlist row machinery and renders offline; creation modal gains the offline-only toggle (forced on while offline - creating offline always produces a local playlist, D8) and Edit can flip it later; explicit Upload-to-Qobuz conversion for non-offline-only locals (confirm dialog, local rows attach via the existing sidecar, entity kept on failure). Never-to-Qobuz guard: queues built from an offline-only playlist carry a core-level stamp cleared by every queue replacement; the QConnect sync skips the cloud push and play-on-peer falls back to local playback while the stamp is set. Deferred (logged in the slice report): plex-row attach on upload, custom reorder for local playlists, folder/favorite/hide on local items, manager sort interleaving, offline cover thumbs for cached qobuz rows. cc
3176 lines
148 KiB
Plaintext
3176 lines
148 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, mixtapes, collections, mixtape-detail, discography-builder }
|
|
|
|
// In-app toast (transient notification). Mirrors Tauri's toastStore:
|
|
// a single toast at a time (a new one replaces the current), auto-hidden
|
|
// by the Rust layer per kind. `buffering` is persistent (no auto-hide).
|
|
export enum ToastKind { success, error, info, warning, buffering }
|
|
|
|
// Local Library (on-disk + Plex music). Greenfield Slint port — the view
|
|
// shows header stats + the browse tabs (Albums / Artists / Folders /
|
|
// Tracks). All management/maintenance/danger actions live under
|
|
// Settings > Local Library; the gear button routes there.
|
|
// One card in the Local Library Artists grid (name + "N albums · M tracks").
|
|
// Images + click-to-detail are a later slice.
|
|
export struct LocalArtistItem {
|
|
name: string, // raw/canonical key (used for selection + matching)
|
|
display-name: string, // canonical display name (after name-merge)
|
|
album-count: int,
|
|
track-count: int,
|
|
image-path: string, // local file path OR cached Qobuz URL ("" = none)
|
|
image: image, // decoded portrait (default-empty = show placeholder)
|
|
}
|
|
|
|
// One A-Z section of the local Artists left rail (master list grouped by the
|
|
// display name's first letter, "#" bucket for non-alphabetic). 1:1 with the
|
|
// Tauri Artists tab, which groups the compact rows under sticky letters.
|
|
export struct LocalArtistSection {
|
|
letter: string,
|
|
artists: [LocalArtistItem],
|
|
}
|
|
|
|
// One visible row in the Folders tree (flattened-model lazy tree — Slint has
|
|
// no native TreeView). Folders expand to fetch children lazily.
|
|
export struct FolderNode {
|
|
path: string, // absolute filesystem path (the node id)
|
|
segment: string, // last path component (display)
|
|
depth: int, // indent level
|
|
is-folder: bool, // folder vs. track row
|
|
expanded: bool, // folder open?
|
|
can-expand: bool, // has children (track_count_under > 0)
|
|
track-count: int, // recursive track count under a folder
|
|
// Select-mode: track rows carry `selected`; folder rows carry a tri-state
|
|
// `select-state` (0 none / 1 partial / 2 all) over their descendant tracks.
|
|
selected: bool,
|
|
select-state: int,
|
|
}
|
|
|
|
// One subfolder cover card in the tree-mode folder-detail pane (1:1 with
|
|
// Tauri's `.subfolder-card`). `artwork` is loaded async from the folder cover.
|
|
export struct FolderSubcardItem {
|
|
path: string, // absolute path (the node id)
|
|
name: string, // last path component (display)
|
|
track-count: int, // recursive track count under the folder
|
|
artwork: image,
|
|
artwork-url: string, // cover file path (artwork-pipeline source)
|
|
}
|
|
|
|
// One album block in the ephemeral-folder pane (an ad-hoc folder can hold
|
|
// several albums). `artwork` is loaded from the cached cover file on the UI
|
|
// thread; `tracks` are the album's tracks in scan order.
|
|
export struct EphemeralAlbum {
|
|
group-key: string,
|
|
title: string,
|
|
artist: string,
|
|
// "year · N tracks" (already formatted).
|
|
meta: string,
|
|
quality-tier: string,
|
|
// This album is a single-file CUE rip (one audio file, virtual tracks).
|
|
is-cue: bool,
|
|
artwork: image,
|
|
tracks: [TrackItem],
|
|
}
|
|
|
|
export global LocalLibraryState {
|
|
// "albums" | "artists" | "folders" | "tracks"
|
|
in-out property <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.
|
|
// Tree is the Slint default (user 2026-06-06).
|
|
in-out property <string> folders-view-mode: "tree"; // "flat" | "tree"
|
|
// Flat mode: `folders` is the full set (artwork target); `folders-visible`
|
|
// / `folders-grouped` are the search/sort/group-derived render sets.
|
|
in property <[AlbumCardItem]> folders: [];
|
|
in property <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). `folder-tree` is the full
|
|
// flattened set; `folder-tree-visible` is the search-filtered render set.
|
|
in property <[FolderNode]> folder-tree: [];
|
|
in property <[FolderNode]> folder-tree-visible: [];
|
|
in property <bool> folder-tree-loading: false;
|
|
// Tree rail width (resizable). Default ~20% narrower than the old 340px;
|
|
// clamped in the UI to [50% of default, 50% of the content area].
|
|
in-out property <length> tree-sidebar-width: 272px;
|
|
// Tree rail header: search box + multi-select mode (1:1 with Tauri).
|
|
in-out property <string> folders-tree-search: "";
|
|
in-out property <bool> tree-select-mode: false;
|
|
in property <int> tree-selected-count: 0;
|
|
in-out property <string> folders-selected-path: "";
|
|
in property <string> folders-selected-name: "";
|
|
// Subfolder cover cards (1:1 with Tauri): `folder-detail-subfolders` is the
|
|
// full set (artwork target); `-visible` is the search-filtered render set.
|
|
in property <[FolderSubcardItem]> folder-detail-subfolders: [];
|
|
in property <[FolderSubcardItem]> folder-detail-subfolders-visible: [];
|
|
in property <[TrackItem]> folder-detail-tracks: [];
|
|
in property <bool> folder-detail-loading: false;
|
|
// Recursive track count under the selected folder (the header "N tracks").
|
|
in property <int> folder-detail-track-count: 0;
|
|
// Per-pane grid/list toggle + subfolder name filter.
|
|
in-out property <string> folder-detail-view: "grid"; // "grid" | "list"
|
|
in-out property <string> folder-detail-search: "";
|
|
|
|
// --- Ephemeral folder (ad-hoc play, NOT indexed into library.db) ----
|
|
// When `ephemeral-active`, the Folders tab shows the ephemeral pane
|
|
// (album-grouped) instead of the normal flat/tree browse. The session is
|
|
// in-memory; the folder path is persisted so it rehydrates on restart.
|
|
in property <bool> ephemeral-active: false;
|
|
in property <bool> ephemeral-loading: false;
|
|
in property <string> ephemeral-path: "";
|
|
in property <string> ephemeral-name: "";
|
|
in property <int> ephemeral-track-count: 0;
|
|
// True when the session spans more than one album → show per-album play.
|
|
in property <bool> ephemeral-multi-album: false;
|
|
in property <[EphemeralAlbum]> ephemeral-albums: [];
|
|
|
|
// --- Artists tab — two-column master/detail (1:1 with Tauri) --------
|
|
// Left rail: the merged/deduped artist master list. `artists` is the full
|
|
// merged set (post name-merge); `artists-grouped` / `artists-alpha` are
|
|
// the search-and-group-derived render sets; `artists-shown` is the count
|
|
// after the search filter (drives the rail header).
|
|
in property <[LocalArtistItem]> artists: [];
|
|
in property <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 */);
|
|
// Tree detail pane: filter the subfolder cards by name.
|
|
callback folder-detail-search(string /* query */);
|
|
// Tree rail header: filter the tree by name; collapse every folder.
|
|
callback folders-tree-search(string /* query */);
|
|
callback folders-collapse-all();
|
|
// Multi-select mode (tree rail): toggle the mode; toggle a folder's
|
|
// descendant tracks / a single track; run a bulk action on the selection.
|
|
callback folders-toggle-select-mode();
|
|
callback folders-toggle-folder-select(string /* folder path */);
|
|
callback folders-toggle-track-select(string /* track path */);
|
|
callback folders-bulk-action(string /* select-all|clear|queue|play-next|add-to-playlist */);
|
|
|
|
// --- Ephemeral folder (ad-hoc play) ---
|
|
// Open the native folder picker → scan → show the ephemeral pane.
|
|
callback ephemeral-open();
|
|
// Play the whole ephemeral folder (all albums, scan order).
|
|
callback ephemeral-play-all();
|
|
// Play one ephemeral track — its album group becomes the queue.
|
|
callback ephemeral-play-track(string /* track id */);
|
|
// Play one ephemeral album (its tracks become the queue).
|
|
callback ephemeral-play-album(string /* album group key */);
|
|
// Clear the ephemeral session (drop the pane + forget the path).
|
|
callback ephemeral-clear();
|
|
|
|
// --- Artists tab ---
|
|
// Filter the master list (two-way bound to artists-search; re-derive).
|
|
callback artists-search(string /* query */);
|
|
// Select an artist -> filter their albums into the right pane.
|
|
callback artists-select(string /* artist name */);
|
|
}
|
|
|
|
// Albums tab quality/format/source filter (1:1 with Tauri matchesQualityFilters:
|
|
// OR within each group, AND between groups; an empty group matches all).
|
|
export global LibAlbumFilterState {
|
|
in-out property <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 add-to-mixtape();
|
|
callback play-track(string /* track id */);
|
|
callback select-version(int);
|
|
// Client-side track filter (mirrors AlbumActions.search), applied over the
|
|
// current version's tracks.
|
|
callback search(string /* query */);
|
|
}
|
|
|
|
// ============================ 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 remove-folder(int /* id */); // remove ONE folder directly (confirm)
|
|
callback toggle-folder-select(int /* id */);
|
|
callback edit-folder(int /* id */); // open the settings modal
|
|
callback save-folder-settings(int /* id */, string /* alias */, bool /* enabled */,
|
|
bool /* is-network */, string /* fs-type */, bool /* user-override */);
|
|
callback change-folder-path(int /* id */); // picker -> update path
|
|
callback scan-all();
|
|
callback scan-folder(int /* id */);
|
|
callback stop-scan();
|
|
callback cleanup-missing();
|
|
callback clear-library(); // two-step confirm
|
|
callback set-filter(string /* query */);
|
|
}
|
|
|
|
// ============================ Plex connection =============================
|
|
// Settings > Local Library > Plex. A LAN-only Plex Media Server connection:
|
|
// PIN-based auth (or manual token), library-section multi-select, and the
|
|
// per-section track caching that downstream browse views read. State is
|
|
// Rust-backed (qbz_app::settings::plex store + qbz-plex core); the controller
|
|
// lives in `plex_auth`. ADR-008: no pills. Status is a (key, args) pair so the
|
|
// Rust side can localize without 5-file JSON sync — `status-text` is the
|
|
// already-resolved label, `status-kind` drives the highlight style.
|
|
|
|
// One Plex music-library section row + its UI selection + cached track count.
|
|
export struct PlexSectionItem {
|
|
key: string, // Plex Directory@key
|
|
title: string, // section title
|
|
count: int, // cached track count (plex_cache_get_tracks(key).len())
|
|
selected: bool, // membership in selected_section_keys
|
|
}
|
|
|
|
export global PlexSettingsState {
|
|
// --- Persisted toggles -------------------------------------------------
|
|
in-out property <bool> enabled: false; // master toggle (default OFF)
|
|
in-out property <bool> ui-collapsed: false; // collapse chevron
|
|
in-out property <bool> metadata-write-enabled: false;
|
|
|
|
// --- Connection --------------------------------------------------------
|
|
// server-url is the RAW host-form text the user types; base-url is the
|
|
// Rust-resolved proto://host:port (set after resolve).
|
|
in-out property <string> server-url: "";
|
|
in property <string> base-url: "";
|
|
in property <string> token: ""; // "" = not authenticated
|
|
in-out property <bool> manual-token-mode: false;
|
|
in-out property <string> token-input: ""; // manual-token field buffer
|
|
|
|
// Derived gates (computed Rust-side, pushed in).
|
|
in property <bool> is-local-address: true; // isLocalPlexAddress(server-url)
|
|
in property <bool> can-use: false; // enabled && local && base!="" && token!=""
|
|
|
|
// --- PIN flow ----------------------------------------------------------
|
|
in property <bool> pin-busy: false; // PIN start / poll in flight
|
|
in property <string> pin-code: ""; // "" = no active code
|
|
in property <string> pin-auth-url: "";
|
|
|
|
// --- General busy + status --------------------------------------------
|
|
in property <bool> busy: false; // ping / sync / auto-setup
|
|
// Status as a resolved label + kind (0 none, 1 info, 2 connected/ok, 3 error).
|
|
in property <string> status-text: "";
|
|
in property <int> status-kind: 0;
|
|
|
|
// --- Library sections --------------------------------------------------
|
|
in property <[PlexSectionItem]> sections: [];
|
|
}
|
|
|
|
export global PlexAuthActions {
|
|
callback enable-toggle(bool);
|
|
callback collapse-toggle(bool);
|
|
callback set-server-url(string /* host-form */);
|
|
callback generate-code(); // PIN start + begin poll
|
|
callback open-auth-url();
|
|
callback copy-code();
|
|
callback manual-token-toggle(bool);
|
|
callback set-token(string /* token */); // save manual token
|
|
callback ping(); // check connection
|
|
callback load-sections(); // get libraries
|
|
callback toggle-section(string /* key */);
|
|
callback metadata-write-toggle(bool);
|
|
callback disconnect(); // confirm + reset
|
|
callback clear-cache(); // confirm + plex_cache_clear
|
|
callback load(); // panel init: seed from store + warm cache
|
|
}
|
|
|
|
// ============================ 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 "")
|
|
// "" (Qobuz) | "local" | "offline" — local playlists (library.db,
|
|
// ids "local:<uuid>") show a hard-drive glyph instead of list-music.
|
|
local-kind: string,
|
|
cover-count: int, // number of cover images, 0-4 (playlists)
|
|
url1: string,
|
|
url2: string,
|
|
url3: string,
|
|
url4: string,
|
|
cover1: image,
|
|
cover2: image,
|
|
cover3: image,
|
|
cover4: image,
|
|
}
|
|
|
|
// Flyout shown when a folder is clicked while the sidebar is collapsed
|
|
// (mini): a popup to the right listing that folder's playlists. The list is
|
|
// populated from Rust because collapsed folders' children are NOT present in
|
|
// the flat `SidebarState.entries`.
|
|
export global SidebarFolderPopupState {
|
|
in-out property <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 */);
|
|
// Add a playlist to a Mixtape/Collection (opens the AddToMixtapeModal with
|
|
// the playlist as a 1-item payload; restricts to mixtapes since a playlist
|
|
// can't go in a Collection).
|
|
callback add-to-mixtape(string /* playlist id */);
|
|
// Filter `menu-folders` by a case-insensitive substring of the
|
|
// move-to-folder search box (Rust does the substring match).
|
|
callback search-folders(string /* query */);
|
|
// Pick a sort option (re-sorts the list). Same five values as Tauri.
|
|
callback set-sort(string /* option */);
|
|
// Re-run the playlist-name filter (called as the search input edits).
|
|
callback search-changed(string /* query */);
|
|
// Re-fetch the playlist list from the network.
|
|
callback refresh-playlists();
|
|
// Open the full Playlist Manager view.
|
|
callback manage-playlists();
|
|
}
|
|
|
|
// "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;
|
|
// D8 "Offline-only playlist" opt-in: ON -> created as a LOCAL entity
|
|
// (library.db, id "local:<uuid>") that never reaches Qobuz.
|
|
in-out property <bool> offline-only: false;
|
|
// True while the app is offline: creation always produces a local
|
|
// playlist, so the toggle shows ON and locked with a hint.
|
|
in property <bool> offline-locked: false;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
// "Create Mixtape / Collection" modal state (My QBZ). Opened from either
|
|
// grid's "+ New" CTA / empty-state button. `kind` is preset by the opening
|
|
// grid ("mixtape" | "collection") and can be flipped via the radio toggle
|
|
// (artist_collection is created via the Discography Builder, not here).
|
|
// `name` is capped at 80 chars (mirrors Tauri's maxlength="80").
|
|
export global MyQbzCreateState {
|
|
in-out property <bool> open: false;
|
|
in-out property <string> kind: "mixtape";
|
|
in-out property <string> name;
|
|
in property <bool> creating: false;
|
|
}
|
|
|
|
export global MyQbzCreateActions {
|
|
callback submit();
|
|
callback close();
|
|
}
|
|
|
|
// === "Add to Mixtape/Collection" picker modal (global singleton) =========
|
|
// Spec 21/50. A global overlay (mounted ONCE at the window root) that adds one
|
|
// or more items (album / track / playlist, from qobuz or local) into an
|
|
// existing Mixtape/Collection — or creates a brand-new one inline and adds
|
|
// into it. Every app-wide "Add to Mixtape/Collection" trigger calls
|
|
// MyQbzAddActions.open(...) with a payload it built; the Rust controller
|
|
// (crate::myqbz_add) holds the pending items and drives the picker.
|
|
//
|
|
// One picker row = one collection (mixtape | collection | artist_collection),
|
|
// with its kind label, "N albums" meta, and an `already-has` flag computed
|
|
// from `item_exists` so the row can show an "already added" hint.
|
|
export struct MyQbzAddRow {
|
|
id: string,
|
|
name: string,
|
|
// "mixtape" | "collection" | "artist_collection".
|
|
kind: string,
|
|
// Row icon by kind ("mixtape" -> cassette, "artist_collection" -> user,
|
|
// else library-big). Resolved in Rust to a key the row maps to a glyph.
|
|
icon: string,
|
|
// Right-aligned kind tag, uppercase ("MIXTAPE" | "COLLECTION" | "ARTIST").
|
|
kind-label: string,
|
|
// "N albums" / "1 album" — ICU plural rendered in Rust (always "album(s)").
|
|
meta: string,
|
|
// True when this collection already contains ALL of the pending items
|
|
// (per item_exists). Drives the per-row "already added" hint.
|
|
already-has: bool,
|
|
}
|
|
|
|
export global MyQbzAddState {
|
|
in-out property <bool> open: false;
|
|
// Whether the picker is loading the collection list.
|
|
in property <bool> loading: false;
|
|
// The collections offered as targets (already kind-restricted +
|
|
// recency-sorted + item_exists-resolved by the Rust controller).
|
|
in property <[MyQbzAddRow]> rows: [];
|
|
// Header title + subtitle (precomputed: single-item title or bulk
|
|
// "{count} items" / "{first} + N more").
|
|
in property <string> header-title;
|
|
in property <string> header-subtitle;
|
|
// True when >1 pending item (bulk add).
|
|
in property <bool> bulk-mode: false;
|
|
// True when ANY pending item is a track/playlist -> targets restricted to
|
|
// Mixtapes; the "Create Collection" button is hidden.
|
|
in property <bool> restrict-to-mixtape: false;
|
|
// Free-text filter over collection names.
|
|
in-out property <string> search;
|
|
// Id of the collection currently being written to (disables rows/buttons).
|
|
in property <string> busy-id;
|
|
// --- Create-new sub-panel ---
|
|
in-out property <bool> creating: false;
|
|
in-out property <string> create-name;
|
|
// "mixtape" | "collection".
|
|
in-out property <string> create-kind: "mixtape";
|
|
in property <bool> create-busy: false;
|
|
}
|
|
|
|
export global MyQbzAddActions {
|
|
// Click a collection row -> add the pending items to it.
|
|
callback pick(string /* collection id */);
|
|
// Filter the list by name.
|
|
callback search-changed(string /* query */);
|
|
// Open the create-new sub-panel preset to a kind ("mixtape"|"collection").
|
|
callback show-create(string /* kind */);
|
|
// Back out of the create sub-panel to the picker.
|
|
callback create-back();
|
|
// Create a new collection (create-kind/create-name) then add the items.
|
|
callback create-and-add();
|
|
callback close();
|
|
}
|
|
|
|
// Ephemeral "play with active queue" choice — shown when the user triggers an
|
|
// ephemeral play action while a queue is already playing. Two outcomes:
|
|
// add to the queue, or clear the queue and start playing.
|
|
export global EphemeralPlayChoiceState {
|
|
in-out property <bool> open: false;
|
|
// "all" | "album" | "track" — what the user pressed play on.
|
|
in-out property <string> intent-kind: "";
|
|
// group key (album) or track id (track); empty for "all".
|
|
in-out property <string> intent-arg: "";
|
|
// Whether "Add to queue" is offered — only when the current queue is itself
|
|
// entirely ephemeral (queue ops may not mix ephemeral with persistent tracks).
|
|
in-out property <bool> enqueue-allowed: false;
|
|
}
|
|
|
|
export global EphemeralPlayChoiceActions {
|
|
callback enqueue(); // add the selection to the current queue
|
|
callback replace(); // clear the queue and start playing the selection
|
|
callback close();
|
|
}
|
|
|
|
// Playlist detail screen state. Rows reuse the shared TrackRow
|
|
// (show-artwork + show-album); populated by crate::playlist.
|
|
export global PlaylistState {
|
|
in property <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;
|
|
// LOCAL playlist (id "local:<uuid>", library.db entity — never on
|
|
// Qobuz). Qobuz-bound header actions (favorite / cache) hide; the
|
|
// "Upload to Qobuz" action appears when not offline-only.
|
|
in property <bool> is-local: false;
|
|
// D8: never offered for upload, never reaches Qobuz/QConnect.
|
|
in property <bool> offline-only: 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;
|
|
// LOCAL playlist (id "local:<uuid>") — edits route to library.db and
|
|
// the offline-only toggle below shows.
|
|
in property <bool> is-local: false;
|
|
// D8 flag, editable so an offline-created playlist can be unmarked
|
|
// later and gain the "Upload to Qobuz" action.
|
|
in-out property <bool> offline-only: false;
|
|
}
|
|
|
|
export global EditPlaylistActions {
|
|
callback save();
|
|
callback delete();
|
|
callback close();
|
|
}
|
|
|
|
// === Playlist Manager ===================================================
|
|
// A full playlist + folder organization surface (Tauri's
|
|
// PlaylistManagerView). Rows are precomputed in Rust (crate::playlist_manager)
|
|
// and pushed as ready-to-render models — no per-row map lookups in .slint.
|
|
|
|
// One folder card / chip / tree-folder. `icon-color` is a solid hex (or
|
|
// "" → use the accent). Gradient folder colors are not supported here
|
|
// (Slint brush limitation); a gradient string falls back to the accent.
|
|
export struct PmFolderItem {
|
|
id: string,
|
|
name: string,
|
|
count: int, // playlists in this folder
|
|
// "preset" or "custom".
|
|
icon-type: string,
|
|
// Lucide preset name ("heart" | "star" | "music" | "folder" | "disc"
|
|
// | "library" | "headphones"); drives which glyph the card shows.
|
|
icon-preset: string,
|
|
// Resolved solid color for the icon square (parsed from the stored
|
|
// hex by the Rust controller). When `has-color` is false the card
|
|
// uses the accent instead (gradient / empty / unparseable colors
|
|
// fall back to accent — Slint has no gradient-brush-in-struct).
|
|
icon-color: color,
|
|
has-color: bool,
|
|
is-hidden: bool,
|
|
// Decoded custom image (icon-type == "custom"); empty otherwise.
|
|
custom-image: image,
|
|
has-custom-image: bool,
|
|
}
|
|
|
|
// One playlist card / list-row / tree-row. All display strings are
|
|
// precomputed (track line, local line) so the view never does lookups.
|
|
export struct PmPlaylistItem {
|
|
id: string, // playlist id (may be negative for offline pending)
|
|
name: string,
|
|
// "12 tracks" — total (remote + local).
|
|
tracks-line: string,
|
|
// "1h 43m" — total playtime, "" when zero (list-row meta only).
|
|
duration-line: string,
|
|
// "(3 local)" — local-only suffix, "" when none.
|
|
local-line: string,
|
|
local-count: int,
|
|
total-count: int,
|
|
play-count: int,
|
|
// "" | "all_local" | "some_local" | "no" — availability badge.
|
|
local-status: string,
|
|
is-favorite: bool,
|
|
is-hidden: bool,
|
|
// LOCAL playlist entity (id "local:<uuid>", library.db) — the card /
|
|
// list-row shows a hard-drive marker; offline-only flavors label it.
|
|
is-local-playlist: bool,
|
|
offline-only: bool,
|
|
folder-id: string, // "" = root
|
|
// Micro-collage covers (same 0-4 scheme as the sidebar).
|
|
cover-count: int,
|
|
url1: string,
|
|
url2: string,
|
|
url3: string,
|
|
url4: string,
|
|
cover1: image,
|
|
cover2: image,
|
|
cover3: image,
|
|
cover4: image,
|
|
}
|
|
|
|
// One flattened row of the tree view — a folder header or a nested /
|
|
// root playlist. The Rust side emits only currently-visible rows
|
|
// (collapsed folders hide their children).
|
|
export struct PmTreeRow {
|
|
kind: string, // "folder" | "playlist"
|
|
expanded: bool, // folders
|
|
folder: PmFolderItem, // populated when kind == "folder"
|
|
playlist: PmPlaylistItem, // populated when kind == "playlist"
|
|
indent: bool, // playlist nested under a folder
|
|
}
|
|
|
|
export global PlaylistManagerState {
|
|
in property <[PmFolderItem]> folders: [];
|
|
// Filtered folder list for the list-row move-to-folder menu. Starts as
|
|
// the full folder set; the menu's search box narrows it via
|
|
// `search-folders` (Slint strings have no `contains`, so Rust filters).
|
|
in property <[PmFolderItem]> menu-folders: [];
|
|
in property <[PmPlaylistItem]> playlists: []; // the filtered+sorted grid/list set
|
|
in property <[PmTreeRow]> tree: [];
|
|
// Header counters: total folders / total playlists (post-filter for
|
|
// the visible playlist count).
|
|
in property <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 */);
|
|
}
|
|
|
|
// === My QBZ — Mixtapes & Collections (read-only index grids) ============
|
|
// Mirrors PlaylistManagerState/PmPlaylistItem. Both the Mixtapes grid and the
|
|
// Collections grid render `MixtapeCardItem`s; the Rust controller (crate::myqbz)
|
|
// filters by kind and precomputes every display string, so the views do no
|
|
// per-row lookups. Mosaic covers (up to 4, or 9 for a 3x3 Collection) are
|
|
// decoded into the item by the artwork pipeline.
|
|
//
|
|
// READ-ONLY SCOPE (Phase-2 Slice 2): the grids DISPLAY rows + route from the
|
|
// sidebar. Card click (open detail) and "+ New" (create) are STUBS that log
|
|
// "not yet wired" — wired in later slices.
|
|
export struct MixtapeCardItem {
|
|
id: string,
|
|
name: string,
|
|
// "mixtape" | "collection" | "artist_collection".
|
|
kind: string,
|
|
// Eyebrow label, uppercase ("MIXTAPE" | "COLLECTION" | "ARTIST").
|
|
label: string,
|
|
// "5 albums" / "1 album" — ICU-plural rendered in Rust (always "album(s)"
|
|
// regardless of item_type, matching the PSD).
|
|
meta: string,
|
|
// Item count (drives the 3x3-vs-2x2 mosaic rule + sort).
|
|
item-count: int,
|
|
play-count: int,
|
|
// updated_at epoch ms (sort key).
|
|
updated-at: int,
|
|
// Custom user-uploaded cover (decoded local file); empty when none. When
|
|
// present it full-bleeds the mosaic (precedence over the collage).
|
|
custom-cover: image,
|
|
has-custom-cover: bool,
|
|
// Mosaic cell count actually used: 0 (empty -> kind glyph), or 4 / 9.
|
|
cover-count: int,
|
|
// Up to 9 collage cover slots (only 0-3 used for 2x2; 0-8 for a 3x3
|
|
// Collection with >=9 items). URLs are pre-downscaled in Rust.
|
|
url1: string, url2: string, url3: string, url4: string,
|
|
url5: string, url6: string, url7: string, url8: string, url9: string,
|
|
cover1: image, cover2: image, cover3: image, cover4: image,
|
|
cover5: image, cover6: image, cover7: image, cover8: image, cover9: image,
|
|
}
|
|
|
|
export global MyQbzState {
|
|
// The Mixtapes grid set (kind == mixtape), filtered + sorted.
|
|
in property <[MixtapeCardItem]> mixtapes: [];
|
|
// The Collections grid set (kind == collection | artist_collection),
|
|
// filtered (by kind-filter) + sorted.
|
|
in property <[MixtapeCardItem]> collections: [];
|
|
in property <bool> loading: false;
|
|
// --- Mixtapes toolbar (session-scoped) ---
|
|
in-out property <string> mix-search;
|
|
// "position" | "name" | "items" | "updated".
|
|
in-out property <string> mix-sort: "position";
|
|
// "asc" | "desc".
|
|
in-out property <string> mix-sort-dir: "asc";
|
|
// "grid" | "list".
|
|
in-out property <string> mix-view: "grid";
|
|
// --- Collections toolbar (session-scoped) ---
|
|
in-out property <string> col-search;
|
|
in-out property <string> col-sort: "position";
|
|
in-out property <string> col-sort-dir: "asc";
|
|
in-out property <string> col-view: "grid";
|
|
// "all" | "collection" | "artist_collection".
|
|
in-out property <string> col-kind-filter: "all";
|
|
}
|
|
|
|
export global MyQbzActions {
|
|
// Card click -> open detail (STUB: logs in this slice).
|
|
callback open-card(string /* id */);
|
|
// "+ New Mixtape" / "+ New Collection" (STUB: logs in this slice).
|
|
callback create-mixtape();
|
|
callback create-collection();
|
|
// --- Mixtapes toolbar ---
|
|
callback mix-search-changed(string /* query */);
|
|
callback mix-set-sort(string /* field */);
|
|
callback mix-set-view(string /* grid|list */);
|
|
callback mix-reset();
|
|
// --- Collections toolbar ---
|
|
callback col-search-changed(string /* query */);
|
|
callback col-set-sort(string /* field */);
|
|
callback col-set-view(string /* grid|list */);
|
|
callback col-set-kind-filter(string /* all|collection|artist_collection */);
|
|
callback col-reset();
|
|
}
|
|
|
|
// === My QBZ — Collection / Mixtape DETAIL view (read-only) ==============
|
|
// Spec 12. One row per collection item (album / track / playlist), rendered
|
|
// in the 8-col list / the grid. The Rust controller (crate::myqbz_detail)
|
|
// precomputes every display string (type label, source kind, quality detail,
|
|
// tracks/year columns, downscaled artwork URL) so the view does no lookups.
|
|
// READ-ONLY SCOPE (Phase-2 Slice 3): the hero action buttons + per-row
|
|
// context-menu items + grid/list view modes render 1:1, but every click is a
|
|
// logging STUB. The "expanded" view-mode toggle + shell render; per-item
|
|
// inline track expansion is DEFERRED (needs the enqueue resolver — a later
|
|
// slice). Edit/rename/delete/cover/DJ-mix modals + the BulkActionBar are also
|
|
// deferred.
|
|
export struct MixtapeDetailItem {
|
|
// 0-based persisted position (the row's stable key; display is +1).
|
|
position: int,
|
|
// "album" | "track" | "playlist".
|
|
item-type: string,
|
|
// "qobuz" | "local".
|
|
source: string,
|
|
source-item-id: string,
|
|
title: string,
|
|
subtitle: string,
|
|
// True for qobuz items whose subtitle is a clickable artist link
|
|
// (spec 12 §6.3 col-2 — local subtitles are plain text).
|
|
subtitle-is-link: bool,
|
|
// Resolved source kind for the source indicator + source filter
|
|
// ("qobuz" | "plex" | "local"). Defaults from `source` until resolveItems
|
|
// lands (deferred — for now equals the raw source mapping).
|
|
source-kind: string,
|
|
// Type-cell label (already localized): "ALBUM" / "TRACK" / "PLAYLIST"
|
|
// (or an EP/Single/… release-type override — overrides are a later slice).
|
|
type-label: string,
|
|
// Quality badge inputs (resolved by the resolveItems pass).
|
|
quality-tier: string, // "hires" | "cd" | ""
|
|
quality-detail: string, // "24-bit / 96 kHz" | ""
|
|
// True while the async resolveItems pass is still pending for this row (no
|
|
// cached resolution yet). Drives the quality-cell LoadingDots skeleton —
|
|
// shown only during the resolve window, collapsing to the real badge (or an
|
|
// empty cell for a no-quality item) once resolution completes.
|
|
quality-resolving: bool,
|
|
// TRACKS column: "1" for a track, else the count or "—".
|
|
tracks-text: string,
|
|
// YEAR column: the year or "".
|
|
year-text: string,
|
|
// Downscaled (_50) row artwork; decoded into `artwork` by the pipeline.
|
|
artwork-url: string,
|
|
artwork: image,
|
|
// UI-owned multi-select state (select-mode — deferred wiring; the column
|
|
// renders when select-mode is on so geometry matches the PSD).
|
|
selected: bool,
|
|
// --- Expanded view-mode inline tracks (spec 12 §8) --------------------
|
|
// In the "expanded" view-mode every album / playlist item shows its
|
|
// tracks rendered directly underneath — always visible, no chevron (1:1
|
|
// with Tauri: "like LocalLibrary's Tracks page with Group-by-album").
|
|
// Whether this item can host inline tracks (album / playlist; never a
|
|
// bare track) — gates the inline section + the auto-fetch.
|
|
can-expand: bool,
|
|
// True once the per-item track fetch has completed (success OR empty), so
|
|
// the view can distinguish "still loading" from "loaded, no results".
|
|
tracks-loaded: bool,
|
|
// True while the per-item track fetch is in flight (spinner state).
|
|
expand-loading: bool,
|
|
// The resolved inline tracks (rendered as shared TrackRows under the row).
|
|
// Fetched once on entering expanded mode, then cached on the row.
|
|
inline-tracks: [TrackItem],
|
|
}
|
|
|
|
export global MyQbzDetailState {
|
|
in property <string> id;
|
|
// "mixtape" | "collection" | "artist_collection".
|
|
in property <string> kind;
|
|
// Eyebrow label, uppercase ("MIXTAPE" | "COLLECTION" | "ARTIST").
|
|
in property <string> kind-label;
|
|
in property <string> name;
|
|
in property <string> description;
|
|
// "5 albums" / "1 album" meta line (ICU plural, rendered in Rust).
|
|
in property <string> meta;
|
|
in property <int> item-count: 0;
|
|
// play_mode ("in_order" | "album_shuffle") — drives the overflow toggle
|
|
// label (deferred wiring; shown for parity).
|
|
in property <string> play-mode: "in_order";
|
|
// True when the user set a custom cover (full-bleeds the hero mosaic).
|
|
in property <bool> has-custom-cover: false;
|
|
in property <image> custom-cover;
|
|
in property <bool> loading: false;
|
|
// False once the load completes with no collection (not-found state).
|
|
in property <bool> found: true;
|
|
|
|
// --- Hero mosaic (CollectionMosaic inputs, same scheme as the card) ----
|
|
// cover-count 0 (empty -> kind glyph) / 4 / 9; up-to-9 downscaled URLs +
|
|
// decoded covers. Mirrors MixtapeCardItem so CollectionMosaic renders it.
|
|
in property <int> cover-count: 0;
|
|
in property <string> url1; in property <string> url2; in property <string> url3;
|
|
in property <string> url4; in property <string> url5; in property <string> url6;
|
|
in property <string> url7; in property <string> url8; in property <string> url9;
|
|
in property <image> cover1; in property <image> cover2; in property <image> cover3;
|
|
in property <image> cover4; in property <image> cover5; in property <image> cover6;
|
|
in property <image> cover7; in property <image> cover8; in property <image> cover9;
|
|
|
|
// --- Item list (the render model: filter -> search -> sort applied) ----
|
|
in property <[MixtapeDetailItem]> items: [];
|
|
|
|
// --- Toolbar (session-scoped) ------------------------------------------
|
|
in-out property <string> search;
|
|
// "position" | "name" | "year" | "tracks".
|
|
in-out property <string> sort: "position";
|
|
in-out property <string> sort-dir: "asc";
|
|
// "all" | "album" | "track" | "playlist" (single-select).
|
|
in-out property <string> type-filter: "all";
|
|
// Multi-select source filter membership (spec 12 §5.5). Three flags
|
|
// (Slint has no Set); Rust derives the badge count.
|
|
in-out property <bool> src-qobuz: false;
|
|
in-out property <bool> src-plex: false;
|
|
in-out property <bool> src-local: false;
|
|
// "list" | "grid" | "expanded".
|
|
in-out property <string> view-mode: "list";
|
|
// Multi-select edit mode (renders the per-row checkbox column; bulk-bar
|
|
// wiring is DEFERRED).
|
|
in-out property <bool> select-mode: false;
|
|
in property <int> selected-count: 0;
|
|
// Rust-computed filter badge count (sources + (type-filter != all)).
|
|
in property <int> filter-count: 0;
|
|
// Whether any filter/sort differs from the defaults (drives the reset btn).
|
|
in property <bool> has-any-filter: false;
|
|
}
|
|
|
|
export global MyQbzDetailActions {
|
|
// Client-side toolbar — re-derive the visible item list (UI thread).
|
|
callback search-changed(string /* query */);
|
|
callback set-sort(string /* field */);
|
|
callback set-type-filter(string /* all|album|track|playlist */);
|
|
callback toggle-source-filter(string /* qobuz|plex|local */);
|
|
callback set-view-mode(string /* list|grid|expanded */);
|
|
callback toggle-select-mode();
|
|
callback reset-filters();
|
|
// Open an item's detail (album / track-album / playlist). Routed to the
|
|
// existing open-album / open-playlist paths in main.rs.
|
|
callback open-item(string /* source */, string /* item-type */, string /* source-item-id */);
|
|
// Open an item's artist page by name (qobuz items only).
|
|
callback open-artist(string /* source */, string /* artist-name */);
|
|
// --- Hero CTAs --------------------------------------------------------
|
|
callback play-all();
|
|
callback shuffle();
|
|
callback dj-mix(); // STUB (the DJ-mix modal is a later slice).
|
|
callback sync(); // STUB / no-op (no discography sync impl, spec §8).
|
|
// --- Hero overflow (⋯) menu (spec 12 §10) -----------------------------
|
|
// Each opens the matching modal or fires the matching mutation + reload.
|
|
callback open-rename(); // -> Rename modal (prefilled with name).
|
|
callback open-description(); // -> Description modal (prefilled).
|
|
callback upload-cover(); // native file picker -> resize -> set + reload.
|
|
callback remove-cover(); // clear custom cover -> reload.
|
|
callback toggle-play-mode(); // in_order <-> album_shuffle -> reload.
|
|
callback convert-kind(); // mixtape <-> collection -> reload + toast.
|
|
callback open-delete(); // -> Delete-confirm modal.
|
|
// Per-row affordances.
|
|
callback play-item(string /* source-item-id */);
|
|
callback item-action(string /* source-item-id */, string /* action */);
|
|
// Remove ONE item from the collection by its 0-based persisted position
|
|
// (the row menu's "Remove from collection"). Routes to the audited bulk
|
|
// remover with a single position so compaction + reload + toast are reused.
|
|
callback remove-item(int /* position */);
|
|
// Toggle one row's selection (select-mode).
|
|
callback toggle-item-select(int /* position */);
|
|
// --- Expanded view-mode inline tracks (spec 12 §8) --------------------
|
|
// Fired when the "expanded" view-mode becomes active (and on the initial
|
|
// open in expanded mode): the Rust side fetches every expandable item's
|
|
// tracks (via the enqueue resolver) that aren't already cached, populating
|
|
// each row's inline-tracks model. Idempotent — cached rows are skipped, so
|
|
// re-entering expanded mode is instant.
|
|
callback ensure-expanded();
|
|
// Play / queue / navigate from an inline track row. `action` is one of
|
|
// "play" | "play-next" | "play-later" | "go-to-album".
|
|
// The parent item's source-item-id scopes the fetch + go-to-album target.
|
|
callback inline-track-action(string /* item-source-item-id */, string /* track-id */, string /* action */);
|
|
// --- Bulk action bar (select-mode, spec 12 §13) -----------------------
|
|
// The bar emits the chosen action id verbatim; the Rust side maps it:
|
|
// "add-to-mixtape" -> open the global AddToMixtapeModal with the
|
|
// selected items' payloads.
|
|
// "remove-selected" -> repo::remove_item each selected position
|
|
// (highest-first so compaction doesn't shift),
|
|
// then reload the detail + clear selection.
|
|
callback bulk-action(string /* id */);
|
|
}
|
|
|
|
// ─────────────────────── Discography Builder (spec 13) ───────────────────
|
|
// The full-page flow that assembles an artist's complete discography into a
|
|
// saved `kind='artist_collection'` collection. It fetches every release for
|
|
// the artist from three sources (Qobuz artist page + local library + Plex
|
|
// cache), dedupes them into GROUPS (one logical album = a chosen "primary" +
|
|
// "alternates"), lets the user check which to include, reorder (by sort),
|
|
// rename, and correct release-type mislabels, then persists the result.
|
|
//
|
|
// One candidate row (a primary OR an alternate). Every display string is
|
|
// precomputed in Rust (crate::myqbz_builder) so the view does no lookups.
|
|
export struct DiscoCandidate {
|
|
// `${source}|${source_item_id}` — the per-candidate selection key.
|
|
key: string,
|
|
// "qobuz" | "local" | "plex" — drives the source indicator icon.
|
|
source: string,
|
|
source-item-id: string,
|
|
title: string,
|
|
artist: string,
|
|
// "2019" or "—" (em-dash placeholder).
|
|
year: string,
|
|
// "12" or "—".
|
|
tracks: string,
|
|
artwork-url: string,
|
|
// Effective release type AFTER the user's override is applied:
|
|
// "album" | "ep" | "single" | "live" | "compilation".
|
|
release-type: string,
|
|
// True when a user override is active for this candidate (drives the
|
|
// dotted-underline marker on the type button).
|
|
is-overridden: bool,
|
|
// QualityBadgeFull inputs.
|
|
quality-tier: string,
|
|
quality-detail: string,
|
|
// Whether THIS candidate is currently checked (included on save).
|
|
checked: bool,
|
|
}
|
|
|
|
// One dedupe GROUP: a primary candidate plus zero or more alternates.
|
|
export struct DiscoGroup {
|
|
// group_key (`${normalizedTitle}|${year}`) — stable row key.
|
|
key: string,
|
|
title: string,
|
|
// "2019" / "—".
|
|
year: string,
|
|
primary: DiscoCandidate,
|
|
alternates: [DiscoCandidate],
|
|
// ALL candidates are compilations → the group is treated as a comp
|
|
// (dimmed primary row, ignored by select-all).
|
|
is-compilation: bool,
|
|
}
|
|
|
|
export global DiscoBuilderState {
|
|
// The artist being built (id + resolved display name + portrait URL).
|
|
in property <string> artist-id;
|
|
in property <string> artist-name;
|
|
in property <string> artist-avatar-url;
|
|
in property <image> artist-avatar;
|
|
|
|
// Render states (spec 13 §4). `loading` true initially + during load;
|
|
// `load-error` non-empty shows the raw error message; otherwise the
|
|
// groups table renders (empty groups -> "No results found").
|
|
in property <bool> loading: true;
|
|
in property <string> load-error: "";
|
|
|
|
// The dedupe groups in CURRENT sort order (Rust re-sorts on order change).
|
|
in property <[DiscoGroup]> groups: [];
|
|
|
|
// The collection-name input (defaulted to "<artist> — Complete Discography").
|
|
in-out property <string> collection-name: "";
|
|
// Whether the name is empty AFTER trimming whitespace (Slint can't trim, so
|
|
// the controller computes this on every name change). Gates the Create
|
|
// button so a whitespace-only name disables it (spec 13 §2.13 / §10.19:
|
|
// disabled = creating || selectedCount===0 || !collectionName.trim()).
|
|
in property <bool> name-trimmed-empty: true;
|
|
|
|
// Order segmented control: "release_date" | "title" | "manual".
|
|
in-out property <string> order-by: "release_date";
|
|
|
|
// Select-all header checkbox state (tri-state via the two flags).
|
|
in property <bool> all-checked: false;
|
|
in property <bool> some-checked: false;
|
|
|
|
// Footer: total checked candidate count + total group count.
|
|
in property <int> selected-count: 0;
|
|
in property <int> group-count: 0;
|
|
|
|
// Disables the Create button while a save is in flight.
|
|
in property <bool> creating: false;
|
|
|
|
// The per-row type-override popover open key (`${source}|${source_item_id}`),
|
|
// or "" when none is open. One menu open at a time.
|
|
in-out property <string> open-type-menu-key: "";
|
|
}
|
|
|
|
export global DiscoBuilderActions {
|
|
// Back to the artist page (clears builder state).
|
|
callback back();
|
|
// Collection-name input edited.
|
|
callback name-changed(string);
|
|
// Order segmented control: "release_date" | "title" | "manual".
|
|
callback set-order(string);
|
|
// Toggle one candidate's checked state (group-key + candidate-key).
|
|
callback toggle-checked(string /* group-key */, string /* candidate-key */);
|
|
// Header tri-state select-all toggle (non-compilation primaries only).
|
|
callback toggle-all();
|
|
// Open an album (Qobuz album view, or local/Plex local-album view).
|
|
callback open-album(string /* source */, string /* source-item-id */);
|
|
// Apply a release-type override to a candidate (writes the sidecar).
|
|
callback set-type-override(string /* source */, string /* source-item-id */, string /* type */);
|
|
// Clear a candidate's override (revert to auto-detected).
|
|
callback reset-type-override(string /* source */, string /* source-item-id */);
|
|
// Save: create the artist_collection + bulk-add the checked groups.
|
|
callback create();
|
|
}
|
|
|
|
// ──────────────────────────── DJ-mix modal ────────────────────────────
|
|
// The "Random queue" sampler (TrackMixModal, spec 21 §C / spec 12 §12).
|
|
// Opened from the hero DJ-mix CTA. On open the Rust controller resolves the
|
|
// collection in-order + computes shuffle::unique_track_count (the slider max);
|
|
// the slider snaps to the discrete buildSizeOptions set (50,100,150,…,All).
|
|
// On confirm it runs dedup_by_similarity + hybrid_sample (thread RNG, sync
|
|
// scope) -> set_queue + play -> stamp queue-source + touch_play, toasting
|
|
// "Playing N of M" when the sampled count is below the requested size.
|
|
export global MyQbzMixState {
|
|
in property <bool> open: false;
|
|
// True while resolving + counting unique tracks ("Loading…" state).
|
|
in property <bool> loading: false;
|
|
// Deduped unique-track count = the slider MAX (and the "All" size).
|
|
in property <int> unique-count: 0;
|
|
// The discrete size options (buildSizeOptions): the slider index maps into
|
|
// this list. e.g. [50,100,150,All(187)]. Empty while loading / for an
|
|
// empty collection.
|
|
in property <[int]> size-options: [];
|
|
// Current slider index into `size-options` (0-based).
|
|
in-out property <int> selected-index: 0;
|
|
// The size at `selected-index` (the value handed to the sampler).
|
|
in property <int> selected-size: 0;
|
|
// True when the selected option is the trailing "All (N)" entry.
|
|
in property <bool> selected-is-all: false;
|
|
// Disables Shuffle while a sample + play is in flight.
|
|
in property <bool> busy: false;
|
|
}
|
|
|
|
export global MyQbzMixActions {
|
|
callback close();
|
|
// Slider moved: the new index into size-options (Rust re-derives the size).
|
|
callback set-index(int /* index */);
|
|
// Confirm -> run the sample + play.
|
|
callback shuffle();
|
|
}
|
|
|
|
// ──────────────────────────── MyQBZ edit modals ───────────────────────
|
|
// The Rename / Description / Delete-confirm modals (spec 12 §11), driven
|
|
// from the hero overflow (⋯) menu. One global backs all three (only one is
|
|
// ever open at a time). `busy` disables submit while the mutation runs.
|
|
export global MyQbzEditState {
|
|
// "" = closed; "rename" | "description" | "delete".
|
|
in-out property <string> mode: "";
|
|
in property <bool> open: false;
|
|
// The collection name (delete-confirm body interpolation + rename prefill).
|
|
in property <string> name;
|
|
// Rename: prefilled with the collection name; submit trims.
|
|
in-out property <string> draft-name;
|
|
// Description: prefilled with the current description ("" clears to null).
|
|
in-out property <string> draft-description;
|
|
// Disables submit while a mutation + reload is in flight.
|
|
in-out property <bool> busy: false;
|
|
}
|
|
|
|
export global MyQbzEditActions {
|
|
callback close();
|
|
callback submit-rename(); // trim; rename_collection -> reload -> close.
|
|
callback submit-description(); // empty -> null; set_description -> reload.
|
|
callback confirm-delete(); // delete_collection -> nav back + cleanup.
|
|
}
|
|
|
|
// Folder editor modal — create + edit (icon preset, solid color, custom
|
|
// image, hidden toggle, delete). Ports Tauri's FolderEditModal. Gradient
|
|
// color presets are intentionally omitted (Slint brush limitation).
|
|
export struct PmIconPreset {
|
|
id: string,
|
|
icon: image,
|
|
}
|
|
|
|
export struct PmColorSwatch {
|
|
// The stored hex value ("" = the "Accent" preset), used as the
|
|
// selection key and persisted into the folder record.
|
|
value: string,
|
|
// Resolved Slint color for the swatch fill (ignored when is-accent).
|
|
color: color,
|
|
is-accent: bool,
|
|
}
|
|
|
|
export global FolderEditState {
|
|
in-out property <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;
|
|
|
|
// --- OFFLINE MODE --------------------------------------------------------
|
|
|
|
// Settings > Offline, MODE group (the app operating without Qobuz — NOT
|
|
// the offline cache). Backed by the shared engine's per-user store, not
|
|
// the audio/playback stores: seeded by OfflineModeActions.load() on panel
|
|
// mount; persisted through settings-bool keys "offline-mode-enabled" /
|
|
// "offline-show-network-folders".
|
|
in-out property <bool> offline-mode-enabled: false;
|
|
in-out property <bool> offline-show-network-folders: false;
|
|
// "Check now" re-probe in flight — flips the status row's button label.
|
|
// Cleared by Rust on the next status broadcast, or after a short timeout
|
|
// when the verdict comes back unchanged (the actor only broadcasts flips).
|
|
in-out property <bool> offline-checking: false;
|
|
}
|
|
|
|
// Appearance settings — backs the Settings > Appearance panel. 1:1 with
|
|
// Tauri's appearance section. This is a visual/structural replica: every
|
|
// row is bound to a property here, but the Rust side does NOT yet persist
|
|
// or apply most of them (wiring is a later step). The few that map onto an
|
|
// existing, already-functional flag are wired through (see notes inline).
|
|
//
|
|
// Dropdowns follow the same pattern as SettingsState: an option list paired
|
|
// with a selected index; the Rust controller (when wired) owns the
|
|
// index -> value mapping. Lists are populated with sensible defaults below
|
|
// so the UI renders standalone even while unwired.
|
|
export global AppearanceState {
|
|
// --- THEME -------------------------------------------------------------
|
|
// 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 -------------------------------------------------------
|
|
// `is-macos` is set once from Rust (cfg!(target_os = "macos")) so the tray
|
|
// section can swap the header to "Menu Bar" and show macOS-only controls.
|
|
in-out property <bool> is-macos: false;
|
|
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;
|
|
// macOS-only: hide the Dock icon when closed to the menu bar (.accessory
|
|
// activation policy). Off = Spotify-style (keep the Dock icon).
|
|
in-out property <bool> tray-mac-hide-dock: 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 */);
|
|
}
|
|
|
|
// "My QBZ" navigation branding (custom label + icon), surfaced in
|
|
// Settings > Appearance and reflected on the sidebar "My QBZ" row.
|
|
//
|
|
// Mirrors the Tauri `myQbzNavStore` contract (label default "My QBZ",
|
|
// icon default the branded /my-qbz.svg). Persistence is per-user JSON
|
|
// (see crate::myqbz_prefs); these properties are the live UI mirror and
|
|
// the callbacks are the action surface (the Rust side persists + re-seeds).
|
|
//
|
|
// - `label` : the current label (never empty — blank input coerces
|
|
// back to the default "My QBZ" string on the Rust side).
|
|
// - `custom-icon` : the user's image, loaded straight from the picked file
|
|
// path and rendered as-is (no tint). Only meaningful when
|
|
// `has-custom-icon` is true; otherwise the consumer falls
|
|
// back to the default branded glyph in markup (mask-tinted
|
|
// via QbzIcon on the sidebar). The default glyph stays a
|
|
// compile-time `@image-url` — the Rust side never decodes it.
|
|
// - `has-custom-icon` : true when a usable user image is set. A stale /
|
|
// deleted custom path falls back to the default on the Rust
|
|
// side (the seed loads the file; a load failure clears the
|
|
// flag without mutating the store), so this flag tracks "a
|
|
// usable custom icon is present".
|
|
export global MyQbzBrandingState {
|
|
in-out property <string> label: "My QBZ";
|
|
in-out property <image> custom-icon;
|
|
in-out property <bool> has-custom-icon: false;
|
|
|
|
// Commit a new label (trimmed-empty coerces to the default on the Rust
|
|
// side, which also re-seeds `label`). Fired on the text input's edit.
|
|
callback set-label(string /* label */);
|
|
// Open the native image picker; on pick, persist the path, load it, and
|
|
// re-seed `icon` + `has-custom-icon`. No-op on cancel.
|
|
callback pick-icon();
|
|
// Reset the icon to the default branded glyph (clears the persisted
|
|
// custom path). The label is reset separately by clearing the input.
|
|
callback reset-icon();
|
|
}
|
|
|
|
// One entry of a nav-tab dropdown.
|
|
export struct NavMenuEntry {
|
|
label: string,
|
|
icon: image,
|
|
has-glyph: bool,
|
|
// Destination route fired when the row is clicked, e.g. "home",
|
|
// "favorites-tracks", "favorites-albums". Empty = no-op (the
|
|
// destination isn't implemented yet).
|
|
route: string,
|
|
}
|
|
|
|
// Header nav-tab dropdown state.
|
|
//
|
|
// The four header tabs (Discover / Library / Local Library / My QBZ) no
|
|
// longer own a PopupWindow each — a PopupWindow grabs the pointer while
|
|
// open, which blocks hover-switching between tabs. Instead a single
|
|
// in-window overlay (HeaderMenuOverlay, a late child of the AppShell
|
|
// root) draws the open tab's menu. Tabs write their identity into this
|
|
// global on hover/click; hovering a different tab overwrites it, giving
|
|
// an instant switch. `open-index` of -1 means no menu is open.
|
|
export global HeaderMenuState {
|
|
// Which tab's menu is open: -1 = none, 0..3 = the tab index.
|
|
in-out property <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();
|
|
|
|
// ---- Scroll-position restore (browser-like, per history entry) ----
|
|
// The mounted scroll container reports its live viewport-y here on every
|
|
// `changed viewport-y`; the Rust nav module stamps it onto the outgoing
|
|
// entry so back/forward can put it back.
|
|
callback report-scroll(length /* viewport-y */);
|
|
// Armed by Rust right before a back/forward view switch: `restore-scope`
|
|
// names which scroll container should restore (a stable per-list id, see
|
|
// nav::scope_for), and `scroll-restore` is the viewport-y to apply. A
|
|
// container restores only when its own scope matches, exactly once per
|
|
// mount, and only after its content has laid out (viewport overflows).
|
|
in-out property <string> restore-scope: "";
|
|
in-out property <length> scroll-restore: 0;
|
|
}
|
|
|
|
// Artist detail screen state.
|
|
// One label row in the artist network sidebar's Labels section.
|
|
export struct LabelEntry {
|
|
id: string,
|
|
name: string,
|
|
}
|
|
|
|
// One similar-artist row in the artist network sidebar's Similar
|
|
// Artists section.
|
|
export struct SimilarEntry {
|
|
id: string,
|
|
name: string,
|
|
}
|
|
|
|
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);
|
|
}
|
|
// Right-side panels — independent now: Queue and Lyrics can BOTH be open
|
|
// at once, stacked vertically in a shared right column (Feishin-style). Each
|
|
// is toggled from its bar button and closed from its own X. The column is
|
|
// visible when either is open. (Replaces the old exclusive `right-panel` int
|
|
// where lyrics closed queue and vice-versa.)
|
|
in-out property <bool> queue-open: false;
|
|
in-out property <bool> lyrics-open: false;
|
|
// 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;
|
|
// Now-playing bar layout mode: 0 = New (default), 1 = Classic. Persisted
|
|
// in ui_prefs.json (`npb_mode` "new"|"classic"); set on startup from the
|
|
// store and on the player bar's "Now Playing view" menu select.
|
|
in-out property <int> npb-mode: 0;
|
|
}
|
|
|
|
// Authenticated user, for the header user element.
|
|
export global SessionState {
|
|
in property <string> user-name: "";
|
|
in property <string> subscription: "";
|
|
}
|
|
|
|
// One entry in the playback queue, for the Queue sidebar list.
|
|
export struct QueueItem {
|
|
id: string,
|
|
title: string,
|
|
artist: string,
|
|
artwork: image,
|
|
playing: bool,
|
|
duration: string, // "M:SS"
|
|
explicit: bool,
|
|
// From an ephemeral folder — gates persistence actions in the row menu.
|
|
is-ephemeral: 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;
|
|
// True when the playing track is from an ephemeral folder (synthetic id):
|
|
// it has no DB row, so favorite / add-to-playlist / track-info are gated off.
|
|
in property <bool> is-ephemeral: false;
|
|
in property <bool> playing: false;
|
|
// Fetch/resolve in flight for the track being started — no audio yet.
|
|
// Set the instant a play is requested (before the Plex/Qobuz/local
|
|
// resolve) and cleared once the poll loop sees the audio advancing.
|
|
// Drives the fetch spinner on the bar, the active track row, and the
|
|
// album play button.
|
|
in property <bool> loading: 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;
|
|
// Whether THIS device currently has a Qobuz Connect session running (drives
|
|
// the connect flyout's Connect/Disconnect button). Distinct from `is-remote`,
|
|
// which means a PEER renderer owns playback. Set by the connect callback.
|
|
in property <bool> qconnect-connected: false;
|
|
// Name of the active Qobuz Connect renderer (shown under the cast button
|
|
// when remote). Empty when local. Populated by the QConnect epic.
|
|
in property <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();
|
|
|
|
// Qobuz Connect — toggle the session on/off from the bar's connect flyout.
|
|
// Wired in main.rs to SlintQconnectService::connect/disconnect.
|
|
callback qconnect-toggle();
|
|
}
|
|
|
|
// One row in the QConnect device picker (a session renderer). Populated by the
|
|
// event sink from `session.renderers`.
|
|
export struct QconnectDevice {
|
|
renderer-id: int,
|
|
name: string,
|
|
is-local: bool, // this is QBZ itself ("Play here")
|
|
is-active: bool, // currently the active renderer
|
|
// Device-type icon key (mirrors Qobuz Connect): "computer" | "web" | "mobile" | "speaker".
|
|
icon: string,
|
|
}
|
|
|
|
// Qobuz Connect DEV diagnostics — live session topology + a rolling event log,
|
|
// rendered by QconnectDevModal (opened from the connect flyout). Lets us inspect
|
|
// QC behavior at runtime WITHOUT a rebuild (Slint builds are slow). `open` is
|
|
// in-out so the flyout button + the modal backdrop toggle it directly.
|
|
//
|
|
// Also hosts the device-picker model (CONTROLLER mode): the available session
|
|
// renderers + the active renderer id, plus the set-active callback the flyout
|
|
// rows invoke to switch playback to another renderer (or back to "Play here").
|
|
export global QconnectDevState {
|
|
in-out property <bool> open: false;
|
|
in property <string> status;
|
|
// Joined log text (newest first, one entry per line). A single string so the
|
|
// modal can render it in a read-only TextEdit (selectable + copyable).
|
|
in property <string> log-text;
|
|
callback clear();
|
|
|
|
// Device picker.
|
|
in property <[QconnectDevice]> devices: [];
|
|
in property <int> active-renderer-id: -1;
|
|
callback set-active(int); // renderer-id; the local id = "Play here"
|
|
}
|
|
|
|
// 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);
|
|
// Reorder the upcoming list: move the page-local row `from` to the insertion
|
|
// slot `to` (0 = before the first upcoming row, N = after the last). The Rust
|
|
// controller resolves the page-local indices to queue-wide upcoming indices
|
|
// and routes to the cloud when connected (WS-authoritative).
|
|
callback reorder(int /* from */, int /* to */);
|
|
// Empty the queue.
|
|
callback clear-queue();
|
|
// Toggle the favorite state of the now-playing track.
|
|
callback toggle-now-playing-favorite();
|
|
// Save the current queue as a new playlist.
|
|
callback save-as-playlist();
|
|
// Toggle infinite-play (auto-refill the queue with similar tracks).
|
|
callback toggle-infinite-play();
|
|
// Re-filter the upcoming list after `search-query` changed.
|
|
callback search-changed();
|
|
// Move to the previous / next paginator page.
|
|
callback prev-page();
|
|
callback next-page();
|
|
// Switch the active tab (0 = Queue, 1 = History).
|
|
callback set-tab(int);
|
|
// Fired when the sidebar becomes visible, so the Rust controller can
|
|
// pull a fresh queue snapshot (the queue may have changed while the
|
|
// panel was closed, e.g. after a session restore).
|
|
callback panel-opened();
|
|
|
|
// Reflects whether infinite-play is currently enabled.
|
|
in property <bool> infinite-play: false;
|
|
}
|
|
|
|
// One row in the "Add to playlist" picker.
|
|
export struct PlaylistPickItem {
|
|
id: string,
|
|
name: string,
|
|
tracks-line: string, // "N tracks"
|
|
// LOCAL playlist (library.db entity, id "local:<uuid>") — the row
|
|
// shows a hard-drive glyph; adds write the local repo (works offline).
|
|
is-local: bool,
|
|
}
|
|
|
|
// "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);
|
|
}
|
|
|
|
// ============================ Offline MODE ================================
|
|
// App-wide offline MODE status (the app operating without Qobuz — NOT the
|
|
// offline CACHE/downloads). Mirrors `qbz_app::offline_mode::OfflineStatus`;
|
|
// pushed by crate::offline_mode::start_ui_forwarder on every engine
|
|
// broadcast. Read by the login screen (offline-boot affordances) and the
|
|
// shell (D2 recovery banner); the gating slice adds more consumers.
|
|
export global OfflineState {
|
|
// Any offline flavor active (mode != online).
|
|
in property <bool> offline: false;
|
|
// 0 = online, 1 = real offline, 2 = induced offline.
|
|
in property <int> mode: 0;
|
|
// Raw connectivity, independent of the mode: 0 unknown, 1 up, 2 down.
|
|
in property <int> connectivity: 0;
|
|
// A probe was answered by a redirect — typical captive portal.
|
|
in property <bool> captive-portal: false;
|
|
// D2: unauthenticated offline session while connectivity is back up —
|
|
// the shell shows the one-click "Sign in" banner.
|
|
in property <bool> show-recovery-banner: false;
|
|
// A previous login exists (last_user_id on disk). Gates the login
|
|
// screen's "Start offline" affordance: offline mode needs a prior
|
|
// user's data, never the empty user-0 profile.
|
|
in property <bool> has-previous-session: false;
|
|
// Startup session-restore failure detail, shown on the login screen
|
|
// ("" = none). Set by the restore task's error arm; cleared on any
|
|
// successful shell entry.
|
|
in property <string> login-error: "";
|
|
}
|
|
|
|
// Settings > Offline, MODE group — Rust-wired actions (crate::offline_mode).
|
|
export global OfflineModeActions {
|
|
// (Re)seed the MODE-group toggle states from the engine store. Fired by
|
|
// the panel's init, so every mount of Settings > Offline re-reads them.
|
|
callback load();
|
|
// Force an immediate connectivity re-probe (the status row's "Check
|
|
// now"). The status text updates by itself via the engine forwarder.
|
|
callback check-now();
|
|
}
|