Files
qbz/crates/qbz-slint/ui/myqbz/CreateMyQbzModal.slint
T
vicrodh be832324bd feat(myqbz): create-new Mixtape/Collection modal + wiring
Phase-2 Slice 4. The grid "+ New" buttons open a create modal (name + kind
radio: mixtape/collection, artist_collection excluded), create via the shared
crate, and drop the user into the new collection's detail (1:1 with Tauri's
submitCreateModal -> openMixtapeDetail). The new row prepends to its kind.

- ui/myqbz/CreateMyQbzModal.slint (mirrors CreatePlaylistModal/CreateFolderModal
  idiom; spec 30 create-modal geometry).
- MyQbzCreateState/MyQbzCreateActions in state.slint; mounted in AppShell.
- src/myqbz.rs create_collection via with_db -> with_connection ->
  qbz_mixtape::repo::create_collection (Manual, 80-char cap).
- Un-stubbed on_create_mixtape/on_create_collection; trim + busy guards; toast.

Backend crates untouched. cargo check -p qbz-slint green.

cc
2026-06-09 03:57:30 -06:00

222 lines
9.2 KiB
Plaintext

// "Create Mixtape / Collection" modal — 1:1 with Tauri's inline create
// modal in `+page.svelte` (spec 30 §3). A Name field, a Kind radio toggle
// (Mixtape / Collection — artist_collection is built via the Discography
// Builder, not here), and Cancel / Create buttons. The title and the Create
// button label switch on `kind` ("Create Mixtape" + "+ New Mixtape" vs
// "Create Collection" + "+ New Collection").
//
// Opened from either grid's "+ New" CTA / empty-state button
// (MyQbzCreateState.open). Backdrop click and Cancel close without creating;
// Enter / Create submit. Create is disabled while busy or the name is blank.
import { Theme } from "../foundation/semantic-colors.slint";
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 { LineEdit } from "std-widgets.slint";
// A single labelled radio option in the Kind toggle.
component KindRadio inherits TouchArea {
in property <string> label;
in property <bool> selected;
mouse-cursor: pointer;
horizontal-stretch: 0;
HorizontalLayout {
spacing: 6px;
// Radio dot.
Rectangle {
width: 18px;
height: 18px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
border-radius: 9px;
border-width: root.selected ? 0px : 1.5px;
border-color: Theme.text-muted;
background: root.selected ? Theme.accent : transparent;
Rectangle {
width: 7px;
height: 7px;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
border-radius: 3.5px;
background: #ffffff;
visible: root.selected;
}
}
Text {
text: root.label;
color: Theme.text-primary;
font-size: Typography.body;
vertical-alignment: center;
}
}
}
export component CreateMyQbzModal inherits Rectangle {
visible: MyQbzCreateState.open;
property <bool> is-mixtape: MyQbzCreateState.kind == "mixtape";
property <bool> can-create: MyQbzCreateState.name != "" && !MyQbzCreateState.creating;
if MyQbzCreateState.open: Rectangle {
background: #000000bf;
TouchArea {
mouse-cursor: default;
clicked => {
MyQbzCreateActions.close();
}
}
Rectangle {
width: Math.min(root.width - 80px, 420px);
height: panel.preferred-height;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
border-radius: Radius.md;
background: Theme.surface-card;
border-width: 1px;
border-color: Theme.border-subtle;
drop-shadow-blur: 32px;
drop-shadow-color: #00000080;
TouchArea { }
panel := VerticalLayout {
padding: 24px;
spacing: 16px;
HorizontalLayout {
Text {
text: root.is-mixtape ? @tr("Create Mixtape") : @tr("Create Collection");
color: Theme.text-primary;
font-size: Typography.heading;
font-weight: Typography.bold;
horizontal-stretch: 1;
vertical-alignment: center;
}
close-x := TouchArea {
width: 28px;
height: 28px;
mouse-cursor: pointer;
clicked => {
MyQbzCreateActions.close();
}
QbzIcon {
source: @image-url("../assets/icons/x.svg");
width: 17px;
height: 17px;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
tint: close-x.has-hover ? Theme.text-primary : Theme.text-muted;
}
}
}
// Name.
VerticalLayout {
spacing: 8px;
Text {
// Uppercase eyebrow (Tauri .field-label text-transform).
text: @tr("NAME");
color: Theme.text-muted;
font-size: Typography.legal;
font-weight: Typography.semibold;
letter-spacing: 1.5px;
}
name-input := LineEdit {
text <=> MyQbzCreateState.name;
accepted => {
MyQbzCreateActions.submit();
}
}
}
// Kind toggle (Mixtape / Collection).
VerticalLayout {
spacing: 8px;
Text {
text: @tr("KIND");
color: Theme.text-muted;
font-size: Typography.legal;
font-weight: Typography.semibold;
letter-spacing: 1.5px;
}
HorizontalLayout {
spacing: 12px;
alignment: start;
KindRadio {
label: @tr("Mixtapes");
selected: root.is-mixtape;
clicked => {
if (!MyQbzCreateState.creating) {
MyQbzCreateState.kind = "mixtape";
}
}
}
KindRadio {
label: @tr("Collections");
selected: !root.is-mixtape;
clicked => {
if (!MyQbzCreateState.creating) {
MyQbzCreateState.kind = "collection";
}
}
}
}
}
// Cancel + Create.
HorizontalLayout {
alignment: end;
spacing: 8px;
// Cancel (secondary).
Rectangle {
width: cancel-label.preferred-width + 32px;
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();
}
}
}
// Create (primary) — label reuses the empty-state CTA.
Rectangle {
width: create-label.preferred-width + 40px;
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();
}
}
}
}
}
}
}
}
}