// My QBZ — DJ-mix "Random queue" sampler modal (TrackMixModal, spec 21 §C / // spec 12 §12). // // Opened from the hero DJ-mix CTA. Lets the user pick how many randomly-sampled // songs to enqueue from this collection. The size choices come from the // backend's buildSizeOptions math (50,100,150,…,All(N)) where N is the deduped // unique-track count; the modal exposes them through a snapping slider (the // prompt's directive — mirror QbzSlider + the modal idiom). On confirm it hands // the chosen size to the Rust controller, which runs dedup_by_similarity + // hybrid_sample and replaces the queue. // // Chrome mirrors MyQbzEditModal: an own backdrop (#000000bf) + a 440px panel, // h2 title (18px bold), and a footer with secondary Cancel + a primary // "Add to queue" button. Backdrop click + Cancel close without committing; // the primary action is disabled while loading / busy or when no size is // available. // // Per spec §23 the button labels are HARDCODED English in the Tauri source; we // keep them @tr here (translate hardcoded UI strings on port). import { Theme } from "../foundation/semantic-colors.slint"; import { Typography } from "../foundation/typography.slint"; import { Radius } from "../foundation/radius.slint"; import { MyQbzMixState, MyQbzMixActions } from "../state.slint"; import { QbzSlider } from "../primitives/QbzSlider.slint"; import { LoadingSpinner } from "../primitives/LoadingSpinner.slint"; import { QbzPrimaryButton } from "../primitives/QbzPrimaryButton.slint"; import { SecondaryButton } from "../primitives/SecondaryButton.slint"; export component MyQbzMixModal inherits Rectangle { visible: MyQbzMixState.open; // The primary "Add to queue" action is allowed once a count is loaded (a // non-empty options list) and nothing is in flight. property can-shuffle: !MyQbzMixState.loading && !MyQbzMixState.busy && MyQbzMixState.size-options.length > 0 && MyQbzMixState.selected-size > 0; // Slider travels over option INDICES (0..options-1). A single option (small // collection -> only "All (N)") still renders a degenerate 0..0 track. property slider-max: Math.max(0, MyQbzMixState.size-options.length - 1); // The dropdown-style display value for the current size: "All (N)" for the // trailing entry, else the plain number. property size-label: MyQbzMixState.selected-is-all ? @tr("All ({})", MyQbzMixState.selected-size) : "\{MyQbzMixState.selected-size}"; if MyQbzMixState.open: Rectangle { background: #000000bf; TouchArea { mouse-cursor: default; clicked => { MyQbzMixActions.close(); } } Rectangle { width: Math.min(root.width - 80px, 440px); height: panel.preferred-height; x: Math.round((parent.width - self.width) / 2 / 1px) * 1px; y: Math.round((parent.height - self.height) / 2 / 1px) * 1px; border-radius: Radius.md; background: Theme.surface-card; border-width: 1px; border-color: Theme.border-subtle; drop-shadow-blur: 32px; drop-shadow-color: #00000080; // Swallow clicks on the panel (so they don't reach the backdrop). TouchArea { } panel := VerticalLayout { padding: 24px; spacing: 18px; // --- Title (h2) — mixModal.title = "Random queue" -------- Text { text: @tr("Random queue"); color: Theme.text-primary; font-size: 18px; font-weight: Typography.bold; } // --- Body copy (mixModal.body) --------------------------- Text { text: @tr("Create a random queue with songs from this collection. How many songs do you want?"); color: Theme.text-secondary; font-size: 14px; wrap: word-wrap; } // --- Loading state (mixModal.computing) ------------------ if MyQbzMixState.loading: HorizontalLayout { spacing: 8px; alignment: start; VerticalLayout { alignment: center; LoadingSpinner { size: 18px; } } Text { text: @tr("Loading…"); color: Theme.text-secondary; font-size: 14px; vertical-alignment: center; } } // --- Size field (slider + count labels) ------------------ if !MyQbzMixState.loading && MyQbzMixState.size-options.length > 0: VerticalLayout { spacing: 8px; // Field label (mixModal.numberOfSongs). Text { text: @tr("Number of songs"); color: Theme.text-primary; font-size: 13px; font-weight: Typography.medium; } // Selected size + slider on one row. HorizontalLayout { spacing: 12px; // The current pick (big, accent), so the slider's effect // is legible. Text { text: root.size-label; color: Theme.accent; font-size: 15px; font-weight: Typography.semibold; vertical-alignment: center; min-width: 64px; } VerticalLayout { horizontal-stretch: 1; alignment: center; QbzSlider { width: 100%; minimum: 0; maximum: root.slider-max; value: MyQbzMixState.selected-index; changed(v) => { MyQbzMixActions.set-index(v); } } } } // Helper (mixModal.songsAvailable) = "{n} songs available". Text { text: @tr("{} songs available", MyQbzMixState.unique-count); color: Theme.text-muted; font-size: 12px; } } // --- Footer (.tm-actions, right-aligned) — shared primitives HorizontalLayout { alignment: end; spacing: 12px; // Cancel (secondary). SecondaryButton { label: @tr("Cancel"); height: 38px; enabled: !MyQbzMixState.busy; clicked => { MyQbzMixActions.close(); } } // Shuffle / Add to queue (primary) — mixModal.confirm. QbzPrimaryButton { label: @tr("Add to queue"); height: 38px; enabled: root.can-shuffle; clicked => { MyQbzMixActions.shuffle(); } } } } } } }