From d5d020e1309fc5352936e9c48d863ee30fcc8b14 Mon Sep 17 00:00:00 2001 From: dranik Date: Wed, 17 Jun 2026 18:08:02 +0300 Subject: [PATCH] input validation, numeric limits and live Save on settings pages --- .../PageProtocolXraySecuritySettings.qml | 30 +++- .../qml/Pages2/PageProtocolXraySettings.qml | 24 ++- .../PageProtocolXrayTransportSettings.qml | 147 ++++++++++++++---- .../PageProtocolXrayXPaddingBytesSettings.qml | 9 +- .../PageProtocolXrayXPaddingSettings.qml | 20 ++- .../Pages2/PageProtocolXrayXmuxSettings.qml | 54 +++++-- 6 files changed, 230 insertions(+), 54 deletions(-) diff --git a/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml index fbe161a14..c91e13992 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml @@ -15,6 +15,8 @@ import "../Components" PageType { id: root + property bool editDirty: false + BackButtonType { id: backButton anchors.top: parent.top @@ -90,6 +92,7 @@ PageType { DropDownType { id: tlsAlpnDropDown + fitContent: true Layout.fillWidth: true Layout.topMargin: 16 Layout.leftMargin: 16 @@ -133,6 +136,7 @@ PageType { DropDownType { id: tlsFingerprintDropDown + fitContent: true Layout.fillWidth: true Layout.topMargin: 8 Layout.leftMargin: 16 @@ -175,14 +179,21 @@ PageType { } TextFieldWithHeaderType { + id: sniFieldTls Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("Server Name (SNI)") textField.text: sni + textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9.*_-]*$/ } + textField.onTextEdited: root.editDirty = (textField.text !== sni) textField.onEditingFinished: { - if (textField.text !== sni) sni = textField.text + var v = textField.text.trim() + if (v !== sni) sni = v + else if (textField.text !== v) textField.text = v + sniFieldTls.errorText = XrayConfigModel.isValidSni(v) ? "" : qsTr("Enter a valid IP address or domain name") + root.editDirty = false } } } @@ -195,6 +206,7 @@ PageType { DropDownType { id: realityFingerprintDropDown + fitContent: true Layout.fillWidth: true Layout.topMargin: 16 Layout.leftMargin: 16 @@ -237,14 +249,21 @@ PageType { } TextFieldWithHeaderType { + id: sniFieldReality Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("Server Name (SNI)") textField.text: sni + textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9.*_-]*$/ } + textField.onTextEdited: root.editDirty = (textField.text !== sni) textField.onEditingFinished: { - if (textField.text !== sni) sni = textField.text + var v = textField.text.trim() + if (v !== sni) sni = v + else if (textField.text !== v) textField.text = v + sniFieldReality.errorText = XrayConfigModel.isValidSni(v) ? "" : qsTr("Enter a valid IP address or domain name") + root.editDirty = false } } } @@ -265,10 +284,15 @@ PageType { anchors.rightMargin: 16 anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - visible: listView.enabled && XrayConfigModel.hasUnsavedChanges + visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty) enabled: visible text: qsTr("Save") clickedFunc: function () { + var errs = XrayConfigModel.validationErrors() + if (errs.length > 0) { + PageController.showErrorMessage(errs.join("\n")) + return + } var headerText = qsTr("Save settings?") var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") var yesButtonText = qsTr("Continue") diff --git a/client/ui/qml/Pages2/PageProtocolXraySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySettings.qml index 66c68e658..5b23b7d6c 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySettings.qml @@ -109,6 +109,7 @@ PageType { Layout.rightMargin: 16 enabled: listView.enabled headerText: qsTr("Port") + subtitleText: qsTr("1–65535") Binding { target: textFieldWithHeaderType.textField @@ -119,8 +120,8 @@ PageType { } textField.maximumLength: 5 - textField.validator: IntValidator { - bottom: 1; top: 65535 + textField.validator: RegularExpressionValidator { + regularExpression: /^(|\d{1,4}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$/ } textField.onActiveFocusChanged: { if (textField.activeFocus && textField.text === "" && port !== "") { @@ -131,9 +132,19 @@ PageType { root.portDirty = (textField.text !== port) } textField.onEditingFinished: { - if (textField.text !== port) { - port = textField.text + var v = textFieldWithHeaderType.textField.text + if (v !== "") { + var n = parseInt(v, 10) + if (isNaN(n) || n < 1) + n = 1 + if (n > 65535) + n = 65535 + v = String(n) + if (textFieldWithHeaderType.textField.text !== v) + textFieldWithHeaderType.textField.text = v } + if (v !== port) + port = v root.portDirty = false } checkEmptyText: true @@ -198,6 +209,11 @@ PageType { text: qsTr("Save") onClicked: function() { forceActiveFocus() + var errs = XrayConfigModel.validationErrors() + if (errs.length > 0) { + PageController.showErrorMessage(errs.join("\n")) + return + } var headerText = qsTr("Save settings?") var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") var yesButtonText = qsTr("Continue") diff --git a/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml index 8a531c54a..35c82e80b 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml @@ -15,6 +15,21 @@ import "../Components" PageType { id: root + property bool editDirty: false + + function clampInt(text, lo, hi) { + if (text === "") + return "" + var n = parseInt(text, 10) + if (isNaN(n)) + return "" + if (n < lo) + n = lo + if (n > hi) + n = hi + return String(n) + } + BackButtonType { id: backButton anchors.top: parent.top @@ -108,10 +123,16 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("TTI") - subtitleText: qsTr("Default: %1 ms", "mKCP TTI").arg(XrayConfigModel.mkcpDefaultTti()) + subtitleText: qsTr("Range 10–100, default %1 ms", "mKCP TTI").arg(XrayConfigModel.mkcpDefaultTti()) textField.text: mkcpTti + textField.maximumLength: 3 + textField.validator: RegularExpressionValidator { regularExpression: /^(|\d{1,2}|100)$/ } + textField.onTextEdited: root.editDirty = (textField.text !== mkcpTti) textField.onEditingFinished: { - if (textField.text !== mkcpTti) mkcpTti = textField.text + var v = root.clampInt(textField.text, 10, 100) + if (v !== mkcpTti) mkcpTti = v + else if (textField.text !== v) textField.text = v + root.editDirty = false } } @@ -121,10 +142,16 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("uplinkCapacity") - subtitleText: qsTr("Default: %1 Mbit/s", "mKCP uplink").arg(XrayConfigModel.mkcpDefaultUplinkCapacity()) + subtitleText: qsTr("≥ 0, default %1 MB/s", "mKCP uplink").arg(XrayConfigModel.mkcpDefaultUplinkCapacity()) textField.text: mkcpUplinkCapacity + textField.maximumLength: 10 + textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ } + textField.onTextEdited: root.editDirty = (textField.text !== mkcpUplinkCapacity) textField.onEditingFinished: { - if (textField.text !== mkcpUplinkCapacity) mkcpUplinkCapacity = textField.text + var v = root.clampInt(textField.text, 0, 2147483647) + if (v !== mkcpUplinkCapacity) mkcpUplinkCapacity = v + else if (textField.text !== v) textField.text = v + root.editDirty = false } } @@ -134,10 +161,16 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("downlinkCapacity") - subtitleText: qsTr("Default: %1 Mbit/s", "mKCP downlink").arg(XrayConfigModel.mkcpDefaultDownlinkCapacity()) + subtitleText: qsTr("≥ 0, default %1 MB/s", "mKCP downlink").arg(XrayConfigModel.mkcpDefaultDownlinkCapacity()) textField.text: mkcpDownlinkCapacity + textField.maximumLength: 10 + textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ } + textField.onTextEdited: root.editDirty = (textField.text !== mkcpDownlinkCapacity) textField.onEditingFinished: { - if (textField.text !== mkcpDownlinkCapacity) mkcpDownlinkCapacity = textField.text + var v = root.clampInt(textField.text, 0, 2147483647) + if (v !== mkcpDownlinkCapacity) mkcpDownlinkCapacity = v + else if (textField.text !== v) textField.text = v + root.editDirty = false } } @@ -147,10 +180,16 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("readBufferSize") - subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultReadBufferSize()) + subtitleText: qsTr("≥ 1, default %1 MB").arg(XrayConfigModel.mkcpDefaultReadBufferSize()) textField.text: mkcpReadBufferSize + textField.maximumLength: 10 + textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ } + textField.onTextEdited: root.editDirty = (textField.text !== mkcpReadBufferSize) textField.onEditingFinished: { - if (textField.text !== mkcpReadBufferSize) mkcpReadBufferSize = textField.text + var v = root.clampInt(textField.text, 1, 2147483647) + if (v !== mkcpReadBufferSize) mkcpReadBufferSize = v + else if (textField.text !== v) textField.text = v + root.editDirty = false } } @@ -160,10 +199,16 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("writeBufferSize") - subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultWriteBufferSize()) + subtitleText: qsTr("≥ 1, default %1 MB").arg(XrayConfigModel.mkcpDefaultWriteBufferSize()) textField.text: mkcpWriteBufferSize + textField.maximumLength: 10 + textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ } + textField.onTextEdited: root.editDirty = (textField.text !== mkcpWriteBufferSize) textField.onEditingFinished: { - if (textField.text !== mkcpWriteBufferSize) mkcpWriteBufferSize = textField.text + var v = root.clampInt(textField.text, 1, 2147483647) + if (v !== mkcpWriteBufferSize) mkcpWriteBufferSize = v + else if (textField.text !== v) textField.text = v + root.editDirty = false } } @@ -187,6 +232,7 @@ PageType { DropDownType { id: modeDropDown + fitContent: true Layout.fillWidth: true Layout.topMargin: 16 Layout.leftMargin: 16 @@ -239,31 +285,46 @@ PageType { } TextFieldWithHeaderType { + id: hostField Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("Host") textField.text: xhttpHost + textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9._:,-]*$/ } + textField.onTextEdited: root.editDirty = (textField.text !== xhttpHost) textField.onEditingFinished: { - if (textField.text !== xhttpHost) xhttpHost = textField.text + var v = textField.text.trim() + if (v !== xhttpHost) xhttpHost = v + else if (textField.text !== v) textField.text = v + hostField.errorText = XrayConfigModel.isValidHost(v) ? "" : qsTr("Enter a valid IP address or domain name") + root.editDirty = false } } TextFieldWithHeaderType { + id: pathField Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("Path") textField.text: xhttpPath + textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9\-._~:\/?#\[\]@!$&'()*+,;=%]*$/ } + textField.onTextEdited: root.editDirty = (textField.text !== xhttpPath) textField.onEditingFinished: { - if (textField.text !== xhttpPath) xhttpPath = textField.text + var v = textField.text.trim() + if (v !== xhttpPath) xhttpPath = v + else if (textField.text !== v) textField.text = v + pathField.errorText = XrayConfigModel.isValidPath(v) ? "" : qsTr("Path must start with \"/\"") + root.editDirty = false } } DropDownType { id: headersDropDown + fitContent: true Layout.fillWidth: true Layout.topMargin: 8 Layout.leftMargin: 16 @@ -307,6 +368,7 @@ PageType { DropDownType { id: uplinkMethodDropDown + fitContent: true Layout.fillWidth: true Layout.topMargin: 8 Layout.leftMargin: 16 @@ -386,6 +448,7 @@ PageType { DropDownType { id: sessionPlacementDropDown + fitContent: true Layout.fillWidth: true Layout.topMargin: 8 Layout.leftMargin: 16 @@ -429,6 +492,7 @@ PageType { DropDownType { id: sessionKeyDropDown + fitContent: true Layout.fillWidth: true Layout.topMargin: 8 Layout.leftMargin: 16 @@ -472,6 +536,7 @@ PageType { DropDownType { id: seqPlacementDropDown + fitContent: true Layout.fillWidth: true Layout.topMargin: 8 Layout.leftMargin: 16 @@ -520,13 +585,19 @@ PageType { Layout.topMargin: 8 headerText: qsTr("SeqKey") textField.text: xhttpSeqKey + textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ } + textField.onTextEdited: root.editDirty = (textField.text !== xhttpSeqKey) textField.onEditingFinished: { - if (textField.text !== xhttpSeqKey) xhttpSeqKey = textField.text + var v = textField.text.trim() + if (v !== xhttpSeqKey) xhttpSeqKey = v + else if (textField.text !== v) textField.text = v + root.editDirty = false } } DropDownType { id: uplinkDataPlacementDropDown + fitContent: true Layout.fillWidth: true Layout.topMargin: 8 Layout.leftMargin: 16 @@ -575,8 +646,13 @@ PageType { Layout.topMargin: 8 headerText: qsTr("UplinkDataKey") textField.text: xhttpUplinkDataKey + textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ } + textField.onTextEdited: root.editDirty = (textField.text !== xhttpUplinkDataKey) textField.onEditingFinished: { - if (textField.text !== xhttpUplinkDataKey) xhttpUplinkDataKey = textField.text + var v = textField.text.trim() + if (v !== xhttpUplinkDataKey) xhttpUplinkDataKey = v + else if (textField.text !== v) textField.text = v + root.editDirty = false } } @@ -597,12 +673,16 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("UplinkChunkSize") + subtitleText: qsTr("≥ 0 (0 = off)") textField.text: xhttpUplinkChunkSize - textField.validator: IntValidator { - bottom: 0 - } + textField.maximumLength: 10 + textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ } + textField.onTextEdited: root.editDirty = (textField.text !== xhttpUplinkChunkSize) textField.onEditingFinished: { - if (textField.text !== xhttpUplinkChunkSize) xhttpUplinkChunkSize = textField.text + var v = root.clampInt(textField.text, 0, 2147483647) + if (v !== xhttpUplinkChunkSize) xhttpUplinkChunkSize = v + else if (textField.text !== v) textField.text = v + root.editDirty = false } } @@ -612,9 +692,16 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 8 headerText: qsTr("scMaxBufferedPosts") + subtitleText: qsTr("≥ 0") textField.text: xhttpScMaxBufferedPosts + textField.maximumLength: 10 + textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ } + textField.onTextEdited: root.editDirty = (textField.text !== xhttpScMaxBufferedPosts) textField.onEditingFinished: { - if (textField.text !== xhttpScMaxBufferedPosts) xhttpScMaxBufferedPosts = textField.text + var v = root.clampInt(textField.text, 0, 2147483647) + if (v !== xhttpScMaxBufferedPosts) xhttpScMaxBufferedPosts = v + else if (textField.text !== v) textField.text = v + root.editDirty = false } } @@ -633,8 +720,9 @@ PageType { Layout.rightMargin: 16 minValue: xhttpScMaxEachPostBytesMin maxValue: xhttpScMaxEachPostBytesMax - onMinChanged: xhttpScMaxEachPostBytesMin = val - onMaxChanged: xhttpScMaxEachPostBytesMax = val + onMinChanged: function(val) { xhttpScMaxEachPostBytesMin = val; root.editDirty = false } + onMaxChanged: function(val) { xhttpScMaxEachPostBytesMax = val; root.editDirty = false } + onEdited: root.editDirty = true } CaptionTextType { @@ -652,8 +740,9 @@ PageType { Layout.rightMargin: 16 minValue: xhttpScStreamUpServerSecsMin maxValue: xhttpScStreamUpServerSecsMax - onMinChanged: xhttpScStreamUpServerSecsMin = val - onMaxChanged: xhttpScStreamUpServerSecsMax = val + onMinChanged: function(val) { xhttpScStreamUpServerSecsMin = val; root.editDirty = false } + onMaxChanged: function(val) { xhttpScStreamUpServerSecsMax = val; root.editDirty = false } + onEdited: root.editDirty = true } CaptionTextType { @@ -671,8 +760,9 @@ PageType { Layout.rightMargin: 16 minValue: xhttpScMinPostsIntervalMsMin maxValue: xhttpScMinPostsIntervalMsMax - onMinChanged: xhttpScMinPostsIntervalMsMin = val - onMaxChanged: xhttpScMinPostsIntervalMsMax = val + onMinChanged: function(val) { xhttpScMinPostsIntervalMsMin = val; root.editDirty = false } + onMaxChanged: function(val) { xhttpScMinPostsIntervalMsMax = val; root.editDirty = false } + onEdited: root.editDirty = true } // ── Padding and multiplexing ────────────────────────── @@ -728,10 +818,15 @@ PageType { anchors.rightMargin: 16 anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - visible: listView.enabled && XrayConfigModel.hasUnsavedChanges + visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty) enabled: visible text: qsTr("Save") clickedFunc: function () { + var errs = XrayConfigModel.validationErrors() + if (errs.length > 0) { + PageController.showErrorMessage(errs.join("\n")) + return + } var headerText = qsTr("Save settings?") var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") var yesButtonText = qsTr("Continue") diff --git a/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml index 11f022927..63227311f 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayXPaddingBytesSettings.qml @@ -15,6 +15,8 @@ import "../Components" PageType { id: root + property bool editDirty: false + BackButtonType { id: backButton anchors.top: parent.top @@ -61,8 +63,9 @@ PageType { Layout.rightMargin: 16 minValue: xPaddingBytesMin maxValue: xPaddingBytesMax - onMinChanged: xPaddingBytesMin = val - onMaxChanged: xPaddingBytesMax = val + onMinChanged: function(val) { xPaddingBytesMin = val; root.editDirty = false } + onMaxChanged: function(val) { xPaddingBytesMax = val; root.editDirty = false } + onEdited: root.editDirty = true } Item { @@ -81,7 +84,7 @@ PageType { anchors.rightMargin: 16 anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - visible: listView.enabled && XrayConfigModel.hasUnsavedChanges + visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty) enabled: visible text: qsTr("Save") clickedFunc: function () { diff --git a/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml index 721d673c5..bc3d0efae 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml @@ -15,6 +15,8 @@ import "../Components" PageType { id: root + property bool editDirty: false + BackButtonType { id: backButton anchors.top: parent.top @@ -78,8 +80,13 @@ PageType { Layout.topMargin: 16 headerText: qsTr("xPaddingKey") textField.text: xPaddingKey + textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ } + textField.onTextEdited: root.editDirty = (textField.text !== xPaddingKey) textField.onEditingFinished: { - if (textField.text !== xPaddingKey) xPaddingKey = textField.text + var v = textField.text.trim() + if (v !== xPaddingKey) xPaddingKey = v + else if (textField.text !== v) textField.text = v + root.editDirty = false } } @@ -90,13 +97,19 @@ PageType { Layout.topMargin: 8 headerText: qsTr("xPaddingHeader") textField.text: xPaddingHeader + textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ } + textField.onTextEdited: root.editDirty = (textField.text !== xPaddingHeader) textField.onEditingFinished: { - if (textField.text !== xPaddingHeader) xPaddingHeader = textField.text + var v = textField.text.trim() + if (v !== xPaddingHeader) xPaddingHeader = v + else if (textField.text !== v) textField.text = v + root.editDirty = false } } DropDownType { id: placementDropDown + fitContent: true Layout.fillWidth: true Layout.topMargin: 8 Layout.leftMargin: 16 @@ -140,6 +153,7 @@ PageType { DropDownType { id: methodDropDown + fitContent: true Layout.fillWidth: true Layout.topMargin: 8 Layout.leftMargin: 16 @@ -197,7 +211,7 @@ PageType { anchors.rightMargin: 16 anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - visible: listView.enabled && XrayConfigModel.hasUnsavedChanges + visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty) enabled: visible text: qsTr("Save") clickedFunc: function () { diff --git a/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml index ddd73990e..b95f3c0a8 100644 --- a/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml @@ -15,6 +15,21 @@ import "../Components" PageType { id: root + property bool editDirty: false + + function clampSigned(text) { + if (text === "" || text === "-") + return "" + var n = parseInt(text, 10) + if (isNaN(n)) + return "" + if (n > 2147483647) + n = 2147483647 + if (n < -2147483648) + n = -2147483648 + return String(n) + } + BackButtonType { id: backButton anchors.top: parent.top @@ -78,8 +93,9 @@ PageType { Layout.rightMargin: 16 minValue: xmuxMaxConcurrencyMin maxValue: xmuxMaxConcurrencyMax - onMinChanged: xmuxMaxConcurrencyMin = val - onMaxChanged: xmuxMaxConcurrencyMax = val + onMinChanged: function(val) { xmuxMaxConcurrencyMin = val; root.editDirty = false } + onMaxChanged: function(val) { xmuxMaxConcurrencyMax = val; root.editDirty = false } + onEdited: root.editDirty = true } // maxConnections @@ -98,8 +114,9 @@ PageType { Layout.rightMargin: 16 minValue: xmuxMaxConnectionsMin maxValue: xmuxMaxConnectionsMax - onMinChanged: xmuxMaxConnectionsMin = val - onMaxChanged: xmuxMaxConnectionsMax = val + onMinChanged: function(val) { xmuxMaxConnectionsMin = val; root.editDirty = false } + onMaxChanged: function(val) { xmuxMaxConnectionsMax = val; root.editDirty = false } + onEdited: root.editDirty = true } // cMaxReuseTimes @@ -118,8 +135,9 @@ PageType { Layout.rightMargin: 16 minValue: xmuxCMaxReuseTimesMin maxValue: xmuxCMaxReuseTimesMax - onMinChanged: xmuxCMaxReuseTimesMin = val - onMaxChanged: xmuxCMaxReuseTimesMax = val + onMinChanged: function(val) { xmuxCMaxReuseTimesMin = val; root.editDirty = false } + onMaxChanged: function(val) { xmuxCMaxReuseTimesMax = val; root.editDirty = false } + onEdited: root.editDirty = true } // hMaxRequestTimes @@ -138,8 +156,9 @@ PageType { Layout.rightMargin: 16 minValue: xmuxHMaxRequestTimesMin maxValue: xmuxHMaxRequestTimesMax - onMinChanged: xmuxHMaxRequestTimesMin = val - onMaxChanged: xmuxHMaxRequestTimesMax = val + onMinChanged: function(val) { xmuxHMaxRequestTimesMin = val; root.editDirty = false } + onMaxChanged: function(val) { xmuxHMaxRequestTimesMax = val; root.editDirty = false } + onEdited: root.editDirty = true } // hMaxReusableSecs @@ -158,8 +177,9 @@ PageType { Layout.rightMargin: 16 minValue: xmuxHMaxReusableSecsMin maxValue: xmuxHMaxReusableSecsMax - onMinChanged: xmuxHMaxReusableSecsMin = val - onMaxChanged: xmuxHMaxReusableSecsMax = val + onMinChanged: function(val) { xmuxHMaxReusableSecsMin = val; root.editDirty = false } + onMaxChanged: function(val) { xmuxHMaxReusableSecsMax = val; root.editDirty = false } + onEdited: root.editDirty = true } TextFieldWithHeaderType { @@ -168,12 +188,16 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 16 headerText: qsTr("hKeepAlivePeriod") + subtitleText: qsTr("Integer, may be negative") textField.text: xmuxHKeepAlivePeriod - textField.validator: IntValidator { - bottom: 0 - } + textField.maximumLength: 11 + textField.validator: RegularExpressionValidator { regularExpression: /^-?\d*$/ } + textField.onTextEdited: root.editDirty = (textField.text !== xmuxHKeepAlivePeriod) textField.onEditingFinished: { - if (textField.text !== xmuxHKeepAlivePeriod) xmuxHKeepAlivePeriod = textField.text + var v = root.clampSigned(textField.text) + if (v !== xmuxHKeepAlivePeriod) xmuxHKeepAlivePeriod = v + else if (textField.text !== v) textField.text = v + root.editDirty = false } } } @@ -194,7 +218,7 @@ PageType { anchors.rightMargin: 16 anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin - visible: listView.enabled && XrayConfigModel.hasUnsavedChanges + visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty) enabled: visible text: qsTr("Save") clickedFunc: function () {