import QtQuick import QtQuick.Controls as Controls import QtQuick.Layouts import org.kde.kirigami as Kirigami Kirigami.ApplicationWindow { id: root title: "Bass Manage 7.1" width: 580 height: 520 minimumWidth: 480 minimumHeight: 420 pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar // Aggregated UI values (conf uses full map; UI links FL/FR & mains) property real fcFold: 0.707 property real dry: 1.0 property real lfeMains: 0.99 property real lfeDisc: 0.99 property real lfeDelay: 0.010 property bool suppressApply: false function valuesMap() { return { "fl_dry": dry, "fr_dry": dry, "fc_to_fl": fcFold, "fc_to_fr": fcFold, "lfe_fl": lfeMains, "lfe_fr": lfeMains, "lfe_fc": lfeMains, "lfe_lfe": lfeDisc, "lfe_rl": lfeMains, "lfe_rr": lfeMains, "lfe_sl": lfeMains, "lfe_sr": lfeMains, "lfe_delay": lfeDelay } } function applyFromMap(m) { suppressApply = true fcFold = (Number(m["fc_to_fl"]) + Number(m["fc_to_fr"])) / 2 dry = (Number(m["fl_dry"]) + Number(m["fr_dry"])) / 2 lfeMains = (Number(m["lfe_fl"]) + Number(m["lfe_fr"]) + Number(m["lfe_fc"]) + Number(m["lfe_rl"]) + Number(m["lfe_rr"]) + Number(m["lfe_sl"]) + Number(m["lfe_sr"])) / 7 lfeDisc = Number(m["lfe_lfe"]) lfeDelay = Number(m["lfe_delay"]) suppressApply = false } function scheduleApply() { if (suppressApply) return applyTimer.restart() } function linToDb(x) { if (x <= 1e-9) return -120 return 20 * Math.log(x) / Math.LN10 } function dbToLin(db) { if (db <= -120) return 0 return Math.pow(10, db / 20) } function iconUrl(name) { return "file://" + Backend.iconDir + "/" + name + ".svg" } component LucideIcon: Kirigami.Icon { property string name source: root.iconUrl(name) color: Kirigami.Theme.textColor isMask: true Layout.preferredWidth: Kirigami.Units.iconSizes.small Layout.preferredHeight: Kirigami.Units.iconSizes.small } Timer { id: applyTimer interval: 40 onTriggered: Backend.applyLive(root.valuesMap()) } Connections { target: Backend function onErrorOccurred(title, message) { errorDialog.title = title errorDialog.text = message errorDialog.open() } } Component.onCompleted: { const m = Backend.loadConf() if (Object.keys(m).length) applyFromMap(m) } pageStack.initialPage: Kirigami.ScrollablePage { id: page title: "Bass Manage 7.1" titleDelegate: Item { // Stretch across the header so actions sit on the right of the title Layout.fillWidth: true implicitHeight: row.implicitHeight width: parent ? parent.width : row.implicitWidth RowLayout { id: row anchors.fill: parent spacing: Kirigami.Units.smallSpacing LucideIcon { name: "audio-lines" Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium } Kirigami.Heading { text: "Bass Manage 7.1" level: 1 } Item { Layout.fillWidth: true } Controls.ToolButton { onClicked: { if (Backend.saveConf(root.valuesMap())) savedDialog.open() } contentItem: RowLayout { spacing: Kirigami.Units.smallSpacing LucideIcon { name: "save" } Controls.Label { text: "Save" } } Accessible.name: "Save" Controls.ToolTip.text: "Save to conf" Controls.ToolTip.visible: hovered Controls.ToolTip.delay: Kirigami.Units.toolTipDelay } Controls.ToolButton { onClicked: { const m = Backend.loadConf() if (Object.keys(m).length) root.applyFromMap(m) } contentItem: RowLayout { spacing: Kirigami.Units.smallSpacing LucideIcon { name: "refresh-cw" } Controls.Label { text: "Reload" } } Accessible.name: "Reload" Controls.ToolTip.text: "Reload from conf" Controls.ToolTip.visible: hovered Controls.ToolTip.delay: Kirigami.Units.toolTipDelay } } } ColumnLayout { spacing: Kirigami.Units.largeSpacing RowLayout { Layout.fillWidth: true LucideIcon { name: "audio-lines" } Controls.Label { text: Backend.status opacity: 0.75 Layout.fillWidth: true wrapMode: Text.WordWrap } } Kirigami.FormLayout { Layout.fillWidth: true wideMode: true Kirigami.Separator { Kirigami.FormData.isSection: true Kirigami.FormData.label: "Center → fronts (add)" } SliderRow { Kirigami.FormData.label: "FC into FL/FR" iconName: "align-center-horizontal" from: 0; to: 1.5; stepSize: 0.01 kind: "gain" value: root.fcFold onValueEdited: (v) => { root.fcFold = v; root.scheduleApply() } } SliderRow { Kirigami.FormData.label: "FL/FR dry" iconName: "volume-2" from: 0; to: 1.5; stepSize: 0.01 kind: "gain" value: root.dry onValueEdited: (v) => { root.dry = v; root.scheduleApply() } } Kirigami.Separator { Kirigami.FormData.isSection: true Kirigami.FormData.label: "LFE mix" } SliderRow { Kirigami.FormData.label: "Mains → LFE" iconName: "sliders-horizontal" from: 0; to: 1.5; stepSize: 0.01 kind: "gain" value: root.lfeMains onValueEdited: (v) => { root.lfeMains = v; root.scheduleApply() } } SliderRow { Kirigami.FormData.label: "Discrete LFE" iconName: "speaker" from: 0; to: 1.5; stepSize: 0.01 kind: "gain" value: root.lfeDisc onValueEdited: (v) => { root.lfeDisc = v; root.scheduleApply() } } SliderRow { Kirigami.FormData.label: "LFE delay" iconName: "timer" from: 0; to: 0.05; stepSize: 0.001 kind: "delay" value: root.lfeDelay onValueEdited: (v) => { root.lfeDelay = v; root.scheduleApply() } } } Controls.Label { Layout.fillWidth: true wrapMode: Text.WordWrap opacity: 0.7 text: "Type a value and press Enter (gains: 0.707 or -3dB; delay: ms). Reload resets from conf. Save writes conf." } } } component SliderRow: RowLayout { id: row property real from property real to property real stepSize property real value property string kind: "gain" property string iconName signal valueEdited(real v) Layout.fillWidth: true spacing: Kirigami.Units.smallSpacing Kirigami.Icon { source: root.iconUrl(row.iconName) color: Kirigami.Theme.textColor isMask: true Layout.preferredWidth: Kirigami.Units.iconSizes.small Layout.preferredHeight: Kirigami.Units.iconSizes.small } Controls.Slider { id: slider Layout.fillWidth: true from: row.from to: row.to stepSize: row.stepSize value: row.value onMoved: row.valueEdited(value) } Controls.TextField { id: field Layout.preferredWidth: Kirigami.Units.gridUnit * 4 horizontalAlignment: Text.AlignRight function formatValue(v) { return row.kind === "delay" ? Number(v * 1000).toFixed(2).replace(/\.?0+$/, "") : Number(v).toPrecision(4) } Component.onCompleted: text = formatValue(row.value) Connections { target: row function onValueChanged() { if (!field.activeFocus) field.text = field.formatValue(row.value) } } function commit() { let s = text.trim().toLowerCase().replace(",", ".") let parsed = NaN if (row.kind === "delay") { if (s.endsWith("ms")) parsed = parseFloat(s.slice(0, -2)) / 1000 else if (s.endsWith("s")) parsed = parseFloat(s.slice(0, -1)) else parsed = parseFloat(s) / 1000 } else { if (s.endsWith("db")) parsed = root.dbToLin(parseFloat(s.slice(0, -2))) else if (s.startsWith("+") || (s.startsWith("-") && s.indexOf("e") < 0)) parsed = root.dbToLin(parseFloat(s)) else parsed = parseFloat(s) } if (isNaN(parsed)) { text = formatValue(row.value) return } parsed = Math.min(row.to, Math.max(row.from, parsed)) row.valueEdited(parsed) } onEditingFinished: commit() Keys.onReturnPressed: commit() Keys.onEnterPressed: commit() } Controls.Label { Layout.preferredWidth: Kirigami.Units.gridUnit * 5 text: row.kind === "delay" ? "ms" : "(" + (root.linToDb(row.value) >= 0 ? "+" : "") + root.linToDb(row.value).toFixed(1) + " dB)" opacity: 0.7 } } Controls.Dialog { id: errorDialog property alias text: errorLabel.text modal: true standardButtons: Controls.Dialog.Ok anchors.centerIn: parent Controls.Label { id: errorLabel wrapMode: Text.WordWrap width: errorDialog.availableWidth } } Controls.Dialog { id: savedDialog title: "Saved" modal: true standardButtons: Controls.Dialog.Ok anchors.centerIn: parent Controls.Label { wrapMode: Text.WordWrap width: savedDialog.availableWidth text: "Wrote " + Backend.confPath + "\nLive params applied." } } }