New QbzSelect row (Auto (account language) + en es fr de it pt nl ja ru,
the owner-approved list) after the Size row; language names are native
literals, the same convention as the Appearance dropdown. The options
array lives on LyricsState (reseeded from Rust on language change) and
the choice persists per-user in lyrics_prefs.json as translation_language
("auto" or an ISO 639-1 code), sanitized on load (unknown -> auto) and
round-tripped through apply_to_ui/persist_from_ui like the other prefs.
Flyout popup height grows by the new row (and the comment now counts the
lite-fill row it had missed).
km
6041 lines
297 KiB
Plaintext
6041 lines
297 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 }
|
||
|
||
// Re-export the hotkey text-input guard from its leaf file so existing
|
||
// `import { UiFocusState } from "state.slint"` sites keep working, while
|
||
// low-level primitives import it directly from "focus-state.slint" (avoids a
|
||
// recursive import — state.slint imports JumpNavBar's JumpNavTab).
|
||
export { UiFocusState } from "focus-state.slint";
|
||
|
||
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
|
||
is-pinned: bool, // filled accent pin badge (Home/For You Pinned section)
|
||
removing: bool, // fading out (un-favorited in the favorites view)
|
||
selected: bool, // checkbox state in a multi-select grid (LocalLibrary)
|
||
// --- 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
|
||
plays: int, // play count — shown ONLY in the Most Played Albums
|
||
// View-all (0 elsewhere, so no count is drawn).
|
||
}
|
||
|
||
// 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 text — now the hover tooltip
|
||
icon: image, // the button glyph (the bar is icon-only)
|
||
danger: bool, // red-bordered destructive button (e.g. Remove)
|
||
needs-selection: bool, // dimmed + disabled while nothing is selected
|
||
}
|
||
|
||
// One titled discography bucket on the artist page (Albums, EPs & Singles,
|
||
// Live, …). Distinct from DiscoverSection so it can carry the raw server
|
||
// release_type key, the per-section sort, and the load-more flag without
|
||
// bleeding artist-only fields into Discover.
|
||
export struct ArtistReleaseSection {
|
||
release-type: string, // raw server key (album/epSingle/live/…)
|
||
title: string,
|
||
albums: [AlbumCardItem],
|
||
has-more: bool, // gates the per-section "Load more" affordance
|
||
sort-by: string, // default/newest/oldest/title-asc/title-desc
|
||
}
|
||
|
||
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,
|
||
// Pinned state — drives the ArtistGridCard pin badge (artist rows only;
|
||
// track/label/award slims are not pinnable and stay false).
|
||
is-pinned: bool,
|
||
}
|
||
|
||
// One editorial award on an album — the sidebar AWARDS list. Carries the
|
||
// award id (may be "" — some /album/get entries omit it; the controller
|
||
// then resolves it by name) so the laurel card can navigate to the award
|
||
// page. Mirrors Tauri's `AlbumAward { id?, name }`.
|
||
export struct AwardEntry {
|
||
id: string,
|
||
name: string,
|
||
}
|
||
|
||
// 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
|
||
is-pinned: bool, // filled accent pin badge (Home/For You Pinned section)
|
||
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,
|
||
// First tag name (uppercased accent subtag under the title; "" = none).
|
||
// Only the single-cover Discover playlist cards populate it.
|
||
category: string,
|
||
// Dominant colour of the single cover, filling the contain-fit letterbox
|
||
// on Discover playlist cards (default neutral dark; replaced once the
|
||
// cover decodes). Mosaic cards ignore it.
|
||
dominant-color: color,
|
||
// Ownership signals driving the card's overlay/menu affordances:
|
||
// is-owned — the user is the Qobuz owner → favorite (heart) only.
|
||
// is-following — the user follows this NON-owned Qobuz playlist → the
|
||
// overlay shows "unfollow" (check) instead of "follow".
|
||
// is-copied — already copied into the user's own playlists → the
|
||
// context-menu "Copy to your library" item is hidden.
|
||
// Editorial/foreign playlists default is-owned=false (follow + copy).
|
||
is-owned: bool,
|
||
is-following: bool,
|
||
is-copied: bool,
|
||
}
|
||
|
||
// One entry in the mixed Pinned carousel (Home / For You). `kind` selects
|
||
// which sub-struct is populated; the other two stay at their defaults
|
||
// (Slint has no unions). Declared after AlbumCardItem / SlimItem /
|
||
// SearchPlaylistItem — Slint structs must be declared before first use.
|
||
export struct PinnedItem {
|
||
kind: string, // "album" | "artist" | "playlist"
|
||
album: AlbumCardItem, // kind == "album"
|
||
artist: SlimItem, // kind == "artist"
|
||
playlist: SearchPlaylistItem, // kind == "playlist"
|
||
}
|
||
|
||
// The user's pinned items, newest pin first. ONE shared global feeds both
|
||
// the Home and For You "pinned" section arms (a single per-user list,
|
||
// mutated only by pin/unpin — no per-tab fetch lifecycle). Rust rebuilds
|
||
// it from the pinned store at session seed and after every toggle.
|
||
export global PinnedState {
|
||
in property <[PinnedItem]> items: [];
|
||
}
|
||
|
||
// Pin/unpin from the card pin glyph. The callback carries the full display
|
||
// snapshot (mirroring BlacklistActions.block-album) so Rust can persist a
|
||
// denormalized row without re-fetching: kind, id, title, subtitle,
|
||
// artwork-url. Wired in main.rs to the pinned store.
|
||
export global PinnedActions {
|
||
callback toggle-pin(/* kind */ string, /* id */ string, /* title */ string,
|
||
/* subtitle */ string, /* artwork-url */ string);
|
||
}
|
||
|
||
// One selectable category tag for the Qobuz Playlists multi-select filter.
|
||
// `name` is pre-localized by the discover-index response.
|
||
export struct PlaylistTagItem {
|
||
slug: string,
|
||
name: string,
|
||
selected: bool,
|
||
}
|
||
|
||
// One row in the search cortinilla (the live as-you-type dropdown under the
|
||
// header search box). Frontend-agnostic: a flat row carrying everything the
|
||
// SlimCard-style render needs. `source` is "qobuz" or "local" — it selects
|
||
// which controller seam fires on click (Qobuz media-action vs local play).
|
||
// `kind` is the navigable category ("album" | "artist" | "track" |
|
||
// "playlist" | "localTrack"). Declared before HomeState so the cortinilla
|
||
// overlay (a separate component file) can import it from state.slint.
|
||
export struct CortinillaRow {
|
||
kind: string, // "album" | "artist" | "track" | "playlist" | "localTrack"
|
||
id: string,
|
||
source: string, // "qobuz" | "local"
|
||
title: string,
|
||
subtitle: string,
|
||
artwork-url: string, // raw URL/fs path — drives the controller artwork job
|
||
artwork: image, // empty until the artwork pipeline resolves it
|
||
// Stable flat selection index across ALL cortinilla rows: 0 = top-result,
|
||
// then 1.. enumerated section by section (declaration order). Set by the
|
||
// controller so the overlay needs no cross-loop accumulator (Slint has
|
||
// none). Used for click dispatch AND keyboard/hover highlight.
|
||
flat-index: int,
|
||
}
|
||
|
||
// One labelled section in the cortinilla (e.g. "Albums", "Tracks",
|
||
// "From your library"). `kind` is the category these rows belong to (drives
|
||
// `cortinilla-view-more`). `has-more` toggles the trailing "View more" row.
|
||
export struct CortinillaSection {
|
||
title: string,
|
||
kind: string,
|
||
rows: [CortinillaRow],
|
||
has-more: bool,
|
||
}
|
||
|
||
// Discover / Home screen state.
|
||
export global HomeState {
|
||
in property <[DiscoverSection]> sections: [];
|
||
in property <[SlimItem]> popular: [];
|
||
in property <[SlimItem]> recent: [];
|
||
in property <[AlbumCardItem]> recent-albums: [];
|
||
// The #566 ported rails — same data pipelines as their ForYouState twins
|
||
// but separate lifecycles (Home loads them eagerly with the discover
|
||
// index; For You lazily on first open). The HomeView arms self-hide
|
||
// while empty. Titles: favorite-albums / release-watch are set by Rust;
|
||
// top-artists' title is @tr in the view arm (bare list, like For You).
|
||
// (qobuzMixes, the fourth ported section, is static tiles — no state.)
|
||
in property <DiscoverSection> favorite-albums;
|
||
// Library Albums (favoriteAlbums) sort order: 0 = Recently added
|
||
// (default), 1 = First added (reverse), 2 = Random (shuffled). Rust
|
||
// reorders HomeState.favorite-albums in place when this changes.
|
||
in-out property <int> library-albums-sort: 0;
|
||
in property <DiscoverSection> release-watch;
|
||
// Most Played Albums rail — top albums by local play count (title set by
|
||
// Rust from `album_play_history::top_albums`). Self-hides while empty.
|
||
in property <DiscoverSection> most-played-albums;
|
||
in property <[SlimItem]> top-artists: [];
|
||
// Qobuz Playlists row (Home + Editor's Picks). Single-cover cards; the
|
||
// active tab's set is swapped in by crate::home::select_tab.
|
||
in property <[SearchPlaylistItem]> playlists: [];
|
||
// Category tags for the Qobuz Playlists multi-select filter (from the
|
||
// discover-index `playlists_tags`). Empty when the response carries none.
|
||
in property <[PlaylistTagItem]> playlist-tags: [];
|
||
// Number of currently-selected category tags (0 = show all).
|
||
in property <int> playlist-tag-count: 0;
|
||
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 + the Qobuz Playlists category filter. Wired in
|
||
// main.rs to crate::home.
|
||
export global HomeActions {
|
||
callback select-tab(string);
|
||
// Toggle one category tag (by slug) in/out of the playlists filter —
|
||
// client-side, re-filters the cached row. Empty selection = show all.
|
||
callback toggle-playlist-tag(string);
|
||
// Clear all selected category tags (back to showing every playlist).
|
||
callback clear-playlist-tags();
|
||
// "View all" on the Recently Played Albums rail -> the full-page listing
|
||
// (RecentAlbumsView), fed from the local play-history store.
|
||
callback open-recent-albums();
|
||
// "View all" on the Qobuz Playlists rail -> the full-page playlist
|
||
// browse (PlaylistBrowseView); a fresh open resets its tag to All.
|
||
callback open-playlist-browse();
|
||
// Opens the full "Most Played Albums" page (MostPlayedAlbumsView), fed
|
||
// from the local play-count store.
|
||
callback open-most-played-albums();
|
||
// Fired by HomeView's init on every (re)mount. Rust re-reads the LOCAL
|
||
// recently-played store into the two rails IF a play was recorded while
|
||
// Home was off-screen (dirty-flag gated in Rust — a no-op otherwise, so
|
||
// mounting Home stays free).
|
||
callback home-mounted();
|
||
// Manual refresh (the row-header button next to the Recently Played
|
||
// Albums nav chevrons): unconditionally re-read the local rails.
|
||
callback refresh-recent();
|
||
// Library Albums sort changed (see HomeState.library-albums-sort). Rust
|
||
// reorders the cached base list and re-pushes favorite-albums + artwork.
|
||
callback set-library-albums-sort(int);
|
||
}
|
||
|
||
// RecentAlbumsView — the full "Recently Played Albums" page opened from the
|
||
// Home rail's "View all". Fed from the LOCAL play-history store (capped at
|
||
// 24, see crate::recently), so it needs no paging/search state; the loader
|
||
// repopulates `albums` on every navigation.
|
||
export global RecentAlbumsState {
|
||
in property <[AlbumCardItem]> albums: [];
|
||
in property <bool> loading: false;
|
||
}
|
||
|
||
// MostPlayedAlbumsView — the full "Most Played Albums" page opened from the
|
||
// rail's "View all". Ranked by local play count; each item carries its
|
||
// `plays`. `search` filters title/artist client-side (visibility only).
|
||
export global MostPlayedAlbumsState {
|
||
in property <[AlbumCardItem]> albums: [];
|
||
in property <bool> loading: false;
|
||
in-out property <string> search: "";
|
||
}
|
||
|
||
export global MostPlayedAlbumsActions {
|
||
// Filter the loaded page by title/artist. Rust re-sets `albums` to the
|
||
// matches and re-fires their artwork (mirrors the Qobuz-playlists filter).
|
||
callback filter(string);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Discover section configurator (Slice 5) — prefs-driven render loop + modal.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// One Discover section as a render-loop descriptor. Rust emits the ordered
|
||
// ENABLED list per tab (DiscoverState.home-sections / editor-sections /
|
||
// foryou-sections); each surface iterates and dispatches on `id`
|
||
// (DiscoverySectionId::as_str). `kind` is a coarse render family used by the
|
||
// one tab-dependent arm (mostStreamed renders as a slim grid on Home, an album
|
||
// carousel on Editor's Picks). `section` carries the album-carousel payload for
|
||
// Home arms that share the Carousel component but have no dedicated HomeState
|
||
// field (newReleases / pressAwards / idealDiscography / editorPicks /
|
||
// qobuzissimes / mostStreamed-album / recentlyPlayedAlbums); For You arms read
|
||
// the typed ForYouState fields by id and ignore `section`.
|
||
export struct SectionDescriptor {
|
||
id: string, // "newReleases" | "qobuzPlaylists" | "qobuzMixes" | ...
|
||
kind: string, // "albumCarousel" | "slimGrid" | "playlistCarousel" | ...
|
||
section: DiscoverSection,
|
||
}
|
||
|
||
// One row in the configurator modal: a section in the active tab's FULL ordered
|
||
// list (enabled AND disabled — reorder applies to disabled rows too). `label`
|
||
// is resolved in Rust from the id->key map (Slint @tr cannot take a runtime
|
||
// key), returning the English string until the Slint gettext pipeline lands.
|
||
export struct ConfigRow {
|
||
id: string,
|
||
label: string,
|
||
enabled: bool,
|
||
}
|
||
|
||
// Single owner of the configurator render-driver lists AND the modal payload.
|
||
// `active-tab` lives here once (render loop + modal target read it); the Slice-3
|
||
// tab pill keeps writing HomeState.active-tab and crate::home::select_tab mirrors
|
||
// it into here (one extra set, no second tab-switch path).
|
||
export global DiscoverState {
|
||
// "home" | "editorPicks" | "forYou". home::select_tab writes this.
|
||
in-out property <string> active-tab: "home";
|
||
|
||
// Ordered ENABLED descriptors per tab (Rust recomputes on every mutation +
|
||
// on tab switch). Per-tab so a tab flip is a pure binding read. When
|
||
// active-tab is "forYou" the home/editor lists are pushed empty so the Home
|
||
// repeater renders nothing for that tab (unconditional-repeater form).
|
||
in property <[SectionDescriptor]> home-sections: [];
|
||
in property <[SectionDescriptor]> editor-sections: [];
|
||
in property <[SectionDescriptor]> foryou-sections: [];
|
||
|
||
// Configurator modal payload (for the modal's current target = active-tab).
|
||
in-out property <bool> configurator-open: false;
|
||
in property <[ConfigRow]> config-rows: []; // FULL list (enabled+disabled).
|
||
in property <int> enabled-count: 0;
|
||
in property <int> total-count: 0;
|
||
}
|
||
|
||
export global DiscoverActions {
|
||
callback open-configurator(); // gear -> opens for active-tab
|
||
callback close-configurator();
|
||
callback toggle-section(/* tab */ string, /* id */ string);
|
||
callback move-section(/* tab */ string, /* id */ string, /* dir -1|+1 */ int);
|
||
callback reset-tab(/* 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();
|
||
}
|
||
|
||
// Qobuz Playlists "View all" full-list page — the playlist twin of
|
||
// DiscoverBrowseState (opened from the Home / Editor's Picks rail).
|
||
// Paginated server-side by category tag + the shared genre selection;
|
||
// searched client-side over the loaded set.
|
||
export global PlaylistBrowseState {
|
||
// Localized page heading, pushed from Rust on every navigate (a @tr
|
||
// property default would not react to a language switch).
|
||
in-out property <string> title;
|
||
// Single-select category tag bar: the /playlist/getTags set, names
|
||
// pre-localized by the API. `selected` flags mirror selected-tag.
|
||
in-out property <[PlaylistTagItem]> tags;
|
||
// Active tag slug; "" = All. Changing it re-fetches from offset 0.
|
||
in-out property <string> selected-tag;
|
||
// Full loaded page set. The view renders `visible` below — equal to
|
||
// `playlists` with no search, or a filtered subset while the search
|
||
// box is non-empty (Rust rebuilds it on search-changed).
|
||
in-out property <[SearchPlaylistItem]> playlists;
|
||
in-out property <[SearchPlaylistItem]> visible;
|
||
in-out property <bool> loading;
|
||
in-out property <bool> loading-more;
|
||
in-out property <bool> has-more;
|
||
// Rust-managed: the next page offset for load-more.
|
||
in-out property <int> next-offset;
|
||
// Client-side search over the loaded playlists; while non-empty,
|
||
// infinite-scroll load-more is disabled (Tauri does the same).
|
||
in-out property <string> search-query;
|
||
// "grid" | "list" — persists across navigations like DiscoverBrowse.
|
||
in-out property <string> view-mode: "grid";
|
||
}
|
||
|
||
export global PlaylistBrowseActions {
|
||
// Fetch the next page (offset = PlaylistBrowseState.next-offset).
|
||
callback load-more();
|
||
// Re-filter `visible` after `search-query` changed (UI thread).
|
||
callback search-changed();
|
||
// Select a category tag (slug; "" = All) — server-side re-fetch from
|
||
// offset 0 keeping the shared genre selection.
|
||
callback select-tag(string);
|
||
}
|
||
|
||
// 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;
|
||
// Favorite Albums — the user's favorited albums (For You #6).
|
||
in property <DiscoverSection> favorite-albums;
|
||
// Most Played Albums — top albums by local play count (same data as
|
||
// HomeState.most-played-albums; local, not tab-specific).
|
||
in property <DiscoverSection> most-played-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;
|
||
}
|
||
|
||
// Discover > Recommendations (the 4th tab). External-recommendations engine
|
||
// (Last.fm + ListenBrainz -> Qobuz) carousels, plus the Qobuz-editorial
|
||
// cold-start fallback. Mirrors ForYouState's lazy-load latch + self-hiding
|
||
// rows (each row's `length > 0` gate). Populated by crate::external_reco.
|
||
export global ExternalRecoState {
|
||
in property <bool> loading: false;
|
||
in-out property <bool> loaded: false;
|
||
// Per-row "still loading" flags. The controller sets the rows it is about
|
||
// to build true at spawn and clears each as its builder resolves, so a SLOW
|
||
// row (the Weekly ListenBrainz playlists resolve a few seconds after the
|
||
// fast rows) shows its own skeleton instead of silently missing — the UI now
|
||
// signals "more is still loading" during the progressive paint.
|
||
in property <bool> pending-rec-artists-common: false;
|
||
in property <bool> pending-rec-artists-recent: false;
|
||
in property <bool> pending-rec-albums: false;
|
||
in property <bool> pending-fresh-releases: false;
|
||
in property <bool> pending-weekly-exploration: false;
|
||
in property <bool> pending-weekly-jams: false;
|
||
in property <bool> pending-deep-cut-albums: false;
|
||
in property <bool> pending-top-albums: false;
|
||
in property <bool> pending-top-artists: false;
|
||
// True when no external source is connected: the editorial fallback
|
||
// (top-albums + top-artists) shows instead of the personalized rows.
|
||
in property <bool> editorial-fallback: false;
|
||
// Recommended artists from your COMMON taste (overall top -> similar).
|
||
in property <[SlimItem]> rec-artists-common: [];
|
||
// Recommended artists from your RECENT taste (1-month top -> similar).
|
||
in property <[SlimItem]> rec-artists-recent: [];
|
||
// Recommended albums (Last.fm artist top-albums, not scrobbled).
|
||
in property <DiscoverSection> rec-albums;
|
||
// Fresh releases (ListenBrainz, from artists you follow).
|
||
in property <DiscoverSection> fresh-releases;
|
||
// Weekly Exploration (ListenBrainz discovery) tracks.
|
||
in property <[SlimItem]> weekly-exploration: [];
|
||
// Weekly Jams (ListenBrainz familiar) tracks.
|
||
in property <[SlimItem]> weekly-jams: [];
|
||
// Deep-cut albums from artists you know.
|
||
in property <DiscoverSection> deep-cut-albums;
|
||
// Cold-start fallback: Qobuz editorial top albums.
|
||
in property <DiscoverSection> top-albums;
|
||
// Cold-start fallback: Qobuz editorial top artists.
|
||
in property <[SlimItem]> top-artists: [];
|
||
// Results-cache freshness window selector index for the Recommendations tab:
|
||
// 0=24h 1=36h 2=48h 3=72h. Seeded from discover_prefs at startup; default 48h.
|
||
in-out property <int> cache-ttl-index: 2;
|
||
}
|
||
|
||
// Recommendations-tab-only actions: cache-window select + force refresh. Wired to
|
||
// discover_prefs (persist) + external_reco (refetch). Other Discover tabs unaffected.
|
||
export global ExternalRecoActions {
|
||
callback set-cache-ttl(int); // index 0..3 -> persist hours + reseed
|
||
callback refresh-now(); // force-invalidate the results cache + refetch
|
||
}
|
||
|
||
// 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,
|
||
// Artist-blacklist greyout: when true the row renders dimmed + inert
|
||
// (no play/drag/favorite/download, nav-only menu) with a blacklist
|
||
// glyph in the number/play cell. Stamped per-row by the controllers
|
||
// (Task 6) from the user's blacklisted-artist set; never set on local
|
||
// / Plex rows. Defaults false (Slint struct fields zero-init).
|
||
is-blacklisted: bool,
|
||
// 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,
|
||
// Multi-disc grouping: the disc number whose "Disc N" header should be
|
||
// rendered ABOVE this row. Set (> 0) only on the first track of each
|
||
// disc, and only when the album spans more than one disc; 0 otherwise
|
||
// (single-disc albums and every non-leading track render flat, with no
|
||
// header). The "Disc" word is translated in the delegate via @tr so it
|
||
// flows through the gettext pipeline, matching the Tauri markup
|
||
// `{$t('album.disc')} {discNum}` (groupByDisc + isMultiDisc).
|
||
disc-header-number: int,
|
||
// Classical "work" grouping: the work-section header (e.g. "Symphony No. 9
|
||
// (Beethoven)") rendered ABOVE this row. Set (non-empty) only on the first
|
||
// track of each consecutive same-work run, and only on albums that carry
|
||
// work metadata; "" otherwise (non-classical albums and every non-leading
|
||
// track render flat, with no header). Mirrors the official Qobuz player's
|
||
// per-work sections (PR #536); the album view stamps it, all other track
|
||
// surfaces leave it "" so the row stays flat.
|
||
//
|
||
// E3: `work-header` now holds the work TITLE only; the composer is split
|
||
// out below so its name can be a clickable link to the artist page.
|
||
work-header: string,
|
||
// Classical work composer (E3): rendered as a clickable artist link inside
|
||
// the work header's parentheses. work-composer-id "" => plain (no link),
|
||
// and an empty name => no parenthetical at all. Set only on the work
|
||
// header's leading row, by the album view; "" everywhere else.
|
||
work-composer-name: string,
|
||
work-composer-id: string,
|
||
}
|
||
|
||
// Album detail screen state.
|
||
// One credited album artist for the header credit line (E1: full
|
||
// "Artist • Pianist • Composer (compositor)" list, not just the interpreter).
|
||
export struct ArtistCredit {
|
||
// Qobuz artist id as a string (clickable → artist page). "" = not clickable.
|
||
id: string,
|
||
name: string,
|
||
// Localized role suffix shown in parens (e.g. "compositor"); "" for the
|
||
// main artist(s).
|
||
role: string,
|
||
}
|
||
|
||
export global AlbumState {
|
||
in property <string> id;
|
||
in property <string> title;
|
||
// Primary interpreter (kept for back-compat: now-playing, fallbacks).
|
||
in property <string> artist;
|
||
in property <string> artist-id;
|
||
// Full credited-artist list with roles for the header credit line.
|
||
in property <[ArtistCredit]> artists: [];
|
||
// 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;
|
||
// Meta-line split around the label so the header can render the label
|
||
// as a clickable link: meta-pre (year) + [label] + meta-post
|
||
// (genre • N tracks • duration). info-line stays the fallback.
|
||
in property <string> meta-pre;
|
||
in property <string> meta-post;
|
||
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;
|
||
// Half-length truncation used when the content area is space-constrained
|
||
// (ShellState.content-constrained) so the header stays compact and the
|
||
// tracks keep priority. Falls back to the full short text when there's room.
|
||
in property <string> description-shorter;
|
||
in property <image> artwork;
|
||
// Static Immersive-style blurred artwork field for the header backdrop.
|
||
in property <image> header-atmosphere;
|
||
// Artwork-derived tint for the header fallback 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 <[AwardEntry]> awards: [];
|
||
// True when the album ships a downloadable booklet/liner-notes PDF
|
||
// (Qobuz goodies). Gates the booklet button in the header — not every
|
||
// album has one.
|
||
in property <bool> has-booklet: false;
|
||
// True when EVERY track of the album is cached offline (cache-status 3).
|
||
// Swaps the header menu's "Make available offline" for "Refresh offline
|
||
// copy" (Tauri's isAlbumFullyDownloaded). Kept live as downloads land.
|
||
in property <bool> album-fully-cached: false;
|
||
// Whether the album is in the user's favorites (library). Drives the
|
||
// header heart (filled accent when on). Seeded from fav_cache on open,
|
||
// flipped optimistically on toggle.
|
||
in property <bool> is-favorite: false;
|
||
// True while a favorite toggle is in flight (spinner on the heart).
|
||
in property <bool> favorite-loading: false;
|
||
// Whether THIS album is blocked (album blacklist). Drives the header menu
|
||
// toggle ("Block this album" / "Unblock album"). Seeded on open.
|
||
in property <bool> is-album-blocked: false;
|
||
// Whether THIS album is pinned to Home; seeded on open, flipped by
|
||
// toggle-pin when ids match.
|
||
in property <bool> pinned: false;
|
||
// Multi-select (track selection) mode for the track list. in-out: the
|
||
// toolbar toggle flips it from the UI; Rust resets it on album change.
|
||
in-out property <bool> multi-select: false;
|
||
// Number of selected rows (drives the bulk bar count + select-all label).
|
||
in property <int> selected-count: 0;
|
||
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;
|
||
// ---- Bottom carousels (webplayer parity) + external links -----------
|
||
// "More from this artist" carousel ("Del mismo artista..." on the
|
||
// webplayer / "By the same artist" in Tauri). Populated after the album
|
||
// renders from /artist/getReleasesGrid. `endpoint` non-empty on the
|
||
// section shows the "View all" link (-> artist discography).
|
||
in property <DiscoverSection> more-from-artist;
|
||
in property <bool> show-more-from-artist: false;
|
||
// "Listening suggestions" carousel ("Sugerencias de escucha" on the
|
||
// webplayer) from /album/suggest. Album cards -> open album.
|
||
in property <DiscoverSection> suggestions-section;
|
||
in property <bool> show-suggestions: false;
|
||
// Second suggestions row, sourced from Last.fm similar artists (one album
|
||
// each), deduped against the Qobuz row. Sits under the same heading, gated
|
||
// on Last.fm being connected. Empty -> hidden.
|
||
in property <DiscoverSection> lastfm-suggestions-section;
|
||
in property <bool> show-lastfm-suggestions: false;
|
||
// External links sidebar block (Last.fm / Discogs / MusicBrainz),
|
||
// Feishin-style. URLs are prebuilt by the controller from artist + title;
|
||
// each row opens its url via AlbumActions.open-external-link. Hidden for
|
||
// local-only albums (no Qobuz artist/title to search).
|
||
in property <bool> show-external-links: false;
|
||
in property <string> lastfm-url;
|
||
in property <string> discogs-url;
|
||
in property <string> musicbrainz-url;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Track Info + Album Info (Credits/Review) modals — 1:1 port of Tauri's
|
||
// TrackInfoModal.svelte + AlbumCreditsModal.svelte. Data is fetched fresh by
|
||
// the Rust controller (crate::info_modals) via core.get_track/get_album and
|
||
// mapped to these plain structs. Roles are parsed + grouped + localized in
|
||
// qbz_qobuz::performers (frontend-agnostic).
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// One grouped-by-role credit cell in the Track Info grid. `role` is the
|
||
// already-localized, UPPER-CASED label; each name is clickable -> musician.
|
||
export struct InfoCreditRow {
|
||
role: string,
|
||
// Original (non-localized) role string, passed to the musician-nav hint.
|
||
role-raw: string,
|
||
names: [string],
|
||
// Names pre-joined with ", " — rendered as a single wrapping Text so a long
|
||
// credit list (e.g. 5 producers) wraps instead of clipping at the column edge.
|
||
names-line: string,
|
||
}
|
||
|
||
// A row in the Track Info 2-column credits grid. The Rust side pairs the
|
||
// ordered cells (left + optional right) so the modal needs no dynamic grid
|
||
// placement. `has-right` is false on a trailing odd cell.
|
||
export struct InfoCreditPair {
|
||
left: InfoCreditRow,
|
||
right: InfoCreditRow,
|
||
has-right: bool,
|
||
}
|
||
|
||
// One performer line inside an expanded Album-Credits track. `roles` is the
|
||
// raw ", Role1, Role2" suffix (empty when the performer has no roles).
|
||
export struct AlbumCreditPerformer {
|
||
name: string,
|
||
roles: string,
|
||
// First role (clean) — the musician-nav role hint (Tauri roles[0]).
|
||
primary-role: string,
|
||
}
|
||
|
||
// One track row in the Album-Credits "Credits" tab.
|
||
export struct AlbumCreditTrack {
|
||
id: string,
|
||
number: string,
|
||
title: string,
|
||
artist: string,
|
||
// True when the row has performers or a copyright line (expandable).
|
||
has-credits: bool,
|
||
performers: [AlbumCreditPerformer],
|
||
copyright: string,
|
||
}
|
||
|
||
// Track Info modal — opened from the NPB (i) button / song-card title /
|
||
// TrackRow context menu via media-action("track", id, "track-info").
|
||
export global TrackInfoState {
|
||
in-out property <bool> open: false;
|
||
in property <bool> loading: false;
|
||
in property <string> error: "";
|
||
in property <string> title;
|
||
in property <string> album;
|
||
in property <string> artist;
|
||
// "" -> artist rendered as plain text (not a link).
|
||
in property <string> artist-id;
|
||
in property <string> duration;
|
||
in property <string> quality;
|
||
// "" -> the row is hidden.
|
||
in property <string> isrc;
|
||
in property <string> label;
|
||
in property <string> label-id;
|
||
// Paired (2-column) ordered credit cells.
|
||
in property <[InfoCreditPair]> credits: [];
|
||
// Credits split into two independent columns (even-index -> left,
|
||
// odd-index -> right) so the modal renders two natural vertical stacks
|
||
// instead of height-coupled paired rows (which left vertical gaps).
|
||
in property <[InfoCreditRow]> credits-left: [];
|
||
in property <[InfoCreditRow]> credits-right: [];
|
||
in property <string> copyright;
|
||
}
|
||
|
||
export global TrackInfoActions {
|
||
callback close();
|
||
callback open-artist(string /* artist-id */);
|
||
callback open-label(string /* label-id */);
|
||
callback open-musician(string /* name */, string /* role */);
|
||
// Populate TrackInfoState for the given track WITHOUT opening the floating
|
||
// modal — used by the immersive split Track Info panel, which renders the
|
||
// same data inline. Fetch+map+apply are identical to open-track-info; the
|
||
// ONLY difference is `open` stays false so the overlay modal never appears.
|
||
callback load-inline(string /* track-id */);
|
||
}
|
||
|
||
// One card in the immersive Suggestions panel (data-panels.md §6): either a
|
||
// curated artist PLAYLIST (`kind == "playlist"`, book-collage of up to 3 album
|
||
// covers) or the seed-track SONG RADIO (`kind == "radio"`, diamond-collage of
|
||
// up to 4 covers). `cover-urls` is the artwork pipeline's input (slot-indexed);
|
||
// `covers` holds the decoded images, applied per-slot by the controller. Slots
|
||
// 0-3 are addressed via the parallel cover0..cover3 fields (Slint structs can't
|
||
// have per-element image arrays painted in place, so flat slots are used — same
|
||
// pattern as the search/favorites playlist collages).
|
||
export struct SuggestionCard {
|
||
// "playlist" | "radio".
|
||
kind: string,
|
||
title: string,
|
||
subtitle: string,
|
||
// Remote URLs for the (up to 4) collage slots — the artwork-job input.
|
||
cover-urls: [string],
|
||
// Decoded collage covers, painted per-slot by the artwork pipeline.
|
||
cover0: image,
|
||
cover1: image,
|
||
cover2: image,
|
||
cover3: image,
|
||
// Playlist-card target id ("" for the radio card).
|
||
playlist-id: string,
|
||
// Radio-card seed (empty for playlist cards).
|
||
seed-track-id: string,
|
||
seed-track-name: string,
|
||
seed-artist-id: string,
|
||
// "qobuz" (playlist) | "qbz" (radio) — drives the corner badge glyph.
|
||
badge: string,
|
||
// Radio card: true while the Song Radio session is building (spinner).
|
||
loading: bool,
|
||
}
|
||
|
||
// Immersive Suggestions panel state (split-only, split-panel == 2). Populated
|
||
// by crate::suggestions::apply_suggestions; the recommended-track list reuses
|
||
// the shared TrackItem struct.
|
||
export global SuggestionsState {
|
||
in property <bool> loading: false;
|
||
in property <string> error: "";
|
||
// The artist whose suggestions are loaded (dedup guard mirror).
|
||
in property <string> artist-id: "";
|
||
// The seed track these suggestions were built for.
|
||
in property <string> seed-track-id: "";
|
||
in property <[SuggestionCard]> cards: [];
|
||
in property <[TrackItem]> tracks: [];
|
||
}
|
||
|
||
export global SuggestionsActions {
|
||
// Play / queue / play-next a curated artist playlist by id.
|
||
callback play-playlist(string /* playlist-id */);
|
||
callback queue-playlist(string /* playlist-id */);
|
||
callback play-next-playlist(string /* playlist-id */);
|
||
// Start the seed-track Song Radio: (seed-track-id, seed-track-name, seed-artist-id).
|
||
callback start-radio(string, string, string);
|
||
// Play a single recommended track by id.
|
||
callback play-track(string /* track-id */);
|
||
// Load suggestions for the given track id (the Rust side reads the
|
||
// artist-id + name off NowPlayingState). Fired on panel entry and on
|
||
// now-playing track change while the panel is open. A "" id resets to the
|
||
// empty (no-track) state.
|
||
callback load(string /* track-id */);
|
||
}
|
||
|
||
// Playlist "Suggested Songs" section (T8). 1:1 port of the Svelte
|
||
// `PlaylistSuggestions` component: a fixed page of suggested tracks (6
|
||
// visible) with per-row add / play / info / dismiss + play-on-hover,
|
||
// driven by a Rust-held pool + pagination (crate::playlist_suggestions).
|
||
// DISTINCT from the immersive SuggestionsState/Actions above (different
|
||
// surface, different controller) — the name split is deliberate.
|
||
export struct PlaylistSuggestionRow {
|
||
// Qobuz ids carried as STRINGS (parsed to u64 in Rust) — matches the
|
||
// shared TrackItem id convention; Qobuz catalog ids can exceed Slint's
|
||
// 32-bit `int`, and album ids are alphanumeric.
|
||
track-id: string,
|
||
title: string,
|
||
artist-name: string,
|
||
artist-id: string,
|
||
album-title: string,
|
||
album-id: string,
|
||
// Remote cover URL (artwork-job input) + the decoded thumbnail.
|
||
artwork-url: string,
|
||
artwork: image,
|
||
duration-label: string,
|
||
// Dev-mode "why suggested" string (empty in production — include_reasons=false).
|
||
reason: string,
|
||
// True while this row's add call is in flight; flips to `added` (a brief
|
||
// check state) on success, just before the row leaves the pool.
|
||
adding: bool,
|
||
added: bool,
|
||
}
|
||
|
||
// Playlist suggestions section state. Populated by
|
||
// crate::playlist_suggestions::*; the row list reuses PlaylistSuggestionRow.
|
||
export global PlaylistSuggestionsState {
|
||
// The user has launched suggestions for the open playlist (manual
|
||
// activation gate — mirrors the Svelte `suggestionsActivated`). Until
|
||
// then the section shows only the "Suggest Songs" CTA.
|
||
in-out property <bool> activated: false;
|
||
in property <bool> loading: false;
|
||
// A background pool expansion (load-more / auto-variety) is in flight.
|
||
in property <bool> loading-more: false;
|
||
// More pages are available beyond the visible 6 (drives the refresh hint).
|
||
in property <bool> has-more: false;
|
||
// The filtered pool is exhausted (nothing left to suggest) — the loaded
|
||
// section hides itself, matching the Svelte `isEmpty` branch.
|
||
in property <bool> is-empty: false;
|
||
in property <string> error: "";
|
||
in property <[PlaylistSuggestionRow]> rows: [];
|
||
}
|
||
|
||
export global PlaylistSuggestionsActions {
|
||
// Launch suggestions for the open playlist: gather seed artists +
|
||
// exclude ids off the loaded Qobuz tracks, then fetch the first pool page.
|
||
callback activate();
|
||
// Advance the visible page (wraps + expands the pool on cycle — Svelte parity).
|
||
callback refresh();
|
||
// Add a suggested track to the open playlist, then drop it from the pool.
|
||
callback add-track(string /* track-id */);
|
||
// Preview / play a single suggested track now.
|
||
callback play-track(string /* track-id */);
|
||
// Dismiss a suggestion (sticky per-playlist) + drop it from the pool.
|
||
callback dismiss-track(string /* track-id */);
|
||
// Open the Track Info modal for a suggestion.
|
||
callback show-info(string /* track-id */);
|
||
// Navigate to a suggestion's album / artist.
|
||
callback go-album(string /* album-id */);
|
||
callback go-artist(string /* artist-id */);
|
||
}
|
||
|
||
// Album Info modal — opened from the AlbumPageView header (i) button via
|
||
// media-action("album", id, "info").
|
||
export global AlbumInfoState {
|
||
in-out property <bool> open: false;
|
||
in property <bool> loading: false;
|
||
in property <string> error: "";
|
||
// "credits" | "review". The Review tab only exists when has-review.
|
||
in-out property <string> active-tab: "credits";
|
||
in property <bool> has-review: false;
|
||
in property <image> artwork;
|
||
in property <string> title;
|
||
in property <string> artist;
|
||
in property <string> label;
|
||
in property <string> label-id;
|
||
// Localized "September 2, 2021" ("" hides the date span).
|
||
in property <string> release-date;
|
||
// "Hard Rock · 10 tracks · 1h 21m".
|
||
in property <string> meta-line;
|
||
// "24-Bit / 96 kHz" ("" hides).
|
||
in property <string> quality;
|
||
in property <[AlbumCreditTrack]> tracks: [];
|
||
// HTML-stripped editorial review ("" -> no Review tab).
|
||
in property <string> review;
|
||
}
|
||
|
||
export global AlbumInfoActions {
|
||
callback close();
|
||
callback set-tab(string /* "credits" | "review" */);
|
||
callback play-track(string /* track-id */);
|
||
callback open-label(string /* label-id */);
|
||
callback open-musician(string /* name */, string /* role */);
|
||
}
|
||
|
||
// Album-view actions wired to Rust callbacks.
|
||
export global AlbumActions {
|
||
// Filter the visible track list by a query (client-side).
|
||
callback search(string);
|
||
// Toggle multi-select (track selection) mode on the album track list.
|
||
callback toggle-multi-select();
|
||
// Bulk-bar action over the selected rows (id = "select-all" | "play-next"
|
||
// | "queue" | "add-to-playlist" | "add-to-favorites" | "make-offline" |
|
||
// "clear").
|
||
callback bulk-action(string);
|
||
// Per-disc context-menu action on a multi-disc album's "Disc N" header —
|
||
// scoped to that disc's tracks only (action = "play" | "shuffle" | "queue"
|
||
// | "play-next"). The whole-album header buttons are unaffected.
|
||
callback disc-action(int /* disc number */, string /* action */);
|
||
// Open an external link (Last.fm / Discogs / MusicBrainz) in the system
|
||
// browser. The url is prebuilt by the controller (AlbumState.*-url).
|
||
callback open-external-link(string /* url */);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Booklet (digital liner-notes PDF) reader — 1:1 port of Tauri's
|
||
// BookletViewer.svelte. The Rust controller (crate::booklet) downloads the
|
||
// album goody PDF, rasterizes pages via the qbz-booklet crate (MuPDF), and
|
||
// pushes the decoded page image here. Mounted as a top overlay in AppShell
|
||
// (ADR-009 z-index >= 3000).
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export global BookletState {
|
||
// Modal open / lifecycle.
|
||
in-out property <bool> open: false;
|
||
// True while the PDF downloads or a page renders (spinner).
|
||
in property <bool> loading: false;
|
||
in property <bool> error: false;
|
||
in property <string> error-text;
|
||
// Album title for the modal header.
|
||
in property <string> title;
|
||
// The current rendered page image + its pixel size (for sizing/scroll).
|
||
in property <image> page-image;
|
||
in property <int> page-pixel-width: 0;
|
||
in property <int> page-pixel-height: 0;
|
||
// 1-based current page + total page count.
|
||
in property <int> current-page: 1;
|
||
in property <int> num-pages: 0;
|
||
// Zoom factor (1.0 = fit-width baseline) + cumulative rotation in degrees.
|
||
in property <float> zoom: 1.0;
|
||
in property <int> rotation: 0;
|
||
}
|
||
|
||
export global BookletActions {
|
||
callback close();
|
||
callback next-page();
|
||
callback prev-page();
|
||
callback zoom-in();
|
||
callback zoom-out();
|
||
callback fit-width();
|
||
callback rotate();
|
||
// Save the open booklet PDF to a user-chosen location.
|
||
callback download();
|
||
}
|
||
|
||
// 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, artist-releases, settings, search, musician, label, label-releases, award, award-albums, location, favorites, mix, playlist, discover-browse, playlist-browse, recent-albums, most-played-albums, playlist-manager, offline-manager, blacklist-manager, local-library, local-album, mixtapes, collections, mixtape-detail, discography-builder, purchases, purchase-album, now-playing }
|
||
|
||
// 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: "";
|
||
// Plex manual sync (#573): `plex-available` gates the header Sync button
|
||
// (Plex enabled + authenticated — seeded at startup from the store, kept
|
||
// fresh by plex_auth's gate refresh); `plex-syncing` drives its spinner.
|
||
in property <bool> plex-available: false;
|
||
in property <bool> plex-syncing: false;
|
||
// 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
|
||
// Album IDENTITY mode — what one "album" IS on the Albums tab:
|
||
// "folder" (one card per album-root folder; default — compilations/box
|
||
// sets stay whole) | "metadata" (split by album+artist tags, #411).
|
||
// Persisted in locallibrary_ui.json; switching re-queries the DB (unlike
|
||
// sort/group, which re-derive client-side).
|
||
in-out property <string> albums-id-mode: "folder";
|
||
in property <[AlphaJump]> albums-alpha: []; // A-Z jumps (alpha group)
|
||
in property <int> albums-shown: 0; // count after filter
|
||
in-out property <bool> albums-multi-select: false; // album checkboxes on
|
||
in property <int> albums-selected-count: 0; // bar count + select-all
|
||
|
||
// --- 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: "";
|
||
// SQL-side sort (defines the pagination order): "default" | "artist-asc"
|
||
// | "artist-desc" | "title-asc" | "title-desc" | "year-desc"
|
||
// | "year-asc" | "added-desc"
|
||
in-out property <string> tracks-sort: "default";
|
||
// 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();
|
||
// Manual Plex re-sync (#573) — the header Sync button, visible only when
|
||
// Plex is configured. Re-fetches sections + tracks, then the Rust side
|
||
// reloads the browse models in place.
|
||
callback sync-plex();
|
||
|
||
// --- 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 the album identity mode (folder|metadata) + persist + reload the
|
||
// albums set (the group key changes — a derive is not enough).
|
||
callback albums-set-id-mode(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 / toggle-select). The Albums
|
||
// grid honors the shared AlbumCollectionView contract.
|
||
callback album-action(string /* album id */, string /* action */);
|
||
// Toggle the Albums-grid multi-select mode.
|
||
callback albums-toggle-multi-select();
|
||
// Albums bulk bar action: select-all | clear | queue | play-next |
|
||
// add-to-playlist | add-to-mixtape.
|
||
callback albums-bulk-action(string /* action */);
|
||
// Windowed grid: the visible row band changed (item indices into
|
||
// albums-visible, prefetch margin included) — dispatch artwork for it.
|
||
callback albums-window-changed(int /* first */, int /* last */);
|
||
|
||
// --- 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 */);
|
||
// SQL sort key (default|artist-asc|...|added-desc), persisted; page-1 re-query.
|
||
callback tracks-set-sort(string /* key */);
|
||
// 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;
|
||
// Distinct track artists in first-appearance order (compilations); the
|
||
// header shows the single artist line + a "+N more artists" chevron that
|
||
// expands into the clickable full list (spec §B2). Empty/1 = no chevron.
|
||
in property <[string]> all-artists: [];
|
||
in property <bool> loading: false;
|
||
in property <[TrackItem]> tracks: [];
|
||
// Versions (distinct copies). >1 -> show the picker. Sorted best-quality
|
||
// first, so version 0 is the highest-quality copy.
|
||
in property <[LocalAlbumVersion]> versions: [];
|
||
in-out property <int> version-index: 0;
|
||
}
|
||
|
||
export global LocalAlbumActions {
|
||
callback play-all();
|
||
callback shuffle();
|
||
callback edit-tags();
|
||
callback add-to-playlist();
|
||
callback add-to-mixtape();
|
||
callback play-track(string /* track id */);
|
||
// Per-row context-menu actions beyond "play" (play-next / queue /
|
||
// add-to-playlist) — resolved against the open version's track cache.
|
||
callback track-menu-action(string /* track id */, string /* action */);
|
||
callback select-version(int);
|
||
// Client-side track filter (mirrors AlbumActions.search), applied over the
|
||
// current version's tracks.
|
||
callback search(string /* query */);
|
||
// Per-disc context-menu action on a multi-disc local album's "Disc N"
|
||
// header — scoped to that disc's tracks only (action = "play" | "shuffle" |
|
||
// "queue" | "play-next"). The whole-album header buttons are unaffected.
|
||
callback disc-action(int /* disc number */, string /* action */);
|
||
}
|
||
|
||
// Unified track context menu — ONE shell-level instance fed by this state
|
||
// instead of a `track-menu := TrackContextMenu` popup per row/view (popup
|
||
// windows are always inlined by the Slint compiler, so every per-site
|
||
// instance generated a full copy: 18 structs / ~22k lines for one logical
|
||
// menu). Openers (TrackRow right-click / "..." gutter, SearchTrackHero) set
|
||
// EVERY field below and then bump `open-token`; the shell instance watches
|
||
// the token and shows itself at (x, y). Dispatch: the shell routes
|
||
// media-action to LocalAlbumActions on the local-album view and to the
|
||
// window's central media-action everywhere else (spec:
|
||
// qbz-nix-docs/context-menu-unification/2026-07-17-single-context-menu-spec.md).
|
||
export global TrackMenuState {
|
||
// Window-relative open position, pre-computed by the opener with the
|
||
// scroll-safe `absolute-position - layout-offset + local-pointer` math.
|
||
in-out property <length> x;
|
||
in-out property <length> y;
|
||
// Payload.
|
||
in-out property <string> track-id;
|
||
in-out property <string> source; // TrackItem.source ("qobuz" | "local" | "plex" | "ephemeral" | ...)
|
||
in-out property <int> cache-status;
|
||
// Row-context inputs, copied 1:1 from the opener's row properties.
|
||
in-out property <bool> row-inert; // blacklisted row → nav-only menu
|
||
in-out property <bool> force-local-menu; // library surface gates
|
||
in-out property <bool> remove-from-playlist-action;
|
||
in-out property <bool> track-info-action: true;
|
||
in-out property <bool> local-goto-actions;
|
||
// Increment to open (a counter, not a bool, so open-while-open re-fires).
|
||
in-out property <int> open-token;
|
||
}
|
||
|
||
// ============================ Local Library management ====================
|
||
// The folder list + scan + maintenance + danger zone live under
|
||
// Settings > Local Library (NOT the browse view). One row per registered
|
||
// library folder; `Option<T>` maps to a "" / 0 sentinel + a Rust-computed
|
||
// label where the raw value is ambiguous.
|
||
|
||
// One registered library folder. Mirrors `qbz_library::LibraryFolder`
|
||
// (8 DB columns) + a UI-owned `selected` checkbox + Rust-computed labels.
|
||
export struct LibraryFolderItem {
|
||
id: int, // i64 folder id (small)
|
||
path: string, // absolute path
|
||
alias: string, // "" when none
|
||
display-name: string, // alias if non-empty else path (Rust-computed)
|
||
enabled: bool,
|
||
is-network: bool,
|
||
network-fs-type: string, // "" when none (e.g. "nfs","cifs")
|
||
user-override-network: bool,
|
||
last-scan: int, // unix seconds; 0 = never
|
||
last-scan-label: string, // "Never" / localized date (Rust-computed)
|
||
accessible: bool, // from the accessibility check; true until resolved
|
||
selected: bool, // UI multi-select state
|
||
}
|
||
|
||
// The folder list + maintenance + danger-zone state (Settings > Local Library).
|
||
export global LibraryFoldersState {
|
||
in property <[LibraryFolderItem]> folders: [];
|
||
in property <bool> loading: false;
|
||
in-out property <string> filter: ""; // folder search (alias??path substring)
|
||
in property <int> selected-count: 0; // drives Edit(==1) / Remove(>0) enablement
|
||
in property <bool> cleaning-missing: false;
|
||
in property <string> cleanup-status: ""; // "Removed N of M" / "" (auto-clears, Rust-side)
|
||
in property <bool> clearing-library: false;
|
||
}
|
||
|
||
// Folder-settings modal state (separate from the playlist FolderEditState).
|
||
export global LibFolderEditState {
|
||
in-out property <bool> open: false;
|
||
in property <int> folder-id: 0;
|
||
in-out property <string> alias: "";
|
||
in property <string> path: ""; // current path (updated after change-path)
|
||
in-out property <bool> enabled: true;
|
||
in-out property <bool> is-network: false;
|
||
in-out property <bool> user-override-network: false;
|
||
// QbzSelect index: 0 auto,1 cifs,2 nfs,3 sshfs,4 rclone,5 webdav,6 glusterfs,7 ceph,8 other.
|
||
in-out property <int> fs-type-index: 0;
|
||
in property <bool> accessible: true;
|
||
in property <bool> checking-accessible: false;
|
||
in property <string> last-scan-label: ""; // "Never" / date
|
||
}
|
||
|
||
// Library scan progress — a single global poll target (scan is not per-row,
|
||
// unlike the offline cache). `progress` is computed Rust-side (total==0 guard).
|
||
export global LibraryScanState {
|
||
in property <bool> scanning: false; // status == Scanning
|
||
in property <int> scan-status: 0; // 0 Idle,1 Scanning,2 Complete,3 Cancelled,4 Error
|
||
in property <int> total-files: 0;
|
||
in property <int> processed-files: 0;
|
||
in property <float> progress: 0.0; // 0..1
|
||
in property <string> current-file: ""; // basename, or the cleanup label
|
||
in property <int> error-count: 0;
|
||
}
|
||
|
||
export global LibraryManageActions {
|
||
callback load(); // (re)load the folder list (panel init)
|
||
callback add-folder(); // native dir picker (Rust)
|
||
callback remove-folders(); // bulk-remove selected (confirm)
|
||
callback remove-folder(int /* id */); // remove ONE folder directly (confirm)
|
||
callback toggle-folder-select(int /* id */);
|
||
callback edit-folder(int /* id */); // open the settings modal
|
||
callback save-folder-settings(int /* id */, string /* alias */, bool /* enabled */,
|
||
bool /* is-network */, string /* fs-type */, bool /* user-override */);
|
||
callback change-folder-path(int /* id */); // picker -> update path
|
||
callback scan-all();
|
||
callback scan-folder(int /* id */);
|
||
callback stop-scan();
|
||
callback cleanup-missing();
|
||
callback clear-library(); // two-step confirm
|
||
callback set-filter(string /* query */);
|
||
}
|
||
|
||
// ============================ Plex connection =============================
|
||
// Settings > Local Library > Plex. A LAN-only Plex Media Server connection:
|
||
// PIN-based auth (or manual token), library-section multi-select, and the
|
||
// per-section track caching that downstream browse views read. State is
|
||
// Rust-backed (qbz_app::settings::plex store + qbz-plex core); the controller
|
||
// lives in `plex_auth`. ADR-008: no pills. Status is a (key, args) pair so the
|
||
// Rust side can localize without 5-file JSON sync — `status-text` is the
|
||
// already-resolved label, `status-kind` drives the highlight style.
|
||
|
||
// One Plex music-library section row + its UI selection + cached track count.
|
||
export struct PlexSectionItem {
|
||
key: string, // Plex Directory@key
|
||
title: string, // section title
|
||
count: int, // cached track count (plex_cache_get_tracks(key).len())
|
||
selected: bool, // membership in selected_section_keys
|
||
}
|
||
|
||
export global PlexSettingsState {
|
||
// --- Persisted toggles -------------------------------------------------
|
||
in-out property <bool> enabled: false; // master toggle (default OFF)
|
||
in-out property <bool> ui-collapsed: false; // collapse chevron
|
||
in-out property <bool> metadata-write-enabled: false;
|
||
|
||
// --- Connection --------------------------------------------------------
|
||
// server-url is the RAW host-form text the user types; base-url is the
|
||
// Rust-resolved proto://host:port (set after resolve).
|
||
in-out property <string> server-url: "";
|
||
in property <string> base-url: "";
|
||
in property <string> token: ""; // "" = not authenticated
|
||
in-out property <bool> manual-token-mode: false;
|
||
in-out property <string> token-input: ""; // manual-token field buffer
|
||
|
||
// Derived gates (computed Rust-side, pushed in).
|
||
in property <bool> is-local-address: true; // isLocalPlexAddress(server-url)
|
||
in property <bool> can-use: false; // enabled && local && base!="" && token!=""
|
||
|
||
// --- PIN flow ----------------------------------------------------------
|
||
in property <bool> pin-busy: false; // PIN start / poll in flight
|
||
in property <string> pin-code: ""; // "" = no active code
|
||
in property <string> pin-auth-url: "";
|
||
|
||
// --- General busy + status --------------------------------------------
|
||
in property <bool> busy: false; // ping / sync / auto-setup
|
||
// Status as a resolved label + kind (0 none, 1 info, 2 connected/ok, 3 error).
|
||
in property <string> status-text: "";
|
||
in property <int> status-kind: 0;
|
||
|
||
// --- Library sections --------------------------------------------------
|
||
in property <[PlexSectionItem]> sections: [];
|
||
}
|
||
|
||
export global PlexAuthActions {
|
||
callback enable-toggle(bool);
|
||
callback collapse-toggle(bool);
|
||
callback set-server-url(string /* host-form */);
|
||
callback generate-code(); // PIN start + begin poll
|
||
callback open-auth-url();
|
||
callback copy-code();
|
||
callback manual-token-toggle(bool);
|
||
callback set-token(string /* token */); // save manual token
|
||
callback ping(); // check connection
|
||
callback load-sections(); // get libraries
|
||
callback toggle-section(string /* key */);
|
||
callback metadata-write-toggle(bool);
|
||
callback disconnect(); // confirm + reset
|
||
callback clear-cache(); // confirm + plex_cache_clear
|
||
callback load(); // panel init: seed from store + warm cache
|
||
}
|
||
|
||
// ============================ Scrobblers ==================================
|
||
// Settings > Integrations — source-agnostic Last.fm + ListenBrainz scrobbling.
|
||
// State mirrors `scrobbler_settings.db`; actions delegate to the Rust
|
||
// `scrobble` controller (auth flows + the now-playing/scrobble fire live
|
||
// there). ADR-008: no pills.
|
||
|
||
// Discord Rich Presence (Settings > Integrations). Opt-in, default OFF; the
|
||
// `enabled` flag is seeded from ui_prefs and `set-enabled` persists + applies it.
|
||
export global DiscordState {
|
||
in-out property <bool> enabled: false;
|
||
callback set-enabled(bool);
|
||
}
|
||
|
||
export global ScrobbleState {
|
||
// --- Master section toggles -------------------------------------------
|
||
in-out property <bool> enabled: false; // master toggle (default OFF)
|
||
in-out property <bool> ui-collapsed: false; // collapse chevron
|
||
|
||
// --- Last.fm ----------------------------------------------------------
|
||
in-out property <bool> lastfm-enabled: false; // per-service scrobble flag
|
||
in property <bool> lastfm-authed: false; // session key present
|
||
in property <string> lastfm-username: ""; // "Signed in as ..."
|
||
in property <string> lastfm-auth-url: ""; // non-empty after connect
|
||
in property <bool> lastfm-busy: false; // get_token / get_session in flight
|
||
|
||
// --- ListenBrainz -----------------------------------------------------
|
||
in-out property <bool> listenbrainz-enabled: false;
|
||
in property <bool> listenbrainz-authed: false;
|
||
in property <string> listenbrainz-username: "";
|
||
in-out property <string> listenbrainz-token-input: ""; // token field buffer
|
||
in property <bool> listenbrainz-busy: false; // set_token in flight
|
||
|
||
// --- Shared status line (0 none, 1 info, 2 ok, 3 error) ---------------
|
||
in property <string> status-text: "";
|
||
in property <int> status-kind: 0;
|
||
}
|
||
|
||
export global ScrobbleActions {
|
||
callback load(); // panel init: seed from store
|
||
callback enable-toggle(bool);
|
||
callback collapse-toggle(bool);
|
||
// Last.fm.
|
||
callback lastfm-enable-toggle(bool);
|
||
callback lastfm-connect(); // get_token + open browser
|
||
callback lastfm-open-auth-url(); // re-open authorize URL
|
||
callback lastfm-confirm(); // get_session after approve
|
||
callback lastfm-disconnect();
|
||
// ListenBrainz.
|
||
callback listenbrainz-enable-toggle(bool);
|
||
callback listenbrainz-set-token(string);
|
||
callback listenbrainz-disconnect();
|
||
}
|
||
|
||
// ============================ Tag editor ==================================
|
||
// Local-album metadata editor (Settings-independent modal launched from the
|
||
// local album detail). Sidecar (default) + opt-in direct file-write. Remote
|
||
// MusicBrainz/Discogs lookup lands as a follow-up sub-slice (needs the
|
||
// remote-metadata crate move).
|
||
|
||
// One editable track row. `cue_start_secs` is f64 (-1.0 = none); `has_cue`
|
||
// is precomputed (cue_file_path != "" || cue_start_secs >= 0) and disables
|
||
// direct file-write. disc/track 0 = unset sentinel (Option<u32>).
|
||
export struct TagTrackEdit {
|
||
id: int,
|
||
file_path: string,
|
||
cue_file_path: string,
|
||
cue_start_secs: float,
|
||
has_cue: bool,
|
||
title: string,
|
||
disc_number: string, // raw text (LineEdit); controller parses on save ("" = unset)
|
||
track_number: string, // raw text (LineEdit); controller parses on save ("" = unset)
|
||
}
|
||
|
||
// One remote (MusicBrainz/Discogs) search-result card. Option fields map to
|
||
// a 0/"" sentinel + a has_* companion where the zero value is valid.
|
||
export struct RemoteResultItem {
|
||
provider: string, // "musicbrainz" | "discogs"
|
||
provider_id: string, // selection key + open-in-browser
|
||
title: string,
|
||
artist: string,
|
||
year: int,
|
||
has_year: bool,
|
||
track_count: int,
|
||
has_track_count: bool,
|
||
country: string,
|
||
format: string,
|
||
label: string,
|
||
catalog_number: string,
|
||
}
|
||
|
||
export global TagEditorState {
|
||
in-out property <bool> open: false;
|
||
in property <string> album_group_key; // == LocalAlbum.id == directory_path
|
||
in-out property <string> album_title;
|
||
in-out property <string> album_artist;
|
||
in-out property <string> year_input; // raw text; controller parses
|
||
in-out property <string> genre;
|
||
in-out property <string> catalog_number;
|
||
in-out property <int> album_total_discs; // collected, NOT persisted (1:1)
|
||
in property <string> directory_path; // reference + is_dir gate
|
||
in property <bool> can_direct_write; // false if any track has_cue
|
||
in-out property <int> persistence_index: 0; // 0 = sidecar, 1 = direct
|
||
in-out property <[TagTrackEdit]> tracks: [];
|
||
in-out property <bool> saving: false;
|
||
in-out property <int> write_progress_current: 0;
|
||
in-out property <int> write_progress_total: 0;
|
||
// Remote lookup (MusicBrainz / Discogs).
|
||
in-out property <int> remote_provider_index: 0; // 0 musicbrainz, 1 discogs
|
||
in-out property <bool> remote_searching: false;
|
||
in-out property <bool> remote_loading: false; // apply (get full) in flight
|
||
in-out property <[RemoteResultItem]> remote_results: [];
|
||
in-out property <string> selected_result_id: ""; // "" = none
|
||
in-out property <bool> show_remote_panel: false;
|
||
in-out property <bool> has_searched: false;
|
||
}
|
||
|
||
export global TagEditorActions {
|
||
callback close();
|
||
callback save();
|
||
callback set-persistence(int); // QbzSelect index -> mode (controller maps)
|
||
callback set-provider(int); // provider QbzSelect index
|
||
callback search-remote();
|
||
callback select-result(string /* provider_id */);
|
||
callback apply-remote();
|
||
callback open-in-browser();
|
||
}
|
||
|
||
// Global toast state, driven by crate::toast. The Toast overlay (mounted
|
||
// at the app root) renders whatever is here. The Rust layer owns timing
|
||
// (show + auto-hide); the UI only sets `visible = false` on the close
|
||
// button. `enabled` gates non-error toasts (errors always show), to match
|
||
// Tauri's "in-app notifications" preference.
|
||
export global ToastState {
|
||
in-out property <bool> visible: false;
|
||
in-out property <string> message;
|
||
in-out property <ToastKind> kind: ToastKind.info;
|
||
in-out property <bool> persistent: false;
|
||
in-out property <bool> enabled: true;
|
||
}
|
||
|
||
// One row in the sidebar playlists list (also used for the folder
|
||
// list in the move-to-folder menu).
|
||
export struct SidebarPlaylistItem {
|
||
id: string,
|
||
name: string,
|
||
}
|
||
|
||
// One flattened entry in the sidebar nav list — a folder header or a
|
||
// playlist row (indented when inside a folder).
|
||
//
|
||
// Playlist rows carry up to four cover-art URLs + decoded images so the
|
||
// row can render a micro-collage instead of the generic list-music glyph
|
||
// (mirrors Tauri's `sidebarPlaylistCollage`). The covers come from the
|
||
// same `get_user_playlists()` payload (images300 / images150 / images),
|
||
// so no extra per-playlist fetch is needed.
|
||
export struct SidebarEntry {
|
||
kind: string, // "folder" | "playlist"
|
||
id: string,
|
||
name: string,
|
||
expanded: bool, // folders
|
||
count: int, // folder playlist count
|
||
indent: bool, // playlist nested in a folder
|
||
folder-id: string, // playlist's current folder id ("" = root; folders "")
|
||
// "" (Qobuz) | "local" | "offline" — local playlists (library.db,
|
||
// ids "local:<uuid>") show a hard-drive glyph instead of list-music.
|
||
local-kind: string,
|
||
cover-count: int, // number of cover images, 0-4 (playlists)
|
||
url1: string,
|
||
url2: string,
|
||
url3: string,
|
||
url4: string,
|
||
cover1: image,
|
||
cover2: image,
|
||
cover3: image,
|
||
cover4: image,
|
||
}
|
||
|
||
// Flyout shown when a folder is clicked while the sidebar is collapsed
|
||
// (mini): a popup to the right listing that folder's playlists. The list is
|
||
// populated from Rust because collapsed folders' children are NOT present in
|
||
// the flat `SidebarState.entries`.
|
||
export global SidebarFolderPopupState {
|
||
in-out property <bool> open: false;
|
||
in-out property <string> folder-id;
|
||
in-out property <string> folder-name;
|
||
in-out property <int> count: 0; // folder's playlist count (caps the height)
|
||
in-out property <length> anchor-x; // window x (sidebar right edge)
|
||
in-out property <length> anchor-y; // window y (folder row top)
|
||
in property <[SidebarEntry]> playlists: [];
|
||
}
|
||
|
||
// Collapsed (mini) sidebar hover tooltip — the row shows only an icon, so a
|
||
// hover bubble surfaces the name (folders also show their playlist count).
|
||
// Mirrors Tauri's native `title` on the collapsed folder/playlist buttons.
|
||
// Rendered at the AppShell level because the sidebar clips its overflow; the
|
||
// anchor is the row's true window position (the collapsed rows live inside a
|
||
// Flickable, so SidebarRow corrects the absolute-position double-count).
|
||
export global SidebarTooltipState {
|
||
in-out property <bool> open: false;
|
||
in-out property <string> id; // owning row id (race-safe leave)
|
||
in-out property <string> text; // "Folder (N)" or "Playlist name"
|
||
in-out property <length> anchor-x; // window x (sidebar right edge)
|
||
in-out property <length> anchor-y; // window y (row top)
|
||
}
|
||
|
||
// Closed-sidebar playlists flyout. When the sidebar is fully closed there is no
|
||
// playlist list on screen; a header button opens this popup over SidebarState's
|
||
// entries (folders + playlists), capped at six visible rows with the rest
|
||
// scrolling, plus an always-visible footer link to the Playlist Manager.
|
||
export global SidebarPlaylistsPopupState {
|
||
in-out property <bool> open: false;
|
||
in-out property <length> anchor-x; // window x (header button left)
|
||
in-out property <length> anchor-y; // window y (below the button)
|
||
}
|
||
|
||
// The sidebar's playlists + folders. Populated by crate::sidebar on
|
||
// shell entry and after create/delete/move.
|
||
export global SidebarState {
|
||
in property <[SidebarEntry]> entries: [];
|
||
// Flat folder list, for the move-to-folder menu.
|
||
in property <[SidebarPlaylistItem]> folders: [];
|
||
// Folder list filtered by the move-to-folder search box. Equals
|
||
// `folders` when the query is empty; a case-insensitive substring
|
||
// subset otherwise. Filled by the Rust side (Slint strings have no
|
||
// `contains`, so the substring filter can't live in markup). The
|
||
// context menu iterates this; the current-folder exclusion stays in
|
||
// markup via the per-row `folder-id`.
|
||
in property <[SidebarPlaylistItem]> menu-folders: [];
|
||
in property <bool> loading: false;
|
||
// The currently-open playlist id, for the active-row highlight.
|
||
in property <string> active-id;
|
||
// PLAYLISTS section collapse (the folder+playlist list hides). Mirrors
|
||
// Tauri's `playlistsCollapsed`.
|
||
in-out property <bool> playlists-collapsed: false;
|
||
// Inline playlist-name search in the header. When open the title is
|
||
// hidden and the input takes the full header width.
|
||
in-out property <bool> search-open: false;
|
||
in-out property <string> search-query;
|
||
// Active sort option: "name" | "recent" | "tracks" | "playcount"
|
||
// | "custom" (mirrors Tauri's SortOption). Persisted by crate::sidebar.
|
||
in-out property <string> sort-option: "name";
|
||
// Micro-collage of track covers on playlist rows. Opt-out (ON by
|
||
// default) to match Tauri's `sidebarPlaylistCollage`. The Settings >
|
||
// Appearance toggle wires into this in a later step.
|
||
in-out property <bool> playlist-collage: true;
|
||
}
|
||
|
||
export global SidebarActions {
|
||
callback open-playlist(string /* id */);
|
||
// Populate SidebarFolderPopupState.playlists for the collapsed-sidebar
|
||
// folder flyout (the folder's children aren't in the flat entries).
|
||
callback load-folder-popup(string /* folder id */);
|
||
callback create-playlist();
|
||
callback create-folder();
|
||
callback toggle-folder(string /* folder id */);
|
||
callback delete-folder(string /* folder id */);
|
||
// Move a playlist into a folder ("" folder id = root).
|
||
callback move-playlist(string /* playlist id */, string /* folder id */);
|
||
// Right-click context menu actions (folder + playlist rows).
|
||
callback edit-playlist(string /* id */);
|
||
callback edit-folder(string /* id */);
|
||
callback hide-playlist(string /* id */);
|
||
callback hide-folder(string /* id */);
|
||
// Add a playlist to a Mixtape/Collection (opens the AddToMixtapeModal with
|
||
// the playlist as a 1-item payload; restricts to mixtapes since a playlist
|
||
// can't go in a Collection).
|
||
callback add-to-mixtape(string /* playlist id */);
|
||
// Filter `menu-folders` by a case-insensitive substring of the
|
||
// move-to-folder search box (Rust does the substring match).
|
||
callback search-folders(string /* query */);
|
||
// Pick a sort option (re-sorts the list). Same five values as Tauri.
|
||
callback set-sort(string /* option */);
|
||
// Re-run the playlist-name filter (called as the search input edits).
|
||
callback search-changed(string /* query */);
|
||
// Re-fetch the playlist list from the network.
|
||
callback refresh-playlists();
|
||
// Open the full Playlist Manager view.
|
||
callback manage-playlists();
|
||
// Open the playlist importer modal (sidebar "..." menu).
|
||
callback import-playlist();
|
||
}
|
||
|
||
// "Create playlist" modal state. Mirrors Tauri's PlaylistModal (create
|
||
// mode): name, optional description, target folder, public flag.
|
||
export global CreatePlaylistState {
|
||
in-out property <bool> open: false;
|
||
in-out property <string> name;
|
||
in-out property <string> description;
|
||
in-out property <bool> is-public: false;
|
||
in property <bool> creating: false;
|
||
// Folder dropdown options — index 0 is always "No folder", the rest
|
||
// are the user's folder names (parallel to `folder-ids`). Built by
|
||
// the create-playlist handler from the sidebar folder list.
|
||
in property <[string]> folder-options: ["No folder"];
|
||
// Folder ids parallel to `folder-options` ("" for "No folder").
|
||
in property <[string]> folder-ids: [""];
|
||
// Selected index into `folder-options` (0 = No folder).
|
||
in-out property <int> folder-index: 0;
|
||
// D8 "Offline-only playlist" opt-in: ON -> created as a LOCAL entity
|
||
// (library.db, id "local:<uuid>") that never reaches Qobuz.
|
||
in-out property <bool> offline-only: false;
|
||
// True while the app is offline: creation always produces a local
|
||
// playlist, so the toggle shows ON and locked with a hint.
|
||
in property <bool> offline-locked: false;
|
||
}
|
||
|
||
export global CreatePlaylistActions {
|
||
callback submit();
|
||
callback close();
|
||
}
|
||
|
||
// "Create folder" modal state.
|
||
export global CreateFolderState {
|
||
in-out property <bool> open: false;
|
||
in-out property <string> name;
|
||
in property <bool> creating: false;
|
||
}
|
||
|
||
export global CreateFolderActions {
|
||
callback submit();
|
||
callback close();
|
||
}
|
||
|
||
// "Export settings" modal state (Settings > Developer, 04 §4.2). Opened from
|
||
// the Export settings… row, which resets `include-auth` to false first so the
|
||
// single --include-auth gate is always default-OFF. confirm() is wired in
|
||
// Rust (crate::settings::export_settings): build the bundle via the shared
|
||
// engine, open a native save dialog, write it 0600, and toast the import
|
||
// command. Cancel / backdrop / close-X set `open = false` directly.
|
||
export global SettingsExportState {
|
||
in-out property <bool> open: false;
|
||
in-out property <bool> include-auth: false;
|
||
}
|
||
|
||
export global SettingsExportActions {
|
||
callback confirm();
|
||
}
|
||
|
||
// "Create Mixtape / Collection" modal state (My QBZ). Opened from either
|
||
// grid's "+ New" CTA / empty-state button. `kind` is preset by the opening
|
||
// grid ("mixtape" | "collection") and can be flipped via the radio toggle
|
||
// (artist_collection is created via the Discography Builder, not here).
|
||
// `name` is capped at 80 chars (mirrors Tauri's maxlength="80").
|
||
export global MyQbzCreateState {
|
||
in-out property <bool> open: false;
|
||
in-out property <string> kind: "mixtape";
|
||
in-out property <string> name;
|
||
in property <bool> creating: false;
|
||
}
|
||
|
||
export global MyQbzCreateActions {
|
||
callback submit();
|
||
callback close();
|
||
}
|
||
|
||
// === "Add to Mixtape/Collection" picker modal (global singleton) =========
|
||
// Spec 21/50. A global overlay (mounted ONCE at the window root) that adds one
|
||
// or more items (album / track / playlist, from qobuz or local) into an
|
||
// existing Mixtape/Collection — or creates a brand-new one inline and adds
|
||
// into it. Every app-wide "Add to Mixtape/Collection" trigger calls
|
||
// MyQbzAddActions.open(...) with a payload it built; the Rust controller
|
||
// (crate::myqbz_add) holds the pending items and drives the picker.
|
||
//
|
||
// One picker row = one collection (mixtape | collection | artist_collection),
|
||
// with its kind label, "N albums" meta, and an `already-has` flag computed
|
||
// from `item_exists` so the row can show an "already added" hint.
|
||
export struct MyQbzAddRow {
|
||
id: string,
|
||
name: string,
|
||
// "mixtape" | "collection" | "artist_collection".
|
||
kind: string,
|
||
// Row icon by kind ("mixtape" -> cassette, "artist_collection" -> user,
|
||
// else library-big). Resolved in Rust to a key the row maps to a glyph.
|
||
icon: string,
|
||
// Right-aligned kind tag, uppercase ("MIXTAPE" | "COLLECTION" | "ARTIST").
|
||
kind-label: string,
|
||
// "N albums" / "1 album" — ICU plural rendered in Rust (always "album(s)").
|
||
meta: string,
|
||
// True when this collection already contains ALL of the pending items
|
||
// (per item_exists). Drives the per-row "already added" hint.
|
||
already-has: bool,
|
||
}
|
||
|
||
export global MyQbzAddState {
|
||
in-out property <bool> open: false;
|
||
// Whether the picker is loading the collection list.
|
||
in property <bool> loading: false;
|
||
// The collections offered as targets (already kind-restricted +
|
||
// recency-sorted + item_exists-resolved by the Rust controller).
|
||
in property <[MyQbzAddRow]> rows: [];
|
||
// Header title + subtitle (precomputed: single-item title or bulk
|
||
// "{count} items" / "{first} + N more").
|
||
in property <string> header-title;
|
||
in property <string> header-subtitle;
|
||
// True when >1 pending item (bulk add).
|
||
in property <bool> bulk-mode: false;
|
||
// True when ANY pending item is a track/playlist -> targets restricted to
|
||
// Mixtapes; the "Create Collection" button is hidden.
|
||
in property <bool> restrict-to-mixtape: false;
|
||
// Free-text filter over collection names.
|
||
in-out property <string> search;
|
||
// Id of the collection currently being written to (disables rows/buttons).
|
||
in property <string> busy-id;
|
||
// --- Create-new sub-panel ---
|
||
in-out property <bool> creating: false;
|
||
in-out property <string> create-name;
|
||
// "mixtape" | "collection".
|
||
in-out property <string> create-kind: "mixtape";
|
||
in property <bool> create-busy: false;
|
||
}
|
||
|
||
export global MyQbzAddActions {
|
||
// Click a collection row -> add the pending items to it.
|
||
callback pick(string /* collection id */);
|
||
// Filter the list by name.
|
||
callback search-changed(string /* query */);
|
||
// Open the create-new sub-panel preset to a kind ("mixtape"|"collection").
|
||
callback show-create(string /* kind */);
|
||
// Back out of the create sub-panel to the picker.
|
||
callback create-back();
|
||
// Create a new collection (create-kind/create-name) then add the items.
|
||
callback create-and-add();
|
||
callback close();
|
||
}
|
||
|
||
// Ephemeral "play with active queue" choice — shown when the user triggers an
|
||
// ephemeral play action while a queue is already playing. Two outcomes:
|
||
// add to the queue, or clear the queue and start playing.
|
||
export global EphemeralPlayChoiceState {
|
||
in-out property <bool> open: false;
|
||
// "all" | "album" | "track" — what the user pressed play on.
|
||
in-out property <string> intent-kind: "";
|
||
// group key (album) or track id (track); empty for "all".
|
||
in-out property <string> intent-arg: "";
|
||
// Whether "Add to queue" is offered — only when the current queue is itself
|
||
// entirely ephemeral (queue ops may not mix ephemeral with persistent tracks).
|
||
in-out property <bool> enqueue-allowed: false;
|
||
}
|
||
|
||
export global EphemeralPlayChoiceActions {
|
||
callback enqueue(); // add the selection to the current queue
|
||
callback replace(); // clear the queue and start playing the selection
|
||
callback close();
|
||
}
|
||
|
||
// Playlist detail screen state. Rows reuse the shared TrackRow
|
||
// (show-artwork + show-album); populated by crate::playlist.
|
||
export global PlaylistState {
|
||
in property <string> id;
|
||
in property <string> name;
|
||
in property <string> owner;
|
||
in property <string> description;
|
||
// Truncated description for the header (~2 lines); full text in
|
||
// the Read-more modal. Equal to description when it fits.
|
||
in property <string> description-short;
|
||
in property <int> track-count: 0;
|
||
in property <string> total-duration;
|
||
in property <string> cover-url;
|
||
in property <image> cover;
|
||
// True when a user-set custom artwork is shown (overrides the
|
||
// track-cover collage).
|
||
in property <bool> has-custom: false;
|
||
in property <bool> is-favorite: false;
|
||
// Whether THIS playlist is pinned to Home; seeded on open, flipped by
|
||
// toggle-pin when ids match.
|
||
in property <bool> pinned: false;
|
||
// True when the playlist is one of the user's own (editable).
|
||
in property <bool> is-owner: false;
|
||
// Followed (subscribed) on Qobuz — an account-level follow that syncs
|
||
// across Qobuz clients, distinct from the internal favorite heart.
|
||
in property <bool> is-following: false;
|
||
// Already copied into the user's own playlists this session (hides the
|
||
// Copy action so it isn't duplicated).
|
||
in property <bool> is-copied: false;
|
||
// LOCAL playlist (id "local:<uuid>", library.db entity — never on
|
||
// Qobuz). Qobuz-bound header actions (favorite / cache) hide; the
|
||
// "Upload to Qobuz" action appears when not offline-only.
|
||
in property <bool> is-local: false;
|
||
// D8: never offered for upload, never reaches Qobuz/QConnect.
|
||
in property <bool> offline-only: false;
|
||
// D11.a: a MIXED (Qobuz-id) playlist rendered OFFLINE from its local
|
||
// sidecar rows only (the Qobuz membership is not enumerable offline).
|
||
// Lets the AppShell mount the detail instead of the OfflinePlaceholder.
|
||
in property <bool> offline-subset: 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);
|
||
// Drag-reorder commit (custom-order list, issue #589): move the row at
|
||
// visible index `from` to insertion slot `to` (0..N; slots `from` and
|
||
// `from+1` are the no-op gaps and never fire). Complements the
|
||
// single-step ("track","move-up"/"move-down") chevron arms.
|
||
callback reorder-track(int /* from */, int /* to */);
|
||
}
|
||
|
||
// "Edit playlist" modal state (rename + description + delete).
|
||
export global EditPlaylistState {
|
||
in-out property <bool> open: false;
|
||
in property <string> id;
|
||
in-out property <string> name;
|
||
in-out property <string> description;
|
||
in property <bool> busy: false;
|
||
// LOCAL playlist (id "local:<uuid>") — edits route to library.db and
|
||
// the offline-only toggle below shows.
|
||
in property <bool> is-local: false;
|
||
// D8 flag, editable so an offline-created playlist can be unmarked
|
||
// later and gain the "Upload to Qobuz" action.
|
||
in-out property <bool> offline-only: false;
|
||
}
|
||
|
||
export global EditPlaylistActions {
|
||
callback save();
|
||
callback delete();
|
||
callback close();
|
||
}
|
||
|
||
// === Playlist Manager ===================================================
|
||
// A full playlist + folder organization surface (Tauri's
|
||
// PlaylistManagerView). Rows are precomputed in Rust (crate::playlist_manager)
|
||
// and pushed as ready-to-render models — no per-row map lookups in .slint.
|
||
|
||
// One folder card / chip / tree-folder. `icon-color` is a solid hex (or
|
||
// "" → use the accent). Gradient folder colors are not supported here
|
||
// (Slint brush limitation); a gradient string falls back to the accent.
|
||
export struct PmFolderItem {
|
||
id: string,
|
||
name: string,
|
||
count: int, // playlists in this folder
|
||
// "preset" or "custom".
|
||
icon-type: string,
|
||
// Lucide preset name ("heart" | "star" | "music" | "folder" | "disc"
|
||
// | "library" | "headphones"); drives which glyph the card shows.
|
||
icon-preset: string,
|
||
// Resolved solid color for the icon square (parsed from the stored
|
||
// hex by the Rust controller). When `has-color` is false the card
|
||
// uses the accent instead (gradient / empty / unparseable colors
|
||
// fall back to accent — Slint has no gradient-brush-in-struct).
|
||
icon-color: color,
|
||
has-color: bool,
|
||
is-hidden: bool,
|
||
// Decoded custom image (icon-type == "custom"); empty otherwise.
|
||
custom-image: image,
|
||
has-custom-image: bool,
|
||
}
|
||
|
||
// One playlist card / list-row / tree-row. All display strings are
|
||
// precomputed (track line, local line) so the view never does lookups.
|
||
export struct PmPlaylistItem {
|
||
id: string, // playlist id (may be negative for offline pending)
|
||
name: string,
|
||
// "12 tracks" — total (remote + local).
|
||
tracks-line: string,
|
||
// "1h 43m" — total playtime, "" when zero (list-row meta only).
|
||
duration-line: string,
|
||
// "(3 local)" — local-only suffix, "" when none.
|
||
local-line: string,
|
||
local-count: int,
|
||
total-count: int,
|
||
play-count: int,
|
||
// "" | "all_local" | "some_local" | "no" — availability badge.
|
||
local-status: string,
|
||
is-favorite: bool,
|
||
is-hidden: bool,
|
||
// LOCAL playlist entity (id "local:<uuid>", library.db) — the card /
|
||
// list-row shows a hard-drive marker; offline-only flavors label it.
|
||
is-local-playlist: bool,
|
||
offline-only: bool,
|
||
folder-id: string, // "" = root
|
||
// Micro-collage covers (same 0-4 scheme as the sidebar).
|
||
cover-count: int,
|
||
url1: string,
|
||
url2: string,
|
||
url3: string,
|
||
url4: string,
|
||
cover1: image,
|
||
cover2: image,
|
||
cover3: image,
|
||
cover4: image,
|
||
}
|
||
|
||
// One flattened row of the tree view — a folder header or a nested /
|
||
// root playlist. The Rust side emits only currently-visible rows
|
||
// (collapsed folders hide their children).
|
||
export struct PmTreeRow {
|
||
kind: string, // "folder" | "playlist"
|
||
expanded: bool, // folders
|
||
folder: PmFolderItem, // populated when kind == "folder"
|
||
playlist: PmPlaylistItem, // populated when kind == "playlist"
|
||
indent: bool, // playlist nested under a folder
|
||
}
|
||
|
||
export global PlaylistManagerState {
|
||
in property <[PmFolderItem]> folders: [];
|
||
// Filtered folder list for the list-row move-to-folder menu. Starts as
|
||
// the full folder set; the menu's search box narrows it via
|
||
// `search-folders` (Slint strings have no `contains`, so Rust filters).
|
||
in property <[PmFolderItem]> menu-folders: [];
|
||
in property <[PmPlaylistItem]> playlists: []; // the filtered+sorted grid/list set
|
||
in property <[PmTreeRow]> tree: [];
|
||
// Header counters: total folders / total playlists (post-filter for
|
||
// the visible playlist count).
|
||
in property <int> folder-count: 0;
|
||
in property <int> playlist-count: 0;
|
||
in property <bool> loading: false;
|
||
// Toolbar state (session-scoped; persisted per-user where cheap).
|
||
in-out property <string> search-query;
|
||
// "all" | "visible" | "hidden".
|
||
in-out property <string> filter: "all";
|
||
// "name" | "recent" | "playcount" | "tracks" | "custom".
|
||
in-out property <string> sort: "name";
|
||
// "grid" | "list" | "tree".
|
||
in-out property <string> view-mode: "grid";
|
||
// Folder mode on (folders shown + folder filtering) vs flat.
|
||
in-out property <bool> folder-mode: true;
|
||
// Folders section collapse.
|
||
in-out property <bool> folders-collapsed: false;
|
||
// Reorder is only allowed when sort == "custom".
|
||
in property <bool> can-reorder: false;
|
||
}
|
||
|
||
export global PlaylistManagerActions {
|
||
// Open a playlist (-> PlaylistView).
|
||
callback open-playlist(string /* id */);
|
||
// Toolbar.
|
||
callback search-changed(string /* query */);
|
||
callback set-filter(string);
|
||
callback set-sort(string);
|
||
callback set-view-mode(string);
|
||
// Filter the list-row move-to-folder menu's folder list (substring,
|
||
// case-insensitive). Empty query restores the full list.
|
||
callback search-folders(string /* query */);
|
||
callback toggle-folder-mode();
|
||
callback toggle-folders-collapsed();
|
||
// Tree-view folder expand/collapse.
|
||
callback toggle-tree-folder(string /* folder id */);
|
||
// Folder CRUD — "new folder" opens the editor in create mode; edit
|
||
// opens it for an existing folder.
|
||
callback new-folder();
|
||
callback edit-folder(string /* folder id */);
|
||
// Per-card playlist actions.
|
||
callback toggle-favorite(string /* playlist id */);
|
||
callback toggle-hidden(string /* playlist id */);
|
||
callback edit-playlist(string /* playlist id */);
|
||
callback add-to-mixtape(string /* playlist id */);
|
||
// Arrow reorder (custom sort only): move a playlist up / down within
|
||
// the current visible order, persisting the new order.
|
||
callback move-up(string /* playlist id */);
|
||
callback move-down(string /* playlist id */);
|
||
// Move a playlist into a folder ("" = root). Used by the tree's
|
||
// folder rows and the future DnD path.
|
||
callback move-to-folder(string /* playlist id */, string /* folder id */);
|
||
}
|
||
|
||
// === Offline Cache Manager =============================================
|
||
// The standalone manage-downloads surface (Tauri's OfflineCacheManagerView).
|
||
// Rows are precomputed in Rust (crate::offline_manager): an album-header row
|
||
// followed by its track rows, flattened (mirrors PmTreeRow).
|
||
export struct OfflineRow {
|
||
kind: string, // "album" | "track"
|
||
album-id: string, // album-header: the album id; track: its album id
|
||
track-id: string, // track rows
|
||
title: string, // album title (album) | track title (track)
|
||
subtitle: string, // artist
|
||
meta: string, // "8 tracks · 320 MB" (album) | "3:45" (track)
|
||
// 0 none | 2 downloading | 3 ready | 4 failed. Album = worst across tracks.
|
||
status: int,
|
||
progress: float, // track downloading 0..1
|
||
cover: image, // album-header cover (loaded from the on-disk cover.jpg)
|
||
number: string, // track position within its album ("" on album rows)
|
||
selected: bool, // multi-select checkbox
|
||
}
|
||
|
||
// One entry in the manager's left A-Z artist rail.
|
||
export struct OfflineArtist {
|
||
name: string,
|
||
meta: string, // "3 albums · 21 tracks"
|
||
selected: bool,
|
||
}
|
||
|
||
export global OfflineManagerState {
|
||
in property <[OfflineRow]> rows: [];
|
||
in property <[OfflineArtist]> artists: [];
|
||
in property <bool> loading: false;
|
||
// Header stats.
|
||
in property <int> tracks-count: 0;
|
||
in property <string> size-text: ""; // "1.2 GB"
|
||
in property <string> limit-text: ""; // "of 5 GB" | "Unlimited"
|
||
in property <float> usage: 0.0; // 0..1 for the bar (0 = unlimited)
|
||
in-out property <int> limit-gb: 5; // edit-limit input
|
||
// Toolbar — set by the controller from the Rust-side filter state so the
|
||
// dropdown selection / toggle / rail highlight reflect the current view.
|
||
in property <string> selected-artist: ""; // "" = all artists
|
||
in property <int> sort-index: 0; // 0 alpha / 1 recent / 2 largest / 3 smallest
|
||
in property <bool> show-only-failed: false;
|
||
// Multi-select: how many TRACK rows are checked (drives the bulk bar).
|
||
in property <int> selected-count: 0;
|
||
}
|
||
|
||
export global OfflineManagerActions {
|
||
// Navigate to the manager (from the Settings "Offline Playback" entry).
|
||
callback open();
|
||
callback refresh();
|
||
// Toolbar.
|
||
callback select-artist(string /* name, "" = all */);
|
||
callback set-sort(int /* index */);
|
||
callback toggle-failed();
|
||
// Multi-select.
|
||
callback toggle-select(string /* track id */);
|
||
callback select-all();
|
||
callback clear-selection();
|
||
callback bulk-redownload();
|
||
callback bulk-remove();
|
||
callback remove-track(string /* track id */);
|
||
callback remove-album(string /* album id */);
|
||
callback redownload-track(string /* track id */);
|
||
callback redownload-album(string /* album id */);
|
||
callback redownload-failed(string /* album id */);
|
||
callback set-limit(int /* gb */);
|
||
callback clear-all();
|
||
callback open-folder();
|
||
callback play-track(string /* track id */);
|
||
}
|
||
|
||
// === Artist Blacklist Manager (Tauri's BlacklistManagerView) ============
|
||
// The standalone manage-blacklist surface reached from the Settings content-
|
||
// filtering row. One blacklisted-artist row carries everything the view needs;
|
||
// the controller (crate::blacklist_manager) precomputes the date string + the
|
||
// has-notes flag so the .slint does no formatting. Search filtering is done
|
||
// controller-side: `items` already holds only the filtered rows, while `count`
|
||
// stays the FULL list count (drives the badge + the empty/no-results split).
|
||
export struct BlacklistedArtistItem {
|
||
// Numeric Qobuz artist id (fits an int for display + passing back on click
|
||
// / remove). Slint has no u64/i64; the controller passes it through as int.
|
||
artist-id: int,
|
||
artist-name: string,
|
||
// Unix seconds (kept for reference; the view renders `added-display`).
|
||
added-at: int,
|
||
// Controller-formatted "MMM D, YYYY" (English; gettext for Rust is unwired).
|
||
added-display: string,
|
||
notes: string,
|
||
has-notes: bool,
|
||
}
|
||
|
||
export struct BlacklistedAlbumItem {
|
||
// Qobuz album id is a STRING (alphanumeric), unlike the artist int id.
|
||
album-id: string,
|
||
album-title: string,
|
||
artist-name: string,
|
||
// Network cover URL; resolved to `cover` by the artwork pipeline.
|
||
cover-url: string,
|
||
cover: image,
|
||
added-at: int,
|
||
added-display: string,
|
||
notes: string,
|
||
has-notes: bool,
|
||
}
|
||
|
||
export global BlacklistState {
|
||
// The CURRENTLY-VISIBLE rows (already search-filtered by the controller).
|
||
in property <[BlacklistedArtistItem]> items: [];
|
||
// Global enable flag (default-enabled, mirrors the backend default).
|
||
// Shared by both axes (artists + albums).
|
||
in property <bool> enabled: true;
|
||
in property <bool> loading: true;
|
||
// FULL list count (NOT the filtered length) — drives the count badge and
|
||
// the empty-vs-no-results branch (count==0 = empty; count>0 && items empty
|
||
// = no search results).
|
||
in property <int> count: 0;
|
||
// Echoed back so the search input reflects the controller's query (and the
|
||
// no-results message can interpolate it).
|
||
in property <string> search-query: "";
|
||
|
||
// --- Album axis (parallel String-keyed list, shown under the Albums tab) ---
|
||
in property <[BlacklistedAlbumItem]> album-items: [];
|
||
// FULL blocked-album count (drives the Albums tab badge + empty state).
|
||
in property <int> album-count: 0;
|
||
// Active manager tab: 0 = Artists, 1 = Albums.
|
||
in property <int> active-tab: 0;
|
||
}
|
||
|
||
export global BlacklistActions {
|
||
// Forward-open seam: the Settings content-filtering row (T10) calls this to
|
||
// navigate INTO the manager (records the nav entry, swaps the view, loads).
|
||
// Mirrors OfflineManagerActions.open — the back chrome is NavButtons.
|
||
callback open();
|
||
callback back();
|
||
callback artist-select(int /* artist id */);
|
||
callback toggle-enabled();
|
||
callback remove(int /* artist id */);
|
||
callback clear-all();
|
||
callback search-changed(string /* query */);
|
||
|
||
// --- Album axis ---
|
||
callback set-tab(int /* 0=artists, 1=albums */);
|
||
callback album-select(string /* album id */);
|
||
callback remove-album(string /* album id */);
|
||
callback clear-all-albums();
|
||
// Block an album from a context menu (id + display fields for the manager).
|
||
callback block-album(string /* id */, string /* title */, string /* artist */, string /* cover-url */);
|
||
}
|
||
|
||
// === My QBZ — Mixtapes & Collections (read-only index grids) ============
|
||
// Mirrors PlaylistManagerState/PmPlaylistItem. Both the Mixtapes grid and the
|
||
// Collections grid render `MixtapeCardItem`s; the Rust controller (crate::myqbz)
|
||
// filters by kind and precomputes every display string, so the views do no
|
||
// per-row lookups. Mosaic covers (up to 4, or 9 for a 3x3 Collection) are
|
||
// decoded into the item by the artwork pipeline.
|
||
//
|
||
// READ-ONLY SCOPE (Phase-2 Slice 2): the grids DISPLAY rows + route from the
|
||
// sidebar. Card click (open detail) and "+ New" (create) are STUBS that log
|
||
// "not yet wired" — wired in later slices.
|
||
export struct MixtapeCardItem {
|
||
id: string,
|
||
name: string,
|
||
// "mixtape" | "collection" | "artist_collection".
|
||
kind: string,
|
||
// Eyebrow label, uppercase ("MIXTAPE" | "COLLECTION" | "ARTIST").
|
||
label: string,
|
||
// "5 albums" / "1 album" — ICU-plural rendered in Rust (always "album(s)"
|
||
// regardless of item_type, matching the PSD).
|
||
meta: string,
|
||
// Item count (drives the 3x3-vs-2x2 mosaic rule + sort).
|
||
item-count: int,
|
||
play-count: int,
|
||
// updated_at epoch ms (sort key).
|
||
updated-at: int,
|
||
// Custom user-uploaded cover (decoded local file); empty when none. When
|
||
// present it full-bleeds the mosaic (precedence over the collage).
|
||
custom-cover: image,
|
||
has-custom-cover: bool,
|
||
// Mosaic cell count actually used: 0 (empty -> kind glyph), or 4 / 9.
|
||
cover-count: int,
|
||
// Up to 9 collage cover slots (only 0-3 used for 2x2; 0-8 for a 3x3
|
||
// Collection with >=9 items). URLs are pre-downscaled in Rust.
|
||
url1: string, url2: string, url3: string, url4: string,
|
||
url5: string, url6: string, url7: string, url8: string, url9: string,
|
||
cover1: image, cover2: image, cover3: image, cover4: image,
|
||
cover5: image, cover6: image, cover7: image, cover8: image, cover9: image,
|
||
}
|
||
|
||
export global MyQbzState {
|
||
// The Mixtapes grid set (kind == mixtape), filtered + sorted.
|
||
in property <[MixtapeCardItem]> mixtapes: [];
|
||
// The Collections grid set (kind == collection | artist_collection),
|
||
// filtered (by kind-filter) + sorted.
|
||
in property <[MixtapeCardItem]> collections: [];
|
||
in property <bool> loading: false;
|
||
// --- Mixtapes toolbar (session-scoped) ---
|
||
in-out property <string> mix-search;
|
||
// "position" | "name" | "items" | "updated".
|
||
in-out property <string> mix-sort: "position";
|
||
// "asc" | "desc".
|
||
in-out property <string> mix-sort-dir: "asc";
|
||
// "grid" | "list".
|
||
in-out property <string> mix-view: "grid";
|
||
// --- Collections toolbar (session-scoped) ---
|
||
in-out property <string> col-search;
|
||
in-out property <string> col-sort: "position";
|
||
in-out property <string> col-sort-dir: "asc";
|
||
in-out property <string> col-view: "grid";
|
||
// "all" | "collection" | "artist_collection".
|
||
in-out property <string> col-kind-filter: "all";
|
||
}
|
||
|
||
export global MyQbzActions {
|
||
// Card click -> open detail (STUB: logs in this slice).
|
||
callback open-card(string /* id */);
|
||
// "+ New Mixtape" / "+ New Collection" (STUB: logs in this slice).
|
||
callback create-mixtape();
|
||
callback create-collection();
|
||
// --- Mixtapes toolbar ---
|
||
callback mix-search-changed(string /* query */);
|
||
callback mix-set-sort(string /* field */);
|
||
callback mix-set-view(string /* grid|list */);
|
||
callback mix-reset();
|
||
// --- Collections toolbar ---
|
||
callback col-search-changed(string /* query */);
|
||
callback col-set-sort(string /* field */);
|
||
callback col-set-view(string /* grid|list */);
|
||
callback col-set-kind-filter(string /* all|collection|artist_collection */);
|
||
callback col-reset();
|
||
}
|
||
|
||
// === My QBZ — Collection / Mixtape DETAIL view (read-only) ==============
|
||
// Spec 12. One row per collection item (album / track / playlist), rendered
|
||
// in the 8-col list / the grid. The Rust controller (crate::myqbz_detail)
|
||
// precomputes every display string (type label, source kind, quality detail,
|
||
// tracks/year columns, downscaled artwork URL) so the view does no lookups.
|
||
// READ-ONLY SCOPE (Phase-2 Slice 3): the hero action buttons + per-row
|
||
// context-menu items + grid/list view modes render 1:1, but every click is a
|
||
// logging STUB. The "expanded" view-mode toggle + shell render; per-item
|
||
// inline track expansion is DEFERRED (needs the enqueue resolver — a later
|
||
// slice). Edit/rename/delete/cover/DJ-mix modals + the BulkActionBar are also
|
||
// deferred.
|
||
export struct MixtapeDetailItem {
|
||
// 0-based persisted position (the row's stable key; display is +1).
|
||
position: int,
|
||
// "album" | "track" | "playlist".
|
||
item-type: string,
|
||
// "qobuz" | "local".
|
||
source: string,
|
||
source-item-id: string,
|
||
title: string,
|
||
subtitle: string,
|
||
// True for qobuz items whose subtitle is a clickable artist link
|
||
// (spec 12 §6.3 col-2 — local subtitles are plain text).
|
||
subtitle-is-link: bool,
|
||
// Numeric Qobuz artist id resolved by the resolveItems pass ("" until it
|
||
// lands / for local-Plex items). Routes the artist link to the QOBUZ
|
||
// artist page; without it the link can only fall back to the
|
||
// LocalLibrary Artists tab (name-based).
|
||
artist-id: string,
|
||
// Resolved source kind for the source indicator + source filter
|
||
// ("qobuz" | "plex" | "local"). Defaults from `source` until resolveItems
|
||
// lands (deferred — for now equals the raw source mapping).
|
||
source-kind: string,
|
||
// Type-cell label (already localized): "ALBUM" / "TRACK" / "PLAYLIST"
|
||
// (or an EP/Single/… release-type override — overrides are a later slice).
|
||
type-label: string,
|
||
// Quality badge inputs (resolved by the resolveItems pass).
|
||
quality-tier: string, // "hires" | "cd" | ""
|
||
quality-detail: string, // "24-bit / 96 kHz" | ""
|
||
// True while the async resolveItems pass is still pending for this row (no
|
||
// cached resolution yet). Drives the quality-cell LoadingDots skeleton —
|
||
// shown only during the resolve window, collapsing to the real badge (or an
|
||
// empty cell for a no-quality item) once resolution completes.
|
||
quality-resolving: bool,
|
||
// TRACKS column: "1" for a track, else the count or "—".
|
||
tracks-text: string,
|
||
// YEAR column: the year or "".
|
||
year-text: string,
|
||
// Downscaled (_50) row artwork; decoded into `artwork` by the pipeline.
|
||
artwork-url: string,
|
||
artwork: image,
|
||
// UI-owned multi-select state (select-mode — deferred wiring; the column
|
||
// renders when select-mode is on so geometry matches the PSD).
|
||
selected: bool,
|
||
// --- Expanded view-mode inline tracks (spec 12 §8) --------------------
|
||
// In the "expanded" view-mode every album / playlist item shows its
|
||
// tracks rendered directly underneath — always visible, no chevron (1:1
|
||
// with Tauri: "like LocalLibrary's Tracks page with Group-by-album").
|
||
// Whether this item can host inline tracks (album / playlist; never a
|
||
// bare track) — gates the inline section + the auto-fetch.
|
||
can-expand: bool,
|
||
// True once the per-item track fetch has completed (success OR empty), so
|
||
// the view can distinguish "still loading" from "loaded, no results".
|
||
tracks-loaded: bool,
|
||
// True while the per-item track fetch is in flight (spinner state).
|
||
expand-loading: bool,
|
||
// The resolved inline tracks (rendered as shared TrackRows under the row).
|
||
// Fetched once on entering expanded mode, then cached on the row.
|
||
inline-tracks: [TrackItem],
|
||
}
|
||
|
||
export global MyQbzDetailState {
|
||
in property <string> id;
|
||
// "mixtape" | "collection" | "artist_collection".
|
||
in property <string> kind;
|
||
// Eyebrow label, uppercase ("MIXTAPE" | "COLLECTION" | "ARTIST").
|
||
in property <string> kind-label;
|
||
in property <string> name;
|
||
in property <string> description;
|
||
// "5 albums" / "1 album" meta line (ICU plural, rendered in Rust).
|
||
in property <string> meta;
|
||
in property <int> item-count: 0;
|
||
// play_mode ("in_order" | "album_shuffle") — drives the overflow toggle
|
||
// label (deferred wiring; shown for parity).
|
||
in property <string> play-mode: "in_order";
|
||
// True when the user set a custom cover (full-bleeds the hero mosaic).
|
||
in property <bool> has-custom-cover: false;
|
||
in property <image> custom-cover;
|
||
in property <bool> loading: false;
|
||
// False once the load completes with no collection (not-found state).
|
||
in property <bool> found: true;
|
||
|
||
// --- Hero mosaic (CollectionMosaic inputs, same scheme as the card) ----
|
||
// cover-count 0 (empty -> kind glyph) / 4 / 9; up-to-9 downscaled URLs +
|
||
// decoded covers. Mirrors MixtapeCardItem so CollectionMosaic renders it.
|
||
in property <int> cover-count: 0;
|
||
in property <string> url1; in property <string> url2; in property <string> url3;
|
||
in property <string> url4; in property <string> url5; in property <string> url6;
|
||
in property <string> url7; in property <string> url8; in property <string> url9;
|
||
in property <image> cover1; in property <image> cover2; in property <image> cover3;
|
||
in property <image> cover4; in property <image> cover5; in property <image> cover6;
|
||
in property <image> cover7; in property <image> cover8; in property <image> cover9;
|
||
|
||
// --- Item list (the render model: filter -> search -> sort applied) ----
|
||
in property <[MixtapeDetailItem]> items: [];
|
||
|
||
// --- Toolbar (session-scoped) ------------------------------------------
|
||
in-out property <string> search;
|
||
// "position" | "name" | "year" | "tracks".
|
||
in-out property <string> sort: "position";
|
||
in-out property <string> sort-dir: "asc";
|
||
// "all" | "album" | "track" | "playlist" (single-select).
|
||
in-out property <string> type-filter: "all";
|
||
// Multi-select source filter membership (spec 12 §5.5). Three flags
|
||
// (Slint has no Set); Rust derives the badge count.
|
||
in-out property <bool> src-qobuz: false;
|
||
in-out property <bool> src-plex: false;
|
||
in-out property <bool> src-local: false;
|
||
// "list" | "grid" | "expanded".
|
||
in-out property <string> view-mode: "list";
|
||
// Multi-select edit mode (renders the per-row checkbox column; bulk-bar
|
||
// wiring is DEFERRED).
|
||
in-out property <bool> select-mode: false;
|
||
in property <int> selected-count: 0;
|
||
// Rust-computed filter badge count (sources + (type-filter != all)).
|
||
in property <int> filter-count: 0;
|
||
// Whether any filter/sort differs from the defaults (drives the reset btn).
|
||
in property <bool> has-any-filter: false;
|
||
}
|
||
|
||
export global MyQbzDetailActions {
|
||
// Client-side toolbar — re-derive the visible item list (UI thread).
|
||
callback search-changed(string /* query */);
|
||
callback set-sort(string /* field */);
|
||
callback set-type-filter(string /* all|album|track|playlist */);
|
||
callback toggle-source-filter(string /* qobuz|plex|local */);
|
||
callback set-view-mode(string /* list|grid|expanded */);
|
||
callback toggle-select-mode();
|
||
callback reset-filters();
|
||
// Open an item's detail (album / track-album / playlist). Routed to the
|
||
// existing open-album / open-playlist paths in main.rs.
|
||
callback open-item(string /* source */, string /* item-type */, string /* source-item-id */);
|
||
// Open an item's artist. Qobuz items route by the resolved numeric
|
||
// artist-id to the Qobuz artist page; local/Plex items (empty id) route
|
||
// by NAME to the LocalLibrary Artists tab.
|
||
callback open-artist(string /* source */, string /* artist-name */, string /* artist-id */);
|
||
// --- Hero CTAs --------------------------------------------------------
|
||
callback play-all();
|
||
callback shuffle();
|
||
callback dj-mix(); // STUB (the DJ-mix modal is a later slice).
|
||
callback sync(); // STUB / no-op (no discography sync impl, spec §8).
|
||
// --- Hero overflow (⋯) menu (spec 12 §10) -----------------------------
|
||
// Each opens the matching modal or fires the matching mutation + reload.
|
||
callback open-rename(); // -> Rename modal (prefilled with name).
|
||
callback open-description(); // -> Description modal (prefilled).
|
||
callback upload-cover(); // native file picker -> resize -> set + reload.
|
||
callback remove-cover(); // clear custom cover -> reload.
|
||
callback toggle-play-mode(); // in_order <-> album_shuffle -> reload.
|
||
callback convert-kind(); // mixtape <-> collection -> reload + toast.
|
||
callback open-delete(); // -> Delete-confirm modal.
|
||
// Per-row affordances.
|
||
callback play-item(string /* source-item-id */);
|
||
callback item-action(string /* source-item-id */, string /* action */);
|
||
// Remove ONE item from the collection by its 0-based persisted position
|
||
// (the row menu's "Remove from collection"). Routes to the audited bulk
|
||
// remover with a single position so compaction + reload + toast are reused.
|
||
callback remove-item(int /* position */);
|
||
// Toggle one row's selection (select-mode).
|
||
callback toggle-item-select(int /* position */);
|
||
// --- Expanded view-mode inline tracks (spec 12 §8) --------------------
|
||
// Fired when the "expanded" view-mode becomes active (and on the initial
|
||
// open in expanded mode): the Rust side fetches every expandable item's
|
||
// tracks (via the enqueue resolver) that aren't already cached, populating
|
||
// each row's inline-tracks model. Idempotent — cached rows are skipped, so
|
||
// re-entering expanded mode is instant.
|
||
callback ensure-expanded();
|
||
// Play / queue / navigate from an inline track row. `action` is one of
|
||
// "play" | "play-next" | "play-later" | "go-to-album".
|
||
// The parent item's source-item-id scopes the fetch + go-to-album target.
|
||
callback inline-track-action(string /* item-source-item-id */, string /* track-id */, string /* action */);
|
||
// --- Bulk action bar (select-mode, spec 12 §13) -----------------------
|
||
// The bar emits the chosen action id verbatim; the Rust side maps it:
|
||
// "add-to-mixtape" -> open the global AddToMixtapeModal with the
|
||
// selected items' payloads.
|
||
// "remove-selected" -> repo::remove_item each selected position
|
||
// (highest-first so compaction doesn't shift),
|
||
// then reload the detail + clear selection.
|
||
callback bulk-action(string /* id */);
|
||
}
|
||
|
||
// ─────────────────────── Discography Builder (spec 13) ───────────────────
|
||
// The full-page flow that assembles an artist's complete discography into a
|
||
// saved `kind='artist_collection'` collection. It fetches every release for
|
||
// the artist from three sources (Qobuz artist page + local library + Plex
|
||
// cache), dedupes them into GROUPS (one logical album = a chosen "primary" +
|
||
// "alternates"), lets the user check which to include, reorder (by sort),
|
||
// rename, and correct release-type mislabels, then persists the result.
|
||
//
|
||
// One candidate row (a primary OR an alternate). Every display string is
|
||
// precomputed in Rust (crate::myqbz_builder) so the view does no lookups.
|
||
export struct DiscoCandidate {
|
||
// `${source}|${source_item_id}` — the per-candidate selection key.
|
||
key: string,
|
||
// "qobuz" | "local" | "plex" — drives the source indicator icon.
|
||
source: string,
|
||
source-item-id: string,
|
||
title: string,
|
||
artist: string,
|
||
// "2019" or "—" (em-dash placeholder).
|
||
year: string,
|
||
// "12" or "—".
|
||
tracks: string,
|
||
artwork-url: string,
|
||
// Effective release type AFTER the user's override is applied:
|
||
// "album" | "ep" | "single" | "live" | "compilation".
|
||
release-type: string,
|
||
// True when a user override is active for this candidate (drives the
|
||
// dotted-underline marker on the type button).
|
||
is-overridden: bool,
|
||
// QualityBadgeFull inputs.
|
||
quality-tier: string,
|
||
quality-detail: string,
|
||
// Whether THIS candidate is currently checked (included on save).
|
||
checked: bool,
|
||
}
|
||
|
||
// One dedupe GROUP: a primary candidate plus zero or more alternates.
|
||
export struct DiscoGroup {
|
||
// group_key (`${normalizedTitle}|${year}`) — stable row key.
|
||
key: string,
|
||
title: string,
|
||
// "2019" / "—".
|
||
year: string,
|
||
primary: DiscoCandidate,
|
||
alternates: [DiscoCandidate],
|
||
// ALL candidates are compilations → the group is treated as a comp
|
||
// (dimmed primary row, ignored by select-all).
|
||
is-compilation: bool,
|
||
}
|
||
|
||
export global DiscoBuilderState {
|
||
// The artist being built (id + resolved display name + portrait URL).
|
||
in property <string> artist-id;
|
||
in property <string> artist-name;
|
||
in property <string> artist-avatar-url;
|
||
in property <image> artist-avatar;
|
||
|
||
// Render states (spec 13 §4). `loading` true initially + during load;
|
||
// `load-error` non-empty shows the raw error message; otherwise the
|
||
// groups table renders (empty groups -> "No results found").
|
||
in property <bool> loading: true;
|
||
in property <string> load-error: "";
|
||
|
||
// The dedupe groups in CURRENT sort order (Rust re-sorts on order change).
|
||
in property <[DiscoGroup]> groups: [];
|
||
|
||
// The collection-name input (defaulted to "<artist> — Complete Discography").
|
||
in-out property <string> collection-name: "";
|
||
// Whether the name is empty AFTER trimming whitespace (Slint can't trim, so
|
||
// the controller computes this on every name change). Gates the Create
|
||
// button so a whitespace-only name disables it (spec 13 §2.13 / §10.19:
|
||
// disabled = creating || selectedCount===0 || !collectionName.trim()).
|
||
in property <bool> name-trimmed-empty: true;
|
||
|
||
// Order segmented control: "release_date" | "title" | "manual".
|
||
in-out property <string> order-by: "release_date";
|
||
|
||
// Select-all header checkbox state (tri-state via the two flags).
|
||
in property <bool> all-checked: false;
|
||
in property <bool> some-checked: false;
|
||
|
||
// Footer: total checked candidate count + total group count.
|
||
in property <int> selected-count: 0;
|
||
in property <int> group-count: 0;
|
||
|
||
// Disables the Create button while a save is in flight.
|
||
in property <bool> creating: false;
|
||
|
||
// The per-row type-override popover open key (`${source}|${source_item_id}`),
|
||
// or "" when none is open. One menu open at a time.
|
||
in-out property <string> open-type-menu-key: "";
|
||
}
|
||
|
||
export global DiscoBuilderActions {
|
||
// Back to the artist page (clears builder state).
|
||
callback back();
|
||
// Collection-name input edited.
|
||
callback name-changed(string);
|
||
// Order segmented control: "release_date" | "title" | "manual".
|
||
callback set-order(string);
|
||
// Toggle one candidate's checked state (group-key + candidate-key).
|
||
callback toggle-checked(string /* group-key */, string /* candidate-key */);
|
||
// Header tri-state select-all toggle (non-compilation primaries only).
|
||
callback toggle-all();
|
||
// Open an album (Qobuz album view, or local/Plex local-album view).
|
||
callback open-album(string /* source */, string /* source-item-id */);
|
||
// Apply a release-type override to a candidate (writes the sidecar).
|
||
callback set-type-override(string /* source */, string /* source-item-id */, string /* type */);
|
||
// Clear a candidate's override (revert to auto-detected).
|
||
callback reset-type-override(string /* source */, string /* source-item-id */);
|
||
// Save: create the artist_collection + bulk-add the checked groups.
|
||
callback create();
|
||
}
|
||
|
||
// ──────────────────────────── DJ-mix modal ────────────────────────────
|
||
// The "Random queue" sampler (TrackMixModal, spec 21 §C / spec 12 §12).
|
||
// Opened from the hero DJ-mix CTA. On open the Rust controller resolves the
|
||
// collection in-order + computes shuffle::unique_track_count (the slider max);
|
||
// the slider snaps to the discrete buildSizeOptions set (50,100,150,…,All).
|
||
// On confirm it runs dedup_by_similarity + hybrid_sample (thread RNG, sync
|
||
// scope) -> set_queue + play -> stamp queue-source + touch_play, toasting
|
||
// "Playing N of M" when the sampled count is below the requested size.
|
||
export global MyQbzMixState {
|
||
in property <bool> open: false;
|
||
// True while resolving + counting unique tracks ("Loading…" state).
|
||
in property <bool> loading: false;
|
||
// Deduped unique-track count = the slider MAX (and the "All" size).
|
||
in property <int> unique-count: 0;
|
||
// The discrete size options (buildSizeOptions): the slider index maps into
|
||
// this list. e.g. [50,100,150,All(187)]. Empty while loading / for an
|
||
// empty collection.
|
||
in property <[int]> size-options: [];
|
||
// Current slider index into `size-options` (0-based).
|
||
in-out property <int> selected-index: 0;
|
||
// The size at `selected-index` (the value handed to the sampler).
|
||
in property <int> selected-size: 0;
|
||
// True when the selected option is the trailing "All (N)" entry.
|
||
in property <bool> selected-is-all: false;
|
||
// Disables Shuffle while a sample + play is in flight.
|
||
in property <bool> busy: false;
|
||
}
|
||
|
||
export global MyQbzMixActions {
|
||
callback close();
|
||
// Slider moved: the new index into size-options (Rust re-derives the size).
|
||
callback set-index(int /* index */);
|
||
// Confirm -> run the sample + play.
|
||
callback shuffle();
|
||
}
|
||
|
||
// ──────────────────────────── MyQBZ edit modals ───────────────────────
|
||
// The Rename / Description / Delete-confirm modals (spec 12 §11), driven
|
||
// from the hero overflow (⋯) menu. One global backs all three (only one is
|
||
// ever open at a time). `busy` disables submit while the mutation runs.
|
||
export global MyQbzEditState {
|
||
// "" = closed; "rename" | "description" | "delete".
|
||
in-out property <string> mode: "";
|
||
in property <bool> open: false;
|
||
// The collection name (delete-confirm body interpolation + rename prefill).
|
||
in property <string> name;
|
||
// Rename: prefilled with the collection name; submit trims.
|
||
in-out property <string> draft-name;
|
||
// Description: prefilled with the current description ("" clears to null).
|
||
in-out property <string> draft-description;
|
||
// Disables submit while a mutation + reload is in flight.
|
||
in-out property <bool> busy: false;
|
||
}
|
||
|
||
export global MyQbzEditActions {
|
||
callback close();
|
||
callback submit-rename(); // trim; rename_collection -> reload -> close.
|
||
callback submit-description(); // empty -> null; set_description -> reload.
|
||
callback confirm-delete(); // delete_collection -> nav back + cleanup.
|
||
}
|
||
|
||
// Folder editor modal — create + edit (icon preset, solid color, custom
|
||
// image, hidden toggle, delete). Ports Tauri's FolderEditModal. Gradient
|
||
// color presets are intentionally omitted (Slint brush limitation).
|
||
export struct PmIconPreset {
|
||
id: string,
|
||
icon: image,
|
||
}
|
||
|
||
export struct PmColorSwatch {
|
||
// The stored hex value ("" = the "Accent" preset), used as the
|
||
// selection key and persisted into the folder record.
|
||
value: string,
|
||
// Resolved Slint color for the swatch fill (ignored when is-accent).
|
||
color: color,
|
||
is-accent: bool,
|
||
}
|
||
|
||
export global FolderEditState {
|
||
in-out property <bool> open: false;
|
||
// Empty id = create mode; non-empty = editing that folder.
|
||
in property <string> id: "";
|
||
in-out property <string> name;
|
||
in-out property <string> icon-preset: "folder";
|
||
// Selected solid color hex; "" = accent.
|
||
in-out property <string> icon-color: "";
|
||
in-out property <bool> is-hidden: false;
|
||
// Custom image path (icon-type == "custom" when set) + decoded image.
|
||
in-out property <string> custom-image-path: "";
|
||
in property <image> custom-image;
|
||
in property <bool> busy: false;
|
||
// The icon-preset grid + the solid-color swatch grid, built once by
|
||
// the Rust controller.
|
||
in property <[PmIconPreset]> icon-presets: [];
|
||
in property <[PmColorSwatch]> color-swatches: [];
|
||
}
|
||
|
||
export global FolderEditActions {
|
||
callback select-preset(string /* preset id */);
|
||
callback select-color(string /* color hex, "" = accent */);
|
||
callback pick-image();
|
||
callback clear-image();
|
||
callback toggle-hidden();
|
||
callback save();
|
||
callback delete();
|
||
callback close();
|
||
}
|
||
|
||
// Settings screen state. Audio and Playback preferences are loaded from
|
||
// the persistence stores by the Rust settings controller and pushed here;
|
||
// changes flow back through the settings-bool / settings-select /
|
||
// settings-slider callbacks.
|
||
export global SettingsState {
|
||
// Sub-section: 0 = Audio, 1 = Playback.
|
||
in-out property <int> section: 0;
|
||
in property <bool> loading: true;
|
||
|
||
// --- AUDIO -------------------------------------------------------------
|
||
|
||
// Audio — dropdowns. Each list is paired with a selected index; the
|
||
// Rust controller owns the index -> value mapping.
|
||
in-out property <[string]> streaming-qualities: [];
|
||
in-out property <int> streaming-quality-index: 0;
|
||
in-out property <[string]> 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;
|
||
// Detected local device limit (#638 fix 3), shown while the toggle above
|
||
// is on. `device-cap-summary` is the Rust-composed value line (e.g.
|
||
// "192 kHz · Hi-Res+"; empty = nothing detected yet → row hidden);
|
||
// `device-cap-detected` false = the probe fell back to an assumed common
|
||
// rate set, which surfaces the plain informative caveat (never an alert).
|
||
in-out property <string> device-cap-summary: "";
|
||
in-out property <bool> device-cap-detected: true;
|
||
in-out property <bool> alsa-hardware-volume: false;
|
||
// DSD delivery mode (DSD plan Phases 2-3): Convert to PCM / DoP /
|
||
// Native DSD. Only meaningful on the ALSA direct backend.
|
||
in-out property <[string]> dsd-modes: [];
|
||
in-out property <int> dsd-mode-index: 0;
|
||
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> backend-is-jack: 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;
|
||
// Discover — opt-out: show the external "Recommendations" tab. Default on.
|
||
// Seeded from persisted discover prefs at session start (crate::discover_prefs).
|
||
in-out property <bool> show-recommendations: true;
|
||
// MusicBrainz metadata — opt-out (default on). Drives the core client's
|
||
// enabled flag, the artist Network/Scene sidebar (mb-available), and the
|
||
// playlist Suggested-Songs CTA. Seeded from ui_prefs at session start
|
||
// (crate::discover_prefs::seed); persisted via the "musicbrainz" key.
|
||
in-out property <bool> musicbrainz-enabled: true;
|
||
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;
|
||
|
||
// Playback — "Auto-connect Qobuz Connect on startup" dropdown
|
||
// (remember-last / on / off). Backed by the QConnect settings DB via the
|
||
// Rust controller, which owns the index -> mode mapping.
|
||
in-out property <[string]> qconnect-startup-modes: [];
|
||
in-out property <int> qconnect-startup-index: 0;
|
||
// Playback — Qobuz Connect device name. The persisted custom override
|
||
// ("" = unset) plus the effective default name shown as the input
|
||
// placeholder ("Qbz - {hostname}"). Backed by the QConnect settings DB
|
||
// via the settings-string callback; applies on the next connect.
|
||
in-out property <string> qconnect-device-name: "";
|
||
in-out property <string> qconnect-device-name-default: "";
|
||
|
||
// --- OFFLINE MODE --------------------------------------------------------
|
||
|
||
// Settings > Offline, MODE group (the app operating without Qobuz — NOT
|
||
// the offline cache). Backed by the shared engine's per-user store, not
|
||
// the audio/playback stores: seeded by OfflineModeActions.load() on panel
|
||
// mount; persisted through the settings-bool key "offline-mode-enabled".
|
||
in-out property <bool> offline-mode-enabled: false;
|
||
// "Check now" re-probe in flight — flips the status row's button label.
|
||
// Cleared by Rust on the next status broadcast, or after a short timeout
|
||
// when the verdict comes back unchanged (the actor only broadcasts flips).
|
||
in-out property <bool> offline-checking: false;
|
||
}
|
||
|
||
// Appearance settings — backs the Settings > Appearance panel. 1:1 with
|
||
// Tauri's appearance section. This is a visual/structural replica: every
|
||
// row is bound to a property here, but the Rust side does NOT yet persist
|
||
// or apply most of them (wiring is a later step). The few that map onto an
|
||
// existing, already-functional flag are wired through (see notes inline).
|
||
//
|
||
// Dropdowns follow the same pattern as SettingsState: an option list paired
|
||
// with a selected index; the Rust controller (when wired) owns the
|
||
// index -> value mapping. Lists are populated with sensible defaults below
|
||
// so the UI renders standalone even while unwired.
|
||
export global AppearanceState {
|
||
// --- THEME -------------------------------------------------------------
|
||
// Seeded at startup by Rust from the qbz-theme registry
|
||
// (theme::dropdown_labels), in registry display order. The list below is a
|
||
// standalone-preview fallback only; the index is DERIVED from the persisted
|
||
// theme slug (theme::index_for_id), never assumed.
|
||
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;
|
||
|
||
// True while the "System" theme is selected (native OS-palette follow).
|
||
in-out property <bool> theme-is-system: false;
|
||
// Auto-Theme (shown when the appended "Auto (dynamic)" theme is selected).
|
||
// Source: 0 = System Colors, 1 = Wallpaper Sync, 2 = Custom Image.
|
||
in-out property <bool> theme-is-auto: false;
|
||
in-out property <[string]> auto-theme-sources: [@tr("System Colors"), @tr("Wallpaper Sync"), @tr("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: "";
|
||
|
||
// --- Custom theme editor (shown when the appended "Custom" theme is
|
||
// selected). The editable base is 12 semantic tokens; Rust seeds these
|
||
// swatch colors from the persisted custom_theme.json and re-derives the
|
||
// whole palette live on every edit. `custom-open-token` holds the key of the
|
||
// single token whose inline ColorPicker is expanded ("" = none).
|
||
in-out property <bool> theme-is-custom: false;
|
||
// Swatch colors for each editable base token, seeded from Rust.
|
||
in-out property <color> custom-surface-main: #000000;
|
||
in-out property <color> custom-surface-card: #0a0a0a;
|
||
in-out property <color> custom-surface-elevated: #1a1a1a;
|
||
in-out property <color> custom-text-primary: #ffffff;
|
||
in-out property <color> custom-text-secondary: #cccccc;
|
||
in-out property <color> custom-accent: #4285f4;
|
||
in-out property <color> custom-danger: #ef4444;
|
||
in-out property <color> custom-warning: #fbbf24;
|
||
in-out property <color> custom-success: #3fae6a;
|
||
in-out property <color> custom-border: #2a2a2a;
|
||
in-out property <color> custom-favorite: #ef4444;
|
||
in-out property <bool> custom-is-dark: true;
|
||
// Key of the token whose inline picker is open ("" = all collapsed).
|
||
in-out property <string> custom-open-token: "";
|
||
// A single token's color changed (drag-live). key = token id, color = value.
|
||
callback custom-set-token(string /* key */, color /* value */);
|
||
// A single token committed from the HEX field. key = token id, hex = #rrggbb.
|
||
callback custom-set-token-hex(string /* key */, string /* hex */);
|
||
// Re-seed the base from the currently active theme ("Start from current").
|
||
callback custom-seed-from-current();
|
||
// Flip the custom theme polarity (dark/light) and re-derive.
|
||
callback custom-toggle-dark(bool /* is-dark */);
|
||
|
||
// --- TYPOGRAPHY / SCALE / LANGUAGE -------------------------------------
|
||
// NOTE: the old Tauri-style percentage `ui-scales` placeholder properties
|
||
// that lived here were superseded by the working interface-size presets
|
||
// (`ui-scale-options` / `ui-scale-index` in the INTERFACE SIZE block
|
||
// further down, wired to SLINT_SCALE_FACTOR in main.rs).
|
||
in-out property <[string]> languages: [@tr("Auto"), "English", "Español", "Français", "Deutsch", "Português", "Русский", "日本語", "Nederlands"];
|
||
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;
|
||
|
||
// Immersive search action — what selecting a result in the Immersive search
|
||
// cortinilla does (index: 0=Disabled, 1=Replace queue, 2=Play next,
|
||
// 3=Add to queue). Persisted via appearance-select("immersive-search-action").
|
||
in-out property <[string]> immersive-search-actions: [@tr("Disabled"), @tr("Replace current queue"), @tr("Play next"), @tr("Add to queue")];
|
||
in-out property <int> immersive-search-action-index: 1;
|
||
|
||
// Immersive default view — which immersive view opens by default.
|
||
// 0 = Remember last (restore the last view that was open),
|
||
// 1 = Album Reactive, 2 = Static, 3 = Coverflow, 4 = Spectrum,
|
||
// 5 = Lyrics, 6 = Queue (all 1-6 pin a FOCUS view-mode foreground).
|
||
// Persisted via appearance-select("immersive-default-view").
|
||
in-out property <[string]> immersive-default-views: [@tr("Remember last"), @tr("Album Reactive"), @tr("Static"), @tr("Coverflow"), @tr("Spectrum"), @tr("Lyrics"), @tr("Queue")];
|
||
in-out property <int> immersive-default-view-index: 0;
|
||
|
||
// --- 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;
|
||
// Intelligent search: smart cache, ranking, and the search preview dropdown.
|
||
in-out property <bool> intelligent-search: true;
|
||
// 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 -----------------------------------------
|
||
// The header IS the title bar (merged model, owner decision 2026-07-03):
|
||
// it is always present; custom chrome (use-system-title-bar off) makes it
|
||
// gain the window controls + drag and drops the system decorations.
|
||
// Defaults are per-OS, seeded from ui_prefs at startup BEFORE the first
|
||
// show (decorations negotiate at surface creation): Linux = system chrome,
|
||
// macOS = overlay (native traffic lights over the header). The .slint
|
||
// default is the safe Linux one.
|
||
in-out property <bool> use-system-title-bar: true;
|
||
// APPLIED chrome state (what the window was actually created with, vs the
|
||
// persisted pref above which the settings toggle edits). All chrome
|
||
// bindings (no-frame, header drag/inset/controls, Purchases placement)
|
||
// read THIS one. Rust decides per platform how the pref propagates:
|
||
// Linux mirrors the pref here live (hot apply, today's behavior); macOS
|
||
// leaves it at the startup value (the overlay attributes are fixed at
|
||
// window creation — restart to apply).
|
||
in-out property <bool> system-title-bar-active: true;
|
||
// Tiling-WM variant: frameless without drawn controls / header drag.
|
||
in-out property <bool> hide-title-bar: false;
|
||
// Phase 2 (deferred): rounded corners matching the desktop decoration.
|
||
in-out property <bool> match-system-chrome: false;
|
||
// True when GPU acceleration is on (gates match-system-chrome).
|
||
in-out property <bool> hardware-accel-enabled: true;
|
||
// Purchases nav entry placement (the only per-item relocation kept —
|
||
// the old search-in-title-bar / nav-tb-* toggles were dropped, owner
|
||
// decision 2026-07-03).
|
||
in-out property <bool> nav-tb-purchases: false;
|
||
|
||
// Window controls group.
|
||
in-out property <[string]> wc-positions: [@tr("Left"), @tr("Right")];
|
||
in-out property <int> wc-position-index: 1;
|
||
in-out property <[string]> wc-styles: [@tr("Rectangular"), @tr("Full-height rounded"), @tr("Circular"), @tr("Square")];
|
||
in-out property <int> wc-style-index: 0;
|
||
in-out property <[string]> wc-sizes: [@tr("Small"), @tr("Normal"), @tr("Large")];
|
||
in-out property <int> wc-size-index: 1;
|
||
in-out property <[string]> wc-color-presets: [@tr("Default"), "macOS", "Adwaita", @tr("Monochrome"), @tr("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: [@tr("Remember last used"), @tr("Micro"), @tr("Compact"), @tr("Artwork"), @tr("Queue"), @tr("Lyrics")];
|
||
in-out property <int> miniplayer-view-index: 0;
|
||
|
||
// --- STARTUP -----------------------------------------------------------
|
||
in-out property <[string]> startup-pages: [@tr("Home"), @tr("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: [@tr("Auto"), @tr("Mono light"), @tr("Mono dark"), @tr("Color")];
|
||
in-out property <int> tray-icon-theme-index: 0;
|
||
|
||
// --- RENDERER ----------------------------------------------------------
|
||
// Linux-only: the renderer tier (wgpu / GL / software) is a startup-time
|
||
// choice there. Hidden by default; Rust flips this on for Linux. macOS is
|
||
// always Skia/Metal, so the row never shows.
|
||
in-out property <bool> renderer-setting-visible: false;
|
||
in-out property <[string]> renderer-options: [@tr("Auto (recommended)"), @tr("GPU"), @tr("GPU (compatibility)"), @tr("Software")];
|
||
in-out property <int> renderer-index: 0;
|
||
// Preferred GPU: options built in Rust from the detected adapters
|
||
// ("Auto" + each GPU by name); index 0 = Auto.
|
||
in-out property <[string]> gpu-power-modes: [@tr("Auto (recommended)")];
|
||
in-out property <int> gpu-power-index: 0;
|
||
|
||
// --- INTERFACE SIZE ----------------------------------------------------
|
||
// Window scale-factor preset (XS 0.8 / Small 0.9 / Default 1.0 /
|
||
// Large 1.2 / XL 1.5) — startup-time choice, restart to apply.
|
||
// Index 2 = Default.
|
||
in-out property <[string]> ui-scale-options: [@tr("Extra small"), @tr("Small"), @tr("Default"), @tr("Large"), @tr("Extra large")];
|
||
in-out property <int> ui-scale-index: 2;
|
||
|
||
// --- IMMERSIVE ---------------------------------------------------------
|
||
in-out property <[string]> immersive-views: [
|
||
@tr("Remember last used"), @tr("Coverflow"), @tr("Static"), @tr("Vinyl"), @tr("Visualizer"),
|
||
@tr("Neon Flow"), @tr("Tunnel"), @tr("Comet"), @tr("Lyrics"), @tr("Queue"),
|
||
@tr("Split: Lyrics"), @tr("Split: Track Info"), @tr("Split: Suggestions"), @tr("Split: Queue")
|
||
];
|
||
in-out property <int> immersive-view-index: 0;
|
||
in-out property <[string]> immersive-background-modes: [@tr("Full"), @tr("Lite"), @tr("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;
|
||
|
||
// --- APP-WIDE DYNAMIC BACKGROUND ("modo Cider") ------------------------
|
||
// Album-art background composited behind the WHOLE shell. Opt-in, wgpu-tier
|
||
// only. 0 = Off, 1 = Ambient (GPU shader scene 7), 2 = Blurred art (the
|
||
// ImmersiveAtmosphere blurred-cover look). Persisted in ui_prefs
|
||
// (`app_background`) via appearance-select("app-background", i).
|
||
in-out property <[string]> app-background-modes: [@tr("Off"), @tr("Ambient"), @tr("Blurred art")];
|
||
in-out property <int> app-background-mode-index: 0;
|
||
// False on the femtovg-GL / software tiers (no wgpu underlay, and
|
||
// reduce-motion is forced ON there → the effect would freeze). Seeded from
|
||
// Rust (= use_gpu_renderer); gates the whole picker row.
|
||
in-out property <bool> app-background-available: false;
|
||
// Live-tunable look knobs, seeded from Rust env at startup (QBZ_BG_DIM /
|
||
// QBZ_BG_SURFACE_ALPHA) so the appearance can be dialed in a single smoke
|
||
// session without recompiling, then baked to constants. `dim` = extra dark
|
||
// scrim over the background for legibility; `surface-alpha` = translucency
|
||
// of the shell chrome when the background is active.
|
||
in-out property <float> app-background-dim: 0.35;
|
||
in-out property <float> app-background-surface-alpha: 0.5;
|
||
// Thin BARS (jump-nav, discover toolbar, network tab row, playlist action
|
||
// bar) go MORE transparent than the panels — they read as chrome strips over
|
||
// the frosted content panel, not solid bars. Env QBZ_BG_BAR_ALPHA.
|
||
in-out property <float> app-background-bar-alpha: 0.3;
|
||
|
||
// Callbacks. Unwired in this replica — the Rust side may bind them
|
||
// later. Kept generic (key + value) so the controller can dispatch.
|
||
callback appearance-bool(string /* key */, bool /* value */);
|
||
callback appearance-select(string /* key */, int /* index */);
|
||
callback appearance-text(string /* key */, string /* value */);
|
||
callback appearance-action(string /* key */);
|
||
}
|
||
|
||
// "My QBZ" navigation branding (custom label + icon), surfaced in
|
||
// Settings > Appearance and reflected on the sidebar "My QBZ" row.
|
||
//
|
||
// Mirrors the Tauri `myQbzNavStore` contract (label default "My QBZ",
|
||
// icon default the branded /my-qbz.svg). Persistence is per-user JSON
|
||
// (see crate::myqbz_prefs); these properties are the live UI mirror and
|
||
// the callbacks are the action surface (the Rust side persists + re-seeds).
|
||
//
|
||
// - `label` : the current label (never empty — blank input coerces
|
||
// back to the default "My QBZ" string on the Rust side).
|
||
// - `custom-icon` : the user's image, loaded straight from the picked file
|
||
// path and rendered as-is (no tint). Only meaningful when
|
||
// `has-custom-icon` is true; otherwise the consumer falls
|
||
// back to the default branded glyph in markup (mask-tinted
|
||
// via QbzIcon on the sidebar). The default glyph stays a
|
||
// compile-time `@image-url` — the Rust side never decodes it.
|
||
// - `has-custom-icon` : true when a usable user image is set. A stale /
|
||
// deleted custom path falls back to the default on the Rust
|
||
// side (the seed loads the file; a load failure clears the
|
||
// flag without mutating the store), so this flag tracks "a
|
||
// usable custom icon is present".
|
||
export global MyQbzBrandingState {
|
||
in-out property <string> label: "My QBZ";
|
||
in-out property <image> custom-icon;
|
||
in-out property <bool> has-custom-icon: false;
|
||
|
||
// Commit a new label (trimmed-empty coerces to the default on the Rust
|
||
// side, which also re-seeds `label`). Fired on the text input's edit.
|
||
callback set-label(string /* label */);
|
||
// Open the native image picker; on pick, persist the path, load it, and
|
||
// re-seed `icon` + `has-custom-icon`. No-op on cancel.
|
||
callback pick-icon();
|
||
// Reset the icon to the default branded glyph (clears the persisted
|
||
// custom path). The label is reset separately by clearing the input.
|
||
callback reset-icon();
|
||
}
|
||
|
||
// One entry of a nav-tab dropdown.
|
||
export struct NavMenuEntry {
|
||
label: string,
|
||
icon: image,
|
||
has-glyph: bool,
|
||
// Destination route fired when the row is clicked, e.g. "home",
|
||
// "favorites-tracks", "favorites-albums". Empty = no-op (the
|
||
// destination isn't implemented yet).
|
||
route: string,
|
||
}
|
||
|
||
// Header nav-tab dropdown state.
|
||
//
|
||
// The four header tabs (Discover / Library / Local Library / My QBZ) no
|
||
// longer own a PopupWindow each — a PopupWindow grabs the pointer while
|
||
// open, which blocks hover-switching between tabs. Instead a single
|
||
// in-window overlay (HeaderMenuOverlay, a late child of the AppShell
|
||
// root) draws the open tab's menu. Tabs write their identity into this
|
||
// global on hover/click; hovering a different tab overwrites it, giving
|
||
// an instant switch. `open-index` of -1 means no menu is open.
|
||
export global HeaderMenuState {
|
||
// Which tab's menu is open: -1 = none, 0..3 = the tab index.
|
||
in-out property <int> open-index: -1;
|
||
// Header x-coordinate of the open tab, so the overlay aligns the
|
||
// menu panel under it.
|
||
in-out property <length> anchor-x: 0px;
|
||
// Window y-coordinate of the anchor — used for sidebar flyouts, which
|
||
// open to the RIGHT of the row rather than under the header.
|
||
in-out property <length> anchor-y: 0px;
|
||
// True when the open menu is a sidebar section flyout (opens right of
|
||
// the row) rather than a header dropdown (opens under the tab).
|
||
in-out property <bool> from-sidebar: false;
|
||
// Optional section-name header shown as the first row (with a hairline
|
||
// separator below it). Empty = no header row.
|
||
in-out property <string> title: "";
|
||
// True while the pointer rests on the TRIGGER (the header tab / compact
|
||
// nav icon / sidebar nav row). Pauses the idle-close like hovering the
|
||
// menu does, so resting on the trigger never lets it close.
|
||
in-out property <bool> trigger-hovered: false;
|
||
// The open tab's menu rows.
|
||
in-out property <[NavMenuEntry]> items: [];
|
||
// Index of the row wired to Home (-1 = none); only Discover uses it.
|
||
in-out property <int> home-item: -1;
|
||
}
|
||
|
||
export global NavState {
|
||
in-out property <ContentView> view: ContentView.home;
|
||
// Browser-like history flags, driven by the Rust nav module.
|
||
in property <bool> can-back: false;
|
||
in property <bool> can-forward: false;
|
||
// Navigation requests routed to Rust so history is recorded.
|
||
callback request-back();
|
||
callback request-forward();
|
||
callback request-settings();
|
||
|
||
// ---- Scroll-position restore (browser-like, per history entry) ----
|
||
// The mounted scroll container reports its live viewport-y here on every
|
||
// `changed viewport-y`; the Rust nav module stamps it onto the outgoing
|
||
// entry so back/forward can put it back.
|
||
callback report-scroll(length /* viewport-y */);
|
||
// Armed by Rust right before a back/forward view switch: `restore-scope`
|
||
// names which scroll container should restore (a stable per-list id, see
|
||
// nav::scope_for), and `scroll-restore` is the viewport-y to apply. A
|
||
// container restores only when its own scope matches, exactly once per
|
||
// mount, and only after its content has laid out (viewport overflows).
|
||
in-out property <string> restore-scope: "";
|
||
in-out property <length> scroll-restore: 0;
|
||
}
|
||
|
||
// Artist detail screen state.
|
||
// One label row in the artist network sidebar's Labels section.
|
||
export struct LabelEntry {
|
||
id: string,
|
||
name: string,
|
||
}
|
||
|
||
// One similar-artist row in the artist network sidebar's Similar
|
||
// Artists section.
|
||
export struct SimilarEntry {
|
||
id: string,
|
||
name: string,
|
||
}
|
||
|
||
// One Magazine/Stories teaser (from /artist/story) shown in the sidebar.
|
||
export struct StoryItem {
|
||
title: string,
|
||
author: string,
|
||
excerpt: string,
|
||
url: string, // https://play.qobuz.com/magazine/story/{id}
|
||
image-url: string, // ready-to-use arc-cdn thumbnail URL
|
||
image: image, // decoded by the artwork pipeline
|
||
}
|
||
|
||
export global ArtistState {
|
||
in property <string> id;
|
||
in property <string> name;
|
||
// Whether the user follows (Qobuz-favorites) this artist — drives the
|
||
// header heart fill; seeded on artist open, toggled by the follow action.
|
||
in property <bool> is-following: false;
|
||
// 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;
|
||
// Static Immersive-style blurred artwork field for the header backdrop.
|
||
in property <image> header-atmosphere;
|
||
// Artwork-derived tint for the header fallback gradient.
|
||
in property <color> header-color: #181820;
|
||
in property <[TrackItem]> top-tracks: [];
|
||
// "Novedad más reciente" — the single latest-release highlight (last_release).
|
||
// `has-last-release` gates the block; nullable on the API (hidden for artists
|
||
// with no recent release, e.g. those with an empty last_release).
|
||
in property <bool> has-last-release: false;
|
||
in property <AlbumCardItem> last-release;
|
||
// "Appears On" (tracks_appears_on) — flat track section of guest appearances.
|
||
in property <[TrackItem]> appears-on: [];
|
||
// Magazine / Stories teasers (from /artist/story) — sidebar section.
|
||
in property <[StoryItem]> stories: [];
|
||
in property <bool> stories-loading: false;
|
||
// Popular Tracks multi-select (track selection) mode + selected count —
|
||
// mirrors AlbumState. in-out: the section toggle flips it from the UI;
|
||
// Rust resets it on artist change.
|
||
in-out property <bool> top-tracks-multi-select: false;
|
||
in property <int> top-tracks-selected-count: 0;
|
||
// Releases grouped into titled sections (Albums, EPs & Singles, ...).
|
||
in property <[ArtistReleaseSection]> 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: [];
|
||
// Curated playlists featuring this artist (the /artist/page `playlists`
|
||
// section). Rendered as a main-column PlaylistCarousel directly above the
|
||
// "Other" releases block. Covers filled by ArtworkTarget::ArtistPlaylistCover.
|
||
in property <[SearchPlaylistItem]> playlists: [];
|
||
// Catalog-vs-library toggle (webplayer "Del catálogo" / "En la biblioteca").
|
||
// The toggle only shows when library-count > 0; "library" swaps the main
|
||
// column to the user's added items for this artist.
|
||
in-out property <string> artist-tab: "catalog"; // "catalog" | "library"
|
||
in property <int> library-count: 0;
|
||
in property <[TrackItem]> library-tracks: [];
|
||
in property <[AlbumCardItem]> library-albums: [];
|
||
// 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;
|
||
// Whether the CURRENTLY displayed artist is blacklisted. Drives the
|
||
// overflow-menu Blacklist/Show toggle label + the hidden-artist
|
||
// banner. Set in navigate_artist from artist_blacklist::is_blacklisted
|
||
// (which auto-gates on the enabled flag — reads false when the
|
||
// feature is disabled, which is acceptable here).
|
||
in-out property <bool> is-blacklisted: false;
|
||
// Whether THIS artist is pinned to Home; seeded on open, flipped by
|
||
// toggle-pin when ids match.
|
||
in property <bool> pinned: 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);
|
||
// Toggle Popular Tracks multi-select mode.
|
||
callback toggle-top-tracks-select();
|
||
// Bulk-bar action over the selected Popular Tracks (id = "select-all" |
|
||
// "play-next" | "queue" | "add-to-playlist" | "add-to-favorites" |
|
||
// "clear").
|
||
callback top-tracks-bulk-action(string);
|
||
// Popular Tracks section "more" menu (id = "next-all" | "queue-all" |
|
||
// "shuffle-all" | "playlist-all").
|
||
callback top-tracks-menu-action(string);
|
||
// Change the persisted sort for one release bucket (release-type, sort).
|
||
// sort = default | newest | oldest | title-asc | title-desc.
|
||
callback set-section-sort(string, string);
|
||
// Load one more page of releases for a bucket (release-type). Capped to
|
||
// 4 pages on the index; the full list lives on the dedicated page.
|
||
callback load-more-section(string);
|
||
// Open the dedicated discography page pre-filtered to a release-type
|
||
// ("all" for the whole discography).
|
||
callback open-releases(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;
|
||
// Active sidebar tab: "network" (Artist Network) | "magazine" (Stories).
|
||
// Lives here (not ArtistState) so it survives sidebar close/reopen; reset
|
||
// to "network" on each new artist.
|
||
in-out property <string> active-tab: "network";
|
||
|
||
// 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;
|
||
// Catalog/library toggle (SegmentedTabBar, mirrors ArtistState.artist-tab
|
||
// + library-*): "catalog" renders the /label/page sections; "library"
|
||
// renders the user's favorite albums on this label (the session
|
||
// library_by_label index). The toggle only shows when count > 0.
|
||
in-out property <string> label-tab: "catalog";
|
||
in property <int> library-count: 0;
|
||
in property <[AlbumCardItem]> library-albums: [];
|
||
in property <[TrackItem]> library-tracks: [];
|
||
// Popular tracks (album-thumbnail rows, multi-select capable).
|
||
in property <[TrackItem]> top-tracks: [];
|
||
// Multi-select (mirrors AlbumState): mode flag + count. Per-row toggle
|
||
// routes through the shared media-action("track",...) arm.
|
||
in-out property <bool> multi-select: false;
|
||
in property <int> selected-count: 0;
|
||
// 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 */);
|
||
}
|
||
|
||
// Award pages (AwardView landing + AwardAlbumsView listing) — mirrors
|
||
// Tauri's AwardView.svelte / AwardAlbumsView.svelte. One global serves
|
||
// both, like LabelState: the landing renders the hero + `albums` preview
|
||
// (first /award/getAlbums page) + `other-awards`; the listing renders the
|
||
// full paginated `albums` with client-side search (`visible`). The grid is
|
||
// always sourced from /award/getAlbums — never `/award/page.releases`
|
||
// (user-scoped, hero-only).
|
||
export global AwardState {
|
||
in property <string> id;
|
||
in property <string> name;
|
||
// Hero image URL (award image, falling back to magazine image) + the
|
||
// decoded image. Empty image-url => laurel/gold placeholder.
|
||
in property <string> image-url;
|
||
in property <image> image;
|
||
// Magazine name shown under the award name (may be "").
|
||
in property <string> magazine-name;
|
||
// Header follow heart — three states: not-following / following /
|
||
// toggling (disabled while the API call is in flight).
|
||
in property <bool> is-following: false;
|
||
in property <bool> following-toggling: false;
|
||
// Loaded album catalog (landing preview OR full listing) + the artwork
|
||
// pipeline target (stable indices). `visible` is the search-filtered
|
||
// subset the listing renders (shares `albums` when the query is empty,
|
||
// so artwork stays live — the DiscoverBrowse/Label pattern).
|
||
in property <[AlbumCardItem]> albums: [];
|
||
in property <[AlbumCardItem]> visible: [];
|
||
in property <int> total: 0;
|
||
in property <bool> has-more: false;
|
||
in property <bool> loading: false;
|
||
in property <bool> load-more-loading: false;
|
||
// True when the releases load failed (drives the error/retry branch).
|
||
in property <bool> load-error: false;
|
||
// "Other awards" carousel (from /award/explore, current award excluded).
|
||
// Reuses SlimItem: title = award name, subtitle = magazine.
|
||
in property <[SlimItem]> other-awards: [];
|
||
// True once the landing page-data load resolves (gates the spinner).
|
||
in property <bool> page-loaded: false;
|
||
// Listing in-view search query + the count actually shown after filter.
|
||
in-out property <string> search-query: "";
|
||
in property <int> shown: 0;
|
||
}
|
||
|
||
export global AwardActions {
|
||
// Header: toggle follow on the open award.
|
||
callback toggle-follow(string /* award id */);
|
||
// Landing "See all" -> open the full albums listing.
|
||
callback open-albums();
|
||
// "Other awards" card click -> open that award's landing.
|
||
callback open-award(string /* award id */, string /* award name */);
|
||
// Listing: fetch the next album page.
|
||
callback load-more();
|
||
// Listing: re-filter `visible` after `search-query` changed (UI thread).
|
||
callback search(string /* query */);
|
||
// Releases error branch: retry the failed load.
|
||
callback retry();
|
||
}
|
||
|
||
// Dedicated artist discography page (one release bucket, fully paginated)
|
||
// reached via "See discography" on the artist page.
|
||
export global ArtistReleasesState {
|
||
in property <string> id; // artist id
|
||
in property <string> name; // artist name (header)
|
||
in property <string> release-type; // raw server key (album/epSingle/…)
|
||
in property <string> title; // display title (header)
|
||
in property <[AlbumCardItem]> albums: [];
|
||
in property <bool> has-more: false;
|
||
in property <bool> loading: false;
|
||
in property <bool> load-more-loading: false;
|
||
in property <bool> load-error: false;
|
||
in-out property <string> sort-by: "default";
|
||
}
|
||
|
||
export global ArtistReleasesActions {
|
||
// Fetch the next page (infinite scroll / Load more).
|
||
callback load-more();
|
||
// Change the sort (default/newest/oldest/title-asc/title-desc).
|
||
callback set-sort(string);
|
||
// Retry after a failed load.
|
||
callback retry();
|
||
}
|
||
|
||
// ArtistsByLocationView state — opened from the Origin section's
|
||
// location link. Populated by `crate::location_view`. Artists ride the
|
||
// shared SlimItem model (subtitle = genres line) so the grid uses the
|
||
// app-wide ArtistGridCard (follow / play / pin / context menu).
|
||
export global LocationViewState {
|
||
in property <string> scene-label;
|
||
in property <string> genre-summary;
|
||
in property <[SlimItem]> 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();
|
||
}
|
||
|
||
// Interface-size preset (UI scale). Seeded once from Rust before the shell
|
||
// renders. The actual scaling happens through the window scale factor
|
||
// (SLINT_SCALE_FACTOR set in main.rs) — this global only lets bindings that
|
||
// must stay PHYSICALLY constant divide the preset back out (e.g. the window
|
||
// minimums, so an XL window still fits a 1366x768 screen).
|
||
export global UiScale {
|
||
in-out property <float> factor: 1.0;
|
||
}
|
||
|
||
// 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 {
|
||
// Kiosk profile active (2.0.2 frente #3). Seeded from Rust at startup and
|
||
// flipped by the NPB "Kiosk/Desktop mode" toggle; drives that toggle's label.
|
||
in-out property <bool> kiosk-profile: false;
|
||
// Set by the kiosk Now Playing while its Cover<->Lyrics toggle shows lyrics.
|
||
// The Rust lyrics-sync engine (lyrics_sync.rs `lyrics_surface_open`) polls
|
||
// this so the karaoke follow runs in the kiosk WITHOUT reusing the desktop
|
||
// `lyrics-open` (which also drives the compact drawer). Reset to false when
|
||
// showing the cover or leaving Now Playing.
|
||
in-out property <bool> kiosk-lyrics-follow: false;
|
||
// --- Reduce motion (weak renderer tiers) -------------------------------
|
||
// Seeded from Rust: true on the femtovg-GL / software tiers, where every
|
||
// animation frame is a full-window repaint on a weak GPU. When set,
|
||
// loading indicators / eq bars step at ~8fps from `coarse-tick-ms`
|
||
// (advanced by one Timer in AppShell) instead of the display-rate
|
||
// animation-tick(). Same phase math, 60fps -> 8fps repaints.
|
||
in-out property <bool> reduce-motion: false;
|
||
in-out property <int> coarse-tick-ms: 0;
|
||
// 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. Large NPB (mode 3) is gated
|
||
// on the OPEN sidebar (it docks the cover + spectrum at the sidebar bottom);
|
||
// so when the cycle leaves the open state while Large is active, the bar
|
||
// reverts LIVE to New (0). The persisted ui_prefs key stays "large", so a
|
||
// later relaunch (sidebar defaults open) restores Large — the revert is a
|
||
// compatibility fallback, not a preference change.
|
||
// Persist the sidebar state / nav placement (wired to ui_prefs in Rust).
|
||
callback persist-sidebar-state(int);
|
||
callback persist-nav(bool);
|
||
callback persist-nav-compact(bool);
|
||
// Persist the current top-level view for "Startup page = where you left off"
|
||
// (Rust reads NavState.view and stores only SAFE, id-free views).
|
||
callback persist-view();
|
||
public function cycle-sidebar() {
|
||
root.sidebar-state = Math.mod(root.sidebar-state + 1, 3);
|
||
if (root.sidebar-state != 0 && root.npb-mode == 3) {
|
||
root.npb-mode = 0;
|
||
}
|
||
root.persist-sidebar-state(root.sidebar-state);
|
||
}
|
||
// 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;
|
||
// Live WINDOW width (set from the AppWindow root). Views read it to make
|
||
// content-area responsiveness decisions independent of their own
|
||
// (sidebar-shrunk) width — e.g. the album Label/Awards sidebar collapses
|
||
// to an icon rail at the now-playing-bar breakpoint (1366px) WHEN a right
|
||
// panel is open. Defaults to the preferred window width.
|
||
in-out property <length> window-width: 1180px;
|
||
// True when the content area is space-constrained: window at/under the
|
||
// NPB responsive breakpoint AND a right panel (Queue/Lyrics) is open.
|
||
out property <bool> content-constrained:
|
||
root.window-width < 1366px && (root.queue-open || root.lyrics-open);
|
||
// Global pointer position in window coords, fed from winit. Used by
|
||
// hover-only chrome that must not install a TouchArea over interactive
|
||
// content (for example auto-hide scrollbars over album cards).
|
||
in-out property <bool> pointer-in-window: false;
|
||
in-out property <length> pointer-x: -1px;
|
||
in-out property <length> pointer-y: -1px;
|
||
// 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;
|
||
// Header-nav flavor while nav-in-sidebar is OFF: when true, the header
|
||
// always uses the icon-only compact nav (the sidebar-fully-closed look)
|
||
// instead of the full text tabs, regardless of the sidebar state. No
|
||
// effect while nav-in-sidebar is ON. Persisted (`nav_header_compact`).
|
||
in-out property <bool> nav-header-compact: false;
|
||
// Now-playing bar layout mode: 0 = New (default), 1 = Classic, 2 = Small,
|
||
// 3 = Large. Persisted in ui_prefs.json (`npb_mode`
|
||
// "new"|"classic"|"small"|"large"); set on startup from the store and on the
|
||
// player bar's "Now Playing view" menu select.
|
||
in-out property <int> npb-mode: 0;
|
||
// Small-mode (2) bar extra bottom clearance (owner ask 2026-07-21): the
|
||
// compact bar's CJK/descender-heavy meta line clipped at the window's
|
||
// bottom edge/bezel on macOS. The bar is bottom-anchored, so it grows
|
||
// UPWARD; PlayerBarSmall pins the song card to its old top-anchored
|
||
// geometry so the art keeps its position and the extra lands as bottom
|
||
// clearance. The AMOUNT is set from Rust at startup (main.rs, macOS-only
|
||
// — 0 elsewhere; QBZ_NPB_SMALL_EXTRA env override for tuning) because a
|
||
// .slint literal costs a ~2 h qbz-ui rebuild on the 8 GB Mac per
|
||
// iteration, a Rust-side constant ~2 min. 2px proved invisible
|
||
// (2026-07-22).
|
||
in-out property <length> npb-small-extra: 0px;
|
||
// (42px == Layout.header-height; literal kept so state.slint needs no
|
||
// foundation import — same precedent as large-dock-width below.)
|
||
out property <length> npb-small-height: 42px + root.npb-small-extra;
|
||
// Large NPB (mode 3) is only valid while the sidebar is OPEN (state 0): the
|
||
// cover + spectrum dock lives at the bottom of the open sidebar. Views read
|
||
// this to mount the dock and to drive the FFT visualizer tap.
|
||
out property <bool> large-active: root.npb-mode == 3 && root.sidebar-state == 0;
|
||
// Large dock column: open sidebar (240) − 16px left gutter − the content
|
||
// panel's 8px left padding, so the cover's RIGHT edge lands 16px from the
|
||
// VISIBLE content bezel — matching the 16px left window gutter (owner: equal
|
||
// side margins). 240 + 8 − 2*16 = 216. (16px == Spacing.md; literal kept so
|
||
// state.slint needs no foundation import.)
|
||
out property <length> large-dock-width: root.sidebar-rendered-width + 8px - 2 * 16px;
|
||
// Total bottom band the dock occupies — MUST byte-for-byte equal
|
||
// SidebarNowPlayingDock's self-derived `height` in BOTH visualizer states
|
||
// (AppShell pins the overlay via `y: root.height - large-dock-height`, so a
|
||
// mismatch un-flushes the cover). ON = spectrum 42 + 10 gap + square cover +
|
||
// 13 (9 top + 4 bottom) pad; OFF = cover + 13 (spectrum reclaimed by the list).
|
||
out property <length> large-dock-height: root.large-visualizer-on
|
||
? (42px + 10px + root.large-dock-width + 13px)
|
||
: (root.large-dock-width + 13px);
|
||
// Large dock spectrum visualization (the click-cycle is now 3-way; the old
|
||
// "Off" state is handled by the toggle below): 0 = Bars (mirrored), 1 =
|
||
// Waveform, 2 = Energy blocks. PERSISTED in ui_prefs (`large_spectrum_mode`)
|
||
// via the Rust ("npb-large", _, "spectrum-cycle") media-action — Slint never
|
||
// mutates it directly, so the choice survives restarts.
|
||
in-out property <int> large-spectrum-mode: 0;
|
||
// Large dock visualizer on/off (the top-right eye toggle on the cover).
|
||
// PERSISTED in ui_prefs (`large_visualizer`) via ("npb-large", _,
|
||
// "viz-toggle"). When off, the spectrum band collapses and the sidebar
|
||
// content reclaims the space; the FFT tap idles (AppShell viz-should-run).
|
||
in-out property <bool> large-visualizer-on: true;
|
||
|
||
// --- APP-WIDE DYNAMIC BACKGROUND (derived) -----------------------------
|
||
// Active only when a mode is picked, the wgpu tier allows it, AND a track is
|
||
// loaded — so with nothing playing the shell restores its opaque theme
|
||
// surfaces (D4). Shell chrome reads this to go translucent; AppShell reads
|
||
// it to mount the background layer.
|
||
out property <bool> app-background-active:
|
||
AppearanceState.app-background-mode-index > 0
|
||
&& AppearanceState.app-background-available
|
||
&& NowPlayingState.has-track;
|
||
}
|
||
|
||
// 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,
|
||
}
|
||
|
||
// ImmersiveView audio visualizers. Driven at ~30fps by the Rust drain
|
||
// (crate::visualizer) from the frontend-agnostic FFT producer in qbz-audio.
|
||
// Magnitudes are normalized 0..1. The tap stays disabled (no capture / no FFT
|
||
// cost) until the immersive view calls set-enabled(true) on open.
|
||
export global VisualizerState {
|
||
in property <bool> enabled: false;
|
||
in property <[float]> bars; // 16 log-spaced FFT bars (viz:data)
|
||
in property <[float]> spectral; // 512 spectral-ribbon bands (viz:spectral)
|
||
in property <[float]> energy; // 5 energy bands: sub-bass..air (viz:energy)
|
||
in property <[float]> waveform; // 256 L then 256 R samples (viz:waveform)
|
||
in property <float> transient: 0; // last transient intensity; UI decays it
|
||
callback set-enabled(bool);
|
||
}
|
||
|
||
// Full-window immersive now-playing overlay. The first Slint slice ships the
|
||
// Album Reactive focus view only; broader panels can hang off this same state.
|
||
export global ImmersiveState {
|
||
in-out property <bool> open: false;
|
||
in property <image> bg-image;
|
||
in property <color> glow-color: #6464ff59;
|
||
// View mode — Tauri's focus/split duality (ImmersivePlayer.svelte focus
|
||
// vs split layout, recon data-panels.md §"two layout contexts"):
|
||
// 0 = FOCUS — full-bleed single panel (the visualizer modes + Lyrics-focus).
|
||
// 1 = SPLIT — artwork + metadata on the LEFT, the active data panel on the
|
||
// RIGHT. The leading header icon (was inert) toggles this 0<->1.
|
||
// Default 0 (focus) so opening lands on a real full-bleed foreground.
|
||
in-out property <int> view-mode: 0;
|
||
// FOCUS-mode foreground for the conditional-mounted panel (read only while
|
||
// view-mode == 0):
|
||
// 0 = Album Reactive, 1 = Static, 2 = Coverflow, 3 = Spectrum,
|
||
// 4 = Lyrics-focus (one giant centered current line),
|
||
// 5 = Queue-focus (full-screen Queue + History panel; reuses QueueState,
|
||
// no extra state field — same component as the split Queue panel).
|
||
// SESSION default 0 (Album Reactive) so opening shows a real foreground.
|
||
// NOTE: this compact enum is a Slint-scaffold contract; it deliberately
|
||
// does NOT match Tauri's focus-tab string order (coverflow first). Product
|
||
// default + persistence ("remember last view") is a later (header) session.
|
||
in-out property <int> mode: 0;
|
||
// SPLIT-mode active panel (read only while view-mode == 1):
|
||
// 0 = Lyrics, 1 = Track Info, 2 = Suggestions, 3 = Queue.
|
||
// Checkpoint A ships only Lyrics (0); the other panels land in their own
|
||
// checkpoints. Default 0.
|
||
in-out property <int> split-panel: 0;
|
||
// Spectrum bar gradient, derived from the album art (contrast colors;
|
||
// negative fallback for dark covers so the bars read against the black bg).
|
||
// Defaults = the Svelte cyan -> purple until the art resolves.
|
||
in property <color> spectrum-primary: #00dcc8;
|
||
in property <color> spectrum-secondary: #9632ff;
|
||
// Lyrics-focus single-line accent: derived from the album art but rotated
|
||
// to the COMPLEMENT of the cover's dominant hue, so it contrasts with the
|
||
// atmosphere (which is built from the same cover) instead of blending into
|
||
// it. Default high-contrast teal until the art resolves / for B&W covers.
|
||
in property <color> lyrics-accent: #3fd9c8;
|
||
|
||
// GPU fragment-shader underlay: a WGSL shader rendered into a wgpu texture by
|
||
// src/shader_underlay.rs and handed back here each drain tick. `shader-mode`
|
||
// selects the scene:
|
||
// 0 = OFF — use the existing atmosphere + visualizer panels.
|
||
// 1 = Plasma — classic sin-field plasma (ui/shaders/plasma.wgsl).
|
||
// 2 = Tunnel — neon polar-warped tunnel (ui/shaders/tunnel.wgsl).
|
||
// 3 = Aurora — domain-warped aurora curtains (ui/shaders/aurora.wgsl).
|
||
// Each scene maps to shader_underlay pipeline index (shader-mode - 1). When
|
||
// shader-mode > 0 the atmosphere + the mode panels are hidden so the shader
|
||
// is the visible background; the auto-hide chrome (scene picker + compact
|
||
// player) stays on top. Pick a scene via the header shader picker, or cycle
|
||
// 0->1->2->3->0 with the 'g' key.
|
||
in property <image> shader-texture;
|
||
in-out property <int> shader-mode: 0;
|
||
// App-wide dynamic-background scene selector, kept SEPARATE from
|
||
// `shader-mode` so opening the immersive overlay (which force-resets
|
||
// shader-mode to 0 in main.rs) does NOT clobber the app-wide background.
|
||
// 0 = none, 7 = Ambient (ui/shaders/ambient.wgsl). The drain renders this
|
||
// scene into `shader-texture` when the immersive overlay is CLOSED (see
|
||
// visualizer.rs); the AppShell root Image binds the same `shader-texture`.
|
||
// Derived: Ambient (mode 1) → 7, everything else → 0. No Rust setter needed.
|
||
out property <int> app-shader-mode: AppearanceState.app-background-mode-index == 1 ? 7 : 0;
|
||
// False on the femtovg-GL / software renderer tiers (no wgpu underlay →
|
||
// shader scenes would render black). Seeded from Rust at startup; gates
|
||
// the picker rows and the `g` cycle in ImmersiveView.
|
||
in-out property <bool> shader-scenes-available: true;
|
||
|
||
// --- Immersive search cortinilla (parallel to SearchState.cortinilla-*) ---
|
||
// Albums/Artists/Playlists only; selecting ACTS ON PLAYBACK (per the Settings
|
||
// "search action") instead of navigating, since immersive has no nav. While
|
||
// search-open is true the HUD must NOT auto-hide. See the immersive-search
|
||
// design spec (qbz-nix-docs/specs/2026-06-20-immersive-search-design.md).
|
||
in-out property <bool> search-open: false;
|
||
// The TRIMMED query the cortinilla reflects (controller-set, display only).
|
||
in property <string> search-query;
|
||
// The raw immersive search TextInput text, two-way bound to the field (in-out)
|
||
// so the controller can CLEAR it after the user activates a result —
|
||
// consistency with the main header cortinilla. Kept SEPARATE from the trimmed
|
||
// `search-query` so the controller writing the trimmed value never fights the
|
||
// user's in-progress text (e.g. trailing spaces).
|
||
in-out property <string> search-input-text: "";
|
||
in property <bool> search-loading: false;
|
||
in property <[CortinillaSection]> search-sections: [];
|
||
in-out property <int> search-selected-index: -1;
|
||
in property <length> search-scroll-y: 0px;
|
||
}
|
||
|
||
// Callbacks for the immersive search cortinilla — parallel to SearchActions but
|
||
// scoped to the immersive overlay (load on type, keyboard nav, select-acts-on-
|
||
// playback, dismiss). Wired in main.rs; active only while ImmersiveState.open.
|
||
export global ImmersiveSearchActions {
|
||
callback live(string /* query */);
|
||
callback row-clicked(int /* flat-index */);
|
||
callback move-selection(int /* delta: -1 up / +1 down */);
|
||
callback dismiss();
|
||
}
|
||
|
||
// Lifecycle hooks for the immersive overlay, wired in main.rs. The ImmersiveView
|
||
// component is conditionally mounted (it exists only while the overlay is OPEN),
|
||
// so its `init` == the overlay opening. `opened()` lets Rust apply the configured
|
||
// default view (pinned, or restore-last); `view-changed()` lets Rust persist the
|
||
// current view when the configured default is "remember last".
|
||
export global ImmersiveActions {
|
||
callback opened();
|
||
callback view-changed();
|
||
}
|
||
|
||
// Native window-control actions for the MAIN AppWindow, exposed to the
|
||
// immersive overlay header cluster. The AppWindow uses native OS decorations,
|
||
// so the only seam to manipulate it is the winit backend (see
|
||
// src/main.rs wire_window_controls): every callback below reaches the MAIN
|
||
// window's winit handle via WinitWindowAccessor::with_winit_window. The two
|
||
// bool props drive the glyph swap on the maximize/fullscreen buttons; they are
|
||
// only updated on OUR OWN toggles, so they can go stale if the user
|
||
// maximizes/fullscreens via the OS chrome (acceptable for v1).
|
||
export global WindowControlActions {
|
||
in property <bool> is-maximized: false;
|
||
in property <bool> is-fullscreen: false;
|
||
callback minimize();
|
||
callback toggle-maximize();
|
||
callback toggle-fullscreen();
|
||
callback close-app();
|
||
callback drag-move();
|
||
}
|
||
|
||
// 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;
|
||
// Secondary alias for the playing track: the LOCAL-LIBRARY ROW ID when the
|
||
// track is an offline-cache copy (queue track-id carries the Qobuz catalog
|
||
// id there; set by refresh_now_playing_meta in playback.rs). Empty for every
|
||
// other source. TrackPlayCell / TrackRow / KioskAlbum match on either id.
|
||
in property <string> local-track-id;
|
||
in property <string> artist;
|
||
in property <string> album;
|
||
in property <string> album-id;
|
||
// Whether the playing track's ALBUM is a library favorite. Seeded from
|
||
// fav_cache in refresh_now_playing_meta (playback.rs) on every track-meta
|
||
// push, and flipped live by set_album_row_favorite (main.rs) when a
|
||
// favorite toggle from ANY surface matches album-id. Drives the "+"
|
||
// flyout's add/remove-album-to-collection entry (TransportControls).
|
||
in property <bool> album-favorite: false;
|
||
in property <string> artist-id;
|
||
in property <image> artwork;
|
||
// Higher-res cover (~300px) for the hover preview that floats above the
|
||
// now-playing bar's small art, so the ~220px popup is crisp instead of an
|
||
// upscale of the 160px bar art. Loaded alongside `artwork`; cleared on
|
||
// track change. Falls back to `artwork` in the UI until it resolves.
|
||
in property <image> artwork-large;
|
||
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;
|
||
// Playing track's source: "qobuz" | "local" | "plex" | "ephemeral". Track
|
||
// info is a Qobuz-only feature, so its trigger is gated on `source ==
|
||
// "qobuz"` (local/Plex/ephemeral tracks have no Qobuz metadata page).
|
||
in property <string> source;
|
||
// Parental-warning / explicit flag for the playing track. Drives the
|
||
// explicit badge in the miniplayer compact + artwork surfaces (Tauri parity).
|
||
in property <bool> explicit: 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"
|
||
// Numeric stream params for the Spectral Ribbon overlay (header + freq axis
|
||
// Nyquist). 0 = unknown.
|
||
in property <int> sample-rate-hz: 0;
|
||
in property <int> bit-depth: 0;
|
||
|
||
// EFFECTIVE (delivered) stream quality — what the engine is actually
|
||
// decoding after the streaming-quality setting downgrade (#590 follow-up).
|
||
// `quality-tier`/`quality-detail` above keep the catalog MAX; while
|
||
// `quality-downgraded` is on the AudioStamp's MAIN line reports the
|
||
// delivered tier/detail instead (#638 fix 1 — owner decision, Tauri
|
||
// parity), the amber arrow turns on, and `quality-true-detail` carries
|
||
// the delivered "16-bit / 44.1 kHz" line. 0 / "" = unknown.
|
||
in property <int> effective-sample-rate-hz: 0;
|
||
in property <int> effective-bit-depth: 0;
|
||
in property <bool> quality-downgraded: false;
|
||
in property <string> quality-true-detail;
|
||
// WHY the delivered stream is below the catalog max (#638 fix 1) — the
|
||
// AudioStamp tooltip names the cause so a downgrade the user configured
|
||
// never reads as a failure. Discriminants of the Rust `QualityLimit`
|
||
// enum: 0 none · 1 streaming-quality preference · 2 local output device
|
||
// cap (fix 3) · 3 per-renderer cap (fix 4) · 4 Qobuz catalog
|
||
// availability.
|
||
in property <int> quality-limit-cause: 0;
|
||
// Delivered tier for the badge's main line while downgraded ("hires" |
|
||
// "cd" | "mp3"); empty = not downgraded (the badge shows the catalog
|
||
// tier). Derived in Rust from the delivered params + the requested tier
|
||
// so an MP3-capped stream is not mislabeled CD.
|
||
in property <string> quality-effective-tier;
|
||
|
||
// Output backend + mode LEDs (Mode C "Small" bar reads these). Mirror of
|
||
// the same four on SettingsState (:2154-2157) — surfaced here so the
|
||
// Small now-playing bar has ONE source for the song card + DAC/EXC
|
||
// cluster. Pushed on every audio snapshot-apply (src/settings.rs).
|
||
in property <string> output-backend-label: "SYST"; // PIPE|ALSA|JACK|PULS|SYST
|
||
in property <string> output-mode-label: "DEFAULT"; // DACPASS|BITPERF|EXCL|DIRECT|LOCKED|ROUTED|SHARED|DEFAULT
|
||
in property <bool> output-backend-active: false;
|
||
in property <bool> output-mode-active: false;
|
||
|
||
// 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 — live (fires on every drag tick)
|
||
callback persist-volume(float); // 0..1 — drag-end only, persists to ui_prefs
|
||
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();
|
||
}
|
||
|
||
// Miniplayer-specific state. The miniplayer is a SECOND Slint Window with its
|
||
// OWN global instances (Slint globals are per-component-tree, NOT shared with
|
||
// AppWindow), so the Rust layer mirrors NowPlayingState/LyricsState into the
|
||
// mini window's instances and drives THIS global for mini-only concerns:
|
||
// the active surface, always-on-top, hover (capsule reveal), the AOT warning
|
||
// chip, and a SELF-CONTAINED navigable queue model (current + full upcoming,
|
||
// NOT QueueState.upcoming-page which is the 40-row sidebar slice).
|
||
export global MiniPlayerState {
|
||
// 0 micro · 1 compact · 2 artwork · 3 queue · 4 lyrics
|
||
in-out property <int> surface: 2;
|
||
// Static artwork-derived background for the mini surfaces.
|
||
in-out property <bool> background-blur: false;
|
||
// Whole-window hover — reveals the window-controls capsule. Fed from the
|
||
// mini window's winit CursorMoved/CursorLeft (or a root TouchArea).
|
||
in-out property <bool> window-hovered: false;
|
||
|
||
// Self-contained navigable queue: current track first, then the FULL
|
||
// de-duplicated upcoming list (no 20-cap). Scrollable in the surface.
|
||
in property <[QueueItem]> queue-tracks: [];
|
||
in property <string> current-track-id;
|
||
|
||
// Surface navigation + window actions (wired in main.rs to crate::miniplayer).
|
||
callback surface-change(int); // 0..4 → apply geometry + persist
|
||
callback expand(); // exit mini, show main app
|
||
callback close-app(); // close the app from the mini
|
||
callback toggle-background-blur(); // toggle static artwork backdrop
|
||
callback start-drag(); // begin a borderless window drag
|
||
// Per-row play — index into queue-tracks. Routes remote-first (REQ-2):
|
||
// play_track_if_remote, bail before mutating local; current row resumes.
|
||
callback queue-play(int);
|
||
}
|
||
|
||
// One rendered lyrics line. `time-ms`/`end-ms` are milliseconds from track
|
||
// start (-1 = unknown / plain line). `has-words` flags native Qobuz word
|
||
// timing held Rust-side (`crate::lyrics::CURRENT_DOC`) — the S4 sync engine
|
||
// reads it for the word-anchored karaoke fill; word data itself never
|
||
// crosses into the UI model.
|
||
export struct LyricsLineItem {
|
||
text: string,
|
||
time-ms: int,
|
||
end-ms: int,
|
||
has-words: bool,
|
||
// Translation of this line (Qobuz v10), aligned 1:1 with `text` by the
|
||
// Rust glue; empty when the doc has no translation (or it wasn't
|
||
// requested). Rendered under the original line only while
|
||
// `LyricsState.show-translation` is on.
|
||
translation-text: string,
|
||
}
|
||
|
||
// One VISUAL line of the ACTIVE logical line, for the per-visual-line karaoke
|
||
// highlight. The engine (`lyrics_measure` + `lyrics_sync`) measures the active
|
||
// line's word-wrap against the current content width + font + size, and pushes
|
||
// one segment per visual row so the highlight fills row 1 to 100% BEFORE row 2
|
||
// starts (matching Tauri) instead of lighting every wrapped row at once.
|
||
//
|
||
// `start-weight` = cumulative width fraction (0..1) of all PRIOR segments;
|
||
// `width-weight` = this segment's width / total line width. The row re-maps the
|
||
// single global `line-progress` to a per-segment local fraction:
|
||
// local = clamp((line-progress - start-weight) / width-weight, 0, 1)
|
||
export struct LyricsSegment {
|
||
text: string,
|
||
start-weight: float,
|
||
width-weight: float,
|
||
}
|
||
|
||
// Lyrics panel state. The Rust glue (`crate::lyrics`) pushes fetch results
|
||
// here on the de-duped track-change edge; the sidebar binds directly. The
|
||
// S4 sync engine drives `active-index` (placeholder -1 until then).
|
||
export global LyricsState {
|
||
// 0 idle · 1 loading · 2 ready · 3 no lyrics · 4 error · 5 offline-unavailable
|
||
in property <int> status: 0;
|
||
in property <[LyricsLineItem]> lines: [];
|
||
in property <bool> synced: false;
|
||
// Provider id ("qobuz" | "lrclib" | "ovh"); `provider-label` is the
|
||
// attribution display name — empty for first-party Qobuz (no attribution
|
||
// needed, spec §3.5).
|
||
in property <string> provider;
|
||
in property <string> provider-label;
|
||
// Playing-track meta for the header ("{title} - {artist}").
|
||
in property <string> track-title;
|
||
in property <string> track-artist;
|
||
|
||
// ---- Translation (Qobuz API v10) ---------------------------------------
|
||
// The current doc advertises at least one translation language
|
||
// (`translation_langs` non-empty) — gates the sidebar's floating toggle.
|
||
// False on every other state, so the sidebar renders exactly as before.
|
||
in property <bool> translation-available: false;
|
||
// User intent: render the translation under each original line. Driven by
|
||
// Rust (`toggle-translation`): ON refetches with the target language and
|
||
// flips this only when the translation actually arrived; OFF is pure UI.
|
||
in-out property <bool> show-translation: false;
|
||
// Sidebar floating-button action (wired in main.rs).
|
||
callback toggle-translation();
|
||
// Surfaced backend error text (status == 4) — dynamic content, not chrome.
|
||
in property <string> error;
|
||
// Active line index; -1 = none. The S4 sync engine drives it.
|
||
in property <int> active-index: -1;
|
||
// Karaoke clip fraction 0..1 of the ACTIVE line's rendered width — the
|
||
// S4 engine pushes it at ~30Hz while playing: word-anchored when the
|
||
// current doc has native Qobuz wsync words, line-proportional for LRC
|
||
// sources (Q2). Drives the clip-rect width in LyricsLinesView.
|
||
in property <float> line-progress: 0.0;
|
||
// Clip-width animation duration (ms). Slightly above the engine tick
|
||
// while progress is continuous so the fill sweeps smoothly between
|
||
// pushes; the engine sets 0 across discontinuities (line change /
|
||
// backward seek) so the fill first-paints exactly at the reported
|
||
// progress (Tauri parity, LyricsLines.svelte:114-138).
|
||
in property <int> fill-anim-ms: 0;
|
||
// Per-visual-line segmentation of the ACTIVE line, for the karaoke
|
||
// highlight on wrapped lines. The engine recomputes it when the active
|
||
// index, content-width, font-index or size-index changes (low frequency)
|
||
// and pushes one entry per visual row. Empty until the first active line.
|
||
in property <[LyricsSegment]> active-segments: [];
|
||
// The lyrics view's current text content width (column width minus side
|
||
// padding). The view pushes it from its root `changed width`; the engine
|
||
// uses it as the wrap budget when segmenting the active line.
|
||
in property <length> content-width: 0px;
|
||
// Fired by the sidebar's `init` (conditional mount, ADR-010): re-requests
|
||
// lyrics for the current track (cache-served when already fetched).
|
||
callback panel-opened();
|
||
|
||
// ---- Display prefs (S5) -------------------------------------------------
|
||
// 1:1 to Tauri's lyricsDisplayStore (defaults lyricsDisplayStore.ts:37-44),
|
||
// persisted per-user as JSON by `crate::lyrics_prefs` and seeded on every
|
||
// session activation. The flyout mutates these in-out props directly and
|
||
// fires `prefs-changed()`; Rust reads them back and persists.
|
||
in-out property <bool> auto-follow: true;
|
||
// 0 System · 1 LINE Seed JP · 2 Montserrat · 3 Noto Sans · 4 Source Sans 3
|
||
// (Q6 SHIPPED: the four families are bundled TTFs embedded at compile time
|
||
// in LyricsLinesView.slint; "System" = empty font-family -> window default).
|
||
in-out property <int> font-index: 0;
|
||
// 0 S · 1 M · 2 L · 3 XL — Tauri default 'medium'. Px mapping (the
|
||
// 280px-adapted scale, Q3) lives in LyricsSidebar.
|
||
in-out property <int> size-index: 1;
|
||
// 0 off · 1 soft · 2 strong (Tauri default 'strong').
|
||
in-out property <int> dimming-mode: 2;
|
||
in-out property <bool> uppercase: false;
|
||
// Lite fill (perf): when true, the active line is highlighted whole instead
|
||
// of the per-word karaoke sweep — no per-frame clip animation, so the lyrics
|
||
// surface stops driving continuous full-window repaints (~4% CPU -> idle
|
||
// between line changes). Default false (smooth karaoke). Kiosk / weak-GPU
|
||
// tiers force it on via ShellState.reduce-motion regardless of this pref.
|
||
in-out property <bool> lite-fill: false;
|
||
// Translation language (Qobuz v10, persisted like the other prefs):
|
||
// 0 Auto (account language) · 1..9 = en es fr de it pt nl ja ru. Option
|
||
// labels: the Auto entry is @tr'd, the language names are native
|
||
// literals — the same convention as AppearanceState.languages. The @tr
|
||
// DEFAULT is not reactive, so `reseed_i18n_labels` re-pushes the array
|
||
// on language change.
|
||
in-out property <int> translation-language-index: 0;
|
||
in-out property <[string]> translation-language-options: [
|
||
@tr("Auto (account language)"),
|
||
"English",
|
||
"Español",
|
||
"Français",
|
||
"Deutsch",
|
||
"Italiano",
|
||
"Português",
|
||
"Nederlands",
|
||
"日本語",
|
||
"Русский",
|
||
];
|
||
// Active (karaoke fill) color. `use-custom-color: false` = follow the theme
|
||
// accent (Tauri activeColor: ''). The palette swatches set both (deviation
|
||
// D6: swatches instead of a native color input — Slint has none).
|
||
in-out property <bool> use-custom-color: false;
|
||
in-out property <color> custom-color: #8b5cf6;
|
||
// Fired by the flyout after ANY pref mutation -> Rust persists the set.
|
||
callback prefs-changed();
|
||
// Footer actions: join current lines -> clipboard (+ toast) / restore the
|
||
// Tauri defaults (+ persist + re-seed).
|
||
callback copy-lyrics();
|
||
callback reset-prefs();
|
||
|
||
// ---- Lyrics cache row (Settings > Offline, S5) --------------------------
|
||
// Stats read from the REAL per-user lyrics.db via qbz_lyrics cache_stats
|
||
// (fix-forward F1 — Tauri measured a stale global path). Pre-formatted in
|
||
// Rust; `cache-loaded` gates the description line.
|
||
in property <bool> cache-loaded: false;
|
||
in property <string> cache-entries-text;
|
||
in property <string> cache-size;
|
||
callback cache-refresh();
|
||
callback cache-clear();
|
||
}
|
||
|
||
// 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"
|
||
}
|
||
|
||
// Cast (Chromecast / DLNA) — distinct from Qobuz Connect above. A discovered
|
||
// renderer for the cast picker. Modeled as a tagged record (the `protocol`
|
||
// field) so Chromecast and DLNA share one list; DLNA carries capability flags.
|
||
export struct CastDevice {
|
||
id: string,
|
||
name: string,
|
||
ip: string,
|
||
protocol: string, // "chromecast" | "dlna"
|
||
model: string,
|
||
can-play: bool, // DLNA has-av-transport (Chromecast: true)
|
||
can-set-volume: bool, // DLNA has-rendering-control (Chromecast: true)
|
||
}
|
||
|
||
// Cast picker + connection + transport-mirror state. Populated by
|
||
// `cast_service.rs`; read by CastPicker and AudioOutputBadges. There is NO
|
||
// error/connecting sub-state in the Tauri original beyond a raw error string.
|
||
export global CastState {
|
||
in-out property <bool> picker-open: false;
|
||
in-out property <string> selected-tab: "chromecast"; // "chromecast" | "dlna"
|
||
in property <bool> scanning: false; // 15s scan-spinner window
|
||
in property <[CastDevice]> devices: [];
|
||
in property <int> chromecast-count: 0;
|
||
in property <int> dlna-count: 0;
|
||
// Connection.
|
||
in property <bool> connected: false;
|
||
in property <string> protocol; // "chromecast" | "dlna" | ""
|
||
in property <string> device-name; // connected device label
|
||
// Transport mirror (cast owns transport while connected).
|
||
in property <bool> is-playing: false;
|
||
in property <float> position-secs: 0;
|
||
in property <float> duration-secs: 0;
|
||
// Delivered quality — surfaced in the badge tooltip / picker banner.
|
||
// MEASURED from the served bytes when probeable (#638 fix 1), catalog
|
||
// fallback otherwise.
|
||
in property <string> quality-label; // coarse, e.g. "Hi-Res FLAC"
|
||
in property <string> quality-detail; // precise, e.g. "24-bit / 192 kHz"
|
||
// Fix-1 cast half (#638): measured-truth disclosure for the picker line.
|
||
// quality-limit-cause carries the same QualityLimit discriminants as
|
||
// NowPlayingState.quality-limit-cause (0 none · 1 preference · 3
|
||
// renderer cap · 4 availability; 2 = local device cap NEVER applies
|
||
// while casting — precedence rule).
|
||
in property <int> quality-limit-cause: 0;
|
||
// The served bytes EXCEED the requested tier (a locally-existing
|
||
// cache/offline copy under a lower cap) — sent as-is, never resampled
|
||
// (owner decision); the picker line discloses it instead of hiding it.
|
||
in property <bool> quality-over-cap: false;
|
||
in property <string> quality-origin; // "cache" | "offline" | "" (network)
|
||
// Per-renderer manual quality cap (#638 fix 4) — the connected device's
|
||
// cap row in the picker. Only offered when the renderer has a STABLE
|
||
// identity (DLNA UDN always; Chromecast only when the mDNS TXT `id`
|
||
// record exists — a fullname-keyed cap would silently detach on
|
||
// rename). Options are pushed from Rust and reseeded on language
|
||
// change; option 0 embeds the live global streaming-quality label. The
|
||
// cap is the user's own quality/bandwidth choice — never a statement
|
||
// that the device cannot play the stream (renderers accept ~anything
|
||
// and downsample internally).
|
||
in property <bool> device-cap-available: false;
|
||
in property <string> device-cap-key; // "dlna:<UDN>" | "chromecast:<id>" | ""
|
||
in property <[string]> device-cap-options: [];
|
||
in property <int> device-cap-index: 0; // 0 follow · 1 hires · 2 cd · 3 mp3
|
||
in property <string> global-quality-label; // live Settings tier, e.g. "Hi-Res+"
|
||
// Raw last-error for the picker error state (empty = none).
|
||
in property <string> error;
|
||
}
|
||
|
||
export global CastActions {
|
||
callback open(); // open picker + start discovery
|
||
callback close(); // close picker + stop discovery
|
||
callback refresh(); // re-arm discovery scan
|
||
callback connect(string, string); // device-id, protocol
|
||
callback disconnect();
|
||
// Persist the manual quality cap for the connected renderer (#638
|
||
// fix 4): cap-key, option index (0 = follow the app setting).
|
||
callback set-device-quality-cap(string, int);
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
// Floating album-art preview (now-playing cover hover). Overlay-rendered like
|
||
// the tooltip so it NEVER steals hover from the trigger — a PopupWindow DID,
|
||
// which flipped the trigger's has-hover and made the preview flicker. The
|
||
// overlay reads the cover straight from NowPlayingState; this only carries the
|
||
// anchor + visibility.
|
||
export global ArtPreviewState {
|
||
in-out property <length> anchor-x; // art centre, window coords
|
||
in-out property <length> anchor-y; // art 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: [];
|
||
|
||
// --- Immersive coverflow (mode 2) ------------------------------------
|
||
// ONE STABLE flat ordered model of the whole queue, oldest-first:
|
||
// [history.reversed (oldest..newest), NOW-PLAYING, upcoming...]
|
||
// This list and its decoded cover handles do NOT change on a pure track
|
||
// advance/jump — only `coverflow-index` (the flat position of NOW) moves,
|
||
// and the .slint `scroll` float animates to it. The model is rebuilt by the
|
||
// Rust controller ONLY when the queue CONTENTS change (different id-sequence:
|
||
// new queue / shuffle / add / remove), gated by `coverflow-seq-hash`. On a
|
||
// pure advance the model setter is SKIPPED entirely (no Repeater rebuild, no
|
||
// Image churn), so visible covers never re-decode. Windowed (±4) lazy decode
|
||
// keeps at most one new cover decoding per advance.
|
||
in property <[QueueItem]> coverflow-tracks: [];
|
||
// The SAME flat model, REVERSED (last..first). Rebuilt under the same
|
||
// id-sequence gate as `coverflow-tracks` (so equally stable — never touched
|
||
// on a pure advance). The RIGHT (upcoming) Repeater iterates THIS list so it
|
||
// paints far-upcoming -> near-upcoming: a Slint Repeater paints in model
|
||
// order and there is no per-element z-index in Slint 1.16, so reversing the
|
||
// model is how we get the nearer (upcoming) cover to paint ON TOP of farther
|
||
// ones — symmetric with the LEFT side, which already paints far->near in
|
||
// ascending index order. A track's element holds the SAME decoded image on
|
||
// both lists (the reversed list is built by cloning + reversing the forward
|
||
// QueueItems), so no extra decode and offsets computed from the original
|
||
// index match the forward side exactly during the ±0.5 slide hand-off.
|
||
in property <[QueueItem]> coverflow-tracks-rev: [];
|
||
// Flat index of the NOW-PLAYING cover within `coverflow-tracks`
|
||
// (= history.len() when a current track exists, else 0). The .slint panel
|
||
// binds an animated `scroll` float to this int so a change eases the fan.
|
||
in property <int> coverflow-index: 0;
|
||
// Order-sensitive rolling fingerprint of the flat id-sequence. The Rust
|
||
// controller compares this to the value it last pushed; equal => pure
|
||
// advance => skip the model rebuild and only update `coverflow-index`.
|
||
in property <int> coverflow-seq-hash: 0;
|
||
|
||
// --- 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 an upcoming entry by its QUEUE-WIDE (unfiltered) index. Used by the
|
||
// immersive coverflow: a side card at flat index `i > coverflow-index` maps
|
||
// to up_index = i - coverflow-index - 1 and dispatches here (page/search-
|
||
// independent) — NOT the paged play-upcoming.
|
||
callback play-coverflow-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);
|
||
// Remove every upcoming entry after the page-local `index` (that row is
|
||
// kept). The Rust controller resolves it to a queue-wide upcoming index.
|
||
callback remove-all-after(int);
|
||
// Open the Add-to-Playlist picker seeded with the upcoming row at the
|
||
// page-local `index`.
|
||
callback add-to-playlist(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;
|
||
|
||
// "Stop after this song" marker: the decimal-string id of the marked track,
|
||
// or "" for none. Each queue row compares its item.id to this to show the
|
||
// CircleStop glyph. `toggle-stop-after` flips the marker for a given track id.
|
||
callback toggle-stop-after(string /* track-id */);
|
||
in property <string> stop-after-id: "";
|
||
}
|
||
|
||
// Sleep timer (queue footer). Frontend-only: a Rust tokio task owns the monotonic
|
||
// deadline + 1 Hz countdown and calls core().pause() at expiry. Non-persistent.
|
||
export global SleepTimerState {
|
||
in property <bool> active: false;
|
||
in property <string> remaining: ""; // pre-formatted by Rust (single source of truth)
|
||
in-out property <bool> popover-open: false;
|
||
in-out property <int> selected-index: 0; // preset index; last entry = Custom
|
||
in-out property <int> custom-minutes: 60;
|
||
}
|
||
export global SleepTimerActions {
|
||
callback set(int /* minutes */);
|
||
callback cancel();
|
||
}
|
||
|
||
// In-app log viewer (Settings > Developer). One row of the [LogRow] model.
|
||
export struct LogRow {
|
||
ts: string,
|
||
level: string,
|
||
target: string,
|
||
message: string,
|
||
}
|
||
export global LogViewerState {
|
||
in-out property <bool> open: false;
|
||
in property <[LogRow]> rows: []; // already filtered + capped by Rust
|
||
in property <int> total: 0; // ring size
|
||
in property <int> shown: 0; // after filter + cap
|
||
in-out property <string> filter-level: "all"; // all|error|warn|info|debug|trace
|
||
in-out property <string> search: "";
|
||
in-out property <bool> auto-tail: false;
|
||
in property <bool> uploading: false;
|
||
in property <string> uploaded-url: "";
|
||
in property <bool> copied: false;
|
||
callback refresh();
|
||
callback clear();
|
||
callback set-level(string);
|
||
callback set-search(string);
|
||
callback copy-all();
|
||
callback copy-bundle();
|
||
callback upload(); // public paste (already redacted) -> uploaded-url
|
||
callback open-log-file();
|
||
callback toggle-auto-tail(bool);
|
||
callback copy-url(); // copy the uploaded paste URL to the clipboard
|
||
}
|
||
|
||
// Report-an-issue modal (hamburger menu). Mirrors the Tauri ReportIssueModal:
|
||
// explains the manual, redacted log-sharing flow, then offers "Go to logs"
|
||
// (opens the log viewer) and "Create issue" (opens the GitHub new-issue page).
|
||
export global ReportIssueState {
|
||
in-out property <bool> open: false;
|
||
}
|
||
export global ReportIssueActions {
|
||
callback create-issue(); // opens the GitHub new-issue URL (Rust: open::that)
|
||
}
|
||
|
||
// Diagnostics panel (Settings > Developer). One saved-vs-runtime table row.
|
||
// status: 0 info | 1 match | 2 mismatch.
|
||
export struct DiagRow {
|
||
label: string,
|
||
saved: string,
|
||
runtime: string,
|
||
status: int,
|
||
}
|
||
export global DiagnosticsState {
|
||
in property <bool> loaded: false;
|
||
in property <bool> loading: false;
|
||
in property <string> error: "";
|
||
in property <string> app-version: "";
|
||
in property <bool> copied: false;
|
||
in property <bool> cast-scanning: false;
|
||
in property <[DiagRow]> system-rows: [];
|
||
in property <[DiagRow]> playback-rows: [];
|
||
in property <[DiagRow]> qconnect-rows: [];
|
||
in property <[DiagRow]> cast-rows: [];
|
||
in property <[DiagRow]> audio-rows: [];
|
||
in property <[DiagRow]> graphics-rows: [];
|
||
in property <[DiagRow]> env-rows: [];
|
||
callback refresh();
|
||
callback export-clipboard();
|
||
callback cast-scan();
|
||
}
|
||
|
||
// One row in the "Add to playlist" picker.
|
||
export struct PlaylistPickItem {
|
||
id: string,
|
||
name: string,
|
||
tracks-line: string, // "N tracks"
|
||
// LOCAL playlist (library.db entity, id "local:<uuid>") — the row
|
||
// shows a hard-drive glyph; adds write the local repo (works offline).
|
||
is-local: bool,
|
||
// Client-side filter result — recomputed Rust-side on every filter
|
||
// keystroke (Slint 1.16 has no string `.contains`, so the substring
|
||
// match lives in Rust). Match ordinal among the filtered rows;
|
||
// -1 = filtered out. Rows ranked >= 8 hide behind the overflow row.
|
||
filter-rank: int,
|
||
}
|
||
|
||
// "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;
|
||
// Client-side filter: case-insensitive substring over playlist names.
|
||
// Pure frontend, no backend call. The list filters live as the user types.
|
||
in-out property <string> filter: "";
|
||
// Total rows matching the current filter — drives the dropdown's
|
||
// no-results/overflow rows and its explicit height.
|
||
in property <int> filter-matches: 0;
|
||
// Inline "Create new playlist" affordance (mirrors Tauri's
|
||
// CREATE_NEW_PLAYLIST inline name field). When `creating-open` is true the
|
||
// name input is revealed; `create-name` holds the typed name; `creating`
|
||
// gates the confirm while the create-and-add runs.
|
||
in-out property <bool> creating-open: false;
|
||
in-out property <string> create-name: "";
|
||
in-out property <bool> creating: false;
|
||
}
|
||
|
||
export global PlaylistPickerActions {
|
||
callback pick(string /* playlist id */);
|
||
callback close();
|
||
// Inline create-and-add: creates a new playlist (Qobuz online / local
|
||
// offline per D8) and adds the carried tracks to it.
|
||
callback create-and-add();
|
||
// Client-side filter changed — Rust recomputes each row's filter-rank
|
||
// plus filter-matches (case-insensitive substring) since Slint 1.16
|
||
// lacks string `.contains`.
|
||
callback filter-changed(string);
|
||
}
|
||
|
||
// "Duplicate tracks" confirm sub-modal — shown when a Qobuz-tracks → Qobuz-
|
||
// playlist add finds tracks already in the target. The track-id Vec<u64> and
|
||
// the duplicate id set live Rust-side in a stash cell (Slint never holds
|
||
// them); this global only carries the counts + busy flag for rendering.
|
||
export global DuplicateConfirmState {
|
||
in-out property <bool> open: false;
|
||
in property <int> duplicate-count: 0;
|
||
in property <int> total-count: 0;
|
||
in property <bool> busy: false;
|
||
in property <string> playlist-name;
|
||
}
|
||
|
||
export global DuplicateConfirmActions {
|
||
callback add-all();
|
||
callback add-new-only();
|
||
callback cancel();
|
||
}
|
||
|
||
// One artist tile in the Favorites > Artists grid.
|
||
export struct FavoriteArtistItem {
|
||
id: string,
|
||
name: string,
|
||
image-url: string,
|
||
image: image,
|
||
// Pin badge state — kept live by main::set_artist_row_pinned when a pin
|
||
// toggles anywhere (mirrors SlimItem.is-pinned).
|
||
is-pinned: bool,
|
||
}
|
||
|
||
// 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 | follow | unfollow | copy */);
|
||
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();
|
||
// Windowed grid: the visible row band changed (item indices into
|
||
// albums-visible, prefetch margin included) — dispatch artwork for it.
|
||
callback albums-window-changed(int /* first */, int /* last */);
|
||
callback unfavorite-track(string /* track id */);
|
||
callback unfavorite-album(string /* album id */);
|
||
callback retry-load();
|
||
}
|
||
|
||
// ── Library "All" — the mixed feed (webplayer /user-library/all) ──
|
||
//
|
||
// One heterogeneous item that renders as a track / album / artist / playlist /
|
||
// label card depending on `kind`. The mixed feed interleaves favorites,
|
||
// purchases and followed items sorted by date added. Sort/filter/source-switch
|
||
// are applied ENTIRELY in Rust (crate::library_all::derive) — Slint renders the
|
||
// pre-computed `items-visible`.
|
||
export struct LibraryFeedItem {
|
||
kind: string, // "track" | "album" | "artist" | "playlist" | "label"
|
||
group: string, // "favorites" | "following" | "purchases"
|
||
source: string, // "qobuz" | "local" | "plex"
|
||
id: string,
|
||
title: string,
|
||
subtitle: string, // "Track • Artist" / "year • artist" / owner·count / …
|
||
artist: string,
|
||
artist-id: string,
|
||
album: string,
|
||
album-id: string, // for "Go to album" (tracks)
|
||
image-url: string,
|
||
image: image, // decoded cover (empty until the pipeline resolves it)
|
||
quality-tier: string, // "hires" | "cd" | ""
|
||
quality-detail: string,// "24-bit / 96 kHz"
|
||
is-favorite: bool,
|
||
removing: bool,
|
||
sort-title: string, // lowercased title for the A-Z sort
|
||
sort-artist: string, // lowercased artist for the artist sort
|
||
// Lowercased genre name for the client-side genre filter (albums + tracks
|
||
// carry it; artists/labels/playlists are ""). "" is excluded when a genre
|
||
// is selected.
|
||
genre: string,
|
||
// Playlist ownership (mirrors SearchPlaylistItem) so the mixed-feed
|
||
// PlaylistCard shows favorite (owned) vs follow/copy (foreign).
|
||
playlist-owned: bool,
|
||
playlist-following: bool,
|
||
playlist-copied: bool,
|
||
}
|
||
|
||
export global LibraryAllState {
|
||
in property <[LibraryFeedItem]> items: []; // full merged feed
|
||
in property <[LibraryFeedItem]> items-visible: []; // after search + source + sort
|
||
in-out property <string> search: "";
|
||
in-out property <string> sort-by: "date"; // "date" | "title" | "artist"
|
||
// Sort direction, PlaylistView-style (re-selecting the active field
|
||
// toggles it). For "date": false = newest-first (default), true = oldest;
|
||
// for "title"/"artist": true = A→Z (default), false = Z→A.
|
||
in-out property <bool> sort-asc: false;
|
||
in-out property <string> view-mode: "grid"; // "grid" | "list"
|
||
// Source-switch filters — the three webplayer toggles. All on = show all.
|
||
in-out property <bool> show-purchases: true;
|
||
in-out property <bool> show-favorites: true;
|
||
in-out property <bool> show-following: true;
|
||
// "show local" — includes local files + Plex (Phase 2). Off until the
|
||
// local-favorites layer lands.
|
||
in-out property <bool> show-local: false;
|
||
in property <int> total: 0; // badge count (deduped union)
|
||
in property <bool> loading: false;
|
||
in property <string> load-error: "";
|
||
}
|
||
|
||
export global LibraryAllActions {
|
||
callback search(string /* query */);
|
||
// Re-selecting the active field toggles direction (PlaylistView pattern).
|
||
callback set-sort(string /* date | title | artist */);
|
||
callback set-view(string /* grid | list */);
|
||
callback toggle-source(string /* purchases | favorites | following | local */);
|
||
callback open-item(string /* kind */, string /* id */);
|
||
callback media-action(string /* kind */, string /* id */, string /* action */);
|
||
callback retry();
|
||
}
|
||
|
||
// ── Purchases (My-Purchases library surface, source-of-truth §2.1) ──
|
||
//
|
||
// A purchased album row/card. `quality-label` is the grid card's kHz form
|
||
// ("24/96 kHz"); `quality-list` is the album-list row's formatQuality form
|
||
// ("24bit/96kHz" or "CD Quality"). `purchase-date` is the SHORT-month date.
|
||
// `downloaded` only drives the hide-downloaded filter (the list view never
|
||
// shows a per-album download badge). The Rust side fills `artwork` once the
|
||
// pipeline resolves the cover.
|
||
export struct PurchaseAlbumItem {
|
||
id: string,
|
||
title: string,
|
||
artist: string,
|
||
artist-id: string,
|
||
artwork-url: string,
|
||
artwork: image,
|
||
quality-label: string, // grid card: "{bd}/{sr} kHz" ("" if either missing)
|
||
quality-list: string, // list row: "24bit/96kHz" | "CD Quality"
|
||
purchase-date: string, // SHORT month "Nov 6, 2025"
|
||
downloadable: bool,
|
||
downloaded: bool,
|
||
}
|
||
|
||
// A purchased track row. `quality` is the BARE "{bit}/{rate}" string (no kHz,
|
||
// §A.6) — "" when either field is missing. `dl-status` is the flattened
|
||
// download-store status for the row's 4-state control:
|
||
// "" | "downloading" | "complete" | "failed". `downloaded` (server registry)
|
||
// OR dl-status=="complete" both render the green redownload check.
|
||
export struct PurchaseTrackItem {
|
||
id: string,
|
||
title: string,
|
||
artist: string,
|
||
artist-id: string,
|
||
album-title: string,
|
||
album-id: string,
|
||
artwork-url: string,
|
||
artwork: image,
|
||
quality: string, // BARE "{bit}/{rate}" (§A.6); "" if either missing
|
||
duration: string, // "m:ss"
|
||
purchase-date: string, // SHORT month
|
||
streamable: bool,
|
||
downloaded: bool, // server registry downloaded flag
|
||
dl-status: string, // "" | downloading | complete | failed
|
||
}
|
||
|
||
// A group-by section for the albums tab (alpha / artist grouping).
|
||
export struct PurchaseAlbumGroup {
|
||
key: string,
|
||
title: string,
|
||
albums: [PurchaseAlbumItem],
|
||
}
|
||
|
||
// A group-by section for the tracks tab (name / artist / album grouping).
|
||
export struct PurchaseTrackGroup {
|
||
key: string,
|
||
title: string,
|
||
tracks: [PurchaseTrackItem],
|
||
}
|
||
|
||
// One option in the anchored format-picker popup (tracks-tab per-track
|
||
// download). `detail` is the "{bd}/{sr}" line shown after the label when both
|
||
// are present.
|
||
export struct PurchaseFormatItem {
|
||
id: int,
|
||
label: string,
|
||
detail: string,
|
||
}
|
||
|
||
export global PurchasesState {
|
||
// "albums" | "tracks"
|
||
in-out property <string> active-tab: "albums";
|
||
// Region-notice banner (above the nav). Shown until dismissed; the
|
||
// persisted "seen" flag seeds the initial value from Rust (Slice 12).
|
||
// Default true so a fresh session shows it, matching Tauri's
|
||
// `getUserItem('qbz-purchases-region-notice-seen') !== 'true'`.
|
||
in property <bool> show-region-notice: true;
|
||
// Search (controller-debounced 300ms). `search-query` is the live input;
|
||
// the Rust side debounces and refetches/filters.
|
||
in-out property <string> search-query: "";
|
||
in property <bool> search-active: false; // query.trim().length > 0
|
||
// Albums toolbar.
|
||
in-out property <string> album-view-mode: "grid"; // "grid" | "list"
|
||
in-out property <bool> album-grouping-enabled: false;
|
||
in-out property <string> album-group-mode: "alpha"; // "alpha" | "artist"
|
||
in-out property <string> album-sort-by: "date"; // date|artist|album|quality
|
||
in-out property <string> album-sort-direction: "desc"; // "asc" | "desc"
|
||
// Tracks toolbar.
|
||
in-out property <bool> track-grouping-enabled: false;
|
||
in-out property <string> track-group-mode: "artist"; // artist|album|name
|
||
// Filter panel (shared albums + tracks). Persisted per-user.
|
||
in-out property <bool> filter-hide-unavailable: false;
|
||
in-out property <string> filter-quality: "all"; // all|hires|cd|lossy
|
||
in-out property <bool> filter-hide-downloaded: false;
|
||
// Mutually-exclusive open menu: "" | albumGroup | albumSort | filter | trackGroup
|
||
in-out property <string> open-menu: "";
|
||
// Derived flags (computed in Rust, pushed here).
|
||
in property <bool> has-active-filters: false;
|
||
in property <int> active-filter-count: 0;
|
||
// Tab counts (server total unless search/filter active → filtered length).
|
||
in property <int> album-tab-count: 0;
|
||
in property <int> track-tab-count: 0;
|
||
// Rendered collections (already filtered/sorted/grouped in Rust). Flat
|
||
// arrays drive the non-grouped render; the *-grouped arrays drive the
|
||
// group-by render.
|
||
in property <[PurchaseAlbumItem]> albums: [];
|
||
in property <[PurchaseAlbumGroup]> albums-grouped: [];
|
||
in property <[PurchaseTrackItem]> tracks: [];
|
||
in property <[PurchaseTrackGroup]> tracks-grouped: [];
|
||
// Stable FULL artwork-target models (every loaded item, in load order).
|
||
// The artwork pipeline writes decoded covers here by index, then dual-sets
|
||
// into the rendered (filtered/sorted/grouped) models by id (the favorites
|
||
// pattern — a sort/filter/group makes the rendered models NOT share these).
|
||
// Not bound by the view; targets only.
|
||
in-out property <[PurchaseAlbumItem]> albums-full: [];
|
||
in-out property <[PurchaseTrackItem]> tracks-full: [];
|
||
// Content state.
|
||
in property <bool> loading: true;
|
||
in property <string> load-error: ""; // i18n key (purchases.*) OR raw string
|
||
// Format-picker popup (anchored). `picker-track-id` non-empty => shown.
|
||
in property <[PurchaseFormatItem]> picker-formats: [];
|
||
in property <string> picker-track-id: "";
|
||
in property <length> picker-anchor-x: 0px;
|
||
in property <length> picker-anchor-y: 0px;
|
||
// Active/playing track (mirror props) for row highlight.
|
||
in property <string> active-track-id: "";
|
||
in property <bool> playback-active: false;
|
||
}
|
||
|
||
export global PurchasesActions {
|
||
// Tabs / search.
|
||
callback select-tab(string /* albums | tracks */);
|
||
callback search(string /* query */);
|
||
callback clear-search();
|
||
// Albums toolbar.
|
||
callback set-album-group(string /* off | alpha | artist */);
|
||
callback select-album-sort(string /* date|artist|album|quality */);
|
||
callback set-album-view-mode(string /* grid | list */);
|
||
// Tracks toolbar.
|
||
callback set-track-group(string /* off | album | artist | name */);
|
||
// Filter panel.
|
||
callback set-filter-hide-unavailable(bool);
|
||
callback set-filter-quality(string /* all|hires|cd|lossy */);
|
||
callback set-filter-hide-downloaded(bool);
|
||
callback clear-all-filters();
|
||
// Menu open/close (mutually exclusive). "" closes.
|
||
callback set-open-menu(string);
|
||
// Region notice dismiss.
|
||
callback dismiss-region-notice();
|
||
// Navigation / play.
|
||
callback open-album(string /* album id */);
|
||
callback open-artist(string /* artist id */);
|
||
callback play-track(string /* track id */);
|
||
// Per-track download flow. `anchor-x`/`anchor-y` position the format
|
||
// picker; the Rust side decides 1-format (no picker) vs >1 (picker).
|
||
callback download-track(string /* track id */, length /* anchor-x */, length /* anchor-y */);
|
||
// Format-picker: a format chosen → execute the download.
|
||
callback pick-format(string /* track id */, int /* format id */);
|
||
callback close-format-picker();
|
||
// Error branch retry (re-fetch the active tab). INTENTIONALLY UNWIRED: the
|
||
// Svelte error state (PurchasesView.svelte:851-855) renders ShoppingBag +
|
||
// text with NO retry trigger, so the 1:1 EmptyBlock has no clickable retry.
|
||
// The handler in main.rs is kept as scaffolding (it would force-reload the
|
||
// active tab) so a future non-1:1 retry affordance has a seam to bind to.
|
||
callback retry();
|
||
}
|
||
|
||
// ── Purchase detail / download view (source-of-truth §2.2) ──────────────────
|
||
|
||
// One row in the detail track table. Mirrors the bespoke Tauri detail row
|
||
// (§2.2.5): bare "{bit}/{rate}" quality (§A.6), a 4-state download control, and
|
||
// disc grouping. `disc-header-number` > 0 marks the FIRST track of a disc on a
|
||
// multi-disc album so a "Disc N" divider renders above it (the AlbumPageView
|
||
// disc-header-number mechanism); 0 otherwise (single-disc → flat).
|
||
//
|
||
// `dl-status` is the format-scoped per-track status ("" | downloading |
|
||
// complete | failed) from `track_status_scoped`. `is-downloaded` is the row's
|
||
// derived green-check state (server `downloaded_format_ids` for the selected
|
||
// format OR a store `complete`). `show-download` gates whether ANY download
|
||
// control renders (album downloadable OR already downloaded, §2.2.5).
|
||
export struct PurchaseDetailTrack {
|
||
id: string,
|
||
track-number: int,
|
||
title: string,
|
||
performer: string, // shown on a 2nd line when ≠ album artist
|
||
show-performer: bool,
|
||
duration: string, // "m:ss"
|
||
quality: string, // BARE "{bit}/{rate}" (§A.6); "" if either missing
|
||
streamable: bool,
|
||
dl-status: string, // "" | downloading | complete | failed (format-scoped)
|
||
is-downloaded: bool, // green redownload check
|
||
show-download: bool, // album.downloadable || is-downloaded
|
||
disc-header-number: int, // > 0 → "Disc N" header above this row (multi-disc)
|
||
}
|
||
|
||
export global PurchaseDetailState {
|
||
// Bound album id. The view watches this and fires `load` on change so a
|
||
// navigation between two different purchase albums RE-FETCHES (§A.3); the
|
||
// controller seeds it before navigating. Empty → nothing to render.
|
||
in-out property <string> album-id: "";
|
||
// Content state. `load-error` is the RAW error string (NOT i18n-mapped,
|
||
// unlike the list view — §2.2.4 / §8.4).
|
||
in property <bool> loading: true;
|
||
in property <string> load-error: "";
|
||
in property <bool> loaded: false;
|
||
// Header.
|
||
in property <string> title: "";
|
||
in property <string> artist: "";
|
||
in property <string> artist-id: ""; // "" → artist is plain text (no link)
|
||
in property <image> artwork;
|
||
in property <bool> downloadable: true; // !downloadable → header overlay + no actions
|
||
in property <string> purchased-on: ""; // LONG-month "November 6, 2025" ("" hides)
|
||
in property <string> label-name: ""; // "" hides the "· label" segment
|
||
in property <string> genre-name: ""; // "" hides the "· genre" segment
|
||
in property <string> quality: ""; // formatQuality "24bit/96kHz" | "CD Quality"
|
||
in property <int> total-tracks: 0;
|
||
in property <string> total-duration: ""; // "{h}h {m}m" | "{m}m"
|
||
// Format dropdown. `format-labels` are the backend fmt.label strings (NOT
|
||
// an i18n key); `format-index` is the currently-selected option (default 0).
|
||
in property <[string]> format-labels: [];
|
||
in property <int> format-index: 0;
|
||
in property <bool> has-formats: false;
|
||
// Download progress (source-of-truth §2.2.1 / §A.4). `all-complete` IS
|
||
// format-scoped; `completed-count` / `progress-percent` / `was-cancelled`
|
||
// are NOT (count ALL statuses regardless of selected format).
|
||
in property <bool> is-downloading-all: false;
|
||
in property <bool> all-complete: false; // format-scoped
|
||
in property <bool> was-cancelled: false; // NOT format-scoped
|
||
in property <int> completed-count: 0; // NOT format-scoped
|
||
in property <float> progress-percent: 0; // completed/total*100, NOT format-scoped
|
||
// Whether the progress section renders at all (downloadingAll || allComplete
|
||
// || wasCancelled) — precomputed in Rust to keep the .slint declarative.
|
||
in property <bool> show-progress: false;
|
||
// Add-to-Library block (only when all-complete && a destination is set).
|
||
in property <bool> show-add-to-library: false;
|
||
in property <bool> adding-to-library: false;
|
||
// Download-all circle gating: disabled while downloading-all OR no format.
|
||
in property <bool> can-download-all: false;
|
||
// Whether the Play-album circle renders (onAlbumPlay present — always true
|
||
// in Slint since play routing exists; kept as a flag for parity).
|
||
in property <bool> show-play: true;
|
||
// Active/playing track for row highlight + playing-bars.
|
||
in property <string> active-track-id: "";
|
||
in property <bool> playback-active: false;
|
||
// The disc-grouped track rows (already flattened with disc-header markers in
|
||
// Rust). The view iterates this once.
|
||
in property <[PurchaseDetailTrack]> tracks: [];
|
||
}
|
||
|
||
export global PurchaseDetailActions {
|
||
// Reactive reload: the view fires this whenever the bound `album-id`
|
||
// changes (and on first mount) so a fresh load runs (§A.3).
|
||
callback load(string /* album id */);
|
||
// Header / navigation.
|
||
callback open-artist(string /* artist id */);
|
||
callback play-album(); // Play-album circle (§A.1, routing in Slice 10)
|
||
callback play-track(string /* track id */); // per-track row play (§A.2)
|
||
// Format dropdown — a new option chosen (index into format-labels).
|
||
callback select-format(int /* index */);
|
||
// Download-all circle → folder picker → startAlbumDownload.
|
||
callback download-all();
|
||
// Per-track download → folder picker → startTrackDownload.
|
||
callback download-track(string /* track id */);
|
||
// Progress section cancel button.
|
||
callback cancel-download();
|
||
// Add-to-Library block button.
|
||
callback add-to-library();
|
||
}
|
||
|
||
// 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;
|
||
// Keyboard/checkbox multi-select (mirrors AlbumState): mode flag + count.
|
||
// Per-row toggle routes through the shared media-action("track",...) arm.
|
||
in-out property <bool> multi-select: false;
|
||
in property <int> selected-count: 0;
|
||
}
|
||
|
||
// 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;
|
||
|
||
// The live header search-box text, two-way bound to the TextInput in
|
||
// HeaderBar. Owned here (not internal to the TextInput) so the controller
|
||
// can CLEAR it after the user activates a cortinilla element (row click /
|
||
// "View more" / Enter) — lingering text would otherwise re-invoke the
|
||
// dropdown when focus bounces back to the field after any UI interaction.
|
||
in-out property <string> header-search-text: "";
|
||
|
||
// ---- Cortinilla (live as-you-type dropdown under the header search) ----
|
||
// Whether the cortinilla overlay is currently shown. The overlay is a
|
||
// LAST child of AppShell, visibility-gated on this flag.
|
||
in-out property <bool> cortinilla-open: false;
|
||
// The query the cortinilla currently reflects (the live header text).
|
||
in property <string> cortinilla-query;
|
||
// A live cortinilla fetch is in flight (spinner / shimmer affordance).
|
||
in property <bool> cortinilla-loading: false;
|
||
// The single best match shown at the very top of the cortinilla. Its
|
||
// flat index is always 0 (see the flat-index convention on
|
||
// SearchActions.cortinilla-row-clicked). `kind == ""` = no top result.
|
||
in property <CortinillaRow> top-result;
|
||
// The labelled result sections (Albums / Tracks / Artists / Playlists /
|
||
// library), each with its own rows + "View more" affordance.
|
||
in property <[CortinillaSection]> sections: [];
|
||
// Keyboard-nav highlight into the FLAT navigable list (top-result = 0,
|
||
// then each section's rows in declaration order). -1 = nothing selected,
|
||
// so Enter falls through to a full search-all instead of opening a row.
|
||
in-out property <int> selected-index: -1;
|
||
// Content-space top-y (px) of the currently keyboard-selected row, set by
|
||
// the controller on arrow nav so the overlay can scroll it into view. 0 when
|
||
// nothing is selected (scroll to top).
|
||
in property <length> cortinilla-scroll-y: 0px;
|
||
}
|
||
|
||
// 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);
|
||
|
||
// ---- Cortinilla (live as-you-type dropdown) ----
|
||
// A cortinilla row was activated (click or Enter on a highlighted row).
|
||
// The argument is a FLAT index into the navigable list, where:
|
||
// 0 = top-result (SearchState.top-result)
|
||
// 1.. = section rows, in section declaration order, each
|
||
// section's rows enumerated in order before the next.
|
||
// The controller resolves the flat index back to a CortinillaRow and
|
||
// dispatches on its `source`/`kind` (Qobuz media-action vs local play).
|
||
callback cortinilla-row-clicked(int /* flat-index */);
|
||
// "View more" pressed on a section — open the full results page (or the
|
||
// LocalLibrary Tracks tab for the local section) for that category.
|
||
callback cortinilla-view-more(string /* kind */);
|
||
// Enter with nothing highlighted (selected-index == -1) — run a full
|
||
// search-all and navigate to the results page.
|
||
callback cortinilla-search-all();
|
||
// Click-outside / Escape — close the cortinilla without acting.
|
||
callback cortinilla-dismiss();
|
||
// Arrow key moved the keyboard highlight: delta -1 = up, +1 = down.
|
||
// Wraps/clamps against the flat navigable list in the controller.
|
||
callback cortinilla-move-selection(int /* delta: -1 up / +1 down */);
|
||
}
|
||
|
||
// ============================ Offline MODE ================================
|
||
// App-wide offline MODE status (the app operating without Qobuz — NOT the
|
||
// offline CACHE/downloads). Mirrors `qbz_app::offline_mode::OfflineStatus`;
|
||
// pushed by crate::offline_mode::start_ui_forwarder on every engine
|
||
// broadcast. Read by the login screen (offline-boot affordances) and the
|
||
// shell (D2 recovery banner); the gating slice adds more consumers.
|
||
export global OfflineState {
|
||
// Any offline flavor active (mode != online).
|
||
in property <bool> offline: false;
|
||
// 0 = online, 1 = real offline, 2 = induced offline.
|
||
in property <int> mode: 0;
|
||
// Raw connectivity, independent of the mode: 0 unknown, 1 up, 2 down.
|
||
in property <int> connectivity: 0;
|
||
// A probe was answered by a redirect — typical captive portal.
|
||
in property <bool> captive-portal: false;
|
||
// D2: unauthenticated offline session while connectivity is back up —
|
||
// drives the header badge's "Sign in" affordance (formerly a banner).
|
||
in property <bool> show-recovery-banner: false;
|
||
// Session started without Qobuz auth ("Start offline" on the login
|
||
// screen). Unlike show-recovery-banner this is true regardless of
|
||
// connectivity — the badge's "Logged out" state needs it even while
|
||
// the link is down.
|
||
in property <bool> offline-session: false;
|
||
// A previous login exists (last_user_id on disk). Gates the login
|
||
// screen's "Start offline" affordance: offline mode needs a prior
|
||
// user's data, never the empty user-0 profile.
|
||
in property <bool> has-previous-session: false;
|
||
// Startup session-restore failure detail, shown on the login screen
|
||
// ("" = none). Set by the restore task's error arm; cleared on any
|
||
// successful shell entry.
|
||
in property <string> login-error: "";
|
||
// Header status-badge state, derived: 0 = hidden (online + authenticated),
|
||
// 1 = hard offline (no connectivity), 2 = manual offline (induced),
|
||
// 3 = logged out (offline session — the actionable fact, so it takes
|
||
// precedence over the other two).
|
||
out property <int> badge-state: self.offline-session ? 3
|
||
: self.mode == 2 ? 2
|
||
: self.mode == 1 ? 1
|
||
: 0;
|
||
}
|
||
|
||
// ============================ Login flow ==================================
|
||
// Browser sign-in progress, driven by the Rust auth flow (crate::auth via
|
||
// main.rs). The app has no embedded webview, so the login screen must
|
||
// narrate the system-browser OAuth — the browser may open in the background
|
||
// without stealing focus, and the code exchange takes a few seconds.
|
||
export global LoginState {
|
||
// 0 = idle · 1 = waiting for the user to finish in the browser ·
|
||
// 2 = authorization code received, exchanging it for a session.
|
||
in property <int> phase: 0;
|
||
// Last sign-in failure ("" = none). Cleared when a new attempt starts
|
||
// and on successful shell entry.
|
||
in property <string> error: "";
|
||
}
|
||
|
||
// Settings > Offline, MODE group — Rust-wired actions (crate::offline_mode).
|
||
export global OfflineModeActions {
|
||
// (Re)seed the MODE-group toggle states from the engine store. Fired by
|
||
// the panel's init, so every mount of Settings > Offline re-reads them.
|
||
callback load();
|
||
// Force an immediate connectivity re-probe (the status row's "Check
|
||
// now"). The status text updates by itself via the engine forwarder.
|
||
callback check-now();
|
||
// Flip induced offline from anywhere outside the Settings panel (the
|
||
// header badge flyout's quick toggle). Routes through the same
|
||
// persistence + #279 snapshot path as the Settings toggle.
|
||
callback set-offline(bool);
|
||
}
|
||
|
||
// B9 — offline Favorites "playable favorites" rail. While OFFLINE the
|
||
// Favorites view is replaced by the OfflinePlaceholder; this rail (mounted
|
||
// as the placeholder's children on the Favorites mount only) lists the
|
||
// favorite tracks that are still playable — offline-cache READY copies and
|
||
// library qobuz_download rows — so the view isn't a dead end.
|
||
export global OfflineFavoritesState {
|
||
// The playable favorite tracks, display order = play order (the Rust
|
||
// side keeps a matching queue; a row click plays the rail from there).
|
||
in property <[SlimItem]> tracks: [];
|
||
}
|
||
|
||
export global OfflineFavoritesActions {
|
||
// (Re)build the rail from the local stores — fired by the rail's init
|
||
// on every mount of the Favorites offline placeholder.
|
||
callback load();
|
||
// Play the rail starting at the clicked track id (offline-capable
|
||
// queue path; the cached copies play through the offline tier).
|
||
callback play(string);
|
||
}
|
||
|
||
// ================= Playlist Importer (public playlists) ==================
|
||
// "Import Playlist" modal state — 1:1 port of Tauri's
|
||
// PlaylistImportModal.svelte. All interpolated strings (log lines, status
|
||
// line, summary block) arrive pre-formatted from the Rust controller
|
||
// (crate::playlist_import); the UI renders them verbatim. Provider
|
||
// detection lives Rust-side too (Slint 1.16 has no string `.contains`) —
|
||
// every URL keystroke round-trips through `url-edited`.
|
||
|
||
// One append-only line in the conversion log.
|
||
export struct ImportLogEntry {
|
||
// Final display string (pre-formatted Rust-side).
|
||
message: string,
|
||
// "info" | "success" | "error" — drives the row color.
|
||
status: string,
|
||
}
|
||
|
||
export global PlaylistImportState {
|
||
in-out property <bool> open: false;
|
||
in-out property <string> url;
|
||
// Rename field — prefilled with the fetched playlist name.
|
||
in-out property <string> custom-name;
|
||
// True during preview fetch AND import execute.
|
||
in property <bool> loading: false;
|
||
// "" = none; shown as the red banner at the top of the body.
|
||
in property <string> error: "";
|
||
// "" | "spotify" | "apple" | "tidal" | "deezer" — the locked-or-
|
||
// detected provider; its source logo renders full-opacity.
|
||
in property <string> active-provider: "";
|
||
// Rust-computed: provider detected && !offline.
|
||
in property <bool> can-fetch: false;
|
||
// Rust-computed: a preview exists AND the URL hasn't been edited
|
||
// since the fetch — flips the modal into step B (rename + Import).
|
||
in property <bool> show-preview: false;
|
||
in property <bool> import-completed: false;
|
||
// The "Conversion progress" panel (any log lines, or loading).
|
||
in property <bool> progress-visible: false;
|
||
// Bar + status/current-track lines visible.
|
||
in property <bool> has-progress: false;
|
||
in property <float> progress: 0.0; // 0..1
|
||
in property <string> status-line: "";
|
||
in property <string> current-track: "";
|
||
in property <[ImportLogEntry]> log: [];
|
||
// Folder dropdown options — exact CreatePlaylistState pattern: index
|
||
// 0 is always "No folder" (id ""), the rest are the user's folder
|
||
// names parallel to `folder-ids`.
|
||
in property <[string]> folder-options: ["No folder"];
|
||
in property <[string]> folder-ids: [""];
|
||
in-out property <int> folder-index: 0;
|
||
// Summary block, pre-formatted lines ("" = hidden).
|
||
in property <string> summary-playlist: "";
|
||
in property <string> summary-matched: "";
|
||
in property <string> summary-skipped: "";
|
||
in property <string> summary-parts: "";
|
||
}
|
||
|
||
export global PlaylistImportActions {
|
||
callback close();
|
||
// Fired on every URL keystroke — Rust recomputes active-provider /
|
||
// can-fetch / show-preview (and the post-completion reset path).
|
||
callback url-edited(string);
|
||
// Keeps Rust's custom_name mirror fresh.
|
||
callback name-edited(string);
|
||
// Step A: preview_public_playlist(url).
|
||
callback fetch();
|
||
// Step B: import_public_playlist(...) with rename + folder choice.
|
||
callback execute();
|
||
}
|
||
|
||
// ── HiFi Wizard (DAC setup) ─────────────────────────────────────────────
|
||
// Native-Slint port + redesign of the Tauri DACSetupWizard. 6-step adaptive
|
||
// flow: welcome → check → select-dacs → review-and-apply → test → done.
|
||
// READ-ONLY by contract: the wizard never writes a system audio config — it
|
||
// auto-detects DACs (pw-dump, Slice 0), generates copy-paste config, and runs
|
||
// an in-app playback test reading the real negotiated rate (N6, Slice 8b).
|
||
|
||
// A "you're missing X" fix for the check step: a caption + a copy-paste command.
|
||
export struct RemediationRow {
|
||
caption: string,
|
||
command: string,
|
||
}
|
||
|
||
// One enumerated sink the user can pick to configure.
|
||
export struct DacCandidateRow {
|
||
id: string, // PipeWire node.name (the value the user used to paste)
|
||
description: string, // pretty name, e.g. "DacMagic Plus Analog Stereo"
|
||
bus: string, // "usb" | "pci" | "bluetooth" | ""
|
||
is-default: bool, // current system default sink
|
||
looks-like-dac: bool, // hardware && bus ∈ {usb,pci} → pre-selected
|
||
checked: bool, // user selection
|
||
rates-label: string, // pre-filled supported rates, e.g. "44.1 / 96 / 192 kHz"
|
||
}
|
||
|
||
// One selected DAC's generated copy-paste config (Slice 10, read-only).
|
||
export struct DacConfigRow {
|
||
name: string,
|
||
node-name: string,
|
||
pipewire-conf: string,
|
||
pulse-conf: string,
|
||
wireplumber-conf: string,
|
||
expanded: bool, // accordion state
|
||
}
|
||
|
||
export global DacWizardState {
|
||
in-out property <bool> open: false;
|
||
// 0 welcome · 1 check · 2 select-dacs · 3 review · 4 test · 5 done
|
||
in-out property <int> step: 0;
|
||
|
||
// welcome
|
||
in-out property <bool> welcome-confirmed: false;
|
||
|
||
// check (Slice 6): distro + init both auto-detected, both overridable
|
||
// (distro → package manager, init → service commands; they're orthogonal).
|
||
in-out property <[string]> distro-options: [];
|
||
in-out property <int> distro-index: 0;
|
||
in-out property <[string]> init-options: [];
|
||
in-out property <int> init-index: 0;
|
||
in-out property <bool> health-ok: false;
|
||
in-out property <string> health-summary: "";
|
||
in-out property <[RemediationRow]> remediations: [];
|
||
// In a Flatpak/Snap sandbox QBZ can't inspect the host stack, so it shows
|
||
// reference setup commands + an info notice instead of a health verdict.
|
||
in-out property <bool> sandboxed: false;
|
||
in-out property <string> sandbox-name: "";
|
||
|
||
// select-dacs (Slice 7)
|
||
in-out property <[DacCandidateRow]> candidates: [];
|
||
in-out property <bool> detecting: false; // enumeration in flight
|
||
in-out property <bool> has-enumeration: true; // false → manual escape hatch
|
||
in-out property <bool> any-dac-selected: false; // gates Next on the DACs step
|
||
in-out property <string> manual-node-name: "";
|
||
in-out property <bool> manual-valid: false;
|
||
in-out property <string> manual-dac-type: ""; // usb|pci|bluetooth|virtual|unknown
|
||
|
||
// test (Slice 9): live read-back lines, set by the controller
|
||
in-out property <string> test-requested-label: ""; // "QBZ requesting 96000 Hz · 24-bit"
|
||
in-out property <string> test-negotiated-label: ""; // "DAC running at 96000 Hz" (N6)
|
||
in-out property <bool> test-rate-matched: false;
|
||
in-out property <bool> test-playing: false;
|
||
|
||
// review-and-apply (Slice 10): generated copy-paste config per selected DAC.
|
||
in-out property <[DacConfigRow]> dac-configs: [];
|
||
in-out property <string> backup-cmd: "";
|
||
in-out property <string> restart-cmd: "";
|
||
in-out property <[string]> created-paths: []; // done step
|
||
// Progress checkboxes (Tauri parity) — help the user track what they ran.
|
||
in-out property <bool> review-backup-done: false;
|
||
in-out property <bool> review-config-done: false;
|
||
in-out property <bool> review-restart-done: false;
|
||
}
|
||
|
||
export global DacWizardActions {
|
||
// Wired in Rust (crate::dac_wizard); plain step nav (back/close) stays
|
||
// inline in the modal via state mutation.
|
||
callback open(); // open + detect distro/init + run health probe
|
||
callback set-distro(int); // recompute remediations for the chosen distro
|
||
callback set-init(int); // recompute service commands for the chosen init
|
||
callback copy-command(string); // → crate::share::copy_to_clipboard
|
||
callback run-detect(); // enumerate sinks → candidates (Slice 7)
|
||
callback toggle-dac(int); // flip one candidate's checked state
|
||
callback validate-manual(string);// validate a pasted node.name (escape hatch)
|
||
callback start-test(); // queue + play the 4 test tracks (Slice 9)
|
||
callback verify-own(); // start the read-back using the user's own music
|
||
callback poll-test(); // refresh the requested-vs-negotiated read-back
|
||
callback test-play-index(int); // jump straight to one of the 4 test tracks
|
||
callback stop-test();
|
||
callback gen-configs(); // build the per-DAC copy-paste config (Slice 10)
|
||
callback toggle-config(int); // collapse/expand one DAC's config accordion
|
||
}
|
||
|
||
// ============================================================================
|
||
// Sandbox (Flatpak/Snap) — Settings section holding the one-time permission
|
||
// commands for sandboxed installs (Tauri parity: Settings > Flatpak / Snap).
|
||
// The section is only shown when install-method is "flatpak" or "snap".
|
||
// ============================================================================
|
||
|
||
export global SandboxState {
|
||
// "flatpak" | "snap" | anything else (section hidden). Seeded once at
|
||
// startup from qbz_app::diagnostics::system_info().install_method.
|
||
in property <string> install-method: "";
|
||
callback copy-command(string); // → crate::share::copy_to_clipboard
|
||
}
|
||
|
||
// ============================================================================
|
||
// Keyboard shortcuts (hotkeys) — port of the Tauri keybindings system.
|
||
// ============================================================================
|
||
|
||
// One action row, prebuilt in Rust (crate::keybindings) with its display
|
||
// shortcut already formatted ("Ctrl + →"). Shared by the read-only cheatsheet
|
||
// and the editable customize editor.
|
||
export struct KeybindingRow {
|
||
id: string, // stable action id, e.g. "playback.toggle"
|
||
label: string, // localized action label
|
||
shortcut: string, // formatted display, e.g. "Ctrl + →" / "Space"
|
||
modified: bool, // differs from the default binding
|
||
contextual: bool, // only fires in a context (immersive / mini)
|
||
}
|
||
|
||
// A category group (Playback / Navigation / Interface / Immersive / Mini Player)
|
||
// with its localized header label and its ordered rows.
|
||
export struct KeybindingCategoryGroup {
|
||
label: string, // localized category header
|
||
rows: [KeybindingRow],
|
||
}
|
||
|
||
export global KeybindingsState {
|
||
// Ordered category groups, rebuilt in Rust whenever a binding changes.
|
||
in property <[KeybindingCategoryGroup]> groups: [];
|
||
// Same groups split round-robin into three columns, for the read-only
|
||
// cheatsheet's 3-column layout (the editor still uses the full `groups`).
|
||
in property <[KeybindingCategoryGroup]> groups-col1: [];
|
||
in property <[KeybindingCategoryGroup]> groups-col2: [];
|
||
in property <[KeybindingCategoryGroup]> groups-col3: [];
|
||
// Count of actions whose binding differs from default ("N modified").
|
||
in property <int> modified-count: 0;
|
||
|
||
// --- Customize editor capture state -------------------------------------
|
||
// Action id currently being recorded ("" = not recording). The winit key
|
||
// handler reads this; while non-empty it captures the next combo instead
|
||
// of dispatching a hotkey.
|
||
in-out property <string> recording-id: "";
|
||
// Live formatted display of the combo being pressed while recording.
|
||
in property <string> pending-display: "";
|
||
// Localized label of the action that already owns the pressed combo
|
||
// ("" = no conflict). When set, the capture is NOT applied.
|
||
in property <string> conflict-label: "";
|
||
}
|
||
|
||
export global KeybindingsActions {
|
||
callback start-record(string); // begin capturing for an action id
|
||
callback cancel-record(); // stop capturing (Escape / click-away)
|
||
callback reset-one(string); // reset one action to its default
|
||
callback reset-all(); // reset every action to defaults
|
||
}
|
||
|
||
// Open flags for the two keybindings modals. The cheatsheet (read-only, `?`)
|
||
// and the editable customize editor. Self-gated mounts in AppShell (ADR-010).
|
||
export global KeyboardShortcutsState {
|
||
in-out property <bool> open: false; // read-only cheatsheet
|
||
in-out property <bool> customize-open: false; // editable customize editor
|
||
}
|
||
|
||
// ============================================================================
|
||
// Open Qobuz Link (Ctrl+L) — cross-platform link resolver modal.
|
||
// ============================================================================
|
||
|
||
export global LinkResolverState {
|
||
in-out property <bool> open: false;
|
||
// Two-way bound to the input field.
|
||
in-out property <string> url: "";
|
||
// Detected platform id for the leading icon: "qobuz" | "spotify" | "apple"
|
||
// | "tidal" | "deezer" | "songlink" | "" (none/unknown). Set by Rust on
|
||
// url-changed; reset to "" from the hamburger menu on open (hence in-out).
|
||
in-out property <string> platform: "";
|
||
in-out property <bool> resolving: false;
|
||
// Localized error message ("" = none).
|
||
in-out property <string> error: "";
|
||
// Playlist banner: the link is a playlist → offer the Playlist Importer.
|
||
in-out property <bool> playlist-detected: false;
|
||
in property <string> playlist-provider: "";
|
||
}
|
||
|
||
export global LinkResolverActions {
|
||
callback submit(string); // resolve + navigate
|
||
callback close();
|
||
callback url-changed(string); // live platform detection for the icon
|
||
callback open-importer(); // playlist banner → open Playlist Importer
|
||
}
|
||
|
||
// UiFocusState (the hotkey text-input guard) lives in the leaf file
|
||
// `focus-state.slint` and is re-exported at the top of this file.
|
||
|
||
// ===================================================================
|
||
// About modal (hamburger menu → "About QBZ").
|
||
// ===================================================================
|
||
|
||
// One author/contributor row: GitHub handle + the profile URL opened on click,
|
||
// plus the GitHub avatar (fetched async by crate::about — default/blank until it
|
||
// arrives).
|
||
export struct AboutContributorRow {
|
||
name: string,
|
||
url: string,
|
||
avatar: image,
|
||
}
|
||
|
||
// One wrap row of contributor chips. Slint has no flex-wrap, so crate::about
|
||
// pre-groups the flat contributor list into fixed-width rows (4 per row) and the
|
||
// modal renders an outer VerticalLayout over the groups + an inner
|
||
// HorizontalLayout over each group's items.
|
||
export struct AboutContributorGroup {
|
||
items: [AboutContributorRow],
|
||
}
|
||
|
||
export global AboutState {
|
||
in-out property <bool> open: false;
|
||
// Seeded by crate::about at install time (and on language change for the
|
||
// attributions descriptions). Static for the lifetime of the app.
|
||
in property <string> version: "";
|
||
in property <string> platform-label: ""; // e.g. "Linux (Slint)"
|
||
in property <string> build-date: ""; // YYYY-MM-DD ("" hides it)
|
||
in property <string> build-commit: ""; // short hash ("" hides it)
|
||
in property <string> release-url: "https://github.com/vicrodh/qbz/releases";
|
||
// Contributors pre-grouped into wrap rows (see AboutContributorGroup).
|
||
in property <[AboutContributorGroup]> contributor-rows: [];
|
||
// The single author row (vicrodh) — kept as discrete fields since it
|
||
// renders differently (slightly larger) from the contributor list.
|
||
in property <string> author-name: "vicrodh";
|
||
in property <string> author-url: "https://github.com/vicrodh";
|
||
in property <image> author-avatar;
|
||
}
|
||
|
||
export global AboutActions {
|
||
// Open an external URL in the system browser (Rust: open::that).
|
||
callback open-url(string);
|
||
}
|
||
|
||
// ===================================================================
|
||
// What's New modal (hamburger menu → "What's New").
|
||
// ===================================================================
|
||
|
||
// One rendered block of the release-notes body. `kind`: 0 = section heading,
|
||
// 1 = bullet, 2 = paragraph. `level` is the bullet nesting depth (1, 2, …).
|
||
// `id` is the slug for section blocks (TOC anchor; empty for bullets/paragraphs).
|
||
export struct WhatsNewBlock {
|
||
kind: int,
|
||
level: int,
|
||
text: string,
|
||
id: string,
|
||
url: string,
|
||
}
|
||
|
||
// One table-of-contents entry (level-0 section headings only).
|
||
export struct WhatsNewTocEntry {
|
||
id: string,
|
||
label: string,
|
||
}
|
||
|
||
export global WhatsNewState {
|
||
in-out property <bool> open: false;
|
||
in property <bool> loading: false;
|
||
in property <string> version: "";
|
||
in property <string> date: ""; // formatted "Mon D, YYYY"
|
||
in property <bool> has-body: false; // false → show the empty state
|
||
in property <[WhatsNewBlock]> blocks: [];
|
||
in property <[WhatsNewTocEntry]> toc: [];
|
||
}
|
||
|
||
export global WhatsNewActions {
|
||
// Open the modal AND kick off the GitHub release fetch (Rust).
|
||
callback open();
|
||
callback close();
|
||
// Open an external URL from a standalone release-notes link (Rust: open::that).
|
||
callback open-url(string);
|
||
}
|