Bucketing now trusts the server release_type: drop the foreign-artist filter that hid band/guest releases (e.g. Vicky Psarakis' Albums were credited to Sicksense), render every non-empty bucket, hide 'Purchase Only' (download), and move 'Other' to the end, collapsed by default. Drop the misleading per-section item counts. New 'Latest release' highlight (last_release) and 'Appears On' track section (tracks_appears_on, previously mislabeled as Compilations). Per-section persisted 5-mode sort (artist_prefs) + per-section load-more capped at 4 pages, reusing get_releases_grid. New dedicated discography page (ArtistReleasesView + artist_releases controller) with sort + infinite scroll, reached via each section's 'See discography' link. Magazine/Stories sidebar tab (artist/story) with 1:1 thumbnails and a Network|Magazine tab switcher; hide the empty similar-artists block to remove the 'no similar' over populated-discovery incoherence. Header Share action. Per-release award ribbons. Fix custom artist AND album images: decode + apply immediately on upload and resolve the local override on navigation, so a custom image now shows for artists/albums with no Qobuz artwork. Data layer: type last_release as PageArtistRelease, add /artist/story endpoint + core/client methods + ArtistStory* models, export ArtistReleases* globals. cc
669 lines
30 KiB
Plaintext
669 lines
30 KiB
Plaintext
// App shell — the post-login frame: header bar, sidebar, content
|
|
// region, and player bar. The content region shows the Discover Home,
|
|
// Album detail, or Artist detail view, driven by NavState.
|
|
//
|
|
// Regions are delineated by their surface colors (chrome over the darker
|
|
// content well); no hairline borders.
|
|
|
|
import { Theme } from "../foundation/semantic-colors.slint";
|
|
import { Layout } from "../foundation/layout.slint";
|
|
import { Radius } from "../foundation/radius.slint";
|
|
import { NavState, ContentView, ShellState, OfflineState, PlaylistState, ImmersiveState, SearchActions, VisualizerState } from "../state.slint";
|
|
import { Sidebar } from "Sidebar.slint";
|
|
import { SidebarNowPlayingDock } from "SidebarNowPlayingDock.slint";
|
|
import { HeaderBar } from "HeaderBar.slint";
|
|
import { HeaderMenuOverlay } from "HeaderMenuOverlay.slint";
|
|
import { TooltipOverlay } from "TooltipOverlay.slint";
|
|
import { ArtPreviewOverlay } from "ArtPreviewOverlay.slint";
|
|
import { SidebarFolderPopup } from "SidebarFolderPopup.slint";
|
|
import { SidebarTooltip } from "SidebarTooltip.slint";
|
|
import { SidebarPlaylistsPopup } from "SidebarPlaylistsPopup.slint";
|
|
import { PlaylistPickerModal } from "../primitives/PlaylistPickerModal.slint";
|
|
import { PlaylistDuplicateConfirmModal } from "../primitives/PlaylistDuplicateConfirmModal.slint";
|
|
import { CreatePlaylistModal } from "../primitives/CreatePlaylistModal.slint";
|
|
import { EditPlaylistModal } from "../primitives/EditPlaylistModal.slint";
|
|
import { CreateFolderModal } from "../primitives/CreateFolderModal.slint";
|
|
import { CreateMyQbzModal } from "../myqbz/CreateMyQbzModal.slint";
|
|
import { MyQbzEditModal } from "../myqbz/MyQbzEditModal.slint";
|
|
import { MyQbzMixModal } from "../myqbz/MyQbzMixModal.slint";
|
|
import { AddToMixtapeModal } from "../myqbz/AddToMixtapeModal.slint";
|
|
import { EphemeralPlayChoiceModal } from "../primitives/EphemeralPlayChoiceModal.slint";
|
|
import { FolderEditModal } from "../primitives/FolderEditModal.slint";
|
|
import { LibFolderEditModal } from "../settings/LibFolderEditModal.slint";
|
|
import { TagEditorModal } from "../primitives/TagEditorModal.slint";
|
|
import { PlaylistImportModal } from "../primitives/PlaylistImportModal.slint";
|
|
import { DacWizardModal } from "../primitives/DacWizardModal.slint";
|
|
import { TrackInfoModal } from "../album/TrackInfoModal.slint";
|
|
import { AlbumCreditsModal } from "../album/AlbumCreditsModal.slint";
|
|
import { ImmersiveView } from "../immersive/ImmersiveView.slint";
|
|
import { QconnectDevModal } from "QconnectDevModal.slint";
|
|
import { CastPicker } from "CastPicker.slint";
|
|
import { DiscoverConfigModal } from "../discover/DiscoverConfigModal.slint";
|
|
import { DragGhost } from "../primitives/DragGhost.slint";
|
|
import { Toast } from "../primitives/Toast.slint";
|
|
import { NowPlayingBar } from "NowPlayingBar.slint";
|
|
import { QueueSidebar } from "QueueSidebar.slint";
|
|
import { LyricsSidebar } from "LyricsSidebar.slint";
|
|
import { HomeView } from "../discover/HomeView.slint";
|
|
import { DiscoverBrowseView } from "../discover/DiscoverBrowseView.slint";
|
|
import { AlbumPageView } from "../album/AlbumPageView.slint";
|
|
import { LocalAlbumView } from "../album/LocalAlbumView.slint";
|
|
import { ArtistPageView } from "../artist/ArtistPageView.slint";
|
|
import { ArtistReleasesView } from "../artist/ArtistReleasesView.slint";
|
|
import { MusicianPageView } from "../musician/MusicianPageView.slint";
|
|
import { LabelReleasesView } from "../label/LabelReleasesView.slint";
|
|
import { LabelPageView } from "../label/LabelPageView.slint";
|
|
import { AwardView } from "../award/AwardView.slint";
|
|
import { AwardAlbumsView } from "../award/AwardAlbumsView.slint";
|
|
import { ArtistsByLocationView } from "../location/ArtistsByLocationView.slint";
|
|
import { FavoritesView } from "../favorites/FavoritesView.slint";
|
|
import { FavoritesOfflineRail } from "../favorites/FavoritesOfflineRail.slint";
|
|
import { MixView } from "../mix/MixView.slint";
|
|
import { PlaylistView } from "../playlist/PlaylistView.slint";
|
|
import { PlaylistManagerView } from "../playlist/PlaylistManagerView.slint";
|
|
import { MixtapesView } from "../myqbz/MixtapesView.slint";
|
|
import { CollectionsView } from "../myqbz/CollectionsView.slint";
|
|
import { MixtapeDetailView } from "../myqbz/MixtapeDetailView.slint";
|
|
import { DiscographyBuilderView } from "../myqbz/DiscographyBuilderView.slint";
|
|
import { OfflineManagerView } from "../offline/OfflineManagerView.slint";
|
|
import { BlacklistManagerView } from "../settings/BlacklistManagerView.slint";
|
|
import { OfflinePlaceholder } from "../offline/OfflinePlaceholder.slint";
|
|
import { LocalLibraryView } from "../locallibrary/LocalLibraryView.slint";
|
|
import { SettingsView } from "../settings/SettingsView.slint";
|
|
import { SearchResultsView } from "../search/SearchResultsView.slint";
|
|
import { Cortinilla } from "../search/Cortinilla.slint";
|
|
|
|
export component AppShell inherits Rectangle {
|
|
callback open-album(string);
|
|
callback open-artist(string);
|
|
callback media-action(string, string, string);
|
|
callback header-menu-navigate(string);
|
|
callback discover-view-all(string, string);
|
|
callback settings-bool(string, bool);
|
|
callback settings-select(string, int);
|
|
callback settings-slider(string, int);
|
|
callback settings-reset();
|
|
callback settings-release-device();
|
|
callback logout();
|
|
callback close-app();
|
|
// D2 recovery sign-in with the saved token — fired by the header
|
|
// status badge's flyout (formerly an in-layout banner).
|
|
callback recovery-login();
|
|
|
|
// Offline gating (slice 3d): the Qobuz-only content views are replaced by
|
|
// the OfflinePlaceholder while ANY offline flavor is active. Views that
|
|
// work without Qobuz (LocalLibrary, local album, offline cache manager,
|
|
// My QBZ lists/detail — local-DB resolve, Settings) keep mounting.
|
|
property <bool> qobuz-view-blocked: OfflineState.offline && (
|
|
NavState.view == ContentView.home
|
|
|| NavState.view == ContentView.discover-browse
|
|
|| NavState.view == ContentView.search
|
|
|| NavState.view == ContentView.favorites
|
|
|| NavState.view == ContentView.album
|
|
|| NavState.view == ContentView.artist
|
|
|| NavState.view == ContentView.artist-releases
|
|
|| NavState.view == ContentView.musician
|
|
|| NavState.view == ContentView.label
|
|
|| NavState.view == ContentView.label-releases
|
|
|| NavState.view == ContentView.award
|
|
|| NavState.view == ContentView.award-albums
|
|
|| NavState.view == ContentView.location
|
|
|| NavState.view == ContentView.mix
|
|
// Playlist: LOCAL playlists and the offline sidecar rendering of a
|
|
// MIXED one (D11.a) mount below; only a plain Qobuz detail blocks.
|
|
|| (NavState.view == ContentView.playlist
|
|
&& !PlaylistState.is-local && !PlaylistState.offline-subset));
|
|
|
|
background: Theme.surface-main;
|
|
|
|
// Close the live search "cortinilla" on ANY view change, whatever drove it
|
|
// (header back/forward, the nav-tab menu, a sidebar item, the player bar, a
|
|
// row click that navigates). Every navigation path ultimately sets
|
|
// `NavState.view`, so a single `changed` on a property mirroring it is the
|
|
// one chokepoint that covers them all — otherwise the dropdown + its
|
|
// click-outside scrim stay mounted over the new page and the scrim eats the
|
|
// first click. The cortinilla's own handlers (submit / search-all / row /
|
|
// dismiss / short-query) still close it directly; this just adds the
|
|
// "navigated away by other means" case.
|
|
property <ContentView> tracked-view: NavState.view;
|
|
changed tracked-view => {
|
|
SearchActions.cortinilla-dismiss();
|
|
}
|
|
|
|
// FFT visualizer tap — single source of truth. The tap (frontend-agnostic
|
|
// producer in qbz-audio, driven by crate::visualizer) must run whenever the
|
|
// immersive overlay OR the Large-NPB sidebar dock is showing a spectrum, and
|
|
// idle otherwise (no capture, no FFT cost). This `changed` catches every
|
|
// transition: immersive open/close, Large enter/exit, and the sidebar
|
|
// cycling out of "open" (which reverts Large in cycle-sidebar). The Large
|
|
// menu arm and immersive-open path in Rust also prime it for the initial
|
|
// value (changed only fires on transitions). Immersive's own close handlers
|
|
// defer to `large-active` so closing it never kills an active Large dock.
|
|
property <bool> viz-should-run: ImmersiveState.open
|
|
|| (ShellState.large-active && ShellState.large-visualizer-on);
|
|
changed viz-should-run => {
|
|
VisualizerState.set-enabled(self.viz-should-run);
|
|
}
|
|
|
|
// Bottom layer: catches the mouse back / forward buttons for history
|
|
// navigation. Declared first so the real UI sits on top of it.
|
|
TouchArea {
|
|
pointer-event(e) => {
|
|
if (e.kind == PointerEventKind.down) {
|
|
if (e.button == PointerEventButton.back) {
|
|
NavState.request-back();
|
|
}
|
|
if (e.button == PointerEventButton.forward) {
|
|
NavState.request-forward();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
VerticalLayout {
|
|
spacing: 0px;
|
|
|
|
// Header spans the full width at the top; the sidebar starts below it.
|
|
// The offline/recovery status now lives INSIDE the header (badge +
|
|
// flyout, left of the app-menu button) — never as an in-layout row,
|
|
// which displaced everything below it (the sidebar and its anchored
|
|
// section flyouts) by its height.
|
|
HeaderBar {
|
|
logout => {
|
|
root.logout();
|
|
}
|
|
close-app => {
|
|
root.close-app();
|
|
}
|
|
recovery-login => {
|
|
root.recovery-login();
|
|
}
|
|
}
|
|
|
|
HorizontalLayout {
|
|
vertical-stretch: 1;
|
|
spacing: 0px;
|
|
|
|
Sidebar {
|
|
navigate(route) => {
|
|
root.header-menu-navigate(route);
|
|
}
|
|
}
|
|
|
|
// Content frame — surface-card chrome around a rounded, inset
|
|
// content panel (Slack-style bezel on all four corners).
|
|
Rectangle {
|
|
horizontal-stretch: 1;
|
|
background: Theme.surface-card;
|
|
VerticalLayout {
|
|
vertical-stretch: 1;
|
|
padding-left: 8px;
|
|
padding-right: 8px;
|
|
// No top padding: the contiguous surface-card gap below the
|
|
// header made the items look high (the perceived header
|
|
// band was taller than its real height). The panel now
|
|
// butts the header; its rounded top corners still bezel.
|
|
padding-top: 0px;
|
|
padding-bottom: 8px;
|
|
|
|
Rectangle {
|
|
// EXPLICIT height = window - header - player bar - this
|
|
// VL's bottom padding. A Slint `VerticalLayout` ignores an
|
|
// explicit `height` (it sizes to content), and a layout
|
|
// does not auto-fill its parent Rectangle — so the content
|
|
// row could not stretch on its own and collapsed once the
|
|
// right panels (which used to drive the height) became
|
|
// conditionally mounted. A Rectangle DOES take an explicit
|
|
// height, so we size the content area arithmetically; the
|
|
// height then cascades UP (this -> VL -> frame -> row ->
|
|
// shell). `root.height` is the AppShell height (pinned to
|
|
// the window in app.slint).
|
|
//
|
|
// NOTE: the bar height is mode-aware. New + Classic + Large
|
|
// use the full `Layout.player-bar-height` (112px); only Small
|
|
// (npb-mode 2) collapses the bar to one header-tall row
|
|
// (42px), so the content area gains ~70px. Computed inline
|
|
// (mirrors the same expression in PlayerBar.slint `height:`)
|
|
// to keep `Layout` a pure leaf independent of ShellState.
|
|
// Large (mode 3) keeps the full bar height — its only
|
|
// restructuring (cover + spectrum) lives inside the sidebar,
|
|
// not in this content-height math.
|
|
height: root.height - Layout.header-height
|
|
- (ShellState.npb-mode == 2 ? Layout.header-height : Layout.player-bar-height)
|
|
- 8px;
|
|
vertical-stretch: 1;
|
|
background: Theme.surface-main;
|
|
border-radius: Radius.md;
|
|
clip: true;
|
|
|
|
// Offline: every Qobuz-only view below mounts only while
|
|
// online; the shared placeholder takes the pane otherwise
|
|
// (ADR-010 — the `if` lives here at the mount site). The
|
|
// Favorites mount alone injects the B9 "playable favorites"
|
|
// rail into the placeholder's children slot.
|
|
if root.qobuz-view-blocked && NavState.view != ContentView.favorites: OfflinePlaceholder { }
|
|
if root.qobuz-view-blocked && NavState.view == ContentView.favorites: OfflinePlaceholder {
|
|
FavoritesOfflineRail { }
|
|
}
|
|
|
|
if NavState.view == ContentView.home && !OfflineState.offline: HomeView {
|
|
album-clicked(id) => {
|
|
root.open-album(id);
|
|
}
|
|
open-artist(id) => {
|
|
root.open-artist(id);
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
discover-view-all(e, t) => {
|
|
root.discover-view-all(e, t);
|
|
}
|
|
}
|
|
if NavState.view == ContentView.discover-browse && !OfflineState.offline: DiscoverBrowseView {
|
|
open-album(id) => {
|
|
root.open-album(id);
|
|
}
|
|
open-artist(id) => {
|
|
root.open-artist(id);
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
if NavState.view == ContentView.album && !OfflineState.offline: AlbumPageView {
|
|
open-artist(id) => {
|
|
root.open-artist(id);
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
// Dedicated Local Library album detail (separate from AlbumView).
|
|
if NavState.view == ContentView.local-album: LocalAlbumView {
|
|
open-artist(name) => {
|
|
root.open-artist(name);
|
|
}
|
|
}
|
|
if NavState.view == ContentView.artist && !OfflineState.offline: ArtistPageView {
|
|
open-album(id) => {
|
|
root.open-album(id);
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
if NavState.view == ContentView.artist-releases && !OfflineState.offline: ArtistReleasesView {
|
|
open-album(id) => {
|
|
root.open-album(id);
|
|
}
|
|
open-artist(id) => {
|
|
root.open-artist(id);
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
if NavState.view == ContentView.musician && !OfflineState.offline: MusicianPageView {
|
|
open-album(id) => {
|
|
root.open-album(id);
|
|
}
|
|
}
|
|
if NavState.view == ContentView.label && !OfflineState.offline: LabelPageView {
|
|
open-album(id) => {
|
|
root.open-album(id);
|
|
}
|
|
open-artist(id) => {
|
|
root.open-artist(id);
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
if NavState.view == ContentView.label-releases && !OfflineState.offline: LabelReleasesView {
|
|
open-album(id) => {
|
|
root.open-album(id);
|
|
}
|
|
open-artist(id) => {
|
|
root.open-artist(id);
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
if NavState.view == ContentView.award && !OfflineState.offline: AwardView {
|
|
open-album(id) => {
|
|
root.open-album(id);
|
|
}
|
|
open-artist(id) => {
|
|
root.open-artist(id);
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
if NavState.view == ContentView.award-albums && !OfflineState.offline: AwardAlbumsView {
|
|
open-album(id) => {
|
|
root.open-album(id);
|
|
}
|
|
open-artist(id) => {
|
|
root.open-artist(id);
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
if NavState.view == ContentView.location && !OfflineState.offline: ArtistsByLocationView {
|
|
open-artist(id) => {
|
|
root.open-artist(id);
|
|
}
|
|
}
|
|
if NavState.view == ContentView.favorites && !OfflineState.offline: FavoritesView {
|
|
open-album(id) => {
|
|
root.open-album(id);
|
|
}
|
|
open-artist(id) => {
|
|
root.open-artist(id);
|
|
}
|
|
}
|
|
if NavState.view == ContentView.playlist-manager: PlaylistManagerView { }
|
|
if NavState.view == ContentView.mixtapes: MixtapesView { }
|
|
if NavState.view == ContentView.collections: CollectionsView { }
|
|
if NavState.view == ContentView.mixtape-detail: MixtapeDetailView { }
|
|
if NavState.view == ContentView.discography-builder: DiscographyBuilderView { }
|
|
if NavState.view == ContentView.offline-manager: OfflineManagerView { }
|
|
if NavState.view == ContentView.blacklist-manager: BlacklistManagerView { }
|
|
if NavState.view == ContentView.local-library: LocalLibraryView { }
|
|
if NavState.view == ContentView.mix && !OfflineState.offline: MixView {
|
|
play-track(id) => {
|
|
root.media-action("track", id, "play");
|
|
}
|
|
play-all => {
|
|
root.media-action("mix", "", "play-all");
|
|
}
|
|
shuffle => {
|
|
root.media-action("mix", "", "shuffle");
|
|
}
|
|
refresh => {
|
|
root.media-action("mix", "", "refresh");
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
// LOCAL playlists (PlaylistState.is-local) render OFFLINE
|
|
// too — they live in library.db, not on Qobuz (D7/D11) —
|
|
// and so does the local-sidecar rendering of a MIXED
|
|
// Qobuz playlist (PlaylistState.offline-subset, D11.a).
|
|
if NavState.view == ContentView.playlist
|
|
&& (!OfflineState.offline
|
|
|| PlaylistState.is-local
|
|
|| PlaylistState.offline-subset): PlaylistView {
|
|
play-track(id) => {
|
|
root.media-action("track", id, "play");
|
|
}
|
|
play-all => {
|
|
root.media-action("playlist", "", "play-all");
|
|
}
|
|
shuffle => {
|
|
root.media-action("playlist", "", "shuffle");
|
|
}
|
|
toggle-favorite => {
|
|
root.media-action("playlist", "", "favorite");
|
|
}
|
|
edit => {
|
|
root.media-action("playlist", "", "edit");
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
if NavState.view == ContentView.settings: SettingsView {
|
|
settings-bool(k, v) => {
|
|
root.settings-bool(k, v);
|
|
}
|
|
settings-select(k, i) => {
|
|
root.settings-select(k, i);
|
|
}
|
|
settings-slider(k, v) => {
|
|
root.settings-slider(k, v);
|
|
}
|
|
settings-reset => {
|
|
root.settings-reset();
|
|
}
|
|
settings-release-device => {
|
|
root.settings-release-device();
|
|
}
|
|
}
|
|
if NavState.view == ContentView.search && !OfflineState.offline: SearchResultsView {
|
|
open-album(id) => {
|
|
root.open-album(id);
|
|
}
|
|
open-artist(id) => {
|
|
root.open-artist(id);
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Right-side panel column — Queue and/or Lyrics stacked vertically,
|
|
// sharing the height (Feishin-style). A layout sibling of the content
|
|
// (shrinks it, no overlay). The column width animates 0<->240 (= the
|
|
// open left-sidebar width); it collapses only when BOTH are closed.
|
|
// When both are open each section vertical-stretches to half; when
|
|
// only one is open it fills.
|
|
Rectangle {
|
|
// 300px so the lines/queue gain +20px of CONTENT width
|
|
// (padding/margins unchanged inside the panels). Both share
|
|
// the column (stacked mode), so both widen together — keeping
|
|
// Queue and Lyrics aligned when stacked.
|
|
width: (ShellState.queue-open || ShellState.lyrics-open) ? 300px : 0px;
|
|
clip: true;
|
|
animate width { duration: 160ms; easing: ease-in-out; }
|
|
background: Theme.surface-card;
|
|
VerticalLayout {
|
|
spacing: 0px;
|
|
if ShellState.queue-open: QueueSidebar {
|
|
vertical-stretch: 1;
|
|
}
|
|
if ShellState.queue-open && ShellState.lyrics-open: Rectangle {
|
|
height: 1px;
|
|
background: Theme.border-subtle;
|
|
}
|
|
if ShellState.lyrics-open: LyricsSidebar {
|
|
vertical-stretch: 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mode-aware bottom bar shell: New/Classic -> PlayerBar, Small ->
|
|
// PlayerBarSmall. Forwards the same bindings the bar always took.
|
|
NowPlayingBar {
|
|
open-album(id) => {
|
|
root.open-album(id);
|
|
}
|
|
open-artist(id) => {
|
|
root.open-artist(id);
|
|
}
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
settings-bool(k, v) => {
|
|
root.settings-bool(k, v);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Large-NPB cover + spectrum dock — a ROOT overlay pinned flush to the window
|
|
// bottom-left (the L's vertical arm), painting over the bar's surface-card in
|
|
// that corner. Declared AFTER the outer VerticalLayout so it stacks above the
|
|
// bar, and BEFORE HeaderMenuOverlay so header flyouts (and every modal/toast
|
|
// below) still stack above it (ADR-009 declaration-order z). Only mounted
|
|
// while Large is active (sidebar open + npb-mode 3). x/width give equal L/R
|
|
// gutters (16 + 208 + 16 = 240); y pins the cover flush to the window bottom.
|
|
// The dock self-derives its height (must equal ShellState.large-dock-height).
|
|
if ShellState.large-active: SidebarNowPlayingDock {
|
|
width: ShellState.large-dock-width;
|
|
x: 16px;
|
|
y: root.height - ShellState.large-dock-height;
|
|
media-action(k, i, a) => {
|
|
root.media-action(k, i, a);
|
|
}
|
|
}
|
|
|
|
// Header nav-tab dropdown menus. Declared LAST so it draws on top of
|
|
// the header and the content region. A plain in-window overlay (not
|
|
// a PopupWindow) — it does not grab the pointer, so the header tabs
|
|
// stay hoverable and menus can be hover-switched instantly.
|
|
HeaderMenuOverlay {
|
|
navigate(route) => {
|
|
root.header-menu-navigate(route);
|
|
}
|
|
}
|
|
|
|
// Live as-you-type search dropdown (the "cortinilla"), anchored under the
|
|
// centered header search box. A plain in-window overlay (NOT a PopupWindow)
|
|
// so the search box stays typeable and the rows stay hoverable while
|
|
// results stream in. Declared after HeaderMenuOverlay (same header-anchored
|
|
// z-tier, above content) and before the feature modals / toasts so those
|
|
// still stack on top. Its callbacks forward straight to SearchActions.
|
|
Cortinilla {
|
|
row-clicked(fi) => {
|
|
SearchActions.cortinilla-row-clicked(fi);
|
|
}
|
|
view-more(kind) => {
|
|
SearchActions.cortinilla-view-more(kind);
|
|
}
|
|
search-all => {
|
|
SearchActions.cortinilla-search-all();
|
|
}
|
|
dismiss => {
|
|
SearchActions.cortinilla-dismiss();
|
|
}
|
|
}
|
|
|
|
// Collapsed-sidebar folder flyout (mini state). Drawn above the content.
|
|
SidebarFolderPopup { }
|
|
|
|
// Collapsed-sidebar hover tooltip (mini state) — name + folder count.
|
|
SidebarTooltip { }
|
|
|
|
// Closed-sidebar playlists flyout (opened from the header playlists button).
|
|
SidebarPlaylistsPopup { }
|
|
|
|
// Global "Add to playlist" picker overlay, opened from any track
|
|
// context menu.
|
|
PlaylistPickerModal { }
|
|
|
|
// "Duplicate tracks" confirm sub-modal — mounted AFTER the picker so it
|
|
// stacks above it (Slint stacking = declaration order; ADR-009/010).
|
|
PlaylistDuplicateConfirmModal { }
|
|
|
|
// Global "Create playlist" modal, opened from the sidebar "+".
|
|
CreatePlaylistModal { }
|
|
|
|
// Global "Edit playlist" modal (rename / delete).
|
|
EditPlaylistModal { }
|
|
|
|
// Global "Create folder" modal.
|
|
CreateFolderModal { }
|
|
|
|
// Global "Create Mixtape / Collection" modal (My QBZ "+ New").
|
|
CreateMyQbzModal { }
|
|
|
|
// Global My QBZ detail edit modals (rename / description / delete-confirm).
|
|
MyQbzEditModal { }
|
|
|
|
// Global My QBZ "DJ-mix / Random queue" sampler modal (TrackMixModal).
|
|
MyQbzMixModal { }
|
|
|
|
// Global "Add to Mixtape/Collection" picker, opened from any track /
|
|
// album / playlist / bulk-selection "Add to Mixtape/Collection" trigger.
|
|
AddToMixtapeModal { }
|
|
|
|
// Ephemeral "already playing" add-vs-replace choice.
|
|
EphemeralPlayChoiceModal { }
|
|
|
|
// Global "Folder editor" modal (Playlist Manager create/edit folder).
|
|
FolderEditModal { }
|
|
|
|
// Global "Local Library folder settings" modal (Settings > Local Library).
|
|
LibFolderEditModal { }
|
|
|
|
// Global "Tag editor" modal (local album metadata).
|
|
TagEditorModal { }
|
|
|
|
// Global "Import Playlist" modal (public-playlist importer). Mounted
|
|
// LAST among the feature modals so it stacks above them (ADR-009
|
|
// declaration-order z; ADR-010 conditional mount via the global's
|
|
// `open`), keeping QconnectDevModal topmost.
|
|
PlaylistImportModal { }
|
|
|
|
// HiFi Wizard (DAC setup) — opened from Audio settings (PipeWire only).
|
|
// Overlay modal; playback keeps running underneath so the test step works.
|
|
DacWizardModal { }
|
|
|
|
// Track Info + Album Info (Credits/Review) modals — fed by crate::info_modals
|
|
// through TrackInfoState / AlbumInfoState. Conditional-mount on their own
|
|
// `open`; stacked above the feature modals (ADR-009/010).
|
|
TrackInfoModal {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
AlbumCreditsModal {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
// Qobuz Connect DEV diagnostics modal (opened from the connect flyout).
|
|
QconnectDevModal {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
// Discover section configurator (Slice 5). Mounted after the other modals
|
|
// so its declaration order keeps it on top of them (ADR-009).
|
|
DiscoverConfigModal {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
// Cast (Chromecast / DLNA) device picker.
|
|
CastPicker {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
// Floating track-drag ghost (follows the cursor, on top of all).
|
|
DragGhost { }
|
|
|
|
if ImmersiveState.open: ImmersiveView {
|
|
width: 100%;
|
|
height: 100%;
|
|
media-action(kind, id, action) => {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
|
|
// In-app toast notifications — bottom-center, above everything.
|
|
Toast { }
|
|
|
|
// Global hover-tooltip overlay — drawn LAST so no control can cover it.
|
|
TooltipOverlay {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
// Floating album-art hover preview — overlay-rendered (not a PopupWindow) so
|
|
// it never steals hover from the trigger. On top of everything.
|
|
ArtPreviewOverlay {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
}
|