Files
qbz/crates/qbz-slint/ui/state.slint
T
vicrodh 880493b73d feat(slint): Discover genre filter (simple view)
Ports Tauri's Filter-by-genre popup (simple grid view). The selection
is shared across the three Discover tabs — matching Tauri's single
"home" genre context — and re-fetches the discover index with the
chosen genre ids.

UI:
  - New GenreFilterPopup: header (icon + title + close), Remember
    selection + Advanced view toggles (QbzToggle), a 3-column grid of
    parent-genre chips with an accent fill + check circle when
    selected, and a Clear filter footer. Anchored top-right under the
    button, with a click-away backdrop.
  - GenreButton turns accent-filled and shows "N genres" / "1 genre"
    when a filter is active (else "Filter by genre"), 1:1 with Tauri.
  - Advanced view (search + 3-level tree + counts) is the next
    increment; its toggle is present and shows a "coming soon" stub.

State / backend:
  - GenreFilterState + GenreFilterActions globals; GenreChip struct.
  - crate::genre_filter loads the parent genres (QbzCore::get_genres)
    and owns the shared selection, persisted to
    <data-dir>/qbz/genre_filter.json when Remember is on (dropped
    when off). Stale ids are pruned against the loaded genres.
  - load_home now takes genre_ids and threads them into
    get_discover_index. The home-load path is extracted into a shared
    `reload_home(active_tab)` used by the initial shell load and by
    genre toggle/clear; it rebuilds all three section sets, restores
    the active tab via select_tab, and re-fires artwork.

cc
2026-05-20 22:43:16 -06:00

781 lines
30 KiB
Plaintext

