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
This commit is contained in:
vicrodh committed 2026-06-09 08:49:39 -06:00
1 parent 16e6232220
commit 4e0d1a005e
8 files changed
+349 -45

No files matched your search

@@ -523,12 +523,17 @@ export component DiscographyBuilderView inherits Rectangle {
spacing: 4px;
horizontal-stretch: 1;
Text {
text: @tr("Build Artist Collection");
// Eyebrow is uppercased (spec 13 §0: eyebrow/field-
// label/col-label use text-transform:uppercase). Slint
// has no text-transform, so the literal is written in
// caps (same idiom as the other MyQBZ eyebrows —
// AddToMixtapeModal "ADD TO MIXTAPE/COLLECTION", the
// grid card label from Rust kind_label).
text: @tr("BUILD ARTIST COLLECTION");
color: D.text-muted;
font-size: 10px;
font-weight: 600;
letter-spacing: 1.5px;
// uppercase via the literal (en already caps the eyebrow).
}
Text {
text: DiscoBuilderState.artist-name == ""
@@ -585,7 +590,9 @@ export component DiscographyBuilderView inherits Rectangle {
VerticalLayout {
alignment: center;
Text {
text: @tr("Order by");
// .field-label is uppercased (spec §2.6 — same
// text-transform:uppercase as the eyebrow / NAME label).
text: @tr("ORDER BY");
color: D.text-muted;
font-size: 10px;
font-weight: 600;
@@ -743,30 +750,31 @@ export component DiscographyBuilderView inherits Rectangle {
}
}
}
// [1..6] column labels.
// [1..6] column labels — uppercased (spec §2.9
// header-row text-transform:uppercase; literal caps).
Rectangle {
width: 56px;
Text { text: @tr("Year"); color: D.text-muted; font-size: 10px; font-weight: 600; letter-spacing: 1.2px; vertical-alignment: center; width: 100%; horizontal-alignment: left; }
Text { text: @tr("YEAR"); color: D.text-muted; font-size: 10px; font-weight: 600; letter-spacing: 1.2px; vertical-alignment: center; width: 100%; horizontal-alignment: left; }
}
Rectangle {
width: 72px;
Text { text: @tr("Type"); color: D.text-muted; font-size: 10px; font-weight: 600; letter-spacing: 1.2px; vertical-alignment: center; width: 100%; horizontal-alignment: left; }
Text { text: @tr("TYPE"); color: D.text-muted; font-size: 10px; font-weight: 600; letter-spacing: 1.2px; vertical-alignment: center; width: 100%; horizontal-alignment: left; }
}
Rectangle {
horizontal-stretch: 1;
Text { text: @tr("Album"); color: D.text-muted; font-size: 10px; font-weight: 600; letter-spacing: 1.2px; vertical-alignment: center; horizontal-alignment: left; }
Text { text: @tr("ALBUM"); color: D.text-muted; font-size: 10px; font-weight: 600; letter-spacing: 1.2px; vertical-alignment: center; horizontal-alignment: left; }
}
Rectangle {
width: 72px;
Text { text: @tr("Tracks"); color: D.text-muted; font-size: 10px; font-weight: 600; letter-spacing: 1.2px; vertical-alignment: center; width: 100%; horizontal-alignment: center; }
Text { text: @tr("TRACKS"); color: D.text-muted; font-size: 10px; font-weight: 600; letter-spacing: 1.2px; vertical-alignment: center; width: 100%; horizontal-alignment: center; }
}
Rectangle {
width: 60px;
Text { text: @tr("Source"); color: D.text-muted; font-size: 10px; font-weight: 600; letter-spacing: 1.2px; vertical-alignment: center; width: 100%; horizontal-alignment: center; }
Text { text: @tr("SOURCE"); color: D.text-muted; font-size: 10px; font-weight: 600; letter-spacing: 1.2px; vertical-alignment: center; width: 100%; horizontal-alignment: center; }
}
Rectangle {
width: 156px;
Text { text: @tr("Quality"); color: D.text-muted; font-size: 10px; font-weight: 600; letter-spacing: 1.2px; vertical-alignment: center; width: 100%; horizontal-alignment: center; }
Text { text: @tr("QUALITY"); color: D.text-muted; font-size: 10px; font-weight: 600; letter-spacing: 1.2px; vertical-alignment: center; width: 100%; horizontal-alignment: center; }
}
}
}
@@ -825,10 +833,14 @@ export component DiscographyBuilderView inherits Rectangle {
HorizontalLayout {
spacing: 8px;
alignment: end;
// disabled = creating || selectedCount===0 ||
// !collectionName.trim() (spec §2.13 / §10.19). The trim is
// computed in the controller (Slint can't trim) and exposed
// as `name-trimmed-empty`, so a whitespace-only name disables.
property <bool> create-disabled:
DiscoBuilderState.creating
|| DiscoBuilderState.selected-count == 0
|| DiscoBuilderState.collection-name == "";
|| DiscoBuilderState.name-trimmed-empty;
// Cancel (secondary).
SecondaryButton {
label: @tr("Cancel");
@@ -854,7 +854,8 @@ export component MixtapeDetailView inherits Rectangle {
clicked => { MyQbzDetailActions.shuffle(); }
}
}
// (3) DJ-mix (DjIcon) — STUB click (modal is a later slice).
// (3) DJ-mix (DjIcon) — opens the MyQbzMixModal (the
// "Random queue" sampler) via the dj-mix action.
VerticalLayout {
alignment: center;
CircleAction {
@@ -1386,10 +1387,13 @@ export component MixtapeDetailView inherits Rectangle {
// ===================== BULK ACTION BAR =============================
// Spec 12 §13: a bottom-docked bar that appears while items are selected
// (placement='bottom' default). Mirrors the local-library MultiSelectBar
// chrome. The action set in THIS view (§13.1, minus the split-queue and
// add-to-playlist/favorites buttons not threaded by this slice): Add to
// Mixtape/Collection + Remove from this collection (danger). The bar emits
// the id; the Rust side routes it (open AddToMixtapeModal / remove + reload).
// chrome (a flat [BulkAction] row — the §13.1 split-queue dropdown isn't
// expressible in MultiSelectBar, so its two actions are flattened to the
// adjacent "Add to queue" + "Play next" buttons, same idiom the Local
// Library tracks bar uses). The §13.1 action set, in order: Add to queue
// (play-later), Play next, Add to playlist, Add to Mixtape/Collection,
// Remove from this collection (danger), Clear (exit-select). The bar emits
// the id; the Rust side routes it.
if MyQbzDetailState.select-mode && MyQbzDetailState.selected-count > 0: Rectangle {
x: 18px;
// Sit just above the fixed player bar (the scroller reserves 100px at
@@ -1401,8 +1405,12 @@ export component MixtapeDetailView inherits Rectangle {
width: 100%;
selected-count: MyQbzDetailState.selected-count;
actions: [
{ id: "add-to-queue", label: @tr("Add to queue"), danger: false, needs-selection: true },
{ id: "play-next", label: @tr("Play next"), danger: false, needs-selection: true },
{ id: "add-to-playlist", label: @tr("Add to playlist"), danger: false, needs-selection: true },
{ id: "add-to-mixtape", label: @tr("Add to Mixtape/Collection"), danger: false, needs-selection: true },
{ id: "remove-selected", label: @tr("Remove"), danger: true, needs-selection: true },
{ id: "clear", label: @tr("Clear"), danger: false, needs-selection: true },
];
action(id) => {
MyQbzDetailActions.bulk-action(id);
@@ -10,9 +10,10 @@
// 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 Shuffle.
// Backdrop click + Cancel close without committing; Shuffle is disabled while
// loading / busy or when no size is available.
// 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).
@@ -29,8 +30,8 @@ import { SecondaryButton } from "../primitives/SecondaryButton.slint";
export component MyQbzMixModal inherits Rectangle {
visible: MyQbzMixState.open;
// Shuffle is allowed once a count is loaded (a non-empty options list) and
// nothing is in flight.
// 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