Files
qbz/crates/qbz-slint/ui/myqbz/MyQbzMixModal.slint
T
vicrodh 4e0d1a005e fix(myqbz): final fidelity pass (uppercase, trim, expanded cache, bulk bar §13.1)
Phase-2 Slice 12d.
- Discography Builder: uppercase eyebrow + ORDER BY + 6 column labels (BUILD ARTIST
  COLLECTION / YEAR/TYPE/ALBUM/TRACKS/SOURCE/QUALITY); Create now disables on a
  whitespace-only name (name-trimmed-empty computed in the controller).
- Expanded-mode: per-item inline-track cache keyed by the stable source|source_item_id
  (survives filter/sort/search re-derive instead of re-fetching), cleared on open.
- Inline tracks: go-to-album passes the parent item_type (playlist parent routes to
  the playlist view); duration 0 -> "--:--"; track number from the resolver order.
- Detail bulk bar: completed the §13.1 set (Add to queue, Play next, Add to playlist,
  Add to Mixtape/Collection, Remove, Clear) via MultiSelectBar slots + bulk_enqueue
  (reuses the resolver) + the existing playlist picker; no invented primitive.
- Stale comments corrected (dj-mix wired; mix-modal primary = Add to queue).

Backend crates untouched. cargo check -p qbz-slint green.

cc
2026-06-09 08:49:39 -06:00

185 lines
7.7 KiB
Plaintext

// 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 <bool> 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 <int> 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 <string> 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();
}
}
}
}
}
}
}