Buttons that rest on surfaceElevated used surfaceHover for their hover fill: a 6% white wash meant for rows that rest transparent. Over an elevated button it lands DARKER than the rest fill — on a modal's dark ground the Cancel/Save/footer buttons went near-black under the pointer. QbzTheme gains surfaceElevatedHover (a lift of surfaceElevated, derived so every palette has it), its ambient half-alpha twin, and the elevatedFill / elevatedHoverFill pair an ambient-aware control paints. Every site with the old pairing (65 across modals, toolbars, selects, circle actions, chips and cards) now hovers to the lift. cc
1245 lines
61 KiB
QML
1245 lines
61 KiB
QML
// HiFi Wizard (DAC setup) — QML port of
|
|
// crates/qbz-ui/ui/primitives/DacWizardModal.slint.
|
|
//
|
|
// Six steps: Welcome · Check · DACs · Configure · Test · Done. The wizard is
|
|
// READ-ONLY — it auto-detects DACs, shows the exact commands and config to
|
|
// copy, and plays four test tracks so the user can watch their DAC switch
|
|
// rates. It NEVER writes a system file.
|
|
//
|
|
// MODAL RECIPE, from the reference (:8-9): an overlay Item, NOT a window grab,
|
|
// so playback keeps running underneath while the test step plays. The scrim,
|
|
// the header X and the footer Close all just hide it; Rust's `close()` stops
|
|
// nothing.
|
|
//
|
|
// MOUNTED AT THE SETTINGS VIEW ROOT, beside SettingsConfirmHost — the panels
|
|
// are Columns inside a Flickable, so a modal mounted in one would be sized by
|
|
// the scrolled content and ride the scroll. ADR-009: z >= 3000.
|
|
//
|
|
// ── WHAT THIS FILE OWNS AND WHAT RUST OWNS ────────────────────────────────
|
|
// Rust (dac_wizard_qt.rs) owns everything it COMPUTES — the probe verdict, the
|
|
// candidate list AND its checkboxes, the manual node name's validity, the
|
|
// generated configs and their accordion state, the read-back labels — plus
|
|
// `open`, because the Settings row's click must reset and probe atomically.
|
|
//
|
|
// This file owns the pure NAVIGATION state: `step`, the welcome checkbox, the
|
|
// three review progress checkboxes and the manual-entry disclosure. That is
|
|
// the reference's own split — DacWizardModal.slint mutates those inline and
|
|
// never calls Rust for them (:817 `step -= 1`, :853 `step += 1`). They reset
|
|
// on `openSeq`, which Rust bumps on every open.
|
|
//
|
|
// The one field that is deliberately BOTH: the manual node.name. Rust stores
|
|
// it (checked_dacs reads it) but this file keeps the TextInput's own text and
|
|
// never writes the document back into it — republishing into a focused input
|
|
// is how a cursor jumps to the end mid-word.
|
|
|
|
import QtQuick
|
|
import com.blitzfc.qbz
|
|
import "../controls"
|
|
import "../theme"
|
|
import "../views/local"
|
|
|
|
Item {
|
|
property bool kioskHost: false
|
|
|
|
id: root
|
|
|
|
// An unopened overlay is an invisible, non-interactive Item and costs
|
|
// nothing. `anchors.fill` + `enabled` live HERE, not at the mount site —
|
|
// the sibling LibFolderEditModal.qml sets the same three, so a mount is
|
|
// one line.
|
|
anchors.fill: parent
|
|
z: 3000
|
|
visible: doc.open === true
|
|
enabled: root.visible
|
|
|
|
readonly property var doc: JSON.parse(QbzDacWizard.wizardJson)
|
|
readonly property var candidates: doc.candidates || []
|
|
readonly property var remediations: doc.remediations || []
|
|
readonly property var dacConfigs: doc.dacConfigs || []
|
|
readonly property var createdPaths: doc.createdPaths || []
|
|
readonly property var testTrackLines: doc.testTracks || []
|
|
|
|
// ---- QML-owned navigation state (see the header) ----------------------
|
|
// 0 welcome · 1 check · 2 select-dacs · 3 review · 4 test · 5 done
|
|
property int step: 0
|
|
property bool welcomeConfirmed: false
|
|
property bool reviewBackupDone: false
|
|
property bool reviewConfigDone: false
|
|
property bool reviewRestartDone: false
|
|
// "Can't see your DAC?" is collapsed by default when enumeration found
|
|
// devices; forced open when it found none.
|
|
property bool showManual: false
|
|
|
|
readonly property int openSeq: doc.openSeq || 0
|
|
onVisibleChanged: if (visible) {
|
|
Qt.callLater(function () { closeX.forceActiveFocus() })
|
|
}
|
|
onOpenSeqChanged: {
|
|
root.step = 0
|
|
root.welcomeConfirmed = false
|
|
root.reviewBackupDone = false
|
|
root.reviewConfigDone = false
|
|
root.reviewRestartDone = false
|
|
root.showManual = false
|
|
manualInput.text = ""
|
|
if (root.visible)
|
|
Qt.callLater(function () { closeX.forceActiveFocus() })
|
|
}
|
|
|
|
Keys.onEscapePressed: function (event) {
|
|
QbzDacWizard.close()
|
|
event.accepted = true
|
|
}
|
|
|
|
// Step labels for the header rail (index = step).
|
|
readonly property var stepLabels: [
|
|
QbzSession.tr("Welcome", QbzSession.trRev),
|
|
QbzSession.tr("Check", QbzSession.trRev),
|
|
QbzSession.tr("DACs", QbzSession.trRev),
|
|
QbzSession.tr("Configure", QbzSession.trRev),
|
|
QbzSession.tr("Test", QbzSession.trRev),
|
|
QbzSession.tr("Done", QbzSession.trRev)
|
|
]
|
|
|
|
// The footer primary's gate, per step (reference :832-840):
|
|
// 0 → the accept checkbox
|
|
// 2 → at least one DAC checked, or a valid manual node.name
|
|
// 3 → all three review checkboxes
|
|
// else → always enabled
|
|
readonly property bool primaryEnabled: {
|
|
if (root.step === 0)
|
|
return root.welcomeConfirmed
|
|
if (root.step === 2)
|
|
return (root.doc.anyDacSelected === true) || (root.doc.manualValid === true)
|
|
if (root.step === 3)
|
|
return root.reviewBackupDone && root.reviewConfigDone && root.reviewRestartDone
|
|
return true
|
|
}
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
// A whole-row-clickable checkbox (HTML <label> behaviour — clicking the
|
|
// text toggles it, not just the box). Reference: the modal's own CheckRow.
|
|
component CheckRow: Item {
|
|
id: checkRow
|
|
property bool checked: false
|
|
property string label: ""
|
|
signal toggled()
|
|
|
|
width: parent ? parent.width : 0
|
|
height: root.kioskHost ? 44 : 28
|
|
activeFocusOnTab: visible && enabled
|
|
Accessible.role: Accessible.CheckBox
|
|
Accessible.name: label
|
|
Accessible.checked: checked
|
|
Accessible.onToggleAction: toggled()
|
|
Keys.onPressed: function (event) {
|
|
if (!event.isAutoRepeat
|
|
&& (event.key === Qt.Key_Space
|
|
|| event.key === Qt.Key_Return
|
|
|| event.key === Qt.Key_Enter)) {
|
|
checkRow.toggled()
|
|
event.accepted = true
|
|
}
|
|
}
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: theme.radiusSm
|
|
color: "transparent"
|
|
border.width: checkRow.activeFocus ? 2 : 0
|
|
border.color: theme.accent
|
|
}
|
|
|
|
QbzCheckbox { kioskHost: root.kioskHost;
|
|
id: box
|
|
anchors.left: parent.left
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
checked: checkRow.checked
|
|
enabled: false
|
|
// DISPLAY-ONLY. The row MouseArea below is declared last, so it
|
|
// sits on top and owns every click — exactly as the reference
|
|
// shadows QbzCheckbox's internal toggle. No `onToggled` here on
|
|
// purpose: if the box's own MouseArea ever did win an event, two
|
|
// handlers would fire and the flip would cancel itself out,
|
|
// leaving a checkbox that looks dead.
|
|
}
|
|
Text {
|
|
anchors.left: box.right
|
|
anchors.leftMargin: 10
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: checkRow.label
|
|
color: theme.textSecondary
|
|
font.pixelSize: root.kioskHost ? (theme.fontBody) * 1.2 : (theme.fontBody)
|
|
elide: Text.ElideRight
|
|
}
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: Qt.PointingHandCursor
|
|
onPressed: checkRow.forceActiveFocus()
|
|
onClicked: checkRow.toggled()
|
|
}
|
|
}
|
|
|
|
// A step body heading.
|
|
component StepTitle: Text {
|
|
width: parent ? parent.width : 0
|
|
color: theme.textPrimary
|
|
font.pixelSize: root.kioskHost ? (theme.fontHeading) * 1.2 : (theme.fontHeading)
|
|
font.weight: theme.weightSemibold
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
|
|
// ------------------------------- scrim --------------------------------
|
|
// Click outside closes (reference :84-89).
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: "#bf000000"
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: QbzDacWizard.close()
|
|
// Wheel-lock (the DiscoverConfigModal rule).
|
|
onWheel: function (wheel) { wheel.accepted = true }
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: card
|
|
anchors.centerIn: parent
|
|
width: Math.min(parent.width - 80, 620)
|
|
// 210 = header 68 + 2 hairlines + stepper 46 + footer 70/24 padding;
|
|
// the body scrolls inside the Flickable past the 90 % clamp.
|
|
height: Math.min(Math.max(500, bodyCol.implicitHeight + 210), parent.height * 0.9)
|
|
radius: theme.radiusLg
|
|
color: theme.surfaceCard
|
|
border.width: 1
|
|
border.color: theme.borderSubtle
|
|
// Swallow clicks so they never reach the scrim.
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
// Wheel-lock (the DiscoverConfigModal rule).
|
|
onWheel: function (wheel) { wheel.accepted = true }
|
|
}
|
|
|
|
readonly property int contentWidth: card.width - 48
|
|
|
|
// ------------------------------ header ----------------------------
|
|
Item {
|
|
id: header
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
height: 68
|
|
|
|
QbzIcon {
|
|
id: wandIcon
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 24
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 26
|
|
height: 26
|
|
name: "gandalf"
|
|
// The reference tints this a literal #3fae6a. `success` has no
|
|
// qrc bake directory, so a degraded tint set would render
|
|
// NOTHING; `accent` is the port's established stand-in for the
|
|
// success tone on GLYPHS (WarningBanner.qml:26-28). Every
|
|
// success-toned TEXT in this file uses theme.success, which is
|
|
// #3fae6a verbatim.
|
|
tintName: "accent"
|
|
}
|
|
Text {
|
|
anchors.left: wandIcon.right
|
|
anchors.leftMargin: 10
|
|
anchors.right: closeX.left
|
|
anchors.rightMargin: 10
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: QbzSession.tr("HiFi Wizard — DAC Setup", QbzSession.trRev)
|
|
color: theme.textPrimary
|
|
font.pixelSize: root.kioskHost ? (theme.fontSection) * 1.2 : (theme.fontSection)
|
|
font.weight: theme.weightSemibold
|
|
elide: Text.ElideRight
|
|
}
|
|
Item {
|
|
id: closeX
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 24
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: root.kioskHost ? 44 : 28
|
|
height: root.kioskHost ? 44 : 28
|
|
activeFocusOnTab: root.visible
|
|
Accessible.role: Accessible.Button
|
|
Accessible.name: QbzSession.tr("Close", QbzSession.trRev)
|
|
Accessible.onPressAction: QbzDacWizard.close()
|
|
Keys.onPressed: function (event) {
|
|
if (!event.isAutoRepeat
|
|
&& (event.key === Qt.Key_Space
|
|
|| event.key === Qt.Key_Return
|
|
|| event.key === Qt.Key_Enter)) {
|
|
QbzDacWizard.close()
|
|
event.accepted = true
|
|
}
|
|
}
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: theme.radiusSm
|
|
color: "transparent"
|
|
border.width: closeX.activeFocus ? 2 : 0
|
|
border.color: theme.accent
|
|
}
|
|
QbzIcon {
|
|
anchors.centerIn: parent
|
|
width: 17
|
|
height: 17
|
|
name: "x"
|
|
tintName: closeTa.containsMouse ? "textPrimary" : "muted"
|
|
}
|
|
MouseArea {
|
|
id: closeTa
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onPressed: closeX.forceActiveFocus()
|
|
onClicked: QbzDacWizard.close()
|
|
}
|
|
}
|
|
}
|
|
Rectangle {
|
|
id: headerDiv
|
|
anchors.top: header.bottom
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
height: 1
|
|
color: theme.borderSubtle
|
|
}
|
|
|
|
// ---------------------------- stepper rail ------------------------
|
|
// Dots + labels, coloured by progress (reference :153-182).
|
|
Row {
|
|
id: stepper
|
|
anchors.top: headerDiv.bottom
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.leftMargin: 24
|
|
anchors.rightMargin: 24
|
|
anchors.topMargin: 14
|
|
height: 18
|
|
spacing: 6
|
|
|
|
Repeater {
|
|
model: root.stepLabels
|
|
delegate: Row {
|
|
required property int index
|
|
required property string modelData
|
|
width: (stepper.width - stepper.spacing * (root.stepLabels.length - 1))
|
|
/ root.stepLabels.length
|
|
spacing: 6
|
|
|
|
Rectangle {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 10
|
|
height: 10
|
|
radius: 5
|
|
color: index <= root.step ? theme.success : theme.borderMuted
|
|
}
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: parent.width - 16
|
|
text: modelData
|
|
color: index === root.step ? theme.textPrimary
|
|
: (index < root.step ? theme.textSecondary : theme.textMuted)
|
|
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
|
|
font.weight: index === root.step ? theme.weightSemibold : theme.weightRegular
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Rectangle {
|
|
id: stepperDiv
|
|
anchors.top: stepper.bottom
|
|
anchors.topMargin: 14
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
height: 1
|
|
color: theme.borderSubtle
|
|
}
|
|
|
|
// ------------------------------- body -----------------------------
|
|
Flickable {
|
|
id: bodyFlick
|
|
anchors.top: stepperDiv.bottom
|
|
anchors.bottom: footerDiv.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
clip: true
|
|
contentWidth: width
|
|
contentHeight: bodyCol.implicitHeight
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
|
|
Column {
|
|
id: bodyCol
|
|
width: card.width
|
|
padding: 24
|
|
spacing: 16
|
|
|
|
// ═══════════════════ Step 0: welcome ═══════════════════════
|
|
Column {
|
|
visible: root.step === 0
|
|
width: card.contentWidth
|
|
spacing: 14
|
|
|
|
StepTitle { text: QbzSession.tr("Guided DAC Setup", QbzSession.trRev) }
|
|
Text {
|
|
width: parent.width
|
|
text: QbzSession.tr("This wizard helps you configure your DAC for bit-perfect playback on Linux. QBZ never changes your system automatically — it auto-detects your DACs, shows you the exact commands and config to copy, and lets you test playback right here.", QbzSession.trRev)
|
|
color: theme.textSecondary
|
|
font.pixelSize: root.kioskHost ? (theme.fontBody) * 1.2 : (theme.fontBody)
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
Text {
|
|
width: parent.width
|
|
text: QbzSession.tr("You are responsible for your own backups. This is a helper, not a system recovery tool.", QbzSession.trRev)
|
|
color: theme.textMuted
|
|
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
WarningBanner {
|
|
variant: "info"
|
|
title: QbzSession.tr("What to test with", QbzSession.trRev)
|
|
body: QbzSession.tr("The built-in test plays four tracks spanning different bit-depths and sample rates (16/44.1, 24/44.1, 24/96, 24/192) so you can see your DAC switch. If you'd rather use your own queue, add one track of each kind first so every rate is exercised.", QbzSession.trRev)
|
|
}
|
|
CheckRow {
|
|
checked: root.welcomeConfirmed
|
|
label: QbzSession.tr("I understand and accept these terms", QbzSession.trRev)
|
|
onToggled: root.welcomeConfirmed = !root.welcomeConfirmed
|
|
}
|
|
}
|
|
|
|
// ═══════════════════ Step 1: check stack ═══════════════════
|
|
Column {
|
|
visible: root.step === 1
|
|
width: card.contentWidth
|
|
spacing: 14
|
|
|
|
StepTitle { text: QbzSession.tr("Check Audio Stack", QbzSession.trRev) }
|
|
|
|
// Sandboxed (Flatpak/Snap): the host probes are blind, so
|
|
// there is no verdict — reference commands instead.
|
|
WarningBanner {
|
|
visible: root.doc.sandboxed === true
|
|
variant: "info"
|
|
title: QbzSession.tr("Sandboxed — host check unavailable", QbzSession.trRev)
|
|
body: QbzSession.tr("QBZ is packaged as Flatpak/Snap and can't inspect your host audio stack. If playback already works, you're set. Pick your distribution and init system below for the right setup commands.", QbzSession.trRev)
|
|
}
|
|
WarningBanner {
|
|
visible: root.doc.sandboxed !== true && root.doc.healthOk === true
|
|
variant: "success"
|
|
title: QbzSession.tr("Audio stack ready", QbzSession.trRev)
|
|
body: root.doc.healthSummary || ""
|
|
}
|
|
WarningBanner {
|
|
visible: root.doc.sandboxed !== true && root.doc.healthOk !== true
|
|
variant: "warning"
|
|
title: QbzSession.tr("Some packages are missing", QbzSession.trRev)
|
|
body: root.doc.healthSummary || ""
|
|
}
|
|
|
|
// Distro — auto-detected, always overridable.
|
|
Column {
|
|
width: parent.width
|
|
spacing: 8
|
|
Text {
|
|
text: QbzSession.tr("Your distribution", QbzSession.trRev)
|
|
color: theme.textSecondary
|
|
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
|
|
font.weight: theme.weightMedium
|
|
}
|
|
QbzSelect { kioskHost: root.kioskHost;
|
|
menuWidth: 360
|
|
options: root.doc.distroOptions || []
|
|
currentIndex: root.doc.distroIndex || 0
|
|
onSelected: function (i) { QbzDacWizard.setDistro(i) }
|
|
}
|
|
}
|
|
// Init system — auto-detected at runtime; override to
|
|
// generate service commands for another machine.
|
|
Column {
|
|
width: parent.width
|
|
spacing: 8
|
|
Text {
|
|
text: QbzSession.tr("Init system (service commands)", QbzSession.trRev)
|
|
color: theme.textSecondary
|
|
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
|
|
font.weight: theme.weightMedium
|
|
}
|
|
QbzSelect { kioskHost: root.kioskHost;
|
|
menuWidth: 240
|
|
options: root.doc.initOptions || []
|
|
currentIndex: root.doc.initIndex || 0
|
|
onSelected: function (i) { QbzDacWizard.setInit(i) }
|
|
}
|
|
}
|
|
// One copy-paste fix per missing piece (empty when ready).
|
|
Repeater {
|
|
model: root.remediations
|
|
delegate: CommandBlock {
|
|
required property var modelData
|
|
width: card.contentWidth
|
|
caption: modelData.caption || ""
|
|
command: modelData.command || ""
|
|
}
|
|
}
|
|
}
|
|
|
|
// ═══════════════════ Step 2: select DACs ═══════════════════
|
|
Column {
|
|
visible: root.step === 2
|
|
width: card.contentWidth
|
|
spacing: 14
|
|
|
|
StepTitle {
|
|
text: QbzSession.tr("Which DACs do you want to configure?", QbzSession.trRev)
|
|
}
|
|
Text {
|
|
visible: root.doc.detecting === true
|
|
width: parent.width
|
|
text: QbzSession.tr("Detecting your DACs…", QbzSession.trRev)
|
|
color: theme.textMuted
|
|
font.pixelSize: root.kioskHost ? (theme.fontBody) * 1.2 : (theme.fontBody)
|
|
}
|
|
|
|
// Enumerated candidates — circular multi-select.
|
|
Repeater {
|
|
model: root.candidates
|
|
delegate: Rectangle {
|
|
id: candRow
|
|
required property int index
|
|
required property var modelData
|
|
|
|
width: card.contentWidth
|
|
height: candCol.implicitHeight + 16
|
|
radius: theme.radiusSm
|
|
color: modelData.checked ? theme.surfaceHover : "transparent"
|
|
|
|
SelectCheck {
|
|
id: candCheck
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 8
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
diameter: 16
|
|
on: candRow.modelData.checked === true
|
|
onToggled: QbzDacWizard.toggleDac(candRow.index)
|
|
}
|
|
Column {
|
|
id: candCol
|
|
anchors.left: candCheck.right
|
|
anchors.leftMargin: 12
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 8
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 4
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: candRow.modelData.description || ""
|
|
color: theme.textPrimary
|
|
font.pixelSize: root.kioskHost ? (theme.fontBody) * 1.2 : (theme.fontBody)
|
|
font.weight: theme.weightMedium
|
|
elide: Text.ElideRight
|
|
}
|
|
Row {
|
|
width: parent.width
|
|
spacing: 8
|
|
// Bus label — a bordered box, NOT a pill (ADR-008).
|
|
Rectangle {
|
|
visible: (candRow.modelData.bus || "") !== ""
|
|
width: visible ? busText.implicitWidth + 12 : 0
|
|
height: 18
|
|
radius: theme.radiusSm
|
|
border.width: 1
|
|
border.color: theme.borderSubtle
|
|
color: theme.surfaceElevated
|
|
Text {
|
|
id: busText
|
|
anchors.centerIn: parent
|
|
text: candRow.modelData.bus || ""
|
|
color: theme.textMuted
|
|
font.pixelSize: root.kioskHost ? (10) * 1.2 : (10)
|
|
font.weight: theme.weightSemibold
|
|
}
|
|
}
|
|
Text {
|
|
visible: candRow.modelData.isDefault === true
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: QbzSession.tr("default", QbzSession.trRev)
|
|
color: theme.success
|
|
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
|
|
}
|
|
Text {
|
|
visible: (candRow.modelData.ratesLabel || "") !== ""
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: candRow.modelData.ratesLabel || ""
|
|
color: theme.textMuted
|
|
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Escape-hatch toggle (only when enumeration found devices;
|
|
// otherwise the manual block shows unconditionally).
|
|
Item {
|
|
id: manualEntryToggle
|
|
visible: root.doc.hasEnumeration === true
|
|
width: parent.width
|
|
height: visible ? 22 : 0
|
|
activeFocusOnTab: visible && enabled
|
|
Accessible.role: Accessible.Button
|
|
Accessible.name: manualToggle.text
|
|
Accessible.onPressAction: root.showManual = !root.showManual
|
|
Keys.onPressed: function (event) {
|
|
if (!event.isAutoRepeat
|
|
&& (event.key === Qt.Key_Space
|
|
|| event.key === Qt.Key_Return
|
|
|| event.key === Qt.Key_Enter)) {
|
|
root.showManual = !root.showManual
|
|
event.accepted = true
|
|
}
|
|
}
|
|
Rectangle {
|
|
anchors.left: parent.left
|
|
anchors.top: parent.top
|
|
width: manualToggle.implicitWidth
|
|
height: parent.height
|
|
radius: theme.radiusSm
|
|
color: "transparent"
|
|
border.width: manualEntryToggle.activeFocus ? 2 : 0
|
|
border.color: theme.accent
|
|
}
|
|
Text {
|
|
id: manualToggle
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: root.showManual
|
|
? QbzSession.tr("Hide manual entry", QbzSession.trRev)
|
|
: QbzSession.tr("Can't see your DAC?", QbzSession.trRev)
|
|
color: theme.accent
|
|
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
|
|
}
|
|
MouseArea {
|
|
anchors.left: parent.left
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
width: manualToggle.implicitWidth
|
|
cursorShape: Qt.PointingHandCursor
|
|
onPressed: manualEntryToggle.forceActiveFocus()
|
|
onClicked: root.showManual = !root.showManual
|
|
}
|
|
}
|
|
|
|
// Manual node.name entry (escape hatch).
|
|
Column {
|
|
visible: root.showManual || root.doc.hasEnumeration !== true
|
|
width: parent.width
|
|
spacing: 10
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: QbzSession.tr("Find your DAC's PipeWire node name and paste it here:", QbzSession.trRev)
|
|
color: theme.textSecondary
|
|
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
CommandBlock {
|
|
caption: QbzSession.tr("1. Find your DAC under 'Audio > Sinks' and note its number", QbzSession.trRev)
|
|
command: "wpctl status"
|
|
}
|
|
CommandBlock {
|
|
caption: QbzSession.tr("2. Run with that number to read the node.name", QbzSession.trRev)
|
|
command: "wpctl inspect <ID> | grep node.name"
|
|
}
|
|
Rectangle {
|
|
width: parent.width
|
|
height: 43
|
|
radius: theme.radiusSm
|
|
color: theme.surfaceCard
|
|
border.width: 1
|
|
border.color: manualInput.activeFocus ? theme.accent : theme.borderSubtle
|
|
|
|
TextInput {
|
|
|
|
QbzTextEditMenu { }
|
|
id: manualInput
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 12
|
|
anchors.rightMargin: 12
|
|
verticalAlignment: TextInput.AlignVCenter
|
|
clip: true
|
|
color: theme.textPrimary
|
|
font.pixelSize: root.kioskHost ? (theme.fontLink) * 1.2 : (theme.fontLink)
|
|
selectByMouse: true
|
|
// No hotkey-guard call here on purpose: this
|
|
// port computes the guard at the DISPATCHER
|
|
// (AppShell.qml:94 tests `activeFocusItem
|
|
// instanceof TextInput`), so a plain TextInput
|
|
// is already guarded. The Slint needs its
|
|
// explicit `UiFocusState.text-input-focused`
|
|
// write because a std-widgets LineEdit's
|
|
// has-focus is an alias into an inner item and
|
|
// never fires at the use site (#619) — that is
|
|
// a Slint problem, not a contract.
|
|
onTextEdited: QbzDacWizard.validateManual(text)
|
|
}
|
|
Text {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 12
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
visible: manualInput.text === ""
|
|
text: "alsa_output.usb-..."
|
|
color: theme.textMuted
|
|
font.pixelSize: root.kioskHost ? (theme.fontLink) * 1.2 : (theme.fontLink)
|
|
}
|
|
}
|
|
Text {
|
|
visible: manualInput.text !== ""
|
|
width: parent.width
|
|
text: root.doc.manualValid === true
|
|
? QbzSession.tr("Valid node name", QbzSession.trRev)
|
|
: QbzSession.tr("Invalid — should contain alsa_output or alsa_input", QbzSession.trRev)
|
|
color: root.doc.manualValid === true ? theme.success : theme.danger
|
|
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
}
|
|
|
|
// ═══════════════════ Step 3: review & apply ════════════════
|
|
Column {
|
|
visible: root.step === 3
|
|
width: card.contentWidth
|
|
spacing: 14
|
|
|
|
StepTitle { text: QbzSession.tr("Review & Apply", QbzSession.trRev) }
|
|
WarningBanner {
|
|
variant: "info"
|
|
title: QbzSession.tr("QBZ never writes these files", QbzSession.trRev)
|
|
body: QbzSession.tr("QBZ already pins rate, sink and exclusive mode at runtime. These files only change system-wide behavior — copy and run them yourself if you want them. Back up first.", QbzSession.trRev)
|
|
}
|
|
|
|
// 1. Backup.
|
|
CommandBlock {
|
|
caption: QbzSession.tr("1. Back up your current PipeWire/WirePlumber config", QbzSession.trRev)
|
|
command: root.doc.backupCmd || ""
|
|
}
|
|
CheckRow {
|
|
checked: root.reviewBackupDone
|
|
label: QbzSession.tr("I created a backup", QbzSession.trRev)
|
|
onToggled: root.reviewBackupDone = !root.reviewBackupDone
|
|
}
|
|
|
|
// 2. Per-DAC config — collapsible accordions.
|
|
Repeater {
|
|
model: root.dacConfigs
|
|
delegate: Column {
|
|
id: cfgCol
|
|
required property int index
|
|
required property var modelData
|
|
|
|
width: card.contentWidth
|
|
spacing: 8
|
|
|
|
Rectangle {
|
|
id: configHeader
|
|
width: parent.width
|
|
height: 40
|
|
radius: theme.radiusSm
|
|
color: cfgHead.containsMouse ? theme.surfaceElevatedHover : theme.surfaceElevated
|
|
border.width: 1
|
|
border.color: activeFocus ? theme.accent : theme.borderSubtle
|
|
activeFocusOnTab: visible && enabled
|
|
Accessible.role: Accessible.Button
|
|
Accessible.name: cfgCol.modelData.name || ""
|
|
Accessible.onPressAction: QbzDacWizard.toggleConfig(cfgCol.index)
|
|
Keys.onPressed: function (event) {
|
|
if (!event.isAutoRepeat
|
|
&& (event.key === Qt.Key_Space
|
|
|| event.key === Qt.Key_Return
|
|
|| event.key === Qt.Key_Enter)) {
|
|
QbzDacWizard.toggleConfig(cfgCol.index)
|
|
event.accepted = true
|
|
}
|
|
}
|
|
|
|
QbzIcon {
|
|
id: cfgChevron
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 12
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 16
|
|
height: 16
|
|
name: cfgCol.modelData.expanded ? "chevron-down" : "chevron-right"
|
|
tintName: "muted"
|
|
}
|
|
Text {
|
|
anchors.left: cfgChevron.right
|
|
anchors.leftMargin: 8
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 12
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: cfgCol.modelData.name || ""
|
|
color: theme.textPrimary
|
|
font.pixelSize: root.kioskHost ? (theme.fontBody) * 1.2 : (theme.fontBody)
|
|
font.weight: theme.weightMedium
|
|
elide: Text.ElideRight
|
|
}
|
|
MouseArea {
|
|
id: cfgHead
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onPressed: configHeader.forceActiveFocus()
|
|
onClicked: QbzDacWizard.toggleConfig(cfgCol.index)
|
|
}
|
|
}
|
|
Column {
|
|
visible: cfgCol.modelData.expanded === true
|
|
width: parent.width
|
|
spacing: 8
|
|
CommandBlock {
|
|
caption: QbzSession.tr("Sample-rate switching (PipeWire)", QbzSession.trRev)
|
|
command: cfgCol.modelData.pipewireConf || ""
|
|
}
|
|
CommandBlock {
|
|
caption: QbzSession.tr("Per-app bit-perfect (PipeWire client)", QbzSession.trRev)
|
|
command: cfgCol.modelData.pulseConf || ""
|
|
}
|
|
CommandBlock {
|
|
caption: QbzSession.tr("Pin the DAC + rates (WirePlumber)", QbzSession.trRev)
|
|
command: cfgCol.modelData.wireplumberConf || ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
CheckRow {
|
|
checked: root.reviewConfigDone
|
|
label: QbzSession.tr("I created the configuration files", QbzSession.trRev)
|
|
onToggled: root.reviewConfigDone = !root.reviewConfigDone
|
|
}
|
|
|
|
// 3. Restart (init-aware).
|
|
CommandBlock {
|
|
caption: QbzSession.tr("3. Restart the audio services to load the config", QbzSession.trRev)
|
|
command: root.doc.restartCmd || ""
|
|
}
|
|
CheckRow {
|
|
checked: root.reviewRestartDone
|
|
label: QbzSession.tr("I restarted the audio services", QbzSession.trRev)
|
|
onToggled: root.reviewRestartDone = !root.reviewRestartDone
|
|
}
|
|
}
|
|
|
|
// ═══════════════════ Step 4: test playback ═════════════════
|
|
Column {
|
|
id: testStep
|
|
visible: root.step === 4
|
|
width: card.contentWidth
|
|
spacing: 14
|
|
|
|
readonly property bool playing: root.doc.testPlaying === true
|
|
|
|
StepTitle { text: QbzSession.tr("Test Playback", QbzSession.trRev) }
|
|
Text {
|
|
width: parent.width
|
|
text: QbzSession.tr("Plays four tracks at different bit-depths and sample rates through the selected DAC. Watch your DAC's display switch rates — and confirm it below.", QbzSession.trRev)
|
|
color: theme.textSecondary
|
|
font.pixelSize: root.kioskHost ? (theme.fontBody) * 1.2 : (theme.fontBody)
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
|
|
// The four tracks — click one to jump straight to it while
|
|
// testing (no need to wait for each to finish).
|
|
Column {
|
|
width: parent.width
|
|
spacing: 4
|
|
Repeater {
|
|
model: root.testTrackLines
|
|
delegate: Rectangle {
|
|
id: trackRow
|
|
required property int index
|
|
required property string modelData
|
|
|
|
width: card.contentWidth
|
|
height: 26
|
|
radius: theme.radiusSm
|
|
color: (trackTa.containsMouse && testStep.playing)
|
|
? theme.surfaceHover : "transparent"
|
|
activeFocusOnTab: testStep.playing
|
|
border.width: activeFocus ? 2 : 0
|
|
border.color: theme.accent
|
|
Accessible.role: Accessible.Button
|
|
Accessible.name: trackRow.modelData
|
|
Accessible.onPressAction: {
|
|
if (testStep.playing)
|
|
QbzDacWizard.testPlayIndex(trackRow.index)
|
|
}
|
|
Keys.onPressed: function (event) {
|
|
if (testStep.playing && !event.isAutoRepeat
|
|
&& (event.key === Qt.Key_Space
|
|
|| event.key === Qt.Key_Return
|
|
|| event.key === Qt.Key_Enter)) {
|
|
QbzDacWizard.testPlayIndex(trackRow.index)
|
|
event.accepted = true
|
|
}
|
|
}
|
|
|
|
// Fixed glyph slot (width AND height) so the
|
|
// ♪→▶ hover swap can never reflow the row.
|
|
Text {
|
|
id: trackGlyph
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 6
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 14
|
|
height: 26
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
text: (trackTa.containsMouse && testStep.playing) ? "▶" : "♪"
|
|
color: (trackTa.containsMouse && testStep.playing)
|
|
? theme.accent : theme.textMuted
|
|
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
|
|
}
|
|
Text {
|
|
anchors.left: trackGlyph.right
|
|
anchors.leftMargin: 8
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 6
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: trackRow.modelData
|
|
color: theme.textSecondary
|
|
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
|
|
elide: Text.ElideRight
|
|
}
|
|
MouseArea {
|
|
id: trackTa
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: testStep.playing ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
onPressed: if (testStep.playing) trackRow.forceActiveFocus()
|
|
onClicked: {
|
|
if (testStep.playing)
|
|
QbzDacWizard.testPlayIndex(trackRow.index)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Play / Stop · Prev / Next · use-my-queue.
|
|
Row {
|
|
width: parent.width
|
|
height: 34
|
|
spacing: 10
|
|
|
|
QbzPrimaryButton {
|
|
btnHeight: 34
|
|
label: testStep.playing
|
|
? QbzSession.tr("Stop", QbzSession.trRev)
|
|
: QbzSession.tr("Play test", QbzSession.trRev)
|
|
onClicked: {
|
|
if (testStep.playing)
|
|
QbzDacWizard.stopTest()
|
|
else
|
|
QbzDacWizard.startTest()
|
|
}
|
|
}
|
|
|
|
// Explicit Prev / Next — they work on whatever queue is
|
|
// playing (the 4 test tracks or the user's own), which
|
|
// is why they call the PLAYER, not the wizard.
|
|
Rectangle {
|
|
id: prevButton
|
|
visible: testStep.playing
|
|
width: visible ? 34 : 0
|
|
height: 34
|
|
radius: theme.radiusSm
|
|
border.width: 1
|
|
border.color: activeFocus ? theme.accent : theme.borderSubtle
|
|
activeFocusOnTab: visible && enabled
|
|
Accessible.role: Accessible.Button
|
|
Accessible.name: QbzSession.tr("Previous", QbzSession.trRev)
|
|
Accessible.onPressAction: QbzPlayer.previous()
|
|
Keys.onPressed: function (event) {
|
|
if (!event.isAutoRepeat
|
|
&& (event.key === Qt.Key_Space
|
|
|| event.key === Qt.Key_Return
|
|
|| event.key === Qt.Key_Enter)) {
|
|
QbzPlayer.previous()
|
|
event.accepted = true
|
|
}
|
|
}
|
|
color: prevTa.containsMouse ? theme.surfaceElevatedHover : theme.surfaceElevated
|
|
QbzIcon {
|
|
anchors.centerIn: parent
|
|
width: 16
|
|
height: 16
|
|
name: "skip-back"
|
|
tintName: prevTa.containsMouse ? "textPrimary" : "secondary"
|
|
}
|
|
MouseArea {
|
|
id: prevTa
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onPressed: prevButton.forceActiveFocus()
|
|
onClicked: QbzPlayer.previous()
|
|
}
|
|
}
|
|
Rectangle {
|
|
id: nextButton
|
|
visible: testStep.playing
|
|
width: visible ? 34 : 0
|
|
height: 34
|
|
radius: theme.radiusSm
|
|
border.width: 1
|
|
border.color: activeFocus ? theme.accent : theme.borderSubtle
|
|
activeFocusOnTab: visible && enabled
|
|
Accessible.role: Accessible.Button
|
|
Accessible.name: QbzSession.tr("Next", QbzSession.trRev)
|
|
Accessible.onPressAction: QbzPlayer.next()
|
|
Keys.onPressed: function (event) {
|
|
if (!event.isAutoRepeat
|
|
&& (event.key === Qt.Key_Space
|
|
|| event.key === Qt.Key_Return
|
|
|| event.key === Qt.Key_Enter)) {
|
|
QbzPlayer.next()
|
|
event.accepted = true
|
|
}
|
|
}
|
|
color: nextTa.containsMouse ? theme.surfaceElevatedHover : theme.surfaceElevated
|
|
QbzIcon {
|
|
anchors.centerIn: parent
|
|
width: 16
|
|
height: 16
|
|
name: "skip-forward"
|
|
tintName: nextTa.containsMouse ? "textPrimary" : "secondary"
|
|
}
|
|
MouseArea {
|
|
id: nextTa
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onPressed: nextButton.forceActiveFocus()
|
|
onClicked: QbzPlayer.next()
|
|
}
|
|
}
|
|
|
|
// Verify with the user's current queue instead of the
|
|
// curated tracks. minWidth 0 — this is a modal footer
|
|
// button, not a Settings row's, and the 160px floor
|
|
// would push the transport buttons off the row.
|
|
SettingsButton { kioskHost: root.kioskHost;
|
|
visible: !testStep.playing
|
|
minWidth: 0
|
|
text: QbzSession.tr("Use my current queue", QbzSession.trRev)
|
|
onClicked: QbzDacWizard.verifyOwn()
|
|
}
|
|
}
|
|
|
|
// Live read-back: requested (honest) + DAC negotiated (N6).
|
|
Rectangle {
|
|
visible: testStep.playing || (root.doc.testRequestedLabel || "") !== ""
|
|
width: parent.width
|
|
height: visible ? readout.implicitHeight + 24 : 0
|
|
radius: theme.radiusSm
|
|
color: theme.surfaceElevated
|
|
border.width: 1
|
|
border.color: theme.borderSubtle
|
|
|
|
Column {
|
|
id: readout
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.margins: 12
|
|
spacing: 6
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: root.doc.testRequestedLabel || ""
|
|
color: theme.textPrimary
|
|
font.pixelSize: root.kioskHost ? (theme.fontBody) * 1.2 : (theme.fontBody)
|
|
font.weight: theme.weightMedium
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
Row {
|
|
width: parent.width
|
|
spacing: 8
|
|
Text {
|
|
visible: root.doc.testRateMatched === true
|
|
text: "✓"
|
|
color: theme.success
|
|
font.pixelSize: root.kioskHost ? (theme.fontBody) * 1.2 : (theme.fontBody)
|
|
font.weight: theme.weightBold
|
|
}
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: root.doc.testNegotiatedLabel || ""
|
|
color: root.doc.testRateMatched === true
|
|
? theme.success : theme.textSecondary
|
|
font.pixelSize: root.kioskHost ? (theme.fontBody) * 1.2 : (theme.fontBody)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Drives the read-back while the test plays. `running` is
|
|
// bound to the document, so leaving the step or stopping
|
|
// the test parks the timer — it never polls in the dark.
|
|
Timer {
|
|
interval: 1500
|
|
repeat: true
|
|
running: testStep.visible && testStep.playing
|
|
onTriggered: QbzDacWizard.pollTest()
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════ Step 5: done ══════════════════════
|
|
Column {
|
|
visible: root.step === 5
|
|
width: card.contentWidth
|
|
spacing: 14
|
|
|
|
Row {
|
|
width: parent.width
|
|
height: 26
|
|
spacing: 10
|
|
QbzIcon {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 24
|
|
height: 24
|
|
name: "circle-check-big"
|
|
// Same reasoning as the header wand.
|
|
tintName: "accent"
|
|
}
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: QbzSession.tr("All set", QbzSession.trRev)
|
|
color: theme.textPrimary
|
|
font.pixelSize: root.kioskHost ? (theme.fontHeading) * 1.2 : (theme.fontHeading)
|
|
font.weight: theme.weightSemibold
|
|
}
|
|
}
|
|
Text {
|
|
width: parent.width
|
|
text: QbzSession.tr("Your DAC is ready for bit-perfect playback. If you applied the config files, restart the audio services (or log out and back in) for them to take effect.", QbzSession.trRev)
|
|
color: theme.textSecondary
|
|
font.pixelSize: root.kioskHost ? (theme.fontBody) * 1.2 : (theme.fontBody)
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
Column {
|
|
visible: root.createdPaths.length > 0
|
|
width: parent.width
|
|
spacing: 6
|
|
Text {
|
|
text: QbzSession.tr("Config files you can create:", QbzSession.trRev)
|
|
color: theme.textSecondary
|
|
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
|
|
font.weight: theme.weightMedium
|
|
}
|
|
Repeater {
|
|
model: root.createdPaths
|
|
delegate: Text {
|
|
required property string modelData
|
|
width: card.contentWidth
|
|
text: modelData
|
|
color: theme.textMuted
|
|
font.pixelSize: root.kioskHost ? (theme.fontLegal) * 1.2 : (theme.fontLegal)
|
|
font.family: "monospace"
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The 14px gutter scrollbar, inset 4px from the right — the port's
|
|
// shared control, attached as a SIBLING of the Flickable.
|
|
QbzScrollBar {
|
|
target: bodyFlick
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 4
|
|
anchors.top: bodyFlick.top
|
|
anchors.bottom: bodyFlick.bottom
|
|
}
|
|
|
|
// ------------------------------ footer ----------------------------
|
|
Rectangle {
|
|
id: footerDiv
|
|
anchors.bottom: footer.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
height: 1
|
|
color: theme.borderSubtle
|
|
}
|
|
Item {
|
|
id: footer
|
|
anchors.bottom: parent.bottom
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
height: 70
|
|
|
|
// Back — hidden on the first step. minWidth 0 on both: these are
|
|
// modal footer buttons (the MyQBZ modals' convention), and the
|
|
// Settings row's 160px floor would make a three-button footer
|
|
// wider than the card.
|
|
SettingsButton { kioskHost: root.kioskHost;
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 24
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
visible: root.step > 0
|
|
minWidth: 0
|
|
text: QbzSession.tr("Back", QbzSession.trRev)
|
|
onClicked: root.step -= 1
|
|
}
|
|
SettingsButton { kioskHost: root.kioskHost;
|
|
id: footerClose
|
|
anchors.right: primaryBtn.left
|
|
anchors.rightMargin: 12
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
minWidth: 0
|
|
text: QbzSession.tr("Close", QbzSession.trRev)
|
|
onClicked: QbzDacWizard.close()
|
|
}
|
|
QbzPrimaryButton {
|
|
id: primaryBtn
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 24
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
btnHeight: 34
|
|
btnEnabled: root.primaryEnabled
|
|
label: root.step === 0 ? QbzSession.tr("Start", QbzSession.trRev)
|
|
: (root.step === 5 ? QbzSession.tr("Finish", QbzSession.trRev)
|
|
: QbzSession.tr("Next", QbzSession.trRev))
|
|
onClicked: {
|
|
if (root.step === 5) {
|
|
QbzDacWizard.close()
|
|
return
|
|
}
|
|
// Entering the DACs step kicks off enumeration; entering
|
|
// review generates the per-DAC config. Both are Rust-side
|
|
// and asynchronous — the step advances immediately and the
|
|
// body renders its "detecting…" / empty state until the
|
|
// document lands (reference :845-853).
|
|
if (root.step === 1)
|
|
QbzDacWizard.runDetect()
|
|
if (root.step === 2)
|
|
QbzDacWizard.genConfigs()
|
|
root.step += 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|