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 { MyQbzAddState, MyQbzAddActions, MyQbzAddRow } from "../state.slint";
|
||||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||||
import { LoadingSpinner } from "../primitives/LoadingSpinner.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";
|
import { LineEdit } from "std-widgets.slint";
|
||||||
|
|
||||||
// A single labelled radio option in the create-panel Kind toggle.
|
// 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).
|
// The icon+label "+ Mixtapes / + Collections" picker-footer chips. Kept as a
|
||||||
component FooterButton inherits Rectangle {
|
// 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 <string> label;
|
||||||
in property <bool> primary: false;
|
|
||||||
in property <bool> enabled-look: true;
|
|
||||||
in property <image> icon;
|
in property <image> icon;
|
||||||
in property <bool> has-icon: false;
|
|
||||||
callback clicked();
|
callback clicked();
|
||||||
height: 38px;
|
height: 38px;
|
||||||
border-radius: Radius.sm;
|
border-radius: Radius.sm;
|
||||||
opacity: root.enabled-look ? 1.0 : 0.5;
|
background: ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
|
||||||
background: root.primary
|
border-width: 1px;
|
||||||
? (ta.has-hover ? #ffffffd6 : Theme.accent)
|
|
||||||
: (ta.has-hover ? Theme.surface-hover : Theme.surface-elevated);
|
|
||||||
border-width: root.primary ? 0px : 1px;
|
|
||||||
border-color: Theme.border-subtle;
|
border-color: Theme.border-subtle;
|
||||||
HorizontalLayout {
|
HorizontalLayout {
|
||||||
padding-left: root.primary ? 20px : 16px;
|
padding-left: 16px;
|
||||||
padding-right: root.primary ? 20px : 16px;
|
padding-right: 16px;
|
||||||
spacing: 6px;
|
spacing: 6px;
|
||||||
alignment: center;
|
alignment: center;
|
||||||
if root.has-icon: VerticalLayout {
|
VerticalLayout {
|
||||||
alignment: center;
|
alignment: center;
|
||||||
QbzIcon {
|
QbzIcon {
|
||||||
source: root.icon;
|
source: root.icon;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
tint: root.primary ? #ffffff : Theme.text-primary;
|
tint: Theme.text-primary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
text: root.label;
|
text: root.label;
|
||||||
color: root.primary ? #ffffff : Theme.text-primary;
|
color: Theme.text-primary;
|
||||||
font-size: Typography.body;
|
font-size: Typography.body;
|
||||||
font-weight: Typography.semibold;
|
font-weight: Typography.semibold;
|
||||||
vertical-alignment: center;
|
vertical-alignment: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ta := TouchArea {
|
ta := TouchArea {
|
||||||
mouse-cursor: root.enabled-look ? pointer : default;
|
mouse-cursor: pointer;
|
||||||
clicked => {
|
clicked => {
|
||||||
if (root.enabled-look) {
|
root.clicked();
|
||||||
root.clicked();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,10 +373,9 @@ export component AddToMixtapeModal inherits Rectangle {
|
|||||||
HorizontalLayout {
|
HorizontalLayout {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
spacing: 8px;
|
spacing: 8px;
|
||||||
FooterButton {
|
FooterIconButton {
|
||||||
horizontal-stretch: 1;
|
horizontal-stretch: 1;
|
||||||
label: @tr("Mixtapes");
|
label: @tr("Mixtapes");
|
||||||
has-icon: true;
|
|
||||||
icon: @image-url("../assets/icons/plus.svg");
|
icon: @image-url("../assets/icons/plus.svg");
|
||||||
clicked => {
|
clicked => {
|
||||||
MyQbzAddActions.show-create("mixtape");
|
MyQbzAddActions.show-create("mixtape");
|
||||||
@@ -387,10 +383,9 @@ export component AddToMixtapeModal inherits Rectangle {
|
|||||||
}
|
}
|
||||||
// Hidden for track/playlist payloads (collections hold
|
// Hidden for track/playlist payloads (collections hold
|
||||||
// whole albums only).
|
// whole albums only).
|
||||||
if !MyQbzAddState.restrict-to-mixtape: FooterButton {
|
if !MyQbzAddState.restrict-to-mixtape: FooterIconButton {
|
||||||
horizontal-stretch: 1;
|
horizontal-stretch: 1;
|
||||||
label: @tr("Collections");
|
label: @tr("Collections");
|
||||||
has-icon: true;
|
|
||||||
icon: @image-url("../assets/icons/plus.svg");
|
icon: @image-url("../assets/icons/plus.svg");
|
||||||
clicked => {
|
clicked => {
|
||||||
MyQbzAddActions.show-create("collection");
|
MyQbzAddActions.show-create("collection");
|
||||||
@@ -460,25 +455,25 @@ export component AddToMixtapeModal inherits Rectangle {
|
|||||||
}
|
}
|
||||||
// Spacer pushes the footer down.
|
// Spacer pushes the footer down.
|
||||||
Rectangle { vertical-stretch: 1; }
|
Rectangle { vertical-stretch: 1; }
|
||||||
// Footer: Back + Create & Add.
|
// Footer: Back (secondary) + Create & Add (primary) —
|
||||||
|
// the app's shared labeled primitives.
|
||||||
HorizontalLayout {
|
HorizontalLayout {
|
||||||
alignment: end;
|
alignment: end;
|
||||||
spacing: 8px;
|
spacing: 8px;
|
||||||
FooterButton {
|
SecondaryButton {
|
||||||
label: @tr("Back");
|
label: @tr("Back");
|
||||||
enabled-look: !MyQbzAddState.create-busy;
|
height: 38px;
|
||||||
|
enabled: !MyQbzAddState.create-busy;
|
||||||
clicked => {
|
clicked => {
|
||||||
MyQbzAddActions.create-back();
|
MyQbzAddActions.create-back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FooterButton {
|
QbzPrimaryButton {
|
||||||
label: @tr("Create & Add");
|
label: @tr("Create & Add");
|
||||||
primary: true;
|
height: 38px;
|
||||||
enabled-look: root.can-create;
|
enabled: root.can-create;
|
||||||
clicked => {
|
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 { Radius } from "../foundation/radius.slint";
|
||||||
import { MyQbzCreateState, MyQbzCreateActions } from "../state.slint";
|
import { MyQbzCreateState, MyQbzCreateActions } from "../state.slint";
|
||||||
import { QbzIcon } from "../primitives/QbzIcon.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";
|
import { LineEdit } from "std-widgets.slint";
|
||||||
|
|
||||||
// A single labelled radio option in the Kind toggle.
|
// 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 {
|
HorizontalLayout {
|
||||||
alignment: end;
|
alignment: end;
|
||||||
spacing: 8px;
|
spacing: 8px;
|
||||||
// Cancel (secondary).
|
// Cancel (secondary).
|
||||||
Rectangle {
|
SecondaryButton {
|
||||||
width: cancel-label.preferred-width + 32px;
|
label: @tr("Cancel");
|
||||||
height: 38px;
|
height: 38px;
|
||||||
border-radius: Radius.sm;
|
clicked => {
|
||||||
background: cancel-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
|
MyQbzCreateActions.close();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Create (primary) — label reuses the empty-state CTA.
|
// Create (primary) — label reuses the empty-state CTA.
|
||||||
Rectangle {
|
QbzPrimaryButton {
|
||||||
width: create-label.preferred-width + 40px;
|
label: root.is-mixtape ? @tr("+ New Mixtape") : @tr("+ New Collection");
|
||||||
height: 38px;
|
height: 38px;
|
||||||
border-radius: Radius.sm;
|
enabled: root.can-create;
|
||||||
opacity: root.can-create ? 1.0 : 0.5;
|
clicked => {
|
||||||
background: create-ta.has-hover ? #ffffffd6 : Theme.accent;
|
MyQbzCreateActions.submit();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { Typography } from "../foundation/typography.slint";
|
|||||||
import { DiscoBuilderState, DiscoBuilderActions, DiscoGroup, DiscoCandidate } from "../state.slint";
|
import { DiscoBuilderState, DiscoBuilderActions, DiscoGroup, DiscoCandidate } from "../state.slint";
|
||||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||||
import { QualityBadgeFull } from "../primitives/QualityBadgeFull.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) ──────────────────────────────────
|
// ── Concrete dark-theme values (spec §0) ──────────────────────────────────
|
||||||
// Resolved here as global consts so the whole view shares one source.
|
// 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");
|
source: @image-url("../assets/icons/check.svg");
|
||||||
width: 11px;
|
width: 11px;
|
||||||
height: 11px;
|
height: 11px;
|
||||||
x: (parent.width - self.width) / 2;
|
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||||
y: (parent.height - self.height) / 2;
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||||
tint: D.btn-primary-text;
|
tint: D.btn-primary-text;
|
||||||
}
|
}
|
||||||
TouchArea {
|
TouchArea {
|
||||||
@@ -718,8 +720,8 @@ export component DiscographyBuilderView inherits Rectangle {
|
|||||||
source: @image-url("../assets/icons/check.svg");
|
source: @image-url("../assets/icons/check.svg");
|
||||||
width: 11px;
|
width: 11px;
|
||||||
height: 11px;
|
height: 11px;
|
||||||
x: (parent.width - self.width) / 2;
|
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||||
y: (parent.height - self.height) / 2;
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||||
tint: D.btn-primary-text;
|
tint: D.btn-primary-text;
|
||||||
}
|
}
|
||||||
// Indeterminate dash.
|
// Indeterminate dash.
|
||||||
@@ -728,8 +730,8 @@ export component DiscographyBuilderView inherits Rectangle {
|
|||||||
width: 9px;
|
width: 9px;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: D.accent;
|
background: D.accent;
|
||||||
x: (parent.width - self.width) / 2;
|
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
||||||
y: (parent.height - self.height) / 2;
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
||||||
}
|
}
|
||||||
TouchArea {
|
TouchArea {
|
||||||
mouse-cursor: pointer;
|
mouse-cursor: pointer;
|
||||||
@@ -819,64 +821,30 @@ export component DiscographyBuilderView inherits Rectangle {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// .footer-actions.
|
// .footer-actions — the app's shared labeled primitives.
|
||||||
HorizontalLayout {
|
HorizontalLayout {
|
||||||
spacing: 8px;
|
spacing: 8px;
|
||||||
alignment: end;
|
alignment: end;
|
||||||
// Cancel (secondary-btn).
|
property <bool> create-disabled:
|
||||||
Rectangle {
|
DiscoBuilderState.creating
|
||||||
width: cancel-row.preferred-width + 32px;
|
|| DiscoBuilderState.selected-count == 0
|
||||||
|
|| DiscoBuilderState.collection-name == "";
|
||||||
|
// Cancel (secondary).
|
||||||
|
SecondaryButton {
|
||||||
|
label: @tr("Cancel");
|
||||||
height: 36px;
|
height: 36px;
|
||||||
background: cancel-ta.has-hover ? D.bg-hover : D.bg-secondary;
|
clicked => {
|
||||||
border-radius: 8px;
|
DiscoBuilderActions.back();
|
||||||
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 (primary).
|
||||||
create-btn := Rectangle {
|
QbzPrimaryButton {
|
||||||
property <bool> disabled:
|
label: DiscoBuilderState.creating
|
||||||
DiscoBuilderState.creating
|
? @tr("Loading...") : @tr("Create Collection");
|
||||||
|| DiscoBuilderState.selected-count == 0
|
|
||||||
|| DiscoBuilderState.collection-name == "";
|
|
||||||
width: create-row.preferred-width + 40px;
|
|
||||||
height: 36px;
|
height: 36px;
|
||||||
background: D.accent;
|
enabled: !parent.create-disabled;
|
||||||
border-radius: 8px;
|
clicked => {
|
||||||
opacity: create-btn.disabled ? 0.45 : 1.0;
|
DiscoBuilderActions.create();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { Typography } from "../foundation/typography.slint";
|
|||||||
import { Radius } from "../foundation/radius.slint";
|
import { Radius } from "../foundation/radius.slint";
|
||||||
import { MyQbzDetailState, MyQbzDetailActions, MixtapeDetailItem, TrackItem, NavState } from "../state.slint";
|
import { MyQbzDetailState, MyQbzDetailActions, MixtapeDetailItem, TrackItem, NavState } from "../state.slint";
|
||||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||||
|
import { CircleAction } from "../primitives/CircleAction.slint";
|
||||||
import { TrackRow } from "../primitives/TrackRow.slint";
|
import { TrackRow } from "../primitives/TrackRow.slint";
|
||||||
import { QualityBadgeFull } from "../primitives/QualityBadgeFull.slint";
|
import { QualityBadgeFull } from "../primitives/QualityBadgeFull.slint";
|
||||||
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
|
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
|
||||||
@@ -32,46 +33,6 @@ import { ListScrollbar } from "../primitives/ListScrollbar.slint";
|
|||||||
import { MultiSelectBar } from "../primitives/MultiSelectBar.slint";
|
import { MultiSelectBar } from "../primitives/MultiSelectBar.slint";
|
||||||
import { CollectionMosaic } from "CollectionMosaic.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 --------------------------------
|
// --- One sort / type-filter dropdown row --------------------------------
|
||||||
component MenuOption inherits Rectangle {
|
component MenuOption inherits Rectangle {
|
||||||
in property <string> label;
|
in property <string> label;
|
||||||
@@ -874,22 +835,21 @@ export component MixtapeDetailView inherits Rectangle {
|
|||||||
HorizontalLayout {
|
HorizontalLayout {
|
||||||
spacing: 8px;
|
spacing: 8px;
|
||||||
alignment: start;
|
alignment: start;
|
||||||
// (1) Play — primary.
|
// (1) Play — primary (filled white disc + black glyph;
|
||||||
ActionCircle {
|
// the shared CircleAction.primary, spec §3.6).
|
||||||
|
CircleAction {
|
||||||
icon: @image-url("../assets/icons/play-fill.svg");
|
icon: @image-url("../assets/icons/play-fill.svg");
|
||||||
primary: true;
|
primary: true;
|
||||||
diameter: 44px;
|
diameter: 44px;
|
||||||
glyph: 20px;
|
|
||||||
enabled: MyQbzDetailState.item-count > 0;
|
enabled: MyQbzDetailState.item-count > 0;
|
||||||
clicked => { MyQbzDetailActions.play-all(); }
|
clicked => { MyQbzDetailActions.play-all(); }
|
||||||
}
|
}
|
||||||
// (2) Shuffle.
|
// (2) Shuffle.
|
||||||
VerticalLayout {
|
VerticalLayout {
|
||||||
alignment: center;
|
alignment: center;
|
||||||
ActionCircle {
|
CircleAction {
|
||||||
icon: @image-url("../assets/icons/shuffle.svg");
|
icon: @image-url("../assets/icons/shuffle.svg");
|
||||||
diameter: 40px;
|
diameter: 40px;
|
||||||
glyph: 18px;
|
|
||||||
enabled: MyQbzDetailState.item-count > 0;
|
enabled: MyQbzDetailState.item-count > 0;
|
||||||
clicked => { MyQbzDetailActions.shuffle(); }
|
clicked => { MyQbzDetailActions.shuffle(); }
|
||||||
}
|
}
|
||||||
@@ -897,10 +857,9 @@ export component MixtapeDetailView inherits Rectangle {
|
|||||||
// (3) DJ-mix (DjIcon) — STUB click (modal is a later slice).
|
// (3) DJ-mix (DjIcon) — STUB click (modal is a later slice).
|
||||||
VerticalLayout {
|
VerticalLayout {
|
||||||
alignment: center;
|
alignment: center;
|
||||||
ActionCircle {
|
CircleAction {
|
||||||
icon: @image-url("../assets/icons/turntable.svg");
|
icon: @image-url("../assets/icons/turntable.svg");
|
||||||
diameter: 40px;
|
diameter: 40px;
|
||||||
glyph: 18px;
|
|
||||||
enabled: MyQbzDetailState.item-count > 0;
|
enabled: MyQbzDetailState.item-count > 0;
|
||||||
clicked => { MyQbzDetailActions.dj-mix(); }
|
clicked => { MyQbzDetailActions.dj-mix(); }
|
||||||
}
|
}
|
||||||
@@ -911,10 +870,9 @@ export component MixtapeDetailView inherits Rectangle {
|
|||||||
overflow-anchor := Rectangle {
|
overflow-anchor := Rectangle {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
ActionCircle {
|
CircleAction {
|
||||||
icon: @image-url("../assets/icons/ellipsis.svg");
|
icon: @image-url("../assets/icons/ellipsis.svg");
|
||||||
diameter: 40px;
|
diameter: 40px;
|
||||||
glyph: 18px;
|
|
||||||
clicked => { overflow-menu.show(); }
|
clicked => { overflow-menu.show(); }
|
||||||
}
|
}
|
||||||
overflow-menu := PopupWindow {
|
overflow-menu := PopupWindow {
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { Typography } from "../foundation/typography.slint";
|
|||||||
import { Radius } from "../foundation/radius.slint";
|
import { Radius } from "../foundation/radius.slint";
|
||||||
import { MyQbzEditState, MyQbzEditActions } from "../state.slint";
|
import { MyQbzEditState, MyQbzEditActions } from "../state.slint";
|
||||||
import { QbzIcon } from "../primitives/QbzIcon.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";
|
import { LineEdit, TextEdit } from "std-widgets.slint";
|
||||||
|
|
||||||
export component MyQbzEditModal inherits Rectangle {
|
export component MyQbzEditModal inherits Rectangle {
|
||||||
@@ -96,67 +98,35 @@ export component MyQbzEditModal inherits Rectangle {
|
|||||||
wrap: word-wrap;
|
wrap: word-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Footer (m-footer) -----------------------------------
|
// --- Footer (m-footer) — shared labeled primitives -------
|
||||||
HorizontalLayout {
|
HorizontalLayout {
|
||||||
alignment: end;
|
alignment: end;
|
||||||
spacing: 8px;
|
spacing: 8px;
|
||||||
|
|
||||||
// Cancel (secondary).
|
// Cancel (secondary).
|
||||||
Rectangle {
|
SecondaryButton {
|
||||||
width: cancel-label.preferred-width + 32px;
|
label: @tr("Cancel");
|
||||||
height: 38px;
|
height: 38px;
|
||||||
border-radius: Radius.sm;
|
enabled: !MyQbzEditState.busy;
|
||||||
background: cancel-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
|
clicked => {
|
||||||
border-width: 1px;
|
MyQbzEditActions.close();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit — primary (rename/description) or destructive
|
// Submit — primary (rename/description) or destructive
|
||||||
// (delete).
|
// (delete) variant of the shared QbzPrimaryButton.
|
||||||
Rectangle {
|
QbzPrimaryButton {
|
||||||
width: submit-label.preferred-width + 40px;
|
label: root.is-delete ? @tr("Delete") : @tr("Save");
|
||||||
height: 38px;
|
height: 38px;
|
||||||
border-radius: Radius.sm;
|
destructive: root.is-delete;
|
||||||
opacity: root.can-submit ? 1.0 : 0.5;
|
enabled: root.can-submit;
|
||||||
background: root.is-delete
|
clicked => {
|
||||||
? (submit-ta.has-hover ? #e57373d9 : #e57373)
|
if (root.is-rename) {
|
||||||
: (submit-ta.has-hover ? #ffffffd6 : Theme.accent);
|
MyQbzEditActions.submit-rename();
|
||||||
submit-label := Text {
|
} else if (root.is-description) {
|
||||||
text: root.is-delete ? @tr("Delete") : @tr("Save");
|
MyQbzEditActions.submit-description();
|
||||||
color: #ffffff;
|
} else {
|
||||||
font-size: Typography.body;
|
MyQbzEditActions.confirm-delete();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import { Radius } from "../foundation/radius.slint";
|
|||||||
import { MyQbzMixState, MyQbzMixActions } from "../state.slint";
|
import { MyQbzMixState, MyQbzMixActions } from "../state.slint";
|
||||||
import { QbzSlider } from "../primitives/QbzSlider.slint";
|
import { QbzSlider } from "../primitives/QbzSlider.slint";
|
||||||
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
|
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
|
||||||
|
import { QbzPrimaryButton } from "../primitives/QbzPrimaryButton.slint";
|
||||||
|
import { SecondaryButton } from "../primitives/SecondaryButton.slint";
|
||||||
|
|
||||||
export component MyQbzMixModal inherits Rectangle {
|
export component MyQbzMixModal inherits Rectangle {
|
||||||
visible: MyQbzMixState.open;
|
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 {
|
HorizontalLayout {
|
||||||
alignment: end;
|
alignment: end;
|
||||||
spacing: 12px;
|
spacing: 12px;
|
||||||
|
|
||||||
// Cancel (secondary).
|
// Cancel (secondary).
|
||||||
Rectangle {
|
SecondaryButton {
|
||||||
width: cancel-label.preferred-width + 32px;
|
label: @tr("Cancel");
|
||||||
height: 38px;
|
height: 38px;
|
||||||
border-radius: Radius.sm;
|
enabled: !MyQbzMixState.busy;
|
||||||
background: cancel-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
|
clicked => {
|
||||||
border-width: 1px;
|
MyQbzMixActions.close();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shuffle / Add to queue (primary) — mixModal.confirm.
|
// Shuffle / Add to queue (primary) — mixModal.confirm.
|
||||||
Rectangle {
|
QbzPrimaryButton {
|
||||||
width: confirm-label.preferred-width + 40px;
|
label: @tr("Add to queue");
|
||||||
height: 38px;
|
height: 38px;
|
||||||
border-radius: Radius.sm;
|
enabled: root.can-shuffle;
|
||||||
opacity: root.can-shuffle ? 1.0 : 0.5;
|
clicked => {
|
||||||
background: confirm-ta.has-hover && root.can-shuffle
|
MyQbzMixActions.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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export component CircleAction inherits Rectangle {
|
|||||||
// While the album's first track is fetching, the icon swaps for a spinner.
|
// While the album's first track is fetching, the icon swaps for a spinner.
|
||||||
in property <bool> loading: false;
|
in property <bool> loading: false;
|
||||||
in property <length> diameter: 40px;
|
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.
|
// Optional hover tooltip (empty = none), using the global TooltipOverlay.
|
||||||
in property <string> tooltip;
|
in property <string> tooltip;
|
||||||
callback clicked();
|
callback clicked();
|
||||||
@@ -24,6 +27,7 @@ export component CircleAction inherits Rectangle {
|
|||||||
width: root.diameter;
|
width: root.diameter;
|
||||||
height: root.diameter;
|
height: root.diameter;
|
||||||
border-radius: root.diameter / 2;
|
border-radius: root.diameter / 2;
|
||||||
|
opacity: root.enabled ? 1.0 : 0.4;
|
||||||
// Overlay-matched contrast: solid white for the primary play, semi-
|
// Overlay-matched contrast: solid white for the primary play, semi-
|
||||||
// transparent white fill + bright 1.5px ring otherwise.
|
// transparent white fill + bright 1.5px ring otherwise.
|
||||||
background: root.primary
|
background: root.primary
|
||||||
@@ -52,9 +56,11 @@ export component CircleAction inherits Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ta := TouchArea {
|
ta := TouchArea {
|
||||||
mouse-cursor: pointer;
|
mouse-cursor: root.enabled ? pointer : default;
|
||||||
clicked => {
|
clicked => {
|
||||||
root.clicked();
|
if (root.enabled) {
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
changed has-hover => {
|
changed has-hover => {
|
||||||
if (self.has-hover && root.tooltip != "") {
|
if (self.has-hover && root.tooltip != "") {
|
||||||
|
|||||||
@@ -7,22 +7,35 @@ import { Typography } from "../foundation/typography.slint";
|
|||||||
export component QbzPrimaryButton inherits Rectangle {
|
export component QbzPrimaryButton inherits Rectangle {
|
||||||
in property <string> label;
|
in property <string> label;
|
||||||
in property <bool> enabled: true;
|
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();
|
callback clicked();
|
||||||
|
|
||||||
height: 48px;
|
height: 48px;
|
||||||
border-radius: Radius.sm;
|
border-radius: Radius.sm;
|
||||||
opacity: root.enabled ? 1.0 : 0.5;
|
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; }
|
animate background { duration: 150ms; }
|
||||||
|
|
||||||
Text {
|
// A padded layout drives the preferred-width (label + 20px each side) so the
|
||||||
text: root.label;
|
// button auto-sizes as a footer chip inside an `alignment: end` row, while
|
||||||
color: Theme.accent-text;
|
// still stretching full-width when the parent layout stretches it (login).
|
||||||
font-size: Typography.button;
|
HorizontalLayout {
|
||||||
font-weight: Typography.semibold;
|
alignment: center;
|
||||||
horizontal-alignment: center;
|
padding-left: 20px;
|
||||||
vertical-alignment: center;
|
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 {
|
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