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:
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
|
||||
|
||||
Reference in new issue
Block a user