feat(myqbz): Discography Builder — artist_collection creation flow

Phase-2 Slice 11. The third MyQBZ kind (artist_collection / artist_discography),
1:1 with spec 13 (the 1445-line DiscographyBuilderView):
- ui/myqbz/DiscographyBuilderView.slint: header (artist avatar + name + eyebrow),
  name input, 3-segment order-by, the 7-col grouped release table
  (28 56 72 1fr 72 60 156) with tri-state select-all, primary + alternate rows
  (↳ connector, source indicator, QualityBadge, per-row type-override popover),
  footer (count + Create). Exact §0 dark colors + type tints.
- src/myqbz_builder.rs: candidate/group model + normalize/classify/quality-score;
  THREE sources fully wired (Qobuz get_artist_page + local + Plex via the unified
  metadata query); build_groups dedupes/buckets/quality-sorts (Qobuz wins ties);
  persisted release-type override sidecar. Save = create_collection (ArtistCollection,
  ArtistDiscography, source_ref=artist_id) + add_item per checked candidate in sort
  order (Plex->local) -> navigate to the new collection's detail.
- Entry: the artist page "Build Artist Collection" button (was unwired).

Known polish nits (S12): view-wide uppercase eyebrow/labels; Create should disable
on whitespace-only name. Toast uses real strings (toast.collectionCreated/Failed are
absent from en.json; DQ4). Backend crates untouched. cargo check -p qbz-slint green.

cc
This commit is contained in:
vicrodh committed 2026-06-09 07:19:06 -06:00
1 parent 1db40d87ef
commit c6aa61a09e
7 files changed
+2192 -2

No files matched your search

