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
781 lines
30 KiB
Plaintext
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);
|
|
}
|