refactor(myqbz): button audit — app primitives + fix glyph centering
Phase-2 Slice 12c (user directive: use the app's circular controls, no invented buttons; fix the glyph-stuck-left bug). - Hero §3.6: deleted the local ActionCircle; the 4 hero controls are now the shared CircleAction (Play primary = filled white disc + black glyph; the loading spinner is now available = more 1:1). Added `enabled` to CircleAction (additive). - All MyQBZ modal footers (Create / Edit / Mix / AddTo / Discography) now use the shared QbzPrimaryButton (+ a new `destructive` variant for Delete) and a new shared SecondaryButton (promoted from AppearanceSettings' inline copy). QbzPrimaryButton auto-sizes as a footer chip while staying full-width in login (verified: login stretches it in a VerticalLayout; footers take preferred width). - Kept legitimately-local icon+label controls (grid "+ New" CTA, AddToMixtape picker chips, toolbar sort/filter/view-mode pills) but verified every glyph is centered. - Fixed 3 glyph-stuck-left bugs (DiscographyBuilder checkboxes used non-snapped centering -> the Math.round(.../1px)*1px crisp idiom). Backend crates untouched. cargo check -p qbz-slint green. cc
This commit is contained in:
9 files changed
+184
-282
No files matched your search
@@ -21,6 +21,8 @@ import { Radius } from "../foundation/radius.slint";
|
||||
import { MyQbzAddState, MyQbzAddActions, MyQbzAddRow } from "../state.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
|
||||
import { QbzPrimaryButton } from "../primitives/QbzPrimaryButton.slint";
|
||||
import { SecondaryButton } from "../primitives/SecondaryButton.slint";
|
||||
import { LineEdit } from "std-widgets.slint";
|
||||
|
||||
// A single labelled radio option in the create-panel Kind toggle.
|
||||
@@ -156,50 +158,45 @@ component AddPickRow inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// A small footer button (Cancel / Back / create-buttons).
|
||||
component FooterButton inherits Rectangle {
|
||||
// The icon+label "+ Mixtapes / + Collections" picker-footer chips. Kept as a
|
||||
// local secondary pill (rule #3 of the button audit: legitimately icon+label,
|
||||
// so it does NOT collapse to the icon-only CircleAction). The leading icon sits
|
||||
// in a centered [icon · gap · label] group — never jammed at the left edge.
|
||||
component FooterIconButton inherits Rectangle {
|
||||
in property <string> label;
|
||||
in property <bool> primary: false;
|
||||
in property <bool> enabled-look: true;
|
||||
in property <image> icon;
|
||||
in property <bool> has-icon: false;
|
||||
callback clicked();
|
||||
height: 38px;
|
||||
border-radius: Radius.sm;
|
||||
opacity: root.enabled-look ? 1.0 : 0.5;
|
||||
background: root.primary
|
||||
? (ta.has-hover ? #ffffffd6 : Theme.accent)
|
||||
: (ta.has-hover ? Theme.surface-hover : Theme.surface-elevated);
|
||||
border-width: root.primary ? 0px : 1px;
|
||||
background: ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
|
||||
border-width: 1px;
|
||||
border-color: Theme.border-subtle;
|
||||
HorizontalLayout {
|
||||
padding-left: root.primary ? 20px : 16px;
|
||||
padding-right: root.primary ? 20px : 16px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
spacing: 6px;
|
||||
alignment: center;
|
||||
if root.has-icon: VerticalLayout {
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
QbzIcon {
|
||||
source: root.icon;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
tint: root.primary ? #ffffff : Theme.text-primary;
|
||||
tint: Theme.text-primary;
|
||||
}
|
||||
}
|
||||
Text {
|
||||
text: root.label;
|
||||
color: root.primary ? #ffffff : Theme.text-primary;
|
||||
color: Theme.text-primary;
|
||||
font-size: Typography.body;
|
||||
font-weight: Typography.semibold;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
ta := TouchArea {
|
||||
mouse-cursor: root.enabled-look ? pointer : default;
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
if (root.enabled-look) {
|
||||
root.clicked();
|
||||
}
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -376,10 +373,9 @@ export component AddToMixtapeModal inherits Rectangle {
|
||||
HorizontalLayout {
|
||||
padding: 12px;
|
||||
spacing: 8px;
|
||||
FooterButton {
|
||||
FooterIconButton {
|
||||
horizontal-stretch: 1;
|
||||
label: @tr("Mixtapes");
|
||||
has-icon: true;
|
||||
icon: @image-url("../assets/icons/plus.svg");
|
||||
clicked => {
|
||||
MyQbzAddActions.show-create("mixtape");
|
||||
@@ -387,10 +383,9 @@ export component AddToMixtapeModal inherits Rectangle {
|
||||
}
|
||||
// Hidden for track/playlist payloads (collections hold
|
||||
// whole albums only).
|
||||
if !MyQbzAddState.restrict-to-mixtape: FooterButton {
|
||||
if !MyQbzAddState.restrict-to-mixtape: FooterIconButton {
|
||||
horizontal-stretch: 1;
|
||||
label: @tr("Collections");
|
||||
has-icon: true;
|
||||
icon: @image-url("../assets/icons/plus.svg");
|
||||
clicked => {
|
||||
MyQbzAddActions.show-create("collection");
|
||||
@@ -460,25 +455,25 @@ export component AddToMixtapeModal inherits Rectangle {
|
||||
}
|
||||
// Spacer pushes the footer down.
|
||||
Rectangle { vertical-stretch: 1; }
|
||||
// Footer: Back + Create & Add.
|
||||
// Footer: Back (secondary) + Create & Add (primary) —
|
||||
// the app's shared labeled primitives.
|
||||
HorizontalLayout {
|
||||
alignment: end;
|
||||
spacing: 8px;
|
||||
FooterButton {
|
||||
SecondaryButton {
|
||||
label: @tr("Back");
|
||||
enabled-look: !MyQbzAddState.create-busy;
|
||||
height: 38px;
|
||||
enabled: !MyQbzAddState.create-busy;
|
||||
clicked => {
|
||||
MyQbzAddActions.create-back();
|
||||
}
|
||||
}
|
||||
FooterButton {
|
||||
QbzPrimaryButton {
|
||||
label: @tr("Create & Add");
|
||||
primary: true;
|
||||
enabled-look: root.can-create;
|
||||
height: 38px;
|
||||
enabled: root.can-create;
|
||||
clicked => {
|
||||
if (root.can-create) {
|
||||
MyQbzAddActions.create-and-add();
|
||||
}
|
||||
MyQbzAddActions.create-and-add();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import { Typography } from "../foundation/typography.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
import { MyQbzCreateState, MyQbzCreateActions } from "../state.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { QbzPrimaryButton } from "../primitives/QbzPrimaryButton.slint";
|
||||
import { SecondaryButton } from "../primitives/SecondaryButton.slint";
|
||||
import { LineEdit } from "std-widgets.slint";
|
||||
|
||||
// A single labelled radio option in the Kind toggle.
|
||||
@@ -163,55 +165,26 @@ export component CreateMyQbzModal inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel + Create.
|
||||
// Cancel + Create — the app's shared labeled primitives
|
||||
// (auto-width, 38px footer chips).
|
||||
HorizontalLayout {
|
||||
alignment: end;
|
||||
spacing: 8px;
|
||||
// Cancel (secondary).
|
||||
Rectangle {
|
||||
width: cancel-label.preferred-width + 32px;
|
||||
SecondaryButton {
|
||||
label: @tr("Cancel");
|
||||
height: 38px;
|
||||
border-radius: Radius.sm;
|
||||
background: cancel-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
|
||||
border-width: 1px;
|
||||
border-color: Theme.border-subtle;
|
||||
cancel-label := Text {
|
||||
text: @tr("Cancel");
|
||||
color: Theme.text-primary;
|
||||
font-size: Typography.body;
|
||||
font-weight: Typography.semibold;
|
||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||
}
|
||||
cancel-ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
MyQbzCreateActions.close();
|
||||
}
|
||||
clicked => {
|
||||
MyQbzCreateActions.close();
|
||||
}
|
||||
}
|
||||
// Create (primary) — label reuses the empty-state CTA.
|
||||
Rectangle {
|
||||
width: create-label.preferred-width + 40px;
|
||||
QbzPrimaryButton {
|
||||
label: root.is-mixtape ? @tr("+ New Mixtape") : @tr("+ New Collection");
|
||||
height: 38px;
|
||||
border-radius: Radius.sm;
|
||||
opacity: root.can-create ? 1.0 : 0.5;
|
||||
background: create-ta.has-hover ? #ffffffd6 : Theme.accent;
|
||||
create-label := Text {
|
||||
text: root.is-mixtape ? @tr("+ New Mixtape") : @tr("+ New Collection");
|
||||
color: #ffffff;
|
||||
font-size: Typography.body;
|
||||
font-weight: Typography.semibold;
|
||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||
}
|
||||
create-ta := TouchArea {
|
||||
mouse-cursor: root.can-create ? pointer : default;
|
||||
clicked => {
|
||||
if (root.can-create) {
|
||||
MyQbzCreateActions.submit();
|
||||
}
|
||||
}
|
||||
enabled: root.can-create;
|
||||
clicked => {
|
||||
MyQbzCreateActions.submit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ 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";
|
||||
import { QbzPrimaryButton } from "../primitives/QbzPrimaryButton.slint";
|
||||
import { SecondaryButton } from "../primitives/SecondaryButton.slint";
|
||||
|
||||
// ── Concrete dark-theme values (spec §0) ──────────────────────────────────
|
||||
// Resolved here as global consts so the whole view shares one source.
|
||||
@@ -314,8 +316,8 @@ component CandidateRow inherits Rectangle {
|
||||
source: @image-url("../assets/icons/check.svg");
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||
tint: D.btn-primary-text;
|
||||
}
|
||||
TouchArea {
|
||||
@@ -718,8 +720,8 @@ export component DiscographyBuilderView inherits Rectangle {
|
||||
source: @image-url("../assets/icons/check.svg");
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||
tint: D.btn-primary-text;
|
||||
}
|
||||
// Indeterminate dash.
|
||||
@@ -728,8 +730,8 @@ export component DiscographyBuilderView inherits Rectangle {
|
||||
width: 9px;
|
||||
height: 2px;
|
||||
background: D.accent;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||
}
|
||||
TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
@@ -819,64 +821,30 @@ export component DiscographyBuilderView inherits Rectangle {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
// .footer-actions.
|
||||
// .footer-actions — the app's shared labeled primitives.
|
||||
HorizontalLayout {
|
||||
spacing: 8px;
|
||||
alignment: end;
|
||||
// Cancel (secondary-btn).
|
||||
Rectangle {
|
||||
width: cancel-row.preferred-width + 32px;
|
||||
property <bool> create-disabled:
|
||||
DiscoBuilderState.creating
|
||||
|| DiscoBuilderState.selected-count == 0
|
||||
|| DiscoBuilderState.collection-name == "";
|
||||
// Cancel (secondary).
|
||||
SecondaryButton {
|
||||
label: @tr("Cancel");
|
||||
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();
|
||||
}
|
||||
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;
|
||||
// Create (primary).
|
||||
QbzPrimaryButton {
|
||||
label: DiscoBuilderState.creating
|
||||
? @tr("Loading...") : @tr("Create Collection");
|
||||
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();
|
||||
}
|
||||
}
|
||||
enabled: !parent.create-disabled;
|
||||
clicked => {
|
||||
DiscoBuilderActions.create();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import { Typography } from "../foundation/typography.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
import { MyQbzDetailState, MyQbzDetailActions, MixtapeDetailItem, TrackItem, NavState } from "../state.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { CircleAction } from "../primitives/CircleAction.slint";
|
||||
import { TrackRow } from "../primitives/TrackRow.slint";
|
||||
import { QualityBadgeFull } from "../primitives/QualityBadgeFull.slint";
|
||||
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
|
||||
@@ -32,46 +33,6 @@ import { ListScrollbar } from "../primitives/ListScrollbar.slint";
|
||||
import { MultiSelectBar } from "../primitives/MultiSelectBar.slint";
|
||||
import { CollectionMosaic } from "CollectionMosaic.slint";
|
||||
|
||||
// --- Hero action button (.action-btn-circle family) ---------------------
|
||||
// Ghost circle (translucent fill + faint ring) homologated with the album /
|
||||
// playlist transport rows; `.primary` keeps the same ghost look (the Tauri
|
||||
// CTA fills accent but our transport row family is ghost everywhere). `enabled`
|
||||
// dims + disables (item-count == 0).
|
||||
component ActionCircle inherits Rectangle {
|
||||
in property <image> icon;
|
||||
in property <bool> primary: false;
|
||||
in property <length> diameter: 44px;
|
||||
in property <bool> enabled: true;
|
||||
in property <length> glyph: 19px;
|
||||
callback clicked();
|
||||
|
||||
width: root.diameter;
|
||||
height: root.diameter;
|
||||
border-radius: root.diameter / 2;
|
||||
opacity: root.enabled ? 1.0 : 0.4;
|
||||
background: root.primary
|
||||
? (ac-ta.has-hover ? #ffffffd6 : #ffffff)
|
||||
: (ac-ta.has-hover ? Theme.surface-hover : transparent);
|
||||
border-width: root.primary ? 0px : 1px;
|
||||
border-color: ac-ta.has-hover ? Theme.text-disabled : Theme.border-subtle;
|
||||
QbzIcon {
|
||||
source: root.icon;
|
||||
width: root.glyph;
|
||||
height: root.glyph;
|
||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||
tint: root.primary ? #000000 : (ac-ta.has-hover ? Theme.text-primary : Theme.text-secondary);
|
||||
}
|
||||
ac-ta := TouchArea {
|
||||
mouse-cursor: root.enabled ? pointer : default;
|
||||
clicked => {
|
||||
if (root.enabled) {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- One sort / type-filter dropdown row --------------------------------
|
||||
component MenuOption inherits Rectangle {
|
||||
in property <string> label;
|
||||
@@ -874,22 +835,21 @@ export component MixtapeDetailView inherits Rectangle {
|
||||
HorizontalLayout {
|
||||
spacing: 8px;
|
||||
alignment: start;
|
||||
// (1) Play — primary.
|
||||
ActionCircle {
|
||||
// (1) Play — primary (filled white disc + black glyph;
|
||||
// the shared CircleAction.primary, spec §3.6).
|
||||
CircleAction {
|
||||
icon: @image-url("../assets/icons/play-fill.svg");
|
||||
primary: true;
|
||||
diameter: 44px;
|
||||
glyph: 20px;
|
||||
enabled: MyQbzDetailState.item-count > 0;
|
||||
clicked => { MyQbzDetailActions.play-all(); }
|
||||
}
|
||||
// (2) Shuffle.
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
ActionCircle {
|
||||
CircleAction {
|
||||
icon: @image-url("../assets/icons/shuffle.svg");
|
||||
diameter: 40px;
|
||||
glyph: 18px;
|
||||
enabled: MyQbzDetailState.item-count > 0;
|
||||
clicked => { MyQbzDetailActions.shuffle(); }
|
||||
}
|
||||
@@ -897,10 +857,9 @@ export component MixtapeDetailView inherits Rectangle {
|
||||
// (3) DJ-mix (DjIcon) — STUB click (modal is a later slice).
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
ActionCircle {
|
||||
CircleAction {
|
||||
icon: @image-url("../assets/icons/turntable.svg");
|
||||
diameter: 40px;
|
||||
glyph: 18px;
|
||||
enabled: MyQbzDetailState.item-count > 0;
|
||||
clicked => { MyQbzDetailActions.dj-mix(); }
|
||||
}
|
||||
@@ -911,10 +870,9 @@ export component MixtapeDetailView inherits Rectangle {
|
||||
overflow-anchor := Rectangle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
ActionCircle {
|
||||
CircleAction {
|
||||
icon: @image-url("../assets/icons/ellipsis.svg");
|
||||
diameter: 40px;
|
||||
glyph: 18px;
|
||||
clicked => { overflow-menu.show(); }
|
||||
}
|
||||
overflow-menu := PopupWindow {
|
||||
|
||||
@@ -16,6 +16,8 @@ import { Typography } from "../foundation/typography.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
import { MyQbzEditState, MyQbzEditActions } from "../state.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { QbzPrimaryButton } from "../primitives/QbzPrimaryButton.slint";
|
||||
import { SecondaryButton } from "../primitives/SecondaryButton.slint";
|
||||
import { LineEdit, TextEdit } from "std-widgets.slint";
|
||||
|
||||
export component MyQbzEditModal inherits Rectangle {
|
||||
@@ -96,67 +98,35 @@ export component MyQbzEditModal inherits Rectangle {
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
// --- Footer (m-footer) -----------------------------------
|
||||
// --- Footer (m-footer) — shared labeled primitives -------
|
||||
HorizontalLayout {
|
||||
alignment: end;
|
||||
spacing: 8px;
|
||||
|
||||
// Cancel (secondary).
|
||||
Rectangle {
|
||||
width: cancel-label.preferred-width + 32px;
|
||||
SecondaryButton {
|
||||
label: @tr("Cancel");
|
||||
height: 38px;
|
||||
border-radius: Radius.sm;
|
||||
background: cancel-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
|
||||
border-width: 1px;
|
||||
border-color: Theme.border-subtle;
|
||||
cancel-label := Text {
|
||||
text: @tr("Cancel");
|
||||
color: Theme.text-primary;
|
||||
font-size: Typography.body;
|
||||
font-weight: Typography.semibold;
|
||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||
}
|
||||
cancel-ta := TouchArea {
|
||||
mouse-cursor: MyQbzEditState.busy ? default : pointer;
|
||||
clicked => {
|
||||
if (!MyQbzEditState.busy) {
|
||||
MyQbzEditActions.close();
|
||||
}
|
||||
}
|
||||
enabled: !MyQbzEditState.busy;
|
||||
clicked => {
|
||||
MyQbzEditActions.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Submit — primary (rename/description) or destructive
|
||||
// (delete).
|
||||
Rectangle {
|
||||
width: submit-label.preferred-width + 40px;
|
||||
// (delete) variant of the shared QbzPrimaryButton.
|
||||
QbzPrimaryButton {
|
||||
label: root.is-delete ? @tr("Delete") : @tr("Save");
|
||||
height: 38px;
|
||||
border-radius: Radius.sm;
|
||||
opacity: root.can-submit ? 1.0 : 0.5;
|
||||
background: root.is-delete
|
||||
? (submit-ta.has-hover ? #e57373d9 : #e57373)
|
||||
: (submit-ta.has-hover ? #ffffffd6 : Theme.accent);
|
||||
submit-label := Text {
|
||||
text: root.is-delete ? @tr("Delete") : @tr("Save");
|
||||
color: #ffffff;
|
||||
font-size: Typography.body;
|
||||
font-weight: Typography.semibold;
|
||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||
}
|
||||
submit-ta := TouchArea {
|
||||
mouse-cursor: root.can-submit ? pointer : default;
|
||||
clicked => {
|
||||
if (root.can-submit) {
|
||||
if (root.is-rename) {
|
||||
MyQbzEditActions.submit-rename();
|
||||
} else if (root.is-description) {
|
||||
MyQbzEditActions.submit-description();
|
||||
} else {
|
||||
MyQbzEditActions.confirm-delete();
|
||||
}
|
||||
}
|
||||
destructive: root.is-delete;
|
||||
enabled: root.can-submit;
|
||||
clicked => {
|
||||
if (root.is-rename) {
|
||||
MyQbzEditActions.submit-rename();
|
||||
} else if (root.is-description) {
|
||||
MyQbzEditActions.submit-description();
|
||||
} else {
|
||||
MyQbzEditActions.confirm-delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ 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;
|
||||
@@ -150,61 +152,28 @@ export component MyQbzMixModal inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Footer (.tm-actions, right-aligned) -----------------
|
||||
// --- Footer (.tm-actions, right-aligned) — shared primitives
|
||||
HorizontalLayout {
|
||||
alignment: end;
|
||||
spacing: 12px;
|
||||
|
||||
// Cancel (secondary).
|
||||
Rectangle {
|
||||
width: cancel-label.preferred-width + 32px;
|
||||
SecondaryButton {
|
||||
label: @tr("Cancel");
|
||||
height: 38px;
|
||||
border-radius: Radius.sm;
|
||||
background: cancel-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
|
||||
border-width: 1px;
|
||||
border-color: Theme.border-subtle;
|
||||
cancel-label := Text {
|
||||
text: @tr("Cancel");
|
||||
color: Theme.text-primary;
|
||||
font-size: Typography.body;
|
||||
font-weight: Typography.semibold;
|
||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||
}
|
||||
cancel-ta := TouchArea {
|
||||
mouse-cursor: MyQbzMixState.busy ? default : pointer;
|
||||
clicked => {
|
||||
if (!MyQbzMixState.busy) {
|
||||
MyQbzMixActions.close();
|
||||
}
|
||||
}
|
||||
enabled: !MyQbzMixState.busy;
|
||||
clicked => {
|
||||
MyQbzMixActions.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Shuffle / Add to queue (primary) — mixModal.confirm.
|
||||
Rectangle {
|
||||
width: confirm-label.preferred-width + 40px;
|
||||
QbzPrimaryButton {
|
||||
label: @tr("Add to queue");
|
||||
height: 38px;
|
||||
border-radius: Radius.sm;
|
||||
opacity: root.can-shuffle ? 1.0 : 0.5;
|
||||
background: confirm-ta.has-hover && root.can-shuffle
|
||||
? #ffffffd6
|
||||
: Theme.accent;
|
||||
confirm-label := Text {
|
||||
text: @tr("Add to queue");
|
||||
color: #ffffff;
|
||||
font-size: Typography.body;
|
||||
font-weight: Typography.semibold;
|
||||
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||
}
|
||||
confirm-ta := TouchArea {
|
||||
mouse-cursor: root.can-shuffle ? pointer : default;
|
||||
clicked => {
|
||||
if (root.can-shuffle) {
|
||||
MyQbzMixActions.shuffle();
|
||||
}
|
||||
}
|
||||
enabled: root.can-shuffle;
|
||||
clicked => {
|
||||
MyQbzMixActions.shuffle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ export component CircleAction inherits Rectangle {
|
||||
// While the album's first track is fetching, the icon swaps for a spinner.
|
||||
in property <bool> loading: false;
|
||||
in property <length> diameter: 40px;
|
||||
// When false the button dims and the click is gated (e.g. an empty
|
||||
// collection's hero CTAs). Additive — existing callers default to enabled.
|
||||
in property <bool> enabled: true;
|
||||
// Optional hover tooltip (empty = none), using the global TooltipOverlay.
|
||||
in property <string> tooltip;
|
||||
callback clicked();
|
||||
@@ -24,6 +27,7 @@ export component CircleAction inherits Rectangle {
|
||||
width: root.diameter;
|
||||
height: root.diameter;
|
||||
border-radius: root.diameter / 2;
|
||||
opacity: root.enabled ? 1.0 : 0.4;
|
||||
// Overlay-matched contrast: solid white for the primary play, semi-
|
||||
// transparent white fill + bright 1.5px ring otherwise.
|
||||
background: root.primary
|
||||
@@ -52,9 +56,11 @@ export component CircleAction inherits Rectangle {
|
||||
}
|
||||
|
||||
ta := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
mouse-cursor: root.enabled ? pointer : default;
|
||||
clicked => {
|
||||
root.clicked();
|
||||
if (root.enabled) {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
changed has-hover => {
|
||||
if (self.has-hover && root.tooltip != "") {
|
||||
|
||||
@@ -7,22 +7,35 @@ import { Typography } from "../foundation/typography.slint";
|
||||
export component QbzPrimaryButton inherits Rectangle {
|
||||
in property <string> label;
|
||||
in property <bool> enabled: true;
|
||||
// Destructive variant (e.g. the Delete confirm) — a red fill instead of the
|
||||
// accent. Additive; existing callers default to the accent primary.
|
||||
in property <bool> destructive: false;
|
||||
callback clicked();
|
||||
|
||||
height: 48px;
|
||||
border-radius: Radius.sm;
|
||||
opacity: root.enabled ? 1.0 : 0.5;
|
||||
background: ta.has-hover && root.enabled ? Theme.accent-hover : Theme.accent;
|
||||
background: root.destructive
|
||||
? (ta.has-hover && root.enabled ? #e57373d9 : #e57373)
|
||||
: (ta.has-hover && root.enabled ? Theme.accent-hover : Theme.accent);
|
||||
|
||||
animate background { duration: 150ms; }
|
||||
|
||||
Text {
|
||||
text: root.label;
|
||||
color: Theme.accent-text;
|
||||
font-size: Typography.button;
|
||||
font-weight: Typography.semibold;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
// A padded layout drives the preferred-width (label + 20px each side) so the
|
||||
// button auto-sizes as a footer chip inside an `alignment: end` row, while
|
||||
// still stretching full-width when the parent layout stretches it (login).
|
||||
HorizontalLayout {
|
||||
alignment: center;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
Text {
|
||||
text: root.label;
|
||||
color: Theme.accent-text;
|
||||
font-size: Typography.button;
|
||||
font-weight: Typography.semibold;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
ta := TouchArea {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Labeled SECONDARY button — the subdued sibling of QbzPrimaryButton, used for
|
||||
// Cancel / Back actions. Promoted to a shared primitive from the inline copy in
|
||||
// AppearanceSettings so the My QBZ modals + the Discography Builder footer read
|
||||
// identically (the button audit, spec 12c). A bordered surface-elevated chip
|
||||
// with a centered label; `enabled` dims + gates the click.
|
||||
//
|
||||
// Default 160px width matches AppearanceSettings; callers that need an
|
||||
// auto-width chip (the right-aligned modal footers) set `width` explicitly.
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
|
||||
export component SecondaryButton inherits Rectangle {
|
||||
in property <string> label;
|
||||
in property <bool> enabled: true;
|
||||
callback clicked();
|
||||
|
||||
height: 34px;
|
||||
border-radius: Radius.sm;
|
||||
border-width: 1px;
|
||||
border-color: Theme.border-subtle;
|
||||
background: ta.has-hover && root.enabled ? Theme.surface-hover : Theme.surface-elevated;
|
||||
opacity: root.enabled ? 1.0 : 0.4;
|
||||
|
||||
// Padded layout → preferred-width = label + 16px each side, so the button
|
||||
// auto-sizes as a footer chip; AppearanceSettings sets `width: 160px`
|
||||
// explicitly to keep its original fixed footprint.
|
||||
HorizontalLayout {
|
||||
alignment: center;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
Text {
|
||||
text: root.label;
|
||||
color: Theme.text-secondary;
|
||||
font-size: Typography.body;
|
||||
font-weight: Typography.medium;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
ta := TouchArea {
|
||||
mouse-cursor: root.enabled ? pointer : default;
|
||||
clicked => {
|
||||
if (root.enabled) {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user