// Shared UI state — structs and globals the Rust controllers populate.
//
// The Rust side builds these models on a worker thread, then sets them
// on the global from the Slint event loop (see src/home.rs, src/main.rs).
import { JumpNavTab } from "primitives/JumpNavBar.slint";
export { JumpNavTab }
export struct AlbumCardItem {
id: string,
title: string,
artist: string,
genre: string, // shown in the hover overlay
year: string, // 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
}
export struct DiscoverSection {
title: string,
albums: [AlbumCardItem],
}
// Compact "slim" card item — a small thumbnail with a title/subtitle,
// optionally rank-numbered (Popular albums, Recently Played).
export struct SlimItem {
id: string,
title: string,
subtitle: string,
rank: string, // "" = no rank
artwork-url: string,
artwork: image,
// Artist follow state — drives the ArtistCard Follow button.
following: bool,
}
// Discover / Home screen state.
export global HomeState {
in property <[DiscoverSection]> sections: [];
in property <[SlimItem]> popular: [];
in property <[SlimItem]> recent: [];
in property <[AlbumCardItem]> recent-albums: [];
in property <bool> loading: false;
// Active Discover tab: "home" | "editorPicks" | "forYou". The
// three tabs draw from the same fetch; switching swaps which
// cached section set is shown (see crate::home::select_tab).
in-out property <string> active-tab: "home";
}
// Discover tab switching. Wired in main.rs to crate::home::select_tab.
export global HomeActions {
callback select-tab(string);
}
// One genre in the Filter-by-genre popup's simple grid.
export struct GenreChip {
id: string,
name: string,
selected: bool,
}
// 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 <int> selected-count: 0;
in-out property <bool> open: false;
in-out property <bool> remember: true;
in-out property <bool> advanced: false;
in property <bool> loading: false;
}
export global GenreFilterActions {
callback toggle(string /* genre id */);
callback clear();
callback set-remember(bool);
callback set-advanced(bool);
}
export struct AlbumTrackItem {
id: string,
number: string,
title: string,
artist: string,
duration: string,
quality-tier: string,
explicit: bool,
// Multi-select state — set true when the row is in the user's
// selection. First used by the artist page's Popular Tracks.
selected: bool,
}
// Album detail screen state.
export global AlbumState {
in property <string> id;
in property <string> title;
in property <string> artist;
in property <string> artist-id;
// Network URL of the cover — used by the artwork right-click
// menu for Open-in-browser and Save-as actions.
in property <string> artwork-url;
in property <bool> has-custom-cover: false;
in property <string> info-line;
in property <string> quality-tier;
// "24-bit / 96 kHz" — quality-badge detail line.
in property <string> quality-detail;
// Editorial description / review (HTML stripped). May be empty.
in property <string> description;
// Truncated description for the header; full text lives in the modal.
in property <string> description-short;
in property <image> artwork;
// Artwork-derived tint for the header gradient.
in property <color> header-color: #181820;
in property <string> label;
in property <[string]> awards: [];
in property <[AlbumTrackItem]> tracks: [];
in property <bool> loading: false;
}
// Album-view actions wired to Rust callbacks.
export global AlbumActions {
// Filter the visible track list by a query (client-side).
callback search(string);
}
// Which content view the shell shows.
export enum ContentView { home, album, artist, settings, search, musician, label, location, favorites }
// Settings screen state. Audio and Playback preferences are loaded from
// the persistence stores by the Rust settings controller and pushed here;
// changes flow back through the settings-bool / settings-select /
// settings-slider callbacks.
export global SettingsState {
// Sub-section: 0 = Audio, 1 = Playback.
in-out property <int> section: 0;
in property <bool> loading: true;
// --- AUDIO -------------------------------------------------------------
// Audio — dropdowns. Each list is paired with a selected index; the
// Rust controller owns the index -> value mapping.
in-out property <[string]> streaming-qualities: [];
in-out property <int> streaming-quality-index: 0;
in-out property <[string]> sample-rates: [];
in-out property <int> sample-rate-index: 0;
in-out property <[string]> backends: [];
in-out property <int> backend-index: 0;
in-out property <[string]> devices: [];
// Parallel to `devices`: true marks a bit-perfect-capable device.
in-out property <[bool]> device-bp: [];
// Parallel to `devices`: a non-empty entry is a section-header label
// shown above that device row (empty = no header).
in-out property <[string]> device-groups: [];
in-out property <int> device-index: 0;
in-out property <[string]> alsa-plugins: [];
in-out property <int> alsa-plugin-index: 0;
// Audio — toggles.
in-out property <bool> limit-quality-to-device: false;
in-out property <bool> alsa-hardware-volume: false;
in-out property <bool> exclusive-mode: false;
in-out property <bool> reserve-dac: false;
in-out property <bool> dac-passthrough: false;
in-out property <bool> pw-force-bitperfect: false;
in-out property <bool> allow-quality-fallback: false;
in-out property <bool> sync-audio-on-startup: false;
in-out property <bool> skip-sink-switch: false;
// Audio — Rust-computed conditional flags driving `if`-gated rows.
in-out property <bool> backend-is-alsa: false;
in-out property <bool> backend-is-pipewire: false;
in-out property <bool> alsa-plugin-is-hw: false;
// --- PLAYBACK ----------------------------------------------------------
// Playback — toggles. `continue-playback` maps to the autoplay mode
// (on = ContinueWithinSource, off = PlayTrackOnly).
in-out property <bool> continue-playback: true;
in-out property <bool> show-context-icon: true;
in-out property <bool> persist-session: false;
in-out property <bool> resume-position: false;
in-out property <bool> gapless: true;
in-out property <bool> stream-uncached: false;
in-out property <bool> streaming-only: false;
// Playback — Initial Buffer Size slider (seconds, 1-10).
in-out property <int> buffer-seconds: 3;
// Playback — "When quality retries fail" dropdown.
in-out property <[string]> retry-behaviors: [];
in-out property <int> retry-behavior-index: 0;
}
// One entry of a nav-tab dropdown.
export struct NavMenuEntry {
label: string,
icon: image,
has-glyph: bool,
// Destination route fired when the row is clicked, e.g. "home",
// "favorites-tracks", "favorites-albums". Empty = no-op (the
// destination isn't implemented yet).
route: string,
}
// Header nav-tab dropdown state.
//
// The four header tabs (Discover / Library / Local Library / My QBZ) no
// longer own a PopupWindow each — a PopupWindow grabs the pointer while
// open, which blocks hover-switching between tabs. Instead a single
// in-window overlay (HeaderMenuOverlay, a late child of the AppShell
// root) draws the open tab's menu. Tabs write their identity into this
// global on hover/click; hovering a different tab overwrites it, giving
// an instant switch. `open-index` of -1 means no menu is open.
export global HeaderMenuState {
// Which tab's menu is open: -1 = none, 0..3 = the tab index.
in-out property <int> open-index: -1;
// Header x-coordinate of the open tab, so the overlay aligns the
// menu panel under it.
in-out property <length> anchor-x: 0px;
// The open tab's menu rows.
in-out property <[NavMenuEntry]> items: [];
// Index of the row wired to Home (-1 = none); only Discover uses it.
in-out property <int> home-item: -1;
}
export global NavState {
in-out property <ContentView> view: ContentView.home;
// Browser-like history flags, driven by the Rust nav module.
in property <bool> can-back: false;
in property <bool> can-forward: false;
// Navigation requests routed to Rust so history is recorded.
callback request-back();
callback request-forward();
callback request-settings();
}
// Artist detail screen state.
// One label row in the artist network sidebar's Labels section.
export struct LabelEntry {
id: string,
name: string,
}
// One similar-artist row in the artist network sidebar's Similar
// Artists section.
export struct SimilarEntry {
id: string,
name: string,
}
export global ArtistState {
in property <string> id;
in property <string> name;
// Network URL of the portrait — used by the artwork right-click
// menu for Open-in-browser and Save-as actions.
in property <string> artwork-url;
// True when the user has registered a local image to override
// the Qobuz-supplied portrait. Drives the menu's
// "Add image" -> "Change image / Remove image" toggle.
in property <bool> has-custom-image: false;
in property <string> bio;
// Truncated bio (word-boundary cut + ellipsis) for the at-rest view;
// the full bio in `bio` is shown when the Read-more toggle expands.
in property <string> bio-short;
in property <bool> bio-truncated: false;
// Editorial source for the biography ("TiVo" etc); shown as a small
// "Source: <name>" line under the Read more toggle when present.
in property <string> bio-source;
in property <image> artwork;
// Artwork-derived tint for the header gradient.
in property <color> header-color: #181820;
in property <[AlbumTrackItem]> top-tracks: [];
// Releases grouped into titled sections (Albums, EPs & Singles, ...).
in property <[DiscoverSection]> release-sections: [];
// Labels collected from the artist's own album releases (deduped by
// id, alphabetically sorted) — feed the Labels section of the
// network sidebar.
in property <[LabelEntry]> labels: [];
// Similar artists from the /artist/page response — feed the Similar
// Artists section of the network sidebar.
in property <[SimilarEntry]> similar-artists: [];
// Filtered JUMP TO tabs for this artist — only the sections
// that actually have content. Built in artist::apply_artist
// from the page response. anchor-y is the page-local y the
// ArtistPageView will scroll to when the tab is clicked.
in property <[JumpNavTab]> jump-tabs: [];
in property <bool> loading: false;
}
// Artwork right-click actions — shared by ArtistPageView and
// AlbumPageView for their respective headers. `kind` is "artist" or
// "album"; `key` is the artist name or album id; `url` is the
// current network URL (for open-in-browser and save-as).
export global ArtworkActions {
callback open-in-browser(string /* url */);
callback save-as(string /* url */, string /* default file name */);
// Opens the system file picker and registers the chosen image
// as the custom override for this artist/album.
callback add-custom(string /* kind */, string /* key */);
callback remove-custom(string /* kind */, string /* key */);
}
// Artist-view actions wired to Rust callbacks.
export global ArtistActions {
// Filter the visible Popular Tracks and release-section albums by
// a query string (client-side substring match, mirrors the album
// track search in AlbumActions).
callback search(string);
}
// Artist network sidebar — Origin / Labels / Similar / Relationships /
// Discovery. The first three lists come from artist state already; this
// global covers the MusicBrainz-driven sections plus the open/closed
// flag the network button toggles.
// One row of the Origin section. Empty strings mark absent fields so the
// view can hide individual rows without an Option wrapper.
export struct MbOriginData {
// True when the artist is a person (vs group/orchestra/etc). Drives
// the row labels: "Born / Born in / Died" vs "Founded / Founded in /
// Disbanded".
is-person: bool,
// Begin date — "1976" / "May 1976" / "May 14 1976". Empty when MB
// has no life_span.begin.
begin-date: string,
// End date — same shape as begin. Empty when life_span.end is
// missing (or the artist is still active).
end-date: string,
// Location display string ("Berlin, Germany" / "United Kingdom").
// Empty when MB has no location.
location-display: string,
// True when the location is precise enough to drill in (city/state
// precision, or precision==country but a city is present somehow).
// When false, the row renders as plain text.
location-clickable: bool,
}
// One member, group, or collaborator row.
export struct MbRelationship {
mbid: string,
name: string,
// Primary role string for the musician-click callback ("Band Member",
// "Guitar", "Producer", ...). Always populated — defaults to "Band
// Member" / "Band" / "Collaborator" by section when MB has no
// attributes for the relation.
role: string,
// Tooltip — roles joined with ", " plus the period in parens when
// present. Falls back to the name.
tooltip: string,
}
// All MB relationship rows for the sidebar. Each list is independently
// hideable.
export struct MbRelationshipsData {
members: [MbRelationship],
groups: [MbRelationship],
collaborators: [MbRelationship],
// Convenience flag — true when any of the three lists has at least
// one row, so the section header can hide on no-data.
has-data: bool,
}
// One row of the "You may also like" discovery section.
export struct DiscoveryArtist {
// MB id — used by the dismiss callback to remember the rejection.
mbid: string,
name: string,
// Qobuz artist id (string form) when the MB candidate matched a
// Qobuz artist; empty when only available on MB. Drives whether the
// row is clickable.
qobuz-id: string,
}
export global NetworkSidebarState {
// Open/closed. The header network button toggles this; persistence
// is handled by the Rust side (`network_sidebar_prefs::set_open`).
// Defaults to open — Rust overrides on startup from the persisted
// value if any.
in-out property <bool> open: true;
// True until proven otherwise. Set to false when MB is disabled in
// settings, or when the name → mbid resolution fails. Drives whether
// the MB-sourced sections render at all (Origin, Relationships,
// Discovery).
in property <bool> mb-available: true;
// Resolved MB id for the current artist; empty before resolve
// completes or when the artist could not be matched. Used by the
// Rust side as the key for relationship/discovery loads.
in property <string> mb-mbid;
// Origin (MB metadata) section
in property <bool> origin-loading: false;
in property <MbOriginData> origin;
// Relationships (MB) section
in property <bool> relationships-loading: false;
in property <MbRelationshipsData> relationships;
// Discovery (MB tag-based) section — populated in phase 8.
in property <bool> discovery-loading: false;
in property <[DiscoveryArtist]> discovery-artists: [];
// Primary tag the current discovery batch was seeded with —
// delivered as the first argument of the discovery-dismissed
// callback so the Rust side can persist the dismiss under the
// right key.
in property <string> discovery-tag;
}
// Callbacks the sidebar emits. Each delivers the minimum payload the
// Rust controller needs; the controller reconstructs richer payloads
// (location precision, affinity seeds) from cached metadata when it
// has to call out to a target view.
export global NetworkSidebarActions {
// Origin location row clicked — opens ArtistsByLocation view.
callback location-clicked(string /* current artist mbid */);
// Label row clicked — opens LabelReleases view.
callback label-clicked(string /* label id */, string /* label name */);
// Similar / discovery / similar-row clicked — opens the Qobuz
// artist page for `qobuz-id`.
callback artist-clicked(string /* qobuz id */);
// Relationship row clicked — opens MusicianPage with the role.
callback musician-clicked(string /* name */, string /* role */);
// Discovery dismiss — removes the row and remembers the rejection.
// Carries both the MBID (model key for removal) and the name
// (persisted into the per-tag dismiss store after normalization).
callback discovery-dismissed(string /* mbid */, string /* name */);
// Sidebar toggled — Rust persists the new open state.
callback toggle();
}
// One row of MusicianPageView's "Appears On" grid — same shape as
// the album cards on Discover, plus a role label stamped onto each
// album by the Qobuz search wrapping (e.g. "Drums" / "Guitar").
export struct MusicianAppearanceItem {
album-id: string,
album-title: string,
artist-name: string,
year: string,
role-on-album: string,
artwork-url: string,
artwork: image,
}
// MusicianPageView state — opened by the artist network sidebar
// when a relationship row is clicked and the musician has no
// confident Qobuz artist match. Populated by `crate::musician`
// from the resolve + appearances backend calls.
export global MusicianState {
in property <string> name;
in property <string> role;
// "confirmed" | "contextual" | "weak" | "none" — the sidebar
// uses this to skip MusicianPageView entirely on Confirmed and
// navigate to the matched artist instead.
in property <string> confidence;
// Bands / projects from MusicBrainz; not yet populated but
// kept in the data shape so the section renders cleanly when
// wired.
in property <[string]> bands: [];
in property <[MusicianAppearanceItem]> appearances: [];
in property <int> total: 0;
in property <bool> loading: false;
in property <bool> load-more-loading: false;
}
export global MusicianActions {
callback load-more();
}
// LabelReleasesView state — opened by the artist network sidebar's
// Labels section. Header (name + image) from /label/page, the
// album grid from /label/getAlbums. Populated by `crate::label`.
export global LabelState {
in property <string> id;
in property <string> name;
in property <string> image-url;
in property <image> image;
in property <[AlbumCardItem]> albums: [];
in property <int> total: 0;
in property <bool> loading: false;
in property <bool> load-more-loading: false;
}
export global LabelActions {
callback load-more();
}
// One artist in the scene-discovery grid (artists from the same
// place as the source artist).
export struct LocationArtistItem {
qobuz-id: string,
name: string,
genres-line: string,
image-url: string,
image: image,
}
// ArtistsByLocationView state — opened from the Origin section's
// location link. Populated by `crate::location_view`.
export global LocationViewState {
in property <string> scene-label;
in property <string> genre-summary;
in property <[LocationArtistItem]> artists: [];
in property <int> total: 0;
in property <bool> loading: false;
in property <bool> load-more-loading: false;
}
export global LocationViewActions {
callback open-artist(string /* qobuz id */);
callback load-more();
}
// Shell chrome state — collapsible left sidebar and right-side panels.
// Globals are imported directly by the shell components, so toggles
// need no callback plumbing.
export global ShellState {
in-out property <bool> sidebar-collapsed: false;
in-out property <length> sidebar-width: 280px;
// 0 = none, 1 = queue, 2 = lyrics
in-out property <int> right-panel: 0;
}
// Authenticated user, for the header user element.
export global SessionState {
in property <string> user-name: "";
in property <string> subscription: "";
}
// One entry in the playback queue, for the Queue sidebar list.
export struct QueueItem {
id: string,
title: string,
artist: string,
artwork: image,
playing: bool,
duration: string, // "M:SS"
explicit: bool,
}
// Now-playing / transport state. The Rust playback poll loop pushes the
// live values here; the PlayerBar and SeekBar bind to it directly.
export global NowPlayingState {
in property <bool> has-track: false;
in property <string> title;
in property <string> artist;
in property <string> album;
in property <string> album-id;
in property <string> artist-id;
in property <image> artwork;
in property <string> track-id;
in property <bool> playing: false;
in property <int> position-secs: 0;
in property <int> duration-secs: 0;
in property <float> progress: 0.0; // 0..1
in property <float> cache: 0.0; // 0..1 buffered
in property <string> elapsed: "0:00";
in property <string> remaining: "0:00";
in property <float> volume: 0.7; // 0..1
in property <bool> muted: false;
in property <bool> shuffle: false;
in property <int> repeat-mode: 0; // 0 off, 1 all, 2 one
// Transport callbacks, wired to the Rust playback controller.
callback toggle-play();
callback next();
callback previous();
callback seek(float); // 0..1
callback set-volume(float); // 0..1
callback toggle-mute();
callback toggle-shuffle();
callback cycle-repeat();
}
// Playback queue model, for the Queue sidebar.
//
// The sidebar shows two tabs. The Queue tab renders `now-playing` plus a
// paginated slice of `upcoming` (40 per page). The History tab renders
// `history` (most-recent first). The Rust queue controller owns the full
// upcoming/history lists and the pagination maths; it pushes the current
// page's slice into `upcoming-page` and the counters that label it.
export global QueueState {
// --- Queue tab -------------------------------------------------------
// Current track card. `has-current` gates the NOW PLAYING section.
in property <bool> has-current: false;
in property <QueueItem> now-playing;
// Whether the now-playing track is in the user's favorites.
in property <bool> now-playing-favorite: false;
// The slice of upcoming tracks for the current page (<= 40 entries).
in property <[QueueItem]> upcoming-page: [];
// Total upcoming-track count (post-filter when a search is active).
in property <int> upcoming-total: 0;
// Tracks remaining after the current one (total - played - 1).
in property <int> upcoming-remaining: 0;
// Zero-based page index and total page count for the paginator.
in-out property <int> page: 0;
in property <int> page-count: 1;
// Index of the first upcoming entry shown, 1-based, for the counter.
in property <int> page-start: 0;
// Index of the last upcoming entry shown, 1-based, for the counter.
in property <int> page-end: 0;
// --- History tab -----------------------------------------------------
in property <[QueueItem]> history: [];
// --- Shared ----------------------------------------------------------
// Active tab: 0 = Queue, 1 = History.
in-out property <int> tab: 0;
// Live search query; the Rust controller re-filters `upcoming-page`.
in-out property <string> search-query: "";
// --- Callbacks (wired to the Rust queue controller) ------------------
// Play the upcoming entry at `index` within the current page slice.
callback play-upcoming(int);
// Play a history entry by its `index` in the `history` list.
callback play-history(int);
// Remove the upcoming entry at `index` within the current page slice.
callback remove-upcoming(int);
// Empty the queue.
callback clear-queue();
// Toggle the favorite state of the now-playing track.
callback toggle-now-playing-favorite();
// Save the current queue as a new playlist.
callback save-as-playlist();
// Toggle infinite-play (auto-refill the queue with similar tracks).
callback toggle-infinite-play();
// Re-filter the upcoming list after `search-query` changed.
callback search-changed();
// Move to the previous / next paginator page.
callback prev-page();
callback next-page();
// Switch the active tab (0 = Queue, 1 = History).
callback set-tab(int);
// Fired when the sidebar becomes visible, so the Rust controller can
// pull a fresh queue snapshot (the queue may have changed while the
// panel was closed, e.g. after a session restore).
callback panel-opened();
// Reflects whether infinite-play is currently enabled.
in property <bool> infinite-play: false;
}
// A track result row in search (cover thumbnail, unlike album tracks).
export struct SearchTrackItem {
id: string,
title: string,
artist: string,
duration: string,
quality-tier: string,
explicit: bool,
artwork-url: string,
artwork: image,
}
// A playlist result in search. Qobuz returns up to four cover URLs per
// playlist; the card renders them as a count-based collage.
export struct SearchPlaylistItem {
id: string,
title: string,
subtitle: string, // owner name + track count
cover-count: int, // number of cover images, 0-4
url1: string,
url2: string,
url3: string,
url4: string,
cover1: image,
cover2: image,
cover3: image,
cover4: image,
}
// One artist tile in the Favorites > Artists grid.
export struct FavoriteArtistItem {
id: string,
name: string,
albums-line: string,
image-url: string,
image: image,
}
// One playlist tile in the Favorites > Playlists grid.
export struct FavoritePlaylistItem {
id: string,
name: string,
owner: string,
tracks-line: string, // "N tracks"
cover-url: string,
cover: image,
}
// One label row in the Favorites > Labels list.
export struct FavoriteLabelItem {
id: string,
name: string,
albums-line: string, // "N releases"
}
// Library > Favorites state. The active tab decides which model is
// shown; all are populated lazily by `crate::favorites` the first
// time their tab is opened. Albums reuse AlbumCardItem, tracks reuse
// SearchTrackItem (artwork-on), artists use FavoriteArtistItem.
export global FavoritesState {
// "tracks" | "albums" | "artists" | "playlists" | "labels"
in-out property <string> active-tab: "tracks";
// Tracks reuse SearchTrackItem so the shared SearchTrackRow can
// render them (artwork-on, per-row cover thumbnail).
in property <[SearchTrackItem]> tracks: [];
in property <[AlbumCardItem]> albums: [];
in property <[FavoriteArtistItem]> artists: [];
in property <[FavoritePlaylistItem]> playlists: [];
in property <[FavoriteLabelItem]> labels: [];
in property <int> tracks-total: 0;
in property <int> albums-total: 0;
in property <int> artists-total: 0;
in property <int> playlists-total: 0;
in property <int> labels-total: 0;
in property <bool> loading: false;
}
export global FavoritesActions {
// Switch tab; the Rust side lazy-loads the tab's data if it
// hasn't been fetched yet.
callback select-tab(string);
callback open-album(string);
callback open-artist(string);
callback open-label(string /* label id */, string /* name */);
callback open-playlist(string /* playlist id */);
callback play-track(string /* track id */);
callback track-action(string /* track id */, string /* action */);
}
// 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 <[SearchTrackItem]> 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 <SearchTrackItem> most-popular-track;
// Active searchType filter, as a dropdown index (0 = no filter).
// The controller owns the index -> API-value mapping.
in property <int> filter-index: 0;
}
// Search actions wired to the Rust search controller.
export global SearchActions {
// Enter / explicit submit — navigate to the full results page.
callback submit(string);
// Live (debounced) search as the user types.
callback live(string);
// User picked a results tab (0..4).
callback tab-changed(int);
// searchType filter changed — the new dropdown index.
callback filter-changed(int);
// Request the next page for the given tab.
callback load-more(int);
}