input validation, numeric limits and live Save on settings pages

This commit is contained in:
dranik
2026-06-17 18:08:02 +03:00
parent cb09713007
commit d5d020e130
6 changed files with 230 additions and 54 deletions
@@ -15,6 +15,8 @@ import "../Components"
PageType { PageType {
id: root id: root
property bool editDirty: false
BackButtonType { BackButtonType {
id: backButton id: backButton
anchors.top: parent.top anchors.top: parent.top
@@ -90,6 +92,7 @@ PageType {
DropDownType { DropDownType {
id: tlsAlpnDropDown id: tlsAlpnDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -133,6 +136,7 @@ PageType {
DropDownType { DropDownType {
id: tlsFingerprintDropDown id: tlsFingerprintDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -175,14 +179,21 @@ PageType {
} }
TextFieldWithHeaderType { TextFieldWithHeaderType {
id: sniFieldTls
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("Server Name (SNI)") headerText: qsTr("Server Name (SNI)")
textField.text: sni textField.text: sni
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9.*_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== sni)
textField.onEditingFinished: { 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 { DropDownType {
id: realityFingerprintDropDown id: realityFingerprintDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -237,14 +249,21 @@ PageType {
} }
TextFieldWithHeaderType { TextFieldWithHeaderType {
id: sniFieldReality
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("Server Name (SNI)") headerText: qsTr("Server Name (SNI)")
textField.text: sni textField.text: sni
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9.*_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== sni)
textField.onEditingFinished: { 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.rightMargin: 16
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
enabled: visible enabled: visible
text: qsTr("Save") text: qsTr("Save")
clickedFunc: function () { clickedFunc: function () {
var errs = XrayConfigModel.validationErrors()
if (errs.length > 0) {
PageController.showErrorMessage(errs.join("\n"))
return
}
var headerText = qsTr("Save settings?") 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 descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
var yesButtonText = qsTr("Continue") var yesButtonText = qsTr("Continue")
@@ -109,6 +109,7 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
enabled: listView.enabled enabled: listView.enabled
headerText: qsTr("Port") headerText: qsTr("Port")
subtitleText: qsTr("165535")
Binding { Binding {
target: textFieldWithHeaderType.textField target: textFieldWithHeaderType.textField
@@ -119,8 +120,8 @@ PageType {
} }
textField.maximumLength: 5 textField.maximumLength: 5
textField.validator: IntValidator { textField.validator: RegularExpressionValidator {
bottom: 1; top: 65535 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: { textField.onActiveFocusChanged: {
if (textField.activeFocus && textField.text === "" && port !== "") { if (textField.activeFocus && textField.text === "" && port !== "") {
@@ -131,9 +132,19 @@ PageType {
root.portDirty = (textField.text !== port) root.portDirty = (textField.text !== port)
} }
textField.onEditingFinished: { textField.onEditingFinished: {
if (textField.text !== port) { var v = textFieldWithHeaderType.textField.text
port = 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 root.portDirty = false
} }
checkEmptyText: true checkEmptyText: true
@@ -198,6 +209,11 @@ PageType {
text: qsTr("Save") text: qsTr("Save")
onClicked: function() { onClicked: function() {
forceActiveFocus() forceActiveFocus()
var errs = XrayConfigModel.validationErrors()
if (errs.length > 0) {
PageController.showErrorMessage(errs.join("\n"))
return
}
var headerText = qsTr("Save settings?") 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 descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
var yesButtonText = qsTr("Continue") var yesButtonText = qsTr("Continue")
@@ -15,6 +15,21 @@ import "../Components"
PageType { PageType {
id: root 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 { BackButtonType {
id: backButton id: backButton
anchors.top: parent.top anchors.top: parent.top
@@ -108,10 +123,16 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("TTI") headerText: qsTr("TTI")
subtitleText: qsTr("Default: %1 ms", "mKCP TTI").arg(XrayConfigModel.mkcpDefaultTti()) subtitleText: qsTr("Range 10100, default %1 ms", "mKCP TTI").arg(XrayConfigModel.mkcpDefaultTti())
textField.text: mkcpTti textField.text: mkcpTti
textField.maximumLength: 3
textField.validator: RegularExpressionValidator { regularExpression: /^(|\d{1,2}|100)$/ }
textField.onTextEdited: root.editDirty = (textField.text !== mkcpTti)
textField.onEditingFinished: { 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.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("uplinkCapacity") 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.text: mkcpUplinkCapacity
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== mkcpUplinkCapacity)
textField.onEditingFinished: { 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.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("downlinkCapacity") 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.text: mkcpDownlinkCapacity
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== mkcpDownlinkCapacity)
textField.onEditingFinished: { 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.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("readBufferSize") headerText: qsTr("readBufferSize")
subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultReadBufferSize()) subtitleText: qsTr("≥ 1, default %1 MB").arg(XrayConfigModel.mkcpDefaultReadBufferSize())
textField.text: mkcpReadBufferSize textField.text: mkcpReadBufferSize
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== mkcpReadBufferSize)
textField.onEditingFinished: { 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.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("writeBufferSize") headerText: qsTr("writeBufferSize")
subtitleText: qsTr("Default: %1 MiB").arg(XrayConfigModel.mkcpDefaultWriteBufferSize()) subtitleText: qsTr("≥ 1, default %1 MB").arg(XrayConfigModel.mkcpDefaultWriteBufferSize())
textField.text: mkcpWriteBufferSize textField.text: mkcpWriteBufferSize
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== mkcpWriteBufferSize)
textField.onEditingFinished: { 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 { DropDownType {
id: modeDropDown id: modeDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 16 Layout.topMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -239,31 +285,46 @@ PageType {
} }
TextFieldWithHeaderType { TextFieldWithHeaderType {
id: hostField
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("Host") headerText: qsTr("Host")
textField.text: xhttpHost textField.text: xhttpHost
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9._:,-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xhttpHost)
textField.onEditingFinished: { 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 { TextFieldWithHeaderType {
id: pathField
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("Path") headerText: qsTr("Path")
textField.text: xhttpPath textField.text: xhttpPath
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9\-._~:\/?#\[\]@!$&'()*+,;=%]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xhttpPath)
textField.onEditingFinished: { 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 { DropDownType {
id: headersDropDown id: headersDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -307,6 +368,7 @@ PageType {
DropDownType { DropDownType {
id: uplinkMethodDropDown id: uplinkMethodDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -386,6 +448,7 @@ PageType {
DropDownType { DropDownType {
id: sessionPlacementDropDown id: sessionPlacementDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -429,6 +492,7 @@ PageType {
DropDownType { DropDownType {
id: sessionKeyDropDown id: sessionKeyDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -472,6 +536,7 @@ PageType {
DropDownType { DropDownType {
id: seqPlacementDropDown id: seqPlacementDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -520,13 +585,19 @@ PageType {
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("SeqKey") headerText: qsTr("SeqKey")
textField.text: xhttpSeqKey textField.text: xhttpSeqKey
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xhttpSeqKey)
textField.onEditingFinished: { 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 { DropDownType {
id: uplinkDataPlacementDropDown id: uplinkDataPlacementDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -575,8 +646,13 @@ PageType {
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("UplinkDataKey") headerText: qsTr("UplinkDataKey")
textField.text: xhttpUplinkDataKey textField.text: xhttpUplinkDataKey
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xhttpUplinkDataKey)
textField.onEditingFinished: { 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.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("UplinkChunkSize") headerText: qsTr("UplinkChunkSize")
subtitleText: qsTr("≥ 0 (0 = off)")
textField.text: xhttpUplinkChunkSize textField.text: xhttpUplinkChunkSize
textField.validator: IntValidator { textField.maximumLength: 10
bottom: 0 textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
} textField.onTextEdited: root.editDirty = (textField.text !== xhttpUplinkChunkSize)
textField.onEditingFinished: { 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.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
headerText: qsTr("scMaxBufferedPosts") headerText: qsTr("scMaxBufferedPosts")
subtitleText: qsTr("≥ 0")
textField.text: xhttpScMaxBufferedPosts textField.text: xhttpScMaxBufferedPosts
textField.maximumLength: 10
textField.validator: RegularExpressionValidator { regularExpression: /^\d*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xhttpScMaxBufferedPosts)
textField.onEditingFinished: { 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 Layout.rightMargin: 16
minValue: xhttpScMaxEachPostBytesMin minValue: xhttpScMaxEachPostBytesMin
maxValue: xhttpScMaxEachPostBytesMax maxValue: xhttpScMaxEachPostBytesMax
onMinChanged: xhttpScMaxEachPostBytesMin = val onMinChanged: function(val) { xhttpScMaxEachPostBytesMin = val; root.editDirty = false }
onMaxChanged: xhttpScMaxEachPostBytesMax = val onMaxChanged: function(val) { xhttpScMaxEachPostBytesMax = val; root.editDirty = false }
onEdited: root.editDirty = true
} }
CaptionTextType { CaptionTextType {
@@ -652,8 +740,9 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
minValue: xhttpScStreamUpServerSecsMin minValue: xhttpScStreamUpServerSecsMin
maxValue: xhttpScStreamUpServerSecsMax maxValue: xhttpScStreamUpServerSecsMax
onMinChanged: xhttpScStreamUpServerSecsMin = val onMinChanged: function(val) { xhttpScStreamUpServerSecsMin = val; root.editDirty = false }
onMaxChanged: xhttpScStreamUpServerSecsMax = val onMaxChanged: function(val) { xhttpScStreamUpServerSecsMax = val; root.editDirty = false }
onEdited: root.editDirty = true
} }
CaptionTextType { CaptionTextType {
@@ -671,8 +760,9 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
minValue: xhttpScMinPostsIntervalMsMin minValue: xhttpScMinPostsIntervalMsMin
maxValue: xhttpScMinPostsIntervalMsMax maxValue: xhttpScMinPostsIntervalMsMax
onMinChanged: xhttpScMinPostsIntervalMsMin = val onMinChanged: function(val) { xhttpScMinPostsIntervalMsMin = val; root.editDirty = false }
onMaxChanged: xhttpScMinPostsIntervalMsMax = val onMaxChanged: function(val) { xhttpScMinPostsIntervalMsMax = val; root.editDirty = false }
onEdited: root.editDirty = true
} }
// ── Padding and multiplexing ────────────────────────── // ── Padding and multiplexing ──────────────────────────
@@ -728,10 +818,15 @@ PageType {
anchors.rightMargin: 16 anchors.rightMargin: 16
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
enabled: visible enabled: visible
text: qsTr("Save") text: qsTr("Save")
clickedFunc: function () { clickedFunc: function () {
var errs = XrayConfigModel.validationErrors()
if (errs.length > 0) {
PageController.showErrorMessage(errs.join("\n"))
return
}
var headerText = qsTr("Save settings?") 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 descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
var yesButtonText = qsTr("Continue") var yesButtonText = qsTr("Continue")
@@ -15,6 +15,8 @@ import "../Components"
PageType { PageType {
id: root id: root
property bool editDirty: false
BackButtonType { BackButtonType {
id: backButton id: backButton
anchors.top: parent.top anchors.top: parent.top
@@ -61,8 +63,9 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
minValue: xPaddingBytesMin minValue: xPaddingBytesMin
maxValue: xPaddingBytesMax maxValue: xPaddingBytesMax
onMinChanged: xPaddingBytesMin = val onMinChanged: function(val) { xPaddingBytesMin = val; root.editDirty = false }
onMaxChanged: xPaddingBytesMax = val onMaxChanged: function(val) { xPaddingBytesMax = val; root.editDirty = false }
onEdited: root.editDirty = true
} }
Item { Item {
@@ -81,7 +84,7 @@ PageType {
anchors.rightMargin: 16 anchors.rightMargin: 16
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
enabled: visible enabled: visible
text: qsTr("Save") text: qsTr("Save")
clickedFunc: function () { clickedFunc: function () {
@@ -15,6 +15,8 @@ import "../Components"
PageType { PageType {
id: root id: root
property bool editDirty: false
BackButtonType { BackButtonType {
id: backButton id: backButton
anchors.top: parent.top anchors.top: parent.top
@@ -78,8 +80,13 @@ PageType {
Layout.topMargin: 16 Layout.topMargin: 16
headerText: qsTr("xPaddingKey") headerText: qsTr("xPaddingKey")
textField.text: xPaddingKey textField.text: xPaddingKey
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xPaddingKey)
textField.onEditingFinished: { 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 Layout.topMargin: 8
headerText: qsTr("xPaddingHeader") headerText: qsTr("xPaddingHeader")
textField.text: xPaddingHeader textField.text: xPaddingHeader
textField.validator: RegularExpressionValidator { regularExpression: /^[A-Za-z0-9_-]*$/ }
textField.onTextEdited: root.editDirty = (textField.text !== xPaddingHeader)
textField.onEditingFinished: { 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 { DropDownType {
id: placementDropDown id: placementDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -140,6 +153,7 @@ PageType {
DropDownType { DropDownType {
id: methodDropDown id: methodDropDown
fitContent: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 8 Layout.topMargin: 8
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -197,7 +211,7 @@ PageType {
anchors.rightMargin: 16 anchors.rightMargin: 16
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
enabled: visible enabled: visible
text: qsTr("Save") text: qsTr("Save")
clickedFunc: function () { clickedFunc: function () {
@@ -15,6 +15,21 @@ import "../Components"
PageType { PageType {
id: root 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 { BackButtonType {
id: backButton id: backButton
anchors.top: parent.top anchors.top: parent.top
@@ -78,8 +93,9 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
minValue: xmuxMaxConcurrencyMin minValue: xmuxMaxConcurrencyMin
maxValue: xmuxMaxConcurrencyMax maxValue: xmuxMaxConcurrencyMax
onMinChanged: xmuxMaxConcurrencyMin = val onMinChanged: function(val) { xmuxMaxConcurrencyMin = val; root.editDirty = false }
onMaxChanged: xmuxMaxConcurrencyMax = val onMaxChanged: function(val) { xmuxMaxConcurrencyMax = val; root.editDirty = false }
onEdited: root.editDirty = true
} }
// maxConnections // maxConnections
@@ -98,8 +114,9 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
minValue: xmuxMaxConnectionsMin minValue: xmuxMaxConnectionsMin
maxValue: xmuxMaxConnectionsMax maxValue: xmuxMaxConnectionsMax
onMinChanged: xmuxMaxConnectionsMin = val onMinChanged: function(val) { xmuxMaxConnectionsMin = val; root.editDirty = false }
onMaxChanged: xmuxMaxConnectionsMax = val onMaxChanged: function(val) { xmuxMaxConnectionsMax = val; root.editDirty = false }
onEdited: root.editDirty = true
} }
// cMaxReuseTimes // cMaxReuseTimes
@@ -118,8 +135,9 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
minValue: xmuxCMaxReuseTimesMin minValue: xmuxCMaxReuseTimesMin
maxValue: xmuxCMaxReuseTimesMax maxValue: xmuxCMaxReuseTimesMax
onMinChanged: xmuxCMaxReuseTimesMin = val onMinChanged: function(val) { xmuxCMaxReuseTimesMin = val; root.editDirty = false }
onMaxChanged: xmuxCMaxReuseTimesMax = val onMaxChanged: function(val) { xmuxCMaxReuseTimesMax = val; root.editDirty = false }
onEdited: root.editDirty = true
} }
// hMaxRequestTimes // hMaxRequestTimes
@@ -138,8 +156,9 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
minValue: xmuxHMaxRequestTimesMin minValue: xmuxHMaxRequestTimesMin
maxValue: xmuxHMaxRequestTimesMax maxValue: xmuxHMaxRequestTimesMax
onMinChanged: xmuxHMaxRequestTimesMin = val onMinChanged: function(val) { xmuxHMaxRequestTimesMin = val; root.editDirty = false }
onMaxChanged: xmuxHMaxRequestTimesMax = val onMaxChanged: function(val) { xmuxHMaxRequestTimesMax = val; root.editDirty = false }
onEdited: root.editDirty = true
} }
// hMaxReusableSecs // hMaxReusableSecs
@@ -158,8 +177,9 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
minValue: xmuxHMaxReusableSecsMin minValue: xmuxHMaxReusableSecsMin
maxValue: xmuxHMaxReusableSecsMax maxValue: xmuxHMaxReusableSecsMax
onMinChanged: xmuxHMaxReusableSecsMin = val onMinChanged: function(val) { xmuxHMaxReusableSecsMin = val; root.editDirty = false }
onMaxChanged: xmuxHMaxReusableSecsMax = val onMaxChanged: function(val) { xmuxHMaxReusableSecsMax = val; root.editDirty = false }
onEdited: root.editDirty = true
} }
TextFieldWithHeaderType { TextFieldWithHeaderType {
@@ -168,12 +188,16 @@ PageType {
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 16 Layout.topMargin: 16
headerText: qsTr("hKeepAlivePeriod") headerText: qsTr("hKeepAlivePeriod")
subtitleText: qsTr("Integer, may be negative")
textField.text: xmuxHKeepAlivePeriod textField.text: xmuxHKeepAlivePeriod
textField.validator: IntValidator { textField.maximumLength: 11
bottom: 0 textField.validator: RegularExpressionValidator { regularExpression: /^-?\d*$/ }
} textField.onTextEdited: root.editDirty = (textField.text !== xmuxHKeepAlivePeriod)
textField.onEditingFinished: { 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.rightMargin: 16
anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
visible: listView.enabled && XrayConfigModel.hasUnsavedChanges visible: listView.enabled && (XrayConfigModel.hasUnsavedChanges || root.editDirty)
enabled: visible enabled: visible
text: qsTr("Save") text: qsTr("Save")
clickedFunc: function () { clickedFunc: function () {