Scroll-position restore was never implemented (only a TODO in nav.rs); back/forward tracked which page was visited, not where it was scrolled. Add per-history-entry scroll memory: the mounted scroll container reports its live viewport-y via NavState.report-scroll, so every navigation (fresh record + back/forward) stamps the outgoing entry without touching the ~30 record call sites. go_back/go_forward hand the saved scroll back to the shell, which arms NavState.restore-scope + scroll-restore before switching the view. A container restores only when its scope matches, once per mount, and only after content has laid out (viewport overflows) to dodge the clamp-to-0 on async data load; it clears the scope once consumed. Wired the four Local Library lists (albums, tracks, folders, artists rail). Tracks is paginated, so its restore is best-effort within loaded pages. cc
2523 lines
115 KiB
Plaintext
2523 lines
115 KiB
Plaintext
// Shared UI state — structs and globals the Rust controllers populate.
|
|
//
|
|
// The Rust side builds these models on a worker thread, then sets them
|
|
// on the global from the Slint event loop (see src/home.rs, src/main.rs).
|
|
|
|
import { JumpNavTab } from "primitives/JumpNavBar.slint";
|
|
export { JumpNavTab }
|
|
|
|
export struct AlbumCardItem {
|
|
id: string,
|
|
title: string,
|
|
artist: string,
|
|
artist-id: string, // for the clickable artist name -> artist page
|
|
genre: string, // shown in the hover overlay
|
|
year: string, // localized "MMM D, YYYY" — shown in the hover overlay
|
|
quality-tier: string, // "hires" | "cd" | "" — drives the badge icon
|
|
quality-label: string, // "Hi-Res: 24-bit / 96 kHz" — badge hover tooltip
|
|
ribbon: string,
|
|
ribbon-kind: string, // "press" | "qobuzissime" | "albumOfTheWeek" | ""
|
|
artwork-url: string,
|
|
artwork: image, // empty until the artwork pipeline resolves it
|
|
is-favorite: bool, // filled accent heart on the card when favorited
|
|
removing: bool, // fading out (un-favorited in the favorites view)
|
|
// --- List-row extras (AlbumListRow; ignored by the grid AlbumCard) ----
|
|
release-type: string, // "Album" | "EP" | "Single" | "Live" | "Compilation"
|
|
source: string, // "qobuz" | "local" | "plex" | "" — hideable column
|
|
quality-detail: string,// "24-bit / 96 kHz" — bare detail for QualityBadgeFull
|
|
track-count: string, // "12" — TRACKS column
|
|
plain-year: string, // "1973" — bare year for the list-row YEAR column
|
|
}
|
|
|
|
// One entry in an A-Z jump strip (favorites tracks name-group / albums
|
|
// A-Z group). `index` is the first row index (tracks); albums jump
|
|
// proportionally by the entry's ordinal so it needs no exact offset.
|
|
export struct AlphaJump {
|
|
letter: string,
|
|
index: int,
|
|
}
|
|
|
|
// One button in the shared multi-select bulk bar (MultiSelectBar).
|
|
export struct BulkAction {
|
|
id: string, // emitted verbatim via MultiSelectBar.action(id)
|
|
label: string, // already-translated button label
|
|
danger: bool, // red-bordered destructive button (e.g. Remove)
|
|
needs-selection: bool, // dimmed + disabled while nothing is selected
|
|
}
|
|
|
|
export struct DiscoverSection {
|
|
title: string,
|
|
// Discover endpoint path this section maps to (e.g. "/discover/newReleases").
|
|
// Empty for sections with no full-list page; non-empty shows the "View all"
|
|
// link and drives the DiscoverBrowse page.
|
|
endpoint: string,
|
|
albums: [AlbumCardItem],
|
|
}
|
|
|
|
// Compact "slim" card item — a small thumbnail with a title/subtitle,
|
|
// optionally rank-numbered (Popular albums, Recently Played).
|
|
export struct SlimItem {
|
|
id: string,
|
|
title: string,
|
|
subtitle: string,
|
|
rank: string, // "" = no rank
|
|
artwork-url: string,
|
|
artwork: image,
|
|
// Artist follow state — drives the ArtistCard Follow button.
|
|
following: bool,
|
|
}
|
|
|
|
// A playlist result in search (and the label landing's Playlists row).
|
|
// Qobuz returns up to four cover URLs per playlist; the card renders them
|
|
// as a count-based collage. Defined here (before LabelState) so the label
|
|
// landing can reuse it — Slint structs must be declared before first use.
|
|
export struct SearchPlaylistItem {
|
|
id: string,
|
|
title: string,
|
|
subtitle: string, // owner name + track count
|
|
cover-count: int, // number of cover images, 0-4
|
|
url1: string,
|
|
url2: string,
|
|
url3: string,
|
|
url4: string,
|
|
cover1: image,
|
|
cover2: image,
|
|
cover3: image,
|
|
cover4: image,
|
|
}
|
|
|
|
// Discover / Home screen state.
|
|
export global HomeState {
|
|
in property <[DiscoverSection]> sections: [];
|
|
in property <[SlimItem]> popular: [];
|
|
in property <[SlimItem]> recent: [];
|
|
in property <[AlbumCardItem]> recent-albums: [];
|
|
in property <bool> loading: false;
|
|
// Active Discover tab: "home" | "editorPicks" | "forYou". The
|
|
// three tabs draw from the same fetch; switching swaps which
|
|
// cached section set is shown (see crate::home::select_tab).
|
|
in-out property <string> active-tab: "home";
|
|
}
|
|
|
|
// Discover tab switching. Wired in main.rs to crate::home::select_tab.
|
|
export global HomeActions {
|
|
callback select-tab(string);
|
|
}
|
|
|
|
// Discover "View all" full-list page — one album module (new releases,
|
|
// qobuzissimes, ...) opened from a Carousel's See-All link. Paginated.
|
|
export global DiscoverBrowseState {
|
|
in-out property <string> title;
|
|
// Rust-managed: the /discover/<x> endpoint + the next page offset, so
|
|
// DiscoverBrowseActions.load-more knows what to fetch next.
|
|
in-out property <string> endpoint;
|
|
in-out property <int> next-offset;
|
|
// Full (unfiltered) loaded page set. The view binds to `visible`
|
|
// below; the Rust side rebuilds `visible` from `albums` whenever the
|
|
// search query changes (client-side title/artist filter).
|
|
in-out property <[AlbumCardItem]> albums;
|
|
// The albums actually rendered — equals `albums` with no search, or a
|
|
// filtered subset while the search box is non-empty.
|
|
in-out property <[AlbumCardItem]> visible;
|
|
in-out property <bool> loading;
|
|
in-out property <bool> loading-more;
|
|
in-out property <bool> has-more;
|
|
// Header tools (1:1 with Tauri DiscoverBrowseView).
|
|
// Client-side search over the loaded albums; while non-empty,
|
|
// infinite-scroll load-more is disabled (Tauri does the same).
|
|
in-out property <string> search-query;
|
|
// "grid" | "list" — the view toggle.
|
|
in-out property <string> view-mode: "grid";
|
|
}
|
|
|
|
export global DiscoverBrowseActions {
|
|
// Fetch the next page (offset = DiscoverBrowseState.next-offset).
|
|
callback load-more();
|
|
// Re-filter `visible` after `search-query` changed (UI thread).
|
|
callback search-changed();
|
|
}
|
|
|
|
// Discover > For You state. The For You tab has heterogeneous,
|
|
// ordered sections (album carousels, a track carousel, artist
|
|
// carousels), so it renders from its own state rather than the
|
|
// uniform HomeState.sections used by Home / Editor's Picks.
|
|
// Populated lazily by crate::foryou the first time the tab opens.
|
|
// One album-seeded radio tile in the For You "Radio Stations" row.
|
|
export struct RadioStationItem {
|
|
album-id: string,
|
|
title: string,
|
|
artist: string,
|
|
artwork-url: string,
|
|
artwork: image,
|
|
}
|
|
|
|
export global ForYouState {
|
|
in property <DiscoverSection> release-watch;
|
|
in property <[RadioStationItem]> radio-stations: [];
|
|
in property <DiscoverSection> recent-albums;
|
|
in property <[SlimItem]> recent-tracks: [];
|
|
in property <[SlimItem]> top-artists: [];
|
|
in property <[SlimItem]> artists-to-follow: [];
|
|
in property <DiscoverSection> more-from-library;
|
|
in property <DiscoverSection> rediscover;
|
|
// Spotlight — one favorite artist highlighted with a hero + a row
|
|
// of their content (top tracks card, radio card, albums).
|
|
in property <bool> spotlight-visible: false;
|
|
in property <string> spotlight-artist-id;
|
|
in property <string> spotlight-name;
|
|
in property <string> spotlight-category;
|
|
in property <string> spotlight-image-url;
|
|
in property <image> spotlight-image;
|
|
in property <bool> spotlight-has-top-tracks: false;
|
|
in property <[AlbumCardItem]> spotlight-albums: [];
|
|
in property <bool> loading: false;
|
|
// True once a load has completed, so the tab does not refetch on
|
|
// every visit.
|
|
in-out property <bool> loaded: false;
|
|
}
|
|
|
|
// One genre in the Filter-by-genre popup's simple grid.
|
|
export struct GenreChip {
|
|
id: string,
|
|
name: string,
|
|
selected: bool,
|
|
}
|
|
|
|
// One row of the advanced-view genre tree, flattened (level 0/1/2)
|
|
// with only currently-visible rows emitted by the Rust side.
|
|
export struct GenreTreeRow {
|
|
id: string,
|
|
name: string,
|
|
level: int, // 0 parent, 1 child, 2 grandchild
|
|
selected: bool,
|
|
expanded: bool,
|
|
has-children: bool,
|
|
count: int, // loaded child count (0 = none / unknown)
|
|
}
|
|
|
|
// Filter-by-genre state. The selection is shared across the three
|
|
// Discover tabs (matching Tauri's single "home" genre context) — it
|
|
// re-fetches the discover index with the chosen genre ids. Populated
|
|
// by crate::genre_filter.
|
|
export global GenreFilterState {
|
|
in property <[GenreChip]> genres: [];
|
|
in property <[GenreTreeRow]> tree: [];
|
|
in property <int> selected-count: 0;
|
|
in-out property <bool> open: false;
|
|
in-out property <bool> remember: true;
|
|
in-out property <bool> advanced: false;
|
|
in-out property <string> search-query;
|
|
in property <bool> loading: false;
|
|
}
|
|
|
|
export global GenreFilterActions {
|
|
// Set the context the popup edits ("discover" | "favorites"), called
|
|
// when a surface opens the shared popup.
|
|
callback set-context(string);
|
|
callback toggle(string /* genre id */);
|
|
callback toggle-expand(string /* genre id */);
|
|
callback search(string /* query */);
|
|
callback clear();
|
|
callback set-remember(bool);
|
|
callback set-advanced(bool);
|
|
}
|
|
|
|
// Small string helpers backed by Rust. Slint 1.16 has no `contains`
|
|
// builtin, so the searchable QbzSelect filter (long device lists) needs a
|
|
// native case-insensitive substring test. Pure so it can be called from
|
|
// property bindings; reusable by any searchable list.
|
|
export global TextUtil {
|
|
pure callback contains-ci(string /* haystack */, string /* needle */) -> bool;
|
|
}
|
|
|
|
// The single, shared track-row model. Superset of every track-list
|
|
// surface (album, artist popular, search, favorites, mix, playlist),
|
|
// rendered by the shared primitives/TrackRow component. Fields that a
|
|
// given surface doesn't use stay empty/false:
|
|
// - `number` explicit track number (album/playlist); rows fall
|
|
// back to their list index when empty.
|
|
// - `album` shown only by rows with show-album (mix/playlist).
|
|
// - `selected` multi-select state (artist Popular, playlist editor).
|
|
// - `artwork` the cover thumbnail for show-artwork rows.
|
|
export struct TrackItem {
|
|
id: string,
|
|
number: string,
|
|
title: string,
|
|
artist: string,
|
|
album: string,
|
|
duration: string,
|
|
quality-tier: string,
|
|
// Exact bit-depth / sample-rate line, e.g. "24-bit / 96 kHz" — feeds the
|
|
// track-row quality badge (empty for unknown / where only the tier shows).
|
|
quality-detail: string,
|
|
explicit: bool,
|
|
selected: bool,
|
|
artwork-url: string,
|
|
artwork: image,
|
|
// Favorite state — drives the heart fill and the optimistic toggle.
|
|
// Populated from the user's favorite-track set when each row is built.
|
|
is-favorite: bool,
|
|
// Ids for the clickable artist/album links in the row. Empty = the
|
|
// text is plain (not a link). artist-id -> artist view, album-id ->
|
|
// album view, both via the media-action bubble channel.
|
|
artist-id: string,
|
|
album-id: string,
|
|
// "qobuz" | "local" | "plex" | "" — drives the Local Library source
|
|
// indicator (shown instead of the favorite/offline columns).
|
|
source: string,
|
|
// Fading out (un-favorited in the favorites view) before removal.
|
|
removing: bool,
|
|
// Offline-cache status: 0 none, 1 queued, 2 downloading, 3 ready, 4 failed.
|
|
// Drives the per-row offline affordance (make-offline / ring / check / warn).
|
|
cache-status: int,
|
|
// Download progress 0.0..1.0 — meaningful only while cache-status == 2.
|
|
cache-progress: float,
|
|
// True while this track's offline CMAF bundle is being decrypted for
|
|
// playback — drives the padlock glyph (swaps the play/equalizer icon).
|
|
unlocking: bool,
|
|
}
|
|
|
|
// Album detail screen state.
|
|
export global AlbumState {
|
|
in property <string> id;
|
|
in property <string> title;
|
|
in property <string> artist;
|
|
in property <string> artist-id;
|
|
// Network URL of the cover — used by the artwork right-click
|
|
// menu for Open-in-browser and Save-as actions.
|
|
in property <string> artwork-url;
|
|
in property <bool> has-custom-cover: false;
|
|
in property <string> info-line;
|
|
in property <string> quality-tier;
|
|
// "24-bit / 96 kHz" — quality-badge detail line.
|
|
in property <string> quality-detail;
|
|
// Editorial description / review (HTML stripped). May be empty.
|
|
in property <string> description;
|
|
// Truncated description for the header; full text lives in the modal.
|
|
in property <string> description-short;
|
|
in property <image> artwork;
|
|
// Artwork-derived tint for the header gradient.
|
|
in property <color> header-color: #181820;
|
|
in property <string> label;
|
|
// Label id for the sidebar label card → label page navigation ("" = none).
|
|
in property <string> label-id;
|
|
in property <[string]> awards: [];
|
|
in property <[TrackItem]> tracks: [];
|
|
in property <bool> loading: false;
|
|
// True when this is a Local Library album (local files / offline copies).
|
|
// Routes the play actions to local playback + hides Qobuz-only chrome.
|
|
in property <bool> is-local: false;
|
|
// Local source ("user" | "qobuz_download" | "plex" | ""). Gates the
|
|
// tag-editor pencil (hidden for Plex; tag write isn't supported there).
|
|
in property <string> source;
|
|
}
|
|
|
|
// Album-view actions wired to Rust callbacks.
|
|
export global AlbumActions {
|
|
// Filter the visible track list by a query (client-side).
|
|
callback search(string);
|
|
}
|
|
|
|
// Track drag-and-drop onto sidebar playlists (a star QBZ feature).
|
|
// Slint has no HTML5 DnD, so this is pointer-based: the source row
|
|
// grabs the pointer and feeds global coords here; the floating ghost
|
|
// + each sidebar playlist row read this to self-detect the drop.
|
|
export global DragState {
|
|
in-out property <bool> active: false;
|
|
// Dragged-track count + ghost text (count == 1 shows title +
|
|
// "artist · album"; count > 1 shows "N tracks").
|
|
in-out property <int> count: 0;
|
|
in-out property <string> ghost-title;
|
|
in-out property <string> ghost-subtitle;
|
|
// Global pointer position (window coords) while dragging.
|
|
in-out property <length> pointer-x;
|
|
in-out property <length> pointer-y;
|
|
// The sidebar playlist id currently under the pointer (drop target).
|
|
in-out property <string> over-playlist-id;
|
|
}
|
|
|
|
export global DragActions {
|
|
// Begin a track drag from a row. `track-id` is the row; the Rust
|
|
// side expands it to the multi-selection when applicable.
|
|
callback start(string /* track id */, string /* title */, string /* subtitle */, length, length);
|
|
callback move(length /* gx */, length /* gy */);
|
|
callback end();
|
|
}
|
|
|
|
// Which content view the shell shows.
|
|
export enum ContentView { home, album, artist, settings, search, musician, label, label-releases, location, favorites, mix, playlist, discover-browse, playlist-manager, offline-manager, local-library, local-album }
|
|
|
|
// In-app toast (transient notification). Mirrors Tauri's toastStore:
|
|
// a single toast at a time (a new one replaces the current), auto-hidden
|
|
// by the Rust layer per kind. `buffering` is persistent (no auto-hide).
|
|
export enum ToastKind { success, error, info, warning, buffering }
|
|
|
|
// Local Library (on-disk + Plex music). Greenfield Slint port — the view
|
|
// shows header stats + the browse tabs (Albums / Artists / Folders /
|
|
// Tracks). All management/maintenance/danger actions live under
|
|
// Settings > Local Library; the gear button routes there.
|
|
// One card in the Local Library Artists grid (name + "N albums · M tracks").
|
|
// Images + click-to-detail are a later slice.
|
|
export struct LocalArtistItem {
|
|
name: string, // raw/canonical key (used for selection + matching)
|
|
display-name: string, // canonical display name (after name-merge)
|
|
album-count: int,
|
|
track-count: int,
|
|
image-path: string, // local file path OR cached Qobuz URL ("" = none)
|
|
image: image, // decoded portrait (default-empty = show placeholder)
|
|
}
|
|
|
|
// One A-Z section of the local Artists left rail (master list grouped by the
|
|
// display name's first letter, "#" bucket for non-alphabetic). 1:1 with the
|
|
// Tauri Artists tab, which groups the compact rows under sticky letters.
|
|
export struct LocalArtistSection {
|
|
letter: string,
|
|
artists: [LocalArtistItem],
|
|
}
|
|
|
|
// One visible row in the Folders tree (flattened-model lazy tree — Slint has
|
|
// no native TreeView). Folders expand to fetch children lazily.
|
|
export struct FolderNode {
|
|
path: string, // absolute filesystem path (the node id)
|
|
segment: string, // last path component (display)
|
|
depth: int, // indent level
|
|
is-folder: bool, // folder vs. track row
|
|
expanded: bool, // folder open?
|
|
can-expand: bool, // has children (track_count_under > 0)
|
|
track-count: int, // recursive track count under a folder
|
|
// 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 play-track(string /* track id */);
|
|
callback select-version(int);
|
|
}
|
|
|
|
// ============================ Local Library management ====================
|
|
// The folder list + scan + maintenance + danger zone live under
|
|
// Settings > Local Library (NOT the browse view). One row per registered
|
|
// library folder; `Option<T>` maps to a "" / 0 sentinel + a Rust-computed
|
|
// label where the raw value is ambiguous.
|
|
|
|
// One registered library folder. Mirrors `qbz_library::LibraryFolder`
|
|
// (8 DB columns) + a UI-owned `selected` checkbox + Rust-computed labels.
|
|
export struct LibraryFolderItem {
|
|
id: int, // i64 folder id (small)
|
|
path: string, // absolute path
|
|
alias: string, // "" when none
|
|
display-name: string, // alias if non-empty else path (Rust-computed)
|
|
enabled: bool,
|
|
is-network: bool,
|
|
network-fs-type: string, // "" when none (e.g. "nfs","cifs")
|
|
user-override-network: bool,
|
|
last-scan: int, // unix seconds; 0 = never
|
|
last-scan-label: string, // "Never" / localized date (Rust-computed)
|
|
accessible: bool, // from the accessibility check; true until resolved
|
|
selected: bool, // UI multi-select state
|
|
}
|
|
|
|
// The folder list + maintenance + danger-zone state (Settings > Local Library).
|
|
export global LibraryFoldersState {
|
|
in property <[LibraryFolderItem]> folders: [];
|
|
in property <bool> loading: false;
|
|
in-out property <string> filter: ""; // folder search (alias??path substring)
|
|
in property <int> selected-count: 0; // drives Edit(==1) / Remove(>0) enablement
|
|
in property <bool> cleaning-missing: false;
|
|
in property <string> cleanup-status: ""; // "Removed N of M" / "" (auto-clears, Rust-side)
|
|
in property <bool> clearing-library: false;
|
|
}
|
|
|
|
// Folder-settings modal state (separate from the playlist FolderEditState).
|
|
export global LibFolderEditState {
|
|
in-out property <bool> open: false;
|
|
in property <int> folder-id: 0;
|
|
in-out property <string> alias: "";
|
|
in property <string> path: ""; // current path (updated after change-path)
|
|
in-out property <bool> enabled: true;
|
|
in-out property <bool> is-network: false;
|
|
in-out property <bool> user-override-network: false;
|
|
// QbzSelect index: 0 auto,1 cifs,2 nfs,3 sshfs,4 rclone,5 webdav,6 glusterfs,7 ceph,8 other.
|
|
in-out property <int> fs-type-index: 0;
|
|
in property <bool> accessible: true;
|
|
in property <bool> checking-accessible: false;
|
|
in property <string> last-scan-label: ""; // "Never" / date
|
|
}
|
|
|
|
// Library scan progress — a single global poll target (scan is not per-row,
|
|
// unlike the offline cache). `progress` is computed Rust-side (total==0 guard).
|
|
export global LibraryScanState {
|
|
in property <bool> scanning: false; // status == Scanning
|
|
in property <int> scan-status: 0; // 0 Idle,1 Scanning,2 Complete,3 Cancelled,4 Error
|
|
in property <int> total-files: 0;
|
|
in property <int> processed-files: 0;
|
|
in property <float> progress: 0.0; // 0..1
|
|
in property <string> current-file: ""; // basename, or the cleanup label
|
|
in property <int> error-count: 0;
|
|
}
|
|
|
|
export global LibraryManageActions {
|
|
callback load(); // (re)load the folder list (panel init)
|
|
callback add-folder(); // native dir picker (Rust)
|
|
callback remove-folders(); // bulk-remove selected (confirm)
|
|
callback toggle-folder-select(int /* id */);
|
|
callback edit-folder(int /* id */); // open the settings modal
|
|
callback save-folder-settings(int /* id */, string /* alias */, bool /* enabled */,
|
|
bool /* is-network */, string /* fs-type */, bool /* user-override */);
|
|
callback change-folder-path(int /* id */); // picker -> update path
|
|
callback scan-all();
|
|
callback scan-folder(int /* id */);
|
|
callback stop-scan();
|
|
callback cleanup-missing();
|
|
callback clear-library(); // two-step confirm
|
|
callback set-filter(string /* query */);
|
|
}
|
|
|
|
// ============================ 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 "")
|
|
cover-count: int, // number of cover images, 0-4 (playlists)
|
|
url1: string,
|
|
url2: string,
|
|
url3: string,
|
|
url4: string,
|
|
cover1: image,
|
|
cover2: image,
|
|
cover3: image,
|
|
cover4: image,
|
|
}
|
|
|
|
// Flyout shown when a folder is clicked while the sidebar is collapsed
|
|
// (mini): a popup to the right listing that folder's playlists. The list is
|
|
// populated from Rust because collapsed folders' children are NOT present in
|
|
// the flat `SidebarState.entries`.
|
|
export global SidebarFolderPopupState {
|
|
in-out property <bool> open: false;
|
|
in-out property <string> folder-id;
|
|
in-out property <string> folder-name;
|
|
in-out property <int> count: 0; // folder's playlist count (caps the height)
|
|
in-out property <length> anchor-x; // window x (sidebar right edge)
|
|
in-out property <length> anchor-y; // window y (folder row top)
|
|
in property <[SidebarEntry]> playlists: [];
|
|
}
|
|
|
|
// Collapsed (mini) sidebar hover tooltip — the row shows only an icon, so a
|
|
// hover bubble surfaces the name (folders also show their playlist count).
|
|
// Mirrors Tauri's native `title` on the collapsed folder/playlist buttons.
|
|
// Rendered at the AppShell level because the sidebar clips its overflow; the
|
|
// anchor is the row's true window position (the collapsed rows live inside a
|
|
// Flickable, so SidebarRow corrects the absolute-position double-count).
|
|
export global SidebarTooltipState {
|
|
in-out property <bool> open: false;
|
|
in-out property <string> id; // owning row id (race-safe leave)
|
|
in-out property <string> text; // "Folder (N)" or "Playlist name"
|
|
in-out property <length> anchor-x; // window x (sidebar right edge)
|
|
in-out property <length> anchor-y; // window y (row top)
|
|
}
|
|
|
|
// Closed-sidebar playlists flyout. When the sidebar is fully closed there is no
|
|
// playlist list on screen; a header button opens this popup over SidebarState's
|
|
// entries (folders + playlists), capped at six visible rows with the rest
|
|
// scrolling, plus an always-visible footer link to the Playlist Manager.
|
|
export global SidebarPlaylistsPopupState {
|
|
in-out property <bool> open: false;
|
|
in-out property <length> anchor-x; // window x (header button left)
|
|
in-out property <length> anchor-y; // window y (below the button)
|
|
}
|
|
|
|
// The sidebar's playlists + folders. Populated by crate::sidebar on
|
|
// shell entry and after create/delete/move.
|
|
export global SidebarState {
|
|
in property <[SidebarEntry]> entries: [];
|
|
// Flat folder list, for the move-to-folder menu.
|
|
in property <[SidebarPlaylistItem]> folders: [];
|
|
// Folder list filtered by the move-to-folder search box. Equals
|
|
// `folders` when the query is empty; a case-insensitive substring
|
|
// subset otherwise. Filled by the Rust side (Slint strings have no
|
|
// `contains`, so the substring filter can't live in markup). The
|
|
// context menu iterates this; the current-folder exclusion stays in
|
|
// markup via the per-row `folder-id`.
|
|
in property <[SidebarPlaylistItem]> menu-folders: [];
|
|
in property <bool> loading: false;
|
|
// The currently-open playlist id, for the active-row highlight.
|
|
in property <string> active-id;
|
|
// PLAYLISTS section collapse (the folder+playlist list hides). Mirrors
|
|
// Tauri's `playlistsCollapsed`.
|
|
in-out property <bool> playlists-collapsed: false;
|
|
// Inline playlist-name search in the header. When open the title is
|
|
// hidden and the input takes the full header width.
|
|
in-out property <bool> search-open: false;
|
|
in-out property <string> search-query;
|
|
// Active sort option: "name" | "recent" | "tracks" | "playcount"
|
|
// | "custom" (mirrors Tauri's SortOption). Persisted by crate::sidebar.
|
|
in-out property <string> sort-option: "name";
|
|
// Micro-collage of track covers on playlist rows. Opt-out (ON by
|
|
// default) to match Tauri's `sidebarPlaylistCollage`. The Settings >
|
|
// Appearance toggle wires into this in a later step.
|
|
in-out property <bool> playlist-collage: true;
|
|
}
|
|
|
|
export global SidebarActions {
|
|
callback open-playlist(string /* id */);
|
|
// Populate SidebarFolderPopupState.playlists for the collapsed-sidebar
|
|
// folder flyout (the folder's children aren't in the flat entries).
|
|
callback load-folder-popup(string /* folder id */);
|
|
callback create-playlist();
|
|
callback create-folder();
|
|
callback toggle-folder(string /* folder id */);
|
|
callback delete-folder(string /* folder id */);
|
|
// Move a playlist into a folder ("" folder id = root).
|
|
callback move-playlist(string /* playlist id */, string /* folder id */);
|
|
// Right-click context menu actions (folder + playlist rows).
|
|
callback edit-playlist(string /* id */);
|
|
callback edit-folder(string /* id */);
|
|
callback hide-playlist(string /* id */);
|
|
callback hide-folder(string /* id */);
|
|
// Filter `menu-folders` by a case-insensitive substring of the
|
|
// move-to-folder search box (Rust does the substring match).
|
|
callback search-folders(string /* query */);
|
|
// Pick a sort option (re-sorts the list). Same five values as Tauri.
|
|
callback set-sort(string /* option */);
|
|
// Re-run the playlist-name filter (called as the search input edits).
|
|
callback search-changed(string /* query */);
|
|
// Re-fetch the playlist list from the network.
|
|
callback refresh-playlists();
|
|
// Open the full Playlist Manager view.
|
|
callback manage-playlists();
|
|
}
|
|
|
|
// "Create playlist" modal state. Mirrors Tauri's PlaylistModal (create
|
|
// mode): name, optional description, target folder, public flag.
|
|
export global CreatePlaylistState {
|
|
in-out property <bool> open: false;
|
|
in-out property <string> name;
|
|
in-out property <string> description;
|
|
in-out property <bool> is-public: false;
|
|
in property <bool> creating: false;
|
|
// Folder dropdown options — index 0 is always "No folder", the rest
|
|
// are the user's folder names (parallel to `folder-ids`). Built by
|
|
// the create-playlist handler from the sidebar folder list.
|
|
in property <[string]> folder-options: ["No folder"];
|
|
// Folder ids parallel to `folder-options` ("" for "No folder").
|
|
in property <[string]> folder-ids: [""];
|
|
// Selected index into `folder-options` (0 = No folder).
|
|
in-out property <int> folder-index: 0;
|
|
}
|
|
|
|
export global CreatePlaylistActions {
|
|
callback submit();
|
|
callback close();
|
|
}
|
|
|
|
// "Create folder" modal state.
|
|
export global CreateFolderState {
|
|
in-out property <bool> open: false;
|
|
in-out property <string> name;
|
|
in property <bool> creating: false;
|
|
}
|
|
|
|
export global CreateFolderActions {
|
|
callback submit();
|
|
callback close();
|
|
}
|
|
|
|
// 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;
|
|
// Track edit mode — rows show a selection checkbox and a bulk bar
|
|
// appears (remove selected). Only available to the owner.
|
|
in-out property <bool> multi-select-mode: false;
|
|
in property <int> selected-count: 0;
|
|
in property <[TrackItem]> tracks: [];
|
|
in property <bool> loading: false;
|
|
// Sort: "" / "default" = playlist order; else title|artist|album|
|
|
// duration. `sort-asc` flips on re-selecting the active field.
|
|
in property <string> sort-field: "default";
|
|
in property <bool> sort-asc: true;
|
|
}
|
|
|
|
export global PlaylistActions {
|
|
// Client-side filter of the visible track list by query.
|
|
callback search(string);
|
|
// Sort by a field (re-selecting the active field toggles asc/desc).
|
|
callback set-sort(string);
|
|
}
|
|
|
|
// "Edit playlist" modal state (rename + description + delete).
|
|
export global EditPlaylistState {
|
|
in-out property <bool> open: false;
|
|
in property <string> id;
|
|
in-out property <string> name;
|
|
in-out property <string> description;
|
|
in property <bool> busy: false;
|
|
}
|
|
|
|
export global EditPlaylistActions {
|
|
callback save();
|
|
callback delete();
|
|
callback close();
|
|
}
|
|
|
|
// === Playlist Manager ===================================================
|
|
// A full playlist + folder organization surface (Tauri's
|
|
// PlaylistManagerView). Rows are precomputed in Rust (crate::playlist_manager)
|
|
// and pushed as ready-to-render models — no per-row map lookups in .slint.
|
|
|
|
// One folder card / chip / tree-folder. `icon-color` is a solid hex (or
|
|
// "" → use the accent). Gradient folder colors are not supported here
|
|
// (Slint brush limitation); a gradient string falls back to the accent.
|
|
export struct PmFolderItem {
|
|
id: string,
|
|
name: string,
|
|
count: int, // playlists in this folder
|
|
// "preset" or "custom".
|
|
icon-type: string,
|
|
// Lucide preset name ("heart" | "star" | "music" | "folder" | "disc"
|
|
// | "library" | "headphones"); drives which glyph the card shows.
|
|
icon-preset: string,
|
|
// Resolved solid color for the icon square (parsed from the stored
|
|
// hex by the Rust controller). When `has-color` is false the card
|
|
// uses the accent instead (gradient / empty / unparseable colors
|
|
// fall back to accent — Slint has no gradient-brush-in-struct).
|
|
icon-color: color,
|
|
has-color: bool,
|
|
is-hidden: bool,
|
|
// Decoded custom image (icon-type == "custom"); empty otherwise.
|
|
custom-image: image,
|
|
has-custom-image: bool,
|
|
}
|
|
|
|
// One playlist card / list-row / tree-row. All display strings are
|
|
// precomputed (track line, local line) so the view never does lookups.
|
|
export struct PmPlaylistItem {
|
|
id: string, // playlist id (may be negative for offline pending)
|
|
name: string,
|
|
// "12 tracks" — total (remote + local).
|
|
tracks-line: string,
|
|
// "1h 43m" — total playtime, "" when zero (list-row meta only).
|
|
duration-line: string,
|
|
// "(3 local)" — local-only suffix, "" when none.
|
|
local-line: string,
|
|
local-count: int,
|
|
total-count: int,
|
|
play-count: int,
|
|
// "" | "all_local" | "some_local" | "no" — availability badge.
|
|
local-status: string,
|
|
is-favorite: bool,
|
|
is-hidden: bool,
|
|
folder-id: string, // "" = root
|
|
// Micro-collage covers (same 0-4 scheme as the sidebar).
|
|
cover-count: int,
|
|
url1: string,
|
|
url2: string,
|
|
url3: string,
|
|
url4: string,
|
|
cover1: image,
|
|
cover2: image,
|
|
cover3: image,
|
|
cover4: image,
|
|
}
|
|
|
|
// One flattened row of the tree view — a folder header or a nested /
|
|
// root playlist. The Rust side emits only currently-visible rows
|
|
// (collapsed folders hide their children).
|
|
export struct PmTreeRow {
|
|
kind: string, // "folder" | "playlist"
|
|
expanded: bool, // folders
|
|
folder: PmFolderItem, // populated when kind == "folder"
|
|
playlist: PmPlaylistItem, // populated when kind == "playlist"
|
|
indent: bool, // playlist nested under a folder
|
|
}
|
|
|
|
export global PlaylistManagerState {
|
|
in property <[PmFolderItem]> folders: [];
|
|
// Filtered folder list for the list-row move-to-folder menu. Starts as
|
|
// the full folder set; the menu's search box narrows it via
|
|
// `search-folders` (Slint strings have no `contains`, so Rust filters).
|
|
in property <[PmFolderItem]> menu-folders: [];
|
|
in property <[PmPlaylistItem]> playlists: []; // the filtered+sorted grid/list set
|
|
in property <[PmTreeRow]> tree: [];
|
|
// Header counters: total folders / total playlists (post-filter for
|
|
// the visible playlist count).
|
|
in property <int> folder-count: 0;
|
|
in property <int> playlist-count: 0;
|
|
in property <bool> loading: false;
|
|
// Toolbar state (session-scoped; persisted per-user where cheap).
|
|
in-out property <string> search-query;
|
|
// "all" | "visible" | "hidden".
|
|
in-out property <string> filter: "all";
|
|
// "name" | "recent" | "playcount" | "tracks" | "custom".
|
|
in-out property <string> sort: "name";
|
|
// "grid" | "list" | "tree".
|
|
in-out property <string> view-mode: "grid";
|
|
// Folder mode on (folders shown + folder filtering) vs flat.
|
|
in-out property <bool> folder-mode: true;
|
|
// Folders section collapse.
|
|
in-out property <bool> folders-collapsed: false;
|
|
// Reorder is only allowed when sort == "custom".
|
|
in property <bool> can-reorder: false;
|
|
}
|
|
|
|
export global PlaylistManagerActions {
|
|
// Open a playlist (-> PlaylistView).
|
|
callback open-playlist(string /* id */);
|
|
// Toolbar.
|
|
callback search-changed(string /* query */);
|
|
callback set-filter(string);
|
|
callback set-sort(string);
|
|
callback set-view-mode(string);
|
|
// Filter the list-row move-to-folder menu's folder list (substring,
|
|
// case-insensitive). Empty query restores the full list.
|
|
callback search-folders(string /* query */);
|
|
callback toggle-folder-mode();
|
|
callback toggle-folders-collapsed();
|
|
// Tree-view folder expand/collapse.
|
|
callback toggle-tree-folder(string /* folder id */);
|
|
// Folder CRUD — "new folder" opens the editor in create mode; edit
|
|
// opens it for an existing folder.
|
|
callback new-folder();
|
|
callback edit-folder(string /* folder id */);
|
|
// Per-card playlist actions.
|
|
callback toggle-favorite(string /* playlist id */);
|
|
callback toggle-hidden(string /* playlist id */);
|
|
callback edit-playlist(string /* playlist id */);
|
|
callback add-to-mixtape(string /* playlist id */);
|
|
// Arrow reorder (custom sort only): move a playlist up / down within
|
|
// the current visible order, persisting the new order.
|
|
callback move-up(string /* playlist id */);
|
|
callback move-down(string /* playlist id */);
|
|
// Move a playlist into a folder ("" = root). Used by the tree's
|
|
// folder rows and the future DnD path.
|
|
callback move-to-folder(string /* playlist id */, string /* folder id */);
|
|
}
|
|
|
|
// === Offline Cache Manager =============================================
|
|
// The standalone manage-downloads surface (Tauri's OfflineCacheManagerView).
|
|
// Rows are precomputed in Rust (crate::offline_manager): an album-header row
|
|
// followed by its track rows, flattened (mirrors PmTreeRow).
|
|
export struct OfflineRow {
|
|
kind: string, // "album" | "track"
|
|
album-id: string, // album-header: the album id; track: its album id
|
|
track-id: string, // track rows
|
|
title: string, // album title (album) | track title (track)
|
|
subtitle: string, // artist
|
|
meta: string, // "8 tracks · 320 MB" (album) | "3:45" (track)
|
|
// 0 none | 2 downloading | 3 ready | 4 failed. Album = worst across tracks.
|
|
status: int,
|
|
progress: float, // track downloading 0..1
|
|
cover: image, // album-header cover (loaded from the on-disk cover.jpg)
|
|
number: string, // track position within its album ("" on album rows)
|
|
selected: bool, // multi-select checkbox
|
|
}
|
|
|
|
// One entry in the manager's left A-Z artist rail.
|
|
export struct OfflineArtist {
|
|
name: string,
|
|
meta: string, // "3 albums · 21 tracks"
|
|
selected: bool,
|
|
}
|
|
|
|
export global OfflineManagerState {
|
|
in property <[OfflineRow]> rows: [];
|
|
in property <[OfflineArtist]> artists: [];
|
|
in property <bool> loading: false;
|
|
// Header stats.
|
|
in property <int> tracks-count: 0;
|
|
in property <string> size-text: ""; // "1.2 GB"
|
|
in property <string> limit-text: ""; // "of 5 GB" | "Unlimited"
|
|
in property <float> usage: 0.0; // 0..1 for the bar (0 = unlimited)
|
|
in-out property <int> limit-gb: 5; // edit-limit input
|
|
// Toolbar — set by the controller from the Rust-side filter state so the
|
|
// dropdown selection / toggle / rail highlight reflect the current view.
|
|
in property <string> selected-artist: ""; // "" = all artists
|
|
in property <int> sort-index: 0; // 0 alpha / 1 recent / 2 largest / 3 smallest
|
|
in property <bool> show-only-failed: false;
|
|
// Multi-select: how many TRACK rows are checked (drives the bulk bar).
|
|
in property <int> selected-count: 0;
|
|
}
|
|
|
|
export global OfflineManagerActions {
|
|
// Navigate to the manager (from the Settings "Offline Playback" entry).
|
|
callback open();
|
|
callback refresh();
|
|
// Toolbar.
|
|
callback select-artist(string /* name, "" = all */);
|
|
callback set-sort(int /* index */);
|
|
callback toggle-failed();
|
|
// Multi-select.
|
|
callback toggle-select(string /* track id */);
|
|
callback select-all();
|
|
callback clear-selection();
|
|
callback bulk-redownload();
|
|
callback bulk-remove();
|
|
callback remove-track(string /* track id */);
|
|
callback remove-album(string /* album id */);
|
|
callback redownload-track(string /* track id */);
|
|
callback redownload-album(string /* album id */);
|
|
callback redownload-failed(string /* album id */);
|
|
callback set-limit(int /* gb */);
|
|
callback clear-all();
|
|
callback open-folder();
|
|
callback play-track(string /* track id */);
|
|
}
|
|
|
|
// Folder editor modal — create + edit (icon preset, solid color, custom
|
|
// image, hidden toggle, delete). Ports Tauri's FolderEditModal. Gradient
|
|
// color presets are intentionally omitted (Slint brush limitation).
|
|
export struct PmIconPreset {
|
|
id: string,
|
|
icon: image,
|
|
}
|
|
|
|
export struct PmColorSwatch {
|
|
// The stored hex value ("" = the "Accent" preset), used as the
|
|
// selection key and persisted into the folder record.
|
|
value: string,
|
|
// Resolved Slint color for the swatch fill (ignored when is-accent).
|
|
color: color,
|
|
is-accent: bool,
|
|
}
|
|
|
|
export global FolderEditState {
|
|
in-out property <bool> open: false;
|
|
// Empty id = create mode; non-empty = editing that folder.
|
|
in property <string> id: "";
|
|
in-out property <string> name;
|
|
in-out property <string> icon-preset: "folder";
|
|
// Selected solid color hex; "" = accent.
|
|
in-out property <string> icon-color: "";
|
|
in-out property <bool> is-hidden: false;
|
|
// Custom image path (icon-type == "custom" when set) + decoded image.
|
|
in-out property <string> custom-image-path: "";
|
|
in property <image> custom-image;
|
|
in property <bool> busy: false;
|
|
// The icon-preset grid + the solid-color swatch grid, built once by
|
|
// the Rust controller.
|
|
in property <[PmIconPreset]> icon-presets: [];
|
|
in property <[PmColorSwatch]> color-swatches: [];
|
|
}
|
|
|
|
export global FolderEditActions {
|
|
callback select-preset(string /* preset id */);
|
|
callback select-color(string /* color hex, "" = accent */);
|
|
callback pick-image();
|
|
callback clear-image();
|
|
callback toggle-hidden();
|
|
callback save();
|
|
callback delete();
|
|
callback close();
|
|
}
|
|
|
|
// Settings screen state. Audio and Playback preferences are loaded from
|
|
// the persistence stores by the Rust settings controller and pushed here;
|
|
// changes flow back through the settings-bool / settings-select /
|
|
// settings-slider callbacks.
|
|
export global SettingsState {
|
|
// Sub-section: 0 = Audio, 1 = Playback.
|
|
in-out property <int> section: 0;
|
|
in property <bool> loading: true;
|
|
|
|
// --- AUDIO -------------------------------------------------------------
|
|
|
|
// Audio — dropdowns. Each list is paired with a selected index; the
|
|
// Rust controller owns the index -> value mapping.
|
|
in-out property <[string]> streaming-qualities: [];
|
|
in-out property <int> streaming-quality-index: 0;
|
|
in-out property <[string]> sample-rates: [];
|
|
in-out property <int> sample-rate-index: 0;
|
|
in-out property <[string]> backends: [];
|
|
in-out property <int> backend-index: 0;
|
|
in-out property <[string]> devices: [];
|
|
// Parallel to `devices`: true marks a bit-perfect-capable device.
|
|
in-out property <[bool]> device-bp: [];
|
|
// Parallel to `devices`: a non-empty entry is a section-header label
|
|
// shown above that device row (empty = no header).
|
|
in-out property <[string]> device-groups: [];
|
|
in-out property <int> device-index: 0;
|
|
in-out property <[string]> alsa-plugins: [];
|
|
in-out property <int> alsa-plugin-index: 0;
|
|
|
|
// Audio — toggles.
|
|
in-out property <bool> limit-quality-to-device: false;
|
|
in-out property <bool> alsa-hardware-volume: false;
|
|
in-out property <bool> exclusive-mode: false;
|
|
in-out property <bool> reserve-dac: false;
|
|
in-out property <bool> dac-passthrough: false;
|
|
in-out property <bool> pw-force-bitperfect: false;
|
|
in-out property <bool> allow-quality-fallback: false;
|
|
in-out property <bool> sync-audio-on-startup: false;
|
|
in-out property <bool> skip-sink-switch: false;
|
|
|
|
// Audio — Rust-computed conditional flags driving `if`-gated rows.
|
|
in-out property <bool> backend-is-alsa: false;
|
|
in-out property <bool> backend-is-pipewire: false;
|
|
in-out property <bool> alsa-plugin-is-hw: false;
|
|
|
|
// --- PLAYBACK ----------------------------------------------------------
|
|
|
|
// Playback — toggles. `continue-playback` maps to the autoplay mode
|
|
// (on = ContinueWithinSource, off = PlayTrackOnly).
|
|
in-out property <bool> continue-playback: true;
|
|
in-out property <bool> show-context-icon: true;
|
|
in-out property <bool> persist-session: false;
|
|
in-out property <bool> resume-position: false;
|
|
in-out property <bool> gapless: true;
|
|
in-out property <bool> stream-uncached: false;
|
|
in-out property <bool> streaming-only: false;
|
|
// Volume normalization (loudness leveling). Applied in the shared player
|
|
// and bypassed automatically when bit-perfect is active. Surfaced in the
|
|
// now-playing bar's normalization group as well as Settings > Playback.
|
|
in-out property <bool> normalization: false;
|
|
|
|
// Audio-output indicators for the now-playing song card (computed in Rust
|
|
// from the real backend + bit-perfect constraints). `*-label` is the fixed
|
|
// short text; `*-active` colours the led when a deliberate / bit-perfect
|
|
// mode is engaged (vs the shared system default).
|
|
in-out property <string> output-backend-label: "SYST"; // PIPE|ALSA|JACK|PULS|SYST
|
|
in-out property <string> output-mode-label: "DEFAULT"; // DACPASS|BITPERF|EXCL|DIRECT|LOCKED|ROUTED|SHARED|DEFAULT
|
|
in-out property <bool> output-backend-active: false;
|
|
in-out property <bool> output-mode-active: false;
|
|
|
|
// Playback — Initial Buffer Size slider (seconds, 1-10).
|
|
in-out property <int> buffer-seconds: 3;
|
|
|
|
// Playback — "When quality retries fail" dropdown.
|
|
in-out property <[string]> retry-behaviors: [];
|
|
in-out property <int> retry-behavior-index: 0;
|
|
}
|
|
|
|
// Appearance settings — backs the Settings > Appearance panel. 1:1 with
|
|
// Tauri's appearance section. This is a visual/structural replica: every
|
|
// row is bound to a property here, but the Rust side does NOT yet persist
|
|
// or apply most of them (wiring is a later step). The few that map onto an
|
|
// existing, already-functional flag are wired through (see notes inline).
|
|
//
|
|
// Dropdowns follow the same pattern as SettingsState: an option list paired
|
|
// with a selected index; the Rust controller (when wired) owns the
|
|
// index -> value mapping. Lists are populated with sensible defaults below
|
|
// so the UI renders standalone even while unwired.
|
|
export global AppearanceState {
|
|
// --- THEME -------------------------------------------------------------
|
|
// Only the themes that are actually ported/wired. Order is load-bearing:
|
|
// the index maps 1:1 to ThemeState.mode (0 Dark, 1 OLED, 2 Tokyo, 3 System).
|
|
in-out property <[string]> themes: [
|
|
"Dark", "OLED Black", "Tokyo Night", "System"
|
|
];
|
|
in-out property <int> theme-index: 0;
|
|
// Theme list filter cycle: 0 = All, 1 = Dark, 2 = Light. Mirrors the
|
|
// SunMoon/Moon/Sun toggle button next to the theme dropdown.
|
|
in-out property <int> theme-filter: 0;
|
|
|
|
in-out property <bool> album-header-gradient: true;
|
|
|
|
// Auto-Theme (shown when the "System" theme is selected). Source:
|
|
// 0 = System Colors, 1 = Wallpaper Sync, 2 = Custom Image.
|
|
in-out property <bool> theme-is-system: false;
|
|
in-out property <[string]> auto-theme-sources: ["System Colors", "Wallpaper Sync", "Custom Image"];
|
|
in-out property <int> auto-theme-source-index: 0;
|
|
in-out property <string> auto-theme-custom-path: "";
|
|
in-out property <bool> auto-theme-generating: false;
|
|
in-out property <string> auto-theme-detected-de: "";
|
|
|
|
// --- TYPOGRAPHY / SCALE / LANGUAGE -------------------------------------
|
|
in-out property <[string]> languages: ["Auto", "English", "Español", "Français", "Deutsch", "Português"];
|
|
in-out property <int> language-index: 0;
|
|
in-out property <[string]> fonts: ["LINE Seed JP", "Montserrat", "Noto Sans", "Source Sans 3", "System"];
|
|
in-out property <int> font-index: 0;
|
|
in-out property <[string]> ui-scales: ["80%", "85%", "90%", "95%", "100%", "110%", "125%", "150%", "175%"];
|
|
in-out property <int> ui-scale-index: 4;
|
|
|
|
// --- VISUAL EXTRAS -----------------------------------------------------
|
|
// sidebar-playlist-collage is wired to SidebarState.playlist-collage in
|
|
// the panel (the only already-functional flag here).
|
|
in-out property <bool> sidebar-playlist-collage: true;
|
|
in-out property <bool> local-library-track-artwork: false;
|
|
// Player volume +/- stepper buttons — opt-in, hidden by default.
|
|
in-out property <bool> show-volume-steppers: false;
|
|
|
|
// --- NOTIFICATIONS -----------------------------------------------------
|
|
in-out property <bool> in-app-toasts: true;
|
|
in-out property <bool> system-notifications: true;
|
|
|
|
// --- WINDOW TITLE ------------------------------------------------------
|
|
in-out property <bool> window-title-show: false;
|
|
in-out property <string> window-title-template: "";
|
|
|
|
// --- TITLE BAR / WINDOW CHROME -----------------------------------------
|
|
in-out property <bool> use-system-title-bar: false;
|
|
in-out property <bool> hide-title-bar: false;
|
|
in-out property <bool> match-system-chrome: false;
|
|
// True when GPU acceleration is on (gates match-system-chrome). Default
|
|
// true so the row is interactive in the unwired replica.
|
|
in-out property <bool> hardware-accel-enabled: true;
|
|
in-out property <bool> search-in-title-bar: false;
|
|
|
|
// Navigation-in-title-bar group: the parent label row plus per-target
|
|
// toggles (Discover/Home, Favorites, Library, My QBZ, Purchases).
|
|
in-out property <bool> nav-tb-discover: false;
|
|
in-out property <bool> nav-tb-favorites: false;
|
|
in-out property <bool> nav-tb-library: false;
|
|
in-out property <bool> nav-tb-myqbz: false;
|
|
in-out property <bool> nav-tb-purchases: false;
|
|
// Nav buttons position: 0 = Auto, 1 = Left, 2 = Right.
|
|
in-out property <[string]> nav-tb-positions: ["Auto (opposite to window controls)", "Left", "Right"];
|
|
in-out property <int> nav-tb-position-index: 0;
|
|
|
|
// Window controls group.
|
|
in-out property <[string]> wc-positions: ["Left", "Right"];
|
|
in-out property <int> wc-position-index: 1;
|
|
in-out property <[string]> wc-styles: ["Rectangular", "Full-height rounded", "Circular", "Square"];
|
|
in-out property <int> wc-style-index: 0;
|
|
in-out property <[string]> wc-sizes: ["Small", "Normal", "Large"];
|
|
in-out property <int> wc-size-index: 1;
|
|
in-out property <[string]> wc-color-presets: ["Default", "macOS", "Adwaita", "Monochrome", "Custom"];
|
|
in-out property <int> wc-color-preset-index: 0;
|
|
in-out property <bool> show-window-controls: true;
|
|
|
|
// --- MINIPLAYER --------------------------------------------------------
|
|
in-out property <[string]> miniplayer-views: ["Remember last used", "Micro", "Compact", "Artwork", "Queue", "Lyrics"];
|
|
in-out property <int> miniplayer-view-index: 0;
|
|
|
|
// --- STARTUP -----------------------------------------------------------
|
|
in-out property <[string]> startup-pages: ["Home", "Where you left off"];
|
|
in-out property <int> startup-page-index: 0;
|
|
|
|
in-out property <bool> show-purchases: false;
|
|
|
|
// --- SYSTEM TRAY -------------------------------------------------------
|
|
// `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 */);
|
|
}
|
|
|
|
// 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"
|
|
}
|
|
|
|
// "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);
|
|
}
|