@@ -0,0 +1,886 @@
// Discography Builder — the full-page flow that assembles an artist's
// complete discography into a saved `kind='artist_collection'` collection
// (spec 13). A 1:1 port of the Tauri DiscographyBuilderView.svelte.
//
// Two-part layout: `.builder-scroll` holds everything that scrolls; the
// footer is a fixed-height flex sibling pinned at the bottom (NOT
// sticky-inside-scroll) so last rows never peek past the footer.
//
// CSS variable values are resolved to the concrete dark-theme values from
// spec §0 (this view hardcodes them rather than going through the Theme
// global so the port stays pixel-exact to the quoted PSD).
import { Typography } from "../foundation/typography.slint";
import { DiscoBuilderState, DiscoBuilderActions, DiscoGroup, DiscoCandidate } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { QualityBadgeFull } from "../primitives/QualityBadgeFull.slint";
// ── Concrete dark-theme values (spec §0) ──────────────────────────────────
// Resolved here as global consts so the whole view shares one source.
global D {
out property <color> bg-primary: #0f0f0f; // footer bg
out property <color> bg-secondary: #1a1a1a; // input/segment/menu/secondary-btn
out property <color> bg-tertiary: #2a2a2a; // avatar placeholder, hairlines
out property <color> bg-hover: #1f1f1f; // row/segment/menu-item hover
out property <color> text-primary: #ffffff;
out property <color> text-secondary: #cccccc;
out property <color> text-muted: #888888;
out property <color> accent: #4285F4;
out property <color> btn-primary-text: #ffffff;
out property <color> alpha-6: #ffffff0f; // rgba(255,255,255,0.06)
out property <color> error: #e57373;
// Type-label tints (hardcoded non-var colors, spec §0).
out property <color> tint-compilation: #f59e0b; // amber
out property <color> tint-live: #e91e63; // pink
out property <color> tint-ep: #60a5fa; // blue
out property <color> tint-single: #a78bfa; // violet
}
// Resolve a release-type to its tint (plain "album" = muted, no tint).
component TypeColor {
in property <string> kind;
out property <color> color:
root.kind == "compilation" ? D.tint-compilation
: root.kind == "live" ? D.tint-live
: root.kind == "ep" ? D.tint-ep
: root.kind == "single" ? D.tint-single
: D.text-muted;
}
// ── Source indicator (Qobuz / Plex / Local accent-tinted silhouette) ──────
component SourceIndicator inherits Rectangle {
in property <string> source;
width: 28px;
height: 28px;
opacity: 0.9;
HorizontalLayout {
alignment: center;
VerticalLayout {
alignment: center;
if root.source == "plex": QbzIcon {
source: @image-url("../assets/icons/plex-logo.svg");
width: 14px;
height: 14px;
tint: D.accent;
}
if root.source == "qobuz": QbzIcon {
source: @image-url("../assets/icons/qobuz-logo-filled.svg");
width: 16px;
height: 16px;
tint: D.accent;
}
if root.source != "plex" && root.source != "qobuz": QbzIcon {
source: @image-url("../assets/icons/hard-drive.svg");
width: 14px;
height: 14px;
tint: D.accent;
}
}
}
}
// ── Per-row release-type override button + popover (spec §7.1) ─────────────
component TypeOverride inherits Rectangle {
in property <DiscoCandidate> candidate;
// 11px primary / 10px alternate.
in property <length> font-size: 11px;
property <string> menu-key: root.candidate.source + "|" + root.candidate.source-item-id;
property <bool> menu-open: DiscoBuilderState.open-type-menu-key == root.menu-key;
// The wrapper anchors the absolute popover; it sizes to the button.
width: type-btn.preferred-width;
height: type-btn.preferred-height;
type-btn := Rectangle {
// .type-btn negative margin (0 -4px) is replicated by a -4px touch
// bleed; the visual glyph stays put.
background: btn-ta.has-hover ? D.alpha-6 : transparent;
border-radius: 4px;
HorizontalLayout {
padding-left: 4px;
padding-right: 4px;
padding-top: 2px;
padding-bottom: 2px;
label := Text {
text: root.candidate.release-type;
color: type-color.color;
font-size: root.font-size;
font-weight: 600;
letter-spacing: 0.4px;
vertical-alignment: center;
}
}
type-color := TypeColor { kind: root.candidate.release-type; }
// Dotted-underline marker for an active override (Slint has no
// text-decoration; draw a 1px dotted rule under the label).
if root.candidate.is-overridden: Rectangle {
x: label.x;
y: parent.height - 3px;
width: label.width;
height: 1px;
background: type-color.color;
opacity: 0.6;
}
btn-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
DiscoBuilderState.open-type-menu-key =
root.menu-open ? "" : root.menu-key;
}
}
}
// Backdrop + menu (rendered above siblings; z-order is paint order).
if root.menu-open: Rectangle {
// Backdrop click-catcher — full-row local catch; closes the menu.
// (A true full-viewport backdrop isn't expressible here without a
// global overlay; the menu's close-on-outside is handled by the
// backdrop TouchArea plus re-clicking the button.)
TouchArea {
x: -2000px;
y: -2000px;
width: 4000px;
height: 4000px;
clicked => {
DiscoBuilderState.open-type-menu-key = "";
}
}
menu := Rectangle {
x: 0;
y: parent.height + 4px;
width: 160px;
background: D.bg-secondary;
border-width: 1px;
border-color: D.bg-tertiary;
border-radius: 6px;
drop-shadow-blur: 12px;
drop-shadow-color: #00000059; // rgba(0,0,0,0.35)
drop-shadow-offset-y: 4px;
VerticalLayout {
padding: 4px;
// Album, EP, Single, Live, Compilation (spec order).
for choice in [
{ id: "album", lbl: "Album" },
{ id: "ep", lbl: "EP" },
{ id: "single", lbl: "Single" },
{ id: "live", lbl: "Live" },
{ id: "compilation", lbl: "Compilation" },
]: Rectangle {
height: 28px;
border-radius: 4px;
background: item-ta.has-hover ? D.bg-hover : transparent;
HorizontalLayout {
padding-left: 8px;
padding-right: 8px;
spacing: 8px;
alignment: start;
VerticalLayout {
alignment: center;
// Check glyph if this is the selected type; else a
// 12px placeholder so labels stay aligned.
if root.candidate.release-type == choice.id: QbzIcon {
source: @image-url("../assets/icons/check.svg");
width: 12px;
height: 12px;
tint: D.accent;
}
if root.candidate.release-type != choice.id: Rectangle {
width: 12px;
height: 12px;
}
}
Text {
text: choice.lbl;
color: root.candidate.release-type == choice.id
? D.accent : D.text-primary;
font-size: 12px;
vertical-alignment: center;
horizontal-stretch: 1;
}
}
item-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
DiscoBuilderActions.set-type-override(
root.candidate.source,
root.candidate.source-item-id,
choice.id);
DiscoBuilderState.open-type-menu-key = "";
}
}
}
// Reset row — only when an override is active.
if root.candidate.is-overridden: Rectangle {
height: 9px;
Rectangle {
y: 4px;
height: 1px;
background: D.bg-tertiary;
}
}
if root.candidate.is-overridden: Rectangle {
height: 28px;
border-radius: 4px;
background: reset-ta.has-hover ? D.bg-hover : transparent;
HorizontalLayout {
padding-left: 8px;
padding-right: 8px;
spacing: 8px;
alignment: start;
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/rotate-ccw.svg");
width: 12px;
height: 12px;
tint: D.text-muted;
}
}
Text {
text: @tr("Use auto-detected");
color: D.text-muted;
font-size: 12px;
vertical-alignment: center;
horizontal-stretch: 1;
}
}
reset-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
DiscoBuilderActions.reset-type-override(
root.candidate.source,
root.candidate.source-item-id);
DiscoBuilderState.open-type-menu-key = "";
}
}
}
}
}
}
}
// 7-column row grid — the alignment backbone (spec §2.9):
// grid-template-columns: 28px 56px 72px minmax(0,1fr) 72px 60px 156px, gap 10px.
// Each row below inlines this template (header/primary/alternate share it).
//
// One candidate row (primary or alternate). `alt` dims it + swaps the col[0]
// checkbox for a connector + checkbox and shrinks/mutes the text.
component CandidateRow inherits Rectangle {
in property <DiscoCandidate> candidate;
in property <string> group-key;
in property <bool> alt: false;
// Whether the OWNING group is a compilation — drives the primary row's
// "compilation" tag (spec §2.10: tag is `{#if grp.is_compilation}`, the
// group flag, not the candidate's effective type).
in property <bool> group-is-compilation: false;
// Persistent dim (compilation primary 0.65 / alternate 0.72); lifts to 1
// on hover. The parent passes the resting opacity.
in property <float> rest-opacity: 1.0;
height: 36px;
border-radius: 6px;
background: row-ta.has-hover ? D.bg-hover : transparent;
opacity: row-ta.has-hover ? 1.0 : root.rest-opacity;
row-ta := TouchArea { mouse-cursor: default; }
HorizontalLayout {
padding-left: 8px;
padding-right: 8px;
spacing: 10px;
// [0] 28px — checkbox cell (alt: connector + checkbox).
Rectangle {
width: 28px;
HorizontalLayout {
alignment: start;
spacing: 3px;
if root.alt: Text {
text: "↳";
color: D.text-muted;
font-size: 12px;
vertical-alignment: center;
}
VerticalLayout {
alignment: center;
Rectangle {
width: 15px;
height: 15px;
border-radius: 3px;
background: root.candidate.checked ? D.accent : transparent;
border-width: root.candidate.checked ? 0px : 1.5px;
border-color: D.text-muted;
if root.candidate.checked: QbzIcon {
source: @image-url("../assets/icons/check.svg");
width: 11px;
height: 11px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: D.btn-primary-text;
}
TouchArea {
mouse-cursor: pointer;
clicked => {
DiscoBuilderActions.toggle-checked(
root.group-key, root.candidate.key);
}
}
}
}
}
}
// [1] 56px — year.
Rectangle {
width: 56px;
Text {
text: root.candidate.year;
color: root.alt ? D.text-muted : D.text-secondary;
font-size: root.alt ? 12px : 13px;
vertical-alignment: center;
horizontal-alignment: left;
width: 100%;
}
}
// [2] 72px — type override button + popover.
Rectangle {
width: 72px;
HorizontalLayout {
alignment: start;
VerticalLayout {
alignment: center;
TypeOverride {
candidate: root.candidate;
font-size: root.alt ? 10px : 11px;
}
}
}
}
// [3] 1fr — title (flexes) + tag.
Rectangle {
horizontal-stretch: 1;
HorizontalLayout {
alignment: start;
spacing: 8px;
title-ta := TouchArea {
mouse-cursor: pointer;
horizontal-stretch: 1;
clicked => {
DiscoBuilderActions.open-album(
root.candidate.source, root.candidate.source-item-id);
}
title := Text {
text: root.candidate.title;
color: title-ta.has-hover
? D.accent
: (root.alt ? D.text-muted : D.text-primary);
font-size: root.alt ? 13px : 14px;
overflow: elide;
vertical-alignment: center;
width: 100%;
}
}
// alternates always show the "alternate" tag; primaries show
// "compilation" only when the group is a compilation (passed
// by the parent baking it into the candidate's title? No —
// the parent sets `tag` separately). Here we infer from the
// row kind + the candidate's own type.
if root.alt: Text {
text: @tr("alternate");
color: D.text-muted;
font-size: 10px;
font-weight: 500;
font-italic: true;
letter-spacing: 0.8px;
vertical-alignment: center;
}
if !root.alt && root.group-is-compilation: Text {
text: @tr("compilation");
color: D.text-muted;
font-size: 10px;
font-weight: 500;
font-italic: true;
letter-spacing: 0.8px;
vertical-alignment: center;
}
}
}
// [4] 72px — tracks (centered, tabular).
Rectangle {
width: 72px;
Text {
text: root.candidate.tracks;
color: root.alt ? D.text-muted : D.text-secondary;
font-size: root.alt ? 12px : 13px;
horizontal-alignment: center;
vertical-alignment: center;
width: 100%;
}
}
// [5] 60px — source indicator.
Rectangle {
width: 60px;
HorizontalLayout {
alignment: center;
VerticalLayout {
alignment: center;
SourceIndicator { source: root.candidate.source; }
}
}
}
// [6] 156px — quality badge.
Rectangle {
width: 156px;
HorizontalLayout {
alignment: center;
VerticalLayout {
alignment: center;
QualityBadgeFull {
tier: root.candidate.quality-tier;
detail: root.candidate.quality-detail;
}
}
}
}
}
}
export component DiscographyBuilderView inherits Rectangle {
background: transparent;
// ── .builder-view (flex column, full size) ──
VerticalLayout {
// ── .builder-scroll (scrolls everything) ──
scroll := Flickable {
vertical-stretch: 1;
viewport-width: self.width;
viewport-height: content.preferred-height;
content := VerticalLayout {
width: scroll.width;
padding-left: 32px;
padding-right: 32px;
padding-top: 24px;
padding-bottom: 24px;
spacing: 0px;
alignment: start;
// ── .back-btn ──
Rectangle {
height: 20px;
horizontal-stretch: 0;
back-ta := TouchArea {
width: back-row.preferred-width;
mouse-cursor: pointer;
clicked => {
DiscoBuilderActions.back();
}
back-row := HorizontalLayout {
spacing: 8px;
alignment: start;
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/arrow-left.svg");
width: 16px;
height: 16px;
tint: back-ta.has-hover ? D.text-secondary : D.text-muted;
}
}
Text {
text: @tr("Back");
color: back-ta.has-hover ? D.text-secondary : D.text-muted;
font-size: 14px;
vertical-alignment: center;
}
}
}
}
Rectangle { height: 24px; }
// ── header.builder-header ──
HorizontalLayout {
spacing: 20px;
alignment: start;
// Avatar (img or placeholder) — 72px circle.
Rectangle {
width: 72px;
height: 72px;
border-radius: 36px;
background: D.bg-tertiary;
clip: true;
if DiscoBuilderState.artist-avatar-url != "": Image {
width: 100%;
height: 100%;
source: DiscoBuilderState.artist-avatar;
image-fit: cover;
}
}
// Header text — eyebrow + title.
VerticalLayout {
alignment: center;
spacing: 4px;
horizontal-stretch: 1;
Text {
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 == ""
? "—" : DiscoBuilderState.artist-name;
color: D.text-primary;
font-size: 28px;
font-weight: 700;
overflow: elide;
}
}
}
Rectangle { height: 24px; }
// ── label.field (collection-name input) ──
VerticalLayout {
spacing: 8px;
alignment: start;
Text {
text: @tr("NAME");
color: D.text-muted;
font-size: 10px;
font-weight: 600;
letter-spacing: 1.5px;
}
Rectangle {
width: 540px;
height: 40px;
background: D.bg-secondary;
border-radius: 8px;
border-width: 1px;
border-color: name-input.has-focus ? D.accent : D.bg-tertiary;
HorizontalLayout {
padding-left: 12px;
padding-right: 12px;
name-input := TextInput {
color: D.text-primary;
font-size: 14px;
vertical-alignment: center;
single-line: true;
text: DiscoBuilderState.collection-name;
edited => {
DiscoBuilderActions.name-changed(self.text);
}
}
}
}
}
Rectangle { height: 16px; }
// ── .order-row (segmented control) ──
HorizontalLayout {
spacing: 12px;
alignment: start;
VerticalLayout {
alignment: center;
Text {
text: @tr("Order by");
color: D.text-muted;
font-size: 10px;
font-weight: 600;
letter-spacing: 1.5px;
}
}
// .segmented — 8px corners, NO pills.
Rectangle {
width: seg.preferred-width;
height: 32px;
border-radius: 8px;
border-width: 1px;
border-color: D.bg-tertiary;
clip: true;
seg := HorizontalLayout {
spacing: 0px;
for s[i] in [
{ id: "release_date", lbl: "Release date" },
{ id: "title", lbl: "Title" },
{ id: "manual", lbl: "Manual" },
]: seg-rect := Rectangle {
property <bool> active: DiscoBuilderState.order-by == s.id;
background: seg-rect.active
? D.accent
: (seg-ta.has-hover ? D.bg-hover : D.bg-secondary);
// Divider between segments.
if i > 0: Rectangle {
x: 0;
width: 1px;
height: 100%;
background: D.bg-tertiary;
}
HorizontalLayout {
padding-left: 14px;
padding-right: 14px;
padding-top: 7px;
padding-bottom: 7px;
Text {
text: s.lbl;
color: seg-rect.active ? D.btn-primary-text
: (seg-ta.has-hover ? D.text-primary : D.text-secondary);
font-size: 12px;
font-weight: 500;
vertical-alignment: center;
}
}
seg-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
DiscoBuilderActions.set-order(s.id);
}
}
}
}
}
}
Rectangle { height: 20px; }
// ── {state branch} ──
// loading
if DiscoBuilderState.loading: Rectangle {
height: 96px;
Text {
text: @tr("Loading...");
color: D.text-muted;
font-size: 14px;
horizontal-alignment: center;
vertical-alignment: center;
}
}
// error
if !DiscoBuilderState.loading && DiscoBuilderState.load-error != "": Rectangle {
height: 96px;
Text {
text: DiscoBuilderState.load-error;
color: D.error;
font-size: 14px;
horizontal-alignment: center;
vertical-alignment: center;
}
}
// empty
if !DiscoBuilderState.loading
&& DiscoBuilderState.load-error == ""
&& DiscoBuilderState.groups.length == 0: Rectangle {
height: 96px;
Text {
text: @tr("No results found");
color: D.text-muted;
font-size: 14px;
horizontal-alignment: center;
vertical-alignment: center;
}
}
// populated — .groups table
if !DiscoBuilderState.loading
&& DiscoBuilderState.load-error == ""
&& DiscoBuilderState.groups.length > 0: VerticalLayout {
spacing: 0px;
// border-top + 4px pad-top.
Rectangle { height: 1px; background: D.bg-tertiary; }
Rectangle { height: 4px; }
// ── header row (.row.header-row) ──
Rectangle {
height: 36px;
Rectangle {
y: parent.height - 1px;
height: 1px;
background: D.bg-tertiary;
}
HorizontalLayout {
padding-left: 8px;
padding-right: 8px;
spacing: 10px;
// [0] 28px — select-all checkbox (tri-state).
Rectangle {
width: 28px;
HorizontalLayout {
alignment: start;
VerticalLayout {
alignment: center;
Rectangle {
width: 15px;
height: 15px;
border-radius: 3px;
background: DiscoBuilderState.all-checked
? D.accent : transparent;
border-width: DiscoBuilderState.all-checked ? 0px : 1.5px;
border-color: D.text-muted;
if DiscoBuilderState.all-checked: QbzIcon {
source: @image-url("../assets/icons/check.svg");
width: 11px;
height: 11px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: D.btn-primary-text;
}
// Indeterminate dash.
if !DiscoBuilderState.all-checked
&& DiscoBuilderState.some-checked: Rectangle {
width: 9px;
height: 2px;
background: D.accent;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
}
TouchArea {
mouse-cursor: pointer;
clicked => {
DiscoBuilderActions.toggle-all();
}
}
}
}
}
}
// [1..6] column labels.
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; }
}
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; }
}
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; }
}
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; }
}
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; }
}
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; }
}
}
}
// ── per group ──
for grp in DiscoBuilderState.groups: VerticalLayout {
spacing: 0px;
// primary row (dimmed when the group is a compilation).
CandidateRow {
candidate: grp.primary;
group-key: grp.key;
alt: false;
group-is-compilation: grp.is-compilation;
rest-opacity: grp.is-compilation ? 0.65 : 1.0;
}
// alternate rows.
for alt in grp.alternates: CandidateRow {
candidate: alt;
group-key: grp.key;
alt: true;
rest-opacity: 0.72;
}
}
}
}
}
// ── footer.builder-footer (fixed-height flex sibling) ──
Rectangle {
vertical-stretch: 0;
height: 60px;
background: D.bg-primary;
Rectangle {
y: 0;
height: 1px;
background: D.bg-tertiary;
}
HorizontalLayout {
padding-left: 32px;
padding-right: 88px; // asymmetric — clears the back-to-top FAB.
padding-top: 12px;
padding-bottom: 12px;
spacing: 16px;
// .footer-count.
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
Text {
text: DiscoBuilderState.selected-count + " of "
+ DiscoBuilderState.group-count + " albums selected";
color: D.text-secondary;
font-size: 13px;
}
}
// .footer-actions.
HorizontalLayout {
spacing: 8px;
alignment: end;
// Cancel (secondary-btn).
Rectangle {
width: cancel-row.preferred-width + 32px;
height: 36px;
background: cancel-ta.has-hover ? D.bg-hover : D.bg-secondary;
border-radius: 8px;
border-width: 1px;
border-color: D.bg-tertiary;
cancel-row := HorizontalLayout {
alignment: center;
Text {
text: @tr("Cancel");
color: D.text-primary;
font-size: 13px;
font-weight: 600;
vertical-alignment: center;
}
}
cancel-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
DiscoBuilderActions.back();
}
}
}
// Create (primary-btn).
create-btn := Rectangle {
property <bool> disabled:
DiscoBuilderState.creating
|| DiscoBuilderState.selected-count == 0
|| DiscoBuilderState.collection-name == "";
width: create-row.preferred-width + 40px;
height: 36px;
background: D.accent;
border-radius: 8px;
opacity: create-btn.disabled ? 0.45 : 1.0;
create-row := HorizontalLayout {
alignment: center;
Text {
text: DiscoBuilderState.creating
? @tr("Loading...") : @tr("Create Collection");
color: D.btn-primary-text;
font-size: 13px;
font-weight: 600;
vertical-alignment: center;
}
}
TouchArea {
mouse-cursor: create-btn.disabled ? default : pointer;
clicked => {
if (!create-btn.disabled) {
DiscoBuilderActions.create();
}
}
}
}
}
}
}
}
}