diff --git a/client/ui/controllers/qml/pageController.h b/client/ui/controllers/qml/pageController.h
index 603e8a8f4..57362aa6a 100644
--- a/client/ui/controllers/qml/pageController.h
+++ b/client/ui/controllers/qml/pageController.h
@@ -80,7 +80,11 @@ namespace PageLoader
PageSetupWizardApiPremiumInfo,
PageSetupWizardApiTrialEmail,
- PageDevMenu
+ PageDevMenu,
+
+ PageProtocolXrayTransportSettings,
+ PageProtocolXrayXmuxSettings,
+ PageProtocolXrayXPaddingSettings,
};
Q_ENUM_NS(PageEnum)
diff --git a/client/ui/qml/Controls2/MinMaxRowType.qml b/client/ui/qml/Controls2/MinMaxRowType.qml
new file mode 100644
index 000000000..130de293a
--- /dev/null
+++ b/client/ui/qml/Controls2/MinMaxRowType.qml
@@ -0,0 +1,61 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+import Style 1.0
+
+import "../Controls2"
+import "../Controls2/TextTypes"
+
+// MinMaxRowType — two side-by-side labeled text fields: Min / Max
+// Usage:
+// MinMaxRowType {
+// minValue: "0"
+// maxValue: "0"
+// onMinChanged: someProperty = val
+// onMaxChanged: someProperty = val
+// }
+Item {
+ id: root
+
+ property string minValue: "0"
+ property string maxValue: "0"
+
+ signal minChanged(string val)
+ signal maxChanged(string val)
+
+ implicitHeight: row.implicitHeight
+ implicitWidth: row.implicitWidth
+
+ RowLayout {
+ id: row
+ anchors.fill: parent
+ spacing: 8
+
+ // Min field
+ TextFieldWithHeaderType {
+ Layout.fillWidth: true
+ headerText: qsTr("Min")
+ textField.text: root.minValue
+ textField.validator: IntValidator { bottom: 0 }
+ textField.onEditingFinished: {
+ if (textField.text !== root.minValue) {
+ root.minChanged(textField.text)
+ }
+ }
+ }
+
+ // Max field
+ TextFieldWithHeaderType {
+ Layout.fillWidth: true
+ headerText: qsTr("Max")
+ textField.text: root.maxValue
+ textField.validator: IntValidator { bottom: 0 }
+ textField.onEditingFinished: {
+ if (textField.text !== root.maxValue) {
+ root.maxChanged(textField.text)
+ }
+ }
+ }
+ }
+}
diff --git a/client/ui/qml/Pages2/PageProtocolXraySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySettings.qml
index 552ec3794..dd9455c9c 100644
--- a/client/ui/qml/Pages2/PageProtocolXraySettings.qml
+++ b/client/ui/qml/Pages2/PageProtocolXraySettings.qml
@@ -46,19 +46,52 @@ PageType {
delegate: ColumnLayout {
width: listView.width
- property alias focusItemId: textFieldWithHeaderType.textField
+ property alias focusItemId: portTextField.textField
spacing: 0
- BaseHeaderType {
+ // ── Header ────────────────────────────────────────────────
+ RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
- headerText: qsTr("XRay settings")
+ Layout.topMargin: 0
+
+ Header2TextType {
+ Layout.fillWidth: true
+ text: qsTr("XRay\nVLESS")
+ wrapMode: Text.WordWrap
+ }
+
+ ImageButtonType {
+ Layout.alignment: Qt.AlignTop | Qt.AlignRight
+ implicitWidth: 40
+ implicitHeight: 40
+ image: "qrc:/images/controls/more-vertical.svg"
+ imageColor: AmneziaStyle.color.mutedGray
+ }
}
+ // ── "More about settings" link ────────────────────────────
+ LabelTextType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 4
+
+ text: qsTr("More about settings")
+ color: AmneziaStyle.color.burntOrange
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: Qt.openUrlExternally("https://docs.amnezia.org")
+ }
+ }
+
+ // ── Port field ────────────────────────────────────────────
TextFieldWithHeaderType {
- id: textFieldWithHeaderType
+ id: portTextField
Layout.fillWidth: true
Layout.topMargin: 32
@@ -67,40 +100,12 @@ PageType {
enabled: listView.enabled
- headerText: qsTr("Disguised as traffic from")
- textField.text: site
-
- textField.onEditingFinished: {
- if (textField.text !== site) {
- var tmpText = textField.text
- tmpText = tmpText.toLocaleLowerCase()
-
- if (tmpText.startsWith("https://")) {
- tmpText = textField.text.substring(8)
- site = tmpText
- } else {
- site = textField.text
- }
- }
- }
-
- checkEmptyText: true
- }
-
- TextFieldWithHeaderType {
- id: portTextField
-
- Layout.fillWidth: true
- Layout.topMargin: 16
- Layout.leftMargin: 16
- Layout.rightMargin: 16
-
- enabled: listView.enabled
-
headerText: qsTr("Port")
textField.text: port
textField.maximumLength: 5
- textField.validator: IntValidator { bottom: 1; top: 65535 }
+ textField.validator: IntValidator {
+ bottom: 1; top: 65535
+ }
textField.onEditingFinished: {
if (textField.text !== port) {
@@ -111,12 +116,70 @@ PageType {
checkEmptyText: true
}
+ // ── Transport row ─────────────────────────────────────────
+ LabelWithButtonType {
+ Layout.fillWidth: true
+ Layout.topMargin: 16
+
+ text: qsTr("Transport")
+ descriptionText: "RAW (TCP)" // TODO: model role
+ rightImageSource: "qrc:/images/controls/chevron-right.svg"
+ enabled: listView.enabled
+
+ clickedFunction: function () {
+ PageController.goToPage(PageEnum.PageProtocolXrayTransportSettings)
+ }
+ }
+
+ DividerType {
+ }
+
+ // ── Security row ──────────────────────────────────────────
+ LabelWithButtonType {
+ Layout.fillWidth: true
+
+ text: qsTr("Security")
+ descriptionText: "TLS" // TODO: model role
+ rightImageSource: "qrc:/images/controls/chevron-right.svg"
+ enabled: listView.enabled
+
+ clickedFunction: function () {
+ PageController.goToPage(PageEnum.PageProtocolXraySecuritySettings)
+ }
+ }
+
+ DividerType {
+ }
+
+ // ── Flow row ──────────────────────────────────────────────
+ LabelWithButtonType {
+ Layout.fillWidth: true
+
+ text: qsTr("Flow")
+ descriptionText: "xtls-rprx-vision" // TODO: model role
+ rightImageSource: "qrc:/images/controls/chevron-right.svg"
+ enabled: listView.enabled
+
+ clickedFunction: function () {
+ PageController.goToPage(PageEnum.PageProtocolXrayFlowSettings)
+ }
+ }
+
+ DividerType {
+ }
+
+ // ── Spacer ────────────────────────────────────────────────
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 24
+ }
+
+ // ── Save button ───────────────────────────────────────────
BasicButtonType {
id: saveButton
Layout.fillWidth: true
- Layout.topMargin: 24
- Layout.bottomMargin: 24
+ Layout.bottomMargin: 8
Layout.leftMargin: 16
Layout.rightMargin: 16
@@ -124,7 +187,7 @@ PageType {
text: qsTr("Save")
- onClicked: function() {
+ onClicked: function () {
forceActiveFocus()
var headerText = qsTr("Save settings?")
@@ -132,16 +195,16 @@ PageType {
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
- var yesButtonFunction = function() {
+ var yesButtonFunction = function () {
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ServersUiController.processedContainerIndex) {
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
return
}
- PageController.goToPage(PageEnum.PageSetupWizardInstalling);
+ PageController.goToPage(PageEnum.PageSetupWizardInstalling)
InstallController.updateContainer(ServersUiController.processedIndex, ServersUiController.processedContainerIndex, ProtocolEnum.Xray)
}
- var noButtonFunction = function() {
+ var noButtonFunction = function () {
if (!GC.isMobile()) {
saveButton.forceActiveFocus()
}
@@ -152,6 +215,37 @@ PageType {
Keys.onEnterPressed: saveButton.clicked()
Keys.onReturnPressed: saveButton.clicked()
}
+
+ // ── Reset settings ────────────────────────────────────────
+ LabelWithButtonType {
+ Layout.fillWidth: true
+
+ text: qsTr("Reset settings")
+ textColor: AmneziaStyle.color.vibrantRed
+ visible: listView.enabled
+
+ clickedFunction: function () {
+ var headerText = qsTr("Reset settings?")
+ var descriptionText = qsTr("All XRay settings will be restored to defaults.")
+ var yesButtonText = qsTr("Reset")
+ var noButtonText = qsTr("Cancel")
+
+ var yesButtonFunction = function () {
+ XrayConfigModel.resetToDefaults()
+ }
+ var noButtonFunction = function () {
+ }
+
+ showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText,
+ yesButtonFunction, noButtonFunction)
+ }
+ }
+
+ // ── Bottom padding ────────────────────────────────────────
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 32
+ }
}
}
}
diff --git a/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml
new file mode 100644
index 000000000..093990fdb
--- /dev/null
+++ b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml
@@ -0,0 +1,702 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+import PageEnum 1.0
+import Style 1.0
+
+import "./"
+import "../Controls2"
+import "../Controls2/TextTypes"
+import "../Config"
+import "../Components"
+
+PageType {
+ id: root
+
+ // Temporary local state — will be replaced by model roles
+ property int selectedTransport: 0 // 0=RAW, 1=XHTTP, 2=mKCP
+
+ // XHTTP fields
+ property string xhttpMode: "Auto"
+ property string xhttpHost: "www.googletagmanager.com"
+ property string xhttpPath: ""
+ property string xhttpHeadersTemplate: "HTTP"
+ property string xhttpUplinkMethod: "POST"
+ property bool xhttpDisableGrpc: true
+ property bool xhttpDisableSse: true
+ // Session & Sequence
+ property string sessionPlacement: "Path"
+ property string sessionKey: "Path"
+ property string seqPlacement: "Path"
+ property string seqKey: ""
+ property string uplinkDataPlacement: "Body"
+ property string uplinkDataKey: ""
+ // Traffic Shaping
+ property string uplinkChunkSize: "0"
+ property string scMaxBufferedPosts: ""
+ // mKCP fields
+ property string mkcpTti: ""
+ property string mkcpUplinkCapacity: ""
+ property string mkcpDownlinkCapacity: ""
+ property string mkcpReadBufferSize: ""
+ property string mkcpWriteBufferSize: ""
+ property bool mkcpCongestion: true
+
+ BackButtonType {
+ id: backButton
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: 20 + PageController.safeAreaTopMargin
+ }
+
+ FlickableType {
+ id: flickable
+ anchors.top: backButton.bottom
+ anchors.bottom: saveButton.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ contentHeight: mainColumn.implicitHeight
+
+ ColumnLayout {
+ id: mainColumn
+ width: flickable.width
+ spacing: 0
+
+ // ── Header ────────────────────────────────────────────────
+ Header2TextType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 0
+ Layout.bottomMargin: 24
+ text: qsTr("Transport")
+ }
+
+ // ── Radio: RAW (TCP) ──────────────────────────────────────
+ VerticalRadioButton {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: qsTr("RAW (TCP)")
+ checked: root.selectedTransport === 0
+ onClicked: root.selectedTransport = 0
+ }
+
+ DividerType {
+ }
+
+ // ── Radio: XHTTP ──────────────────────────────────────────
+ VerticalRadioButton {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: qsTr("XHTTP")
+ descriptionText: qsTr("Advanced users")
+ checked: root.selectedTransport === 1
+ onClicked: root.selectedTransport = 1
+ }
+
+ DividerType {
+ }
+
+ // ── Radio: mKCP ───────────────────────────────────────────
+ VerticalRadioButton {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: qsTr("mKCP")
+ checked: root.selectedTransport === 2
+ onClicked: root.selectedTransport = 2
+ }
+
+ DividerType {
+ }
+
+ // ══════════════════════════════════════════════════════════
+ // mKCP Settings (visible when mKCP selected)
+ // ══════════════════════════════════════════════════════════
+ ColumnLayout {
+ visible: root.selectedTransport === 2
+ Layout.fillWidth: true
+ spacing: 0
+
+ CaptionTextType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 24
+ Layout.bottomMargin: 8
+ text: qsTr("mKCP Settings")
+ color: AmneziaStyle.color.mutedGray
+ }
+
+ TextFieldWithHeaderType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 8
+ headerText: qsTr("TTI")
+ textField.text: root.mkcpTti
+ textField.onEditingFinished: root.mkcpTti = textField.text
+ }
+
+ TextFieldWithHeaderType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 8
+ headerText: qsTr("uplinkCapacity")
+ textField.text: root.mkcpUplinkCapacity
+ textField.onEditingFinished: root.mkcpUplinkCapacity = textField.text
+ }
+
+ TextFieldWithHeaderType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 8
+ headerText: qsTr("downlinkCapacity")
+ textField.text: root.mkcpDownlinkCapacity
+ textField.onEditingFinished: root.mkcpDownlinkCapacity = textField.text
+ }
+
+ TextFieldWithHeaderType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 8
+ headerText: qsTr("readBufferSize")
+ textField.text: root.mkcpReadBufferSize
+ textField.onEditingFinished: root.mkcpReadBufferSize = textField.text
+ }
+
+ TextFieldWithHeaderType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 8
+ headerText: qsTr("writeBufferSize")
+ textField.text: root.mkcpWriteBufferSize
+ textField.onEditingFinished: root.mkcpWriteBufferSize = textField.text
+ }
+
+ SwitcherType {
+ Layout.fillWidth: true
+ Layout.margins: 16
+ Layout.topMargin: 8
+ text: qsTr("Congestion")
+ checked: root.mkcpCongestion
+ onToggled: root.mkcpCongestion = checked
+ }
+ }
+
+ // ══════════════════════════════════════════════════════════
+ // XHTTP Settings (visible when XHTTP selected)
+ // ══════════════════════════════════════════════════════════
+ ColumnLayout {
+ visible: root.selectedTransport === 1
+ Layout.fillWidth: true
+ spacing: 0
+
+ // Mode dropdown
+ DropDownType {
+ id: modeDropDown
+ Layout.fillWidth: true
+ Layout.topMargin: 16
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: root.xhttpMode
+ descriptionText: qsTr("Mode")
+ headerText: qsTr("Mode")
+ drawerParent: root
+ listView: ListViewWithRadioButtonType {
+ id: modeListView
+ rootWidth: root.width
+ model: ListModel {
+ ListElement {
+ name: "Auto"
+ }
+ ListElement {
+ name: "Packet-up"
+ }
+ ListElement {
+ name: "Stream-up"
+ }
+ ListElement {
+ name: "Stream-one"
+ }
+ }
+ clickedFunction: function () {
+ root.xhttpMode = selectedText
+ modeDropDown.text = selectedText
+ modeDropDown.closeTriggered()
+ }
+ Component.onCompleted: {
+ for (var i = 0; i < model.count; i++) {
+ if (model.get(i).name === root.xhttpMode) {
+ selectedIndex = i;
+ break
+ }
+ }
+ }
+ }
+ }
+
+ // HTTP Profile label
+ CaptionTextType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 24
+ Layout.bottomMargin: 8
+ text: qsTr("HTTP Profile")
+ color: AmneziaStyle.color.mutedGray
+ }
+
+ TextFieldWithHeaderType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 8
+ headerText: qsTr("Host")
+ textField.text: root.xhttpHost
+ textField.onEditingFinished: root.xhttpHost = textField.text
+ }
+
+ TextFieldWithHeaderType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 8
+ headerText: qsTr("Path")
+ textField.text: root.xhttpPath
+ textField.onEditingFinished: root.xhttpPath = textField.text
+ }
+
+ // Headers template dropdown
+ DropDownType {
+ id: headersDropDown
+ Layout.fillWidth: true
+ Layout.topMargin: 8
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: root.xhttpHeadersTemplate
+ descriptionText: qsTr("Headers template")
+ headerText: qsTr("Headers template")
+ drawerParent: root
+ listView: ListViewWithRadioButtonType {
+ rootWidth: root.width
+ model: ListModel {
+ ListElement {
+ name: "HTTP"
+ }
+ ListElement {
+ name: "None"
+ }
+ }
+ clickedFunction: function () {
+ root.xhttpHeadersTemplate = selectedText
+ headersDropDown.text = selectedText
+ headersDropDown.closeTriggered()
+ }
+ Component.onCompleted: {
+ for (var i = 0; i < model.count; i++) {
+ if (model.get(i).name === root.xhttpHeadersTemplate) {
+ selectedIndex = i;
+ break
+ }
+ }
+ }
+ }
+ }
+
+ // UplinkHTTPMethod dropdown
+ DropDownType {
+ id: uplinkMethodDropDown
+ Layout.fillWidth: true
+ Layout.topMargin: 8
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: root.xhttpUplinkMethod
+ descriptionText: qsTr("UplinkHTTPMethod")
+ headerText: qsTr("UplinkHTTPMethod")
+ drawerParent: root
+ listView: ListViewWithRadioButtonType {
+ rootWidth: root.width
+ model: ListModel {
+ ListElement {
+ name: "POST"
+ }
+ ListElement {
+ name: "PUT"
+ }
+ ListElement {
+ name: "PATCH"
+ }
+ }
+ clickedFunction: function () {
+ root.xhttpUplinkMethod = selectedText
+ uplinkMethodDropDown.text = selectedText
+ uplinkMethodDropDown.closeTriggered()
+ }
+ Component.onCompleted: {
+ for (var i = 0; i < model.count; i++) {
+ if (model.get(i).name === root.xhttpUplinkMethod) {
+ selectedIndex = i;
+ break
+ }
+ }
+ }
+ }
+ }
+
+ // Disable gRPC Header
+ SwitcherType {
+ Layout.fillWidth: true
+ Layout.margins: 16
+ Layout.topMargin: 16
+ text: qsTr("Disable gRPC Header")
+ descriptionText: qsTr("noGRPCHeader")
+ checked: root.xhttpDisableGrpc
+ onToggled: root.xhttpDisableGrpc = checked
+ }
+
+ DividerType {
+ }
+
+ // Disable SSE Header
+ SwitcherType {
+ Layout.fillWidth: true
+ Layout.margins: 16
+ text: qsTr("Disable SSE Header")
+ descriptionText: qsTr("noSSEHeader")
+ checked: root.xhttpDisableSse
+ onToggled: root.xhttpDisableSse = checked
+ }
+
+ DividerType {
+ }
+
+ // ── Session & Sequence ────────────────────────────────
+ CaptionTextType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 24
+ Layout.bottomMargin: 8
+ text: qsTr("Session & Sequence")
+ color: AmneziaStyle.color.mutedGray
+ }
+
+ DropDownType {
+ id: sessionPlacementDropDown
+ Layout.fillWidth: true
+ Layout.topMargin: 8
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: root.sessionPlacement
+ descriptionText: qsTr("SessionPlacement")
+ headerText: qsTr("SessionPlacement")
+ drawerParent: root
+ listView: ListViewWithRadioButtonType {
+ rootWidth: root.width
+ model: ListModel {
+ ListElement {
+ name: "Path"
+ }
+ ListElement {
+ name: "Header"
+ }
+ ListElement {
+ name: "Cookie"
+ }
+ ListElement {
+ name: "None"
+ }
+ }
+ clickedFunction: function () {
+ root.sessionPlacement = selectedText
+ sessionPlacementDropDown.text = selectedText
+ sessionPlacementDropDown.closeTriggered()
+ }
+ Component.onCompleted: {
+ for (var i = 0; i < model.count; i++) {
+ if (model.get(i).name === root.sessionPlacement) {
+ selectedIndex = i;
+ break
+ }
+ }
+ }
+ }
+ }
+
+ DropDownType {
+ id: sessionKeyDropDown
+ Layout.fillWidth: true
+ Layout.topMargin: 8
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: root.sessionKey
+ descriptionText: qsTr("SessionKey")
+ headerText: qsTr("SessionKey")
+ drawerParent: root
+ listView: ListViewWithRadioButtonType {
+ rootWidth: root.width
+ model: ListModel {
+ ListElement {
+ name: "Path"
+ }
+ ListElement {
+ name: "Header"
+ }
+ ListElement {
+ name: "None"
+ }
+ }
+ clickedFunction: function () {
+ root.sessionKey = selectedText
+ sessionKeyDropDown.text = selectedText
+ sessionKeyDropDown.closeTriggered()
+ }
+ Component.onCompleted: {
+ for (var i = 0; i < model.count; i++) {
+ if (model.get(i).name === root.sessionKey) {
+ selectedIndex = i;
+ break
+ }
+ }
+ }
+ }
+ }
+
+ DropDownType {
+ id: seqPlacementDropDown
+ Layout.fillWidth: true
+ Layout.topMargin: 8
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: root.seqPlacement
+ descriptionText: qsTr("SeqPlacement")
+ headerText: qsTr("SeqPlacement")
+ drawerParent: root
+ listView: ListViewWithRadioButtonType {
+ rootWidth: root.width
+ model: ListModel {
+ ListElement {
+ name: "Path"
+ }
+ ListElement {
+ name: "Header"
+ }
+ ListElement {
+ name: "Cookie"
+ }
+ ListElement {
+ name: "None"
+ }
+ }
+ clickedFunction: function () {
+ root.seqPlacement = selectedText
+ seqPlacementDropDown.text = selectedText
+ seqPlacementDropDown.closeTriggered()
+ }
+ Component.onCompleted: {
+ for (var i = 0; i < model.count; i++) {
+ if (model.get(i).name === root.seqPlacement) {
+ selectedIndex = i;
+ break
+ }
+ }
+ }
+ }
+ }
+
+ TextFieldWithHeaderType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 8
+ headerText: qsTr("SeqKey")
+ textField.text: root.seqKey
+ textField.onEditingFinished: root.seqKey = textField.text
+ }
+
+ DropDownType {
+ id: uplinkDataPlacementDropDown
+ Layout.fillWidth: true
+ Layout.topMargin: 8
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: root.uplinkDataPlacement
+ descriptionText: qsTr("UplinkDataPlacement")
+ headerText: qsTr("UplinkDataPlacement")
+ drawerParent: root
+ listView: ListViewWithRadioButtonType {
+ rootWidth: root.width
+ model: ListModel {
+ ListElement {
+ name: "Body"
+ }
+ ListElement {
+ name: "Query"
+ }
+ }
+ clickedFunction: function () {
+ root.uplinkDataPlacement = selectedText
+ uplinkDataPlacementDropDown.text = selectedText
+ uplinkDataPlacementDropDown.closeTriggered()
+ }
+ Component.onCompleted: {
+ for (var i = 0; i < model.count; i++) {
+ if (model.get(i).name === root.uplinkDataPlacement) {
+ selectedIndex = i;
+ break
+ }
+ }
+ }
+ }
+ }
+
+ TextFieldWithHeaderType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 8
+ headerText: qsTr("UplinkDataKey")
+ textField.text: root.uplinkDataKey
+ textField.onEditingFinished: root.uplinkDataKey = textField.text
+ }
+
+ // ── Traffic Shaping ───────────────────────────────────
+ CaptionTextType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 24
+ Layout.bottomMargin: 8
+ text: qsTr("Traffic Shaping")
+ color: AmneziaStyle.color.mutedGray
+ }
+
+ TextFieldWithHeaderType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 8
+ headerText: qsTr("UplinkChunkSize")
+ textField.text: root.uplinkChunkSize
+ textField.validator: IntValidator {
+ bottom: 0
+ }
+ textField.onEditingFinished: root.uplinkChunkSize = textField.text
+ }
+
+ TextFieldWithHeaderType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 8
+ headerText: qsTr("scMaxBufferedPosts")
+ textField.text: root.scMaxBufferedPosts
+ textField.onEditingFinished: root.scMaxBufferedPosts = textField.text
+ }
+
+ // scMaxEachPostBytes → nav row
+ LabelWithButtonType {
+ Layout.fillWidth: true
+ Layout.topMargin: 8
+ text: qsTr("scMaxEachPostBytes")
+ descriptionText: qsTr("1—100")
+ rightImageSource: "qrc:/images/controls/chevron-right.svg"
+ clickedFunction: function () { /* navigate */
+ }
+ }
+ DividerType {
+ }
+
+ // scMinPostsIntervalMs → nav row
+ LabelWithButtonType {
+ Layout.fillWidth: true
+ text: qsTr("scMinPostsIntervalMs")
+ descriptionText: qsTr("100—600ms")
+ rightImageSource: "qrc:/images/controls/chevron-right.svg"
+ clickedFunction: function () { /* navigate */
+ }
+ }
+ DividerType {
+ }
+
+ // scStreamUpServerSecs → nav row
+ LabelWithButtonType {
+ Layout.fillWidth: true
+ text: qsTr("scStreamUpServerSecs")
+ descriptionText: qsTr("1—100")
+ rightImageSource: "qrc:/images/controls/chevron-right.svg"
+ clickedFunction: function () { /* navigate */
+ }
+ }
+ DividerType {
+ }
+
+ // ── Padding and multiplexing ──────────────────────────
+ CaptionTextType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 24
+ Layout.bottomMargin: 8
+ text: qsTr("Padding and multiplexing")
+ color: AmneziaStyle.color.mutedGray
+ }
+
+ LabelWithButtonType {
+ Layout.fillWidth: true
+ text: qsTr("xPadding")
+ rightImageSource: "qrc:/images/controls/chevron-right.svg"
+ clickedFunction: function () {
+ PageController.goToPage(PageEnum.PageProtocolXrayXPaddingSettings)
+ }
+ }
+ DividerType {
+ }
+
+ LabelWithButtonType {
+ Layout.fillWidth: true
+ text: qsTr("XMux")
+ descriptionText: qsTr("On")
+ rightImageSource: "qrc:/images/controls/chevron-right.svg"
+ clickedFunction: function () {
+ PageController.goToPage(PageEnum.PageProtocolXrayXmuxSettings)
+ }
+ }
+ DividerType {
+ }
+ }
+
+ Item {
+ Layout.preferredHeight: 16
+ }
+ }
+ }
+
+ // ── Save button ───────────────────────────────────────────────────
+ BasicButtonType {
+ id: saveButton
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
+ anchors.leftMargin: 16
+ anchors.rightMargin: 16
+
+ text: qsTr("Save")
+ onClicked: {
+ forceActiveFocus()
+ // XrayConfigModel.setTransport(...)
+ }
+ Keys.onEnterPressed: clicked()
+ Keys.onReturnPressed: clicked()
+ }
+}
diff --git a/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml
new file mode 100644
index 000000000..e99045697
--- /dev/null
+++ b/client/ui/qml/Pages2/PageProtocolXrayXPaddingSettings.qml
@@ -0,0 +1,211 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+import PageEnum 1.0
+import Style 1.0
+
+import "./"
+import "../Controls2"
+import "../Controls2/TextTypes"
+import "../Config"
+import "../Components"
+
+PageType {
+ id: root
+
+ // Temporary local state
+ property string xPaddingBytes: "0—0"
+ property bool xPaddingObfsMode: true
+ property string xPaddingKey: "www.googletagmanager.com"
+ property string xPaddingHeader: ""
+ property string xPaddingPlacement: "Cookie"
+ property string xPaddingMethod: "Repeat-x"
+
+ BackButtonType {
+ id: backButton
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: 20 + PageController.safeAreaTopMargin
+ }
+
+ FlickableType {
+ id: flickable
+ anchors.top: backButton.bottom
+ anchors.bottom: saveButton.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ contentHeight: mainColumn.implicitHeight
+
+ ColumnLayout {
+ id: mainColumn
+ width: flickable.width
+ spacing: 0
+
+ // ── Header ────────────────────────────────────────────────
+ Header2TextType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 0
+ Layout.bottomMargin: 24
+ text: qsTr("xPadding")
+ }
+
+ // ── xPaddingBytes nav row ─────────────────────────────────
+ LabelWithButtonType {
+ Layout.fillWidth: true
+ text: qsTr("xPaddingBytes")
+ descriptionText: root.xPaddingBytes
+ rightImageSource: "qrc:/images/controls/chevron-right.svg"
+ clickedFunction: function () {
+ PageController.goToPage(PageEnum.PageProtocolXrayXPaddingBytesSettings)
+ }
+ }
+
+ DividerType {
+ }
+
+ // ── xPaddingObfsMode switcher ─────────────────────────────
+ SwitcherType {
+ Layout.fillWidth: true
+ Layout.margins: 16
+ text: qsTr("xPaddingObfsMode")
+ checked: root.xPaddingObfsMode
+ onToggled: root.xPaddingObfsMode = checked
+ }
+
+ DividerType {
+ }
+
+ // ── xPaddingKey ───────────────────────────────────────────
+ TextFieldWithHeaderType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 16
+ headerText: qsTr("xPaddingKey")
+ textField.text: root.xPaddingKey
+ textField.onEditingFinished: root.xPaddingKey = textField.text
+ }
+
+ // ── xPaddingHeader ────────────────────────────────────────
+ TextFieldWithHeaderType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 8
+ headerText: qsTr("xPaddingHeader")
+ textField.text: root.xPaddingHeader
+ textField.onEditingFinished: root.xPaddingHeader = textField.text
+ }
+
+ // ── xPaddingPlacement dropdown ────────────────────────────
+ DropDownType {
+ id: placementDropDown
+ Layout.fillWidth: true
+ Layout.topMargin: 8
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: root.xPaddingPlacement
+ descriptionText: qsTr("xPaddingPlacement")
+ headerText: qsTr("xPaddingPlacement")
+ drawerParent: root
+ listView: ListViewWithRadioButtonType {
+ rootWidth: root.width
+ model: ListModel {
+ ListElement {
+ name: "Cookie"
+ }
+ ListElement {
+ name: "Header"
+ }
+ ListElement {
+ name: "Query"
+ }
+ ListElement {
+ name: "Body"
+ }
+ }
+ clickedFunction: function () {
+ root.xPaddingPlacement = selectedText
+ placementDropDown.text = selectedText
+ placementDropDown.closeTriggered()
+ }
+ Component.onCompleted: {
+ for (var i = 0; i < model.count; i++) {
+ if (model.get(i).name === root.xPaddingPlacement) {
+ selectedIndex = i;
+ break
+ }
+ }
+ }
+ }
+ }
+
+ // ── xPaddingMethod dropdown ───────────────────────────────
+ DropDownType {
+ id: methodDropDown
+ Layout.fillWidth: true
+ Layout.topMargin: 8
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: root.xPaddingMethod
+ descriptionText: qsTr("xPaddingMethod")
+ headerText: qsTr("xPaddingMethod")
+ drawerParent: root
+ listView: ListViewWithRadioButtonType {
+ rootWidth: root.width
+ model: ListModel {
+ ListElement {
+ name: "Repeat-x"
+ }
+ ListElement {
+ name: "Random"
+ }
+ ListElement {
+ name: "Zero"
+ }
+ }
+ clickedFunction: function () {
+ root.xPaddingMethod = selectedText
+ methodDropDown.text = selectedText
+ methodDropDown.closeTriggered()
+ }
+ Component.onCompleted: {
+ for (var i = 0; i < model.count; i++) {
+ if (model.get(i).name === root.xPaddingMethod) {
+ selectedIndex = i;
+ break
+ }
+ }
+ }
+ }
+ }
+
+ Item {
+ Layout.preferredHeight: 16
+ }
+ }
+ }
+
+ // ── Save button ───────────────────────────────────────────────────
+ BasicButtonType {
+ id: saveButton
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
+ anchors.leftMargin: 16
+ anchors.rightMargin: 16
+
+ text: qsTr("Save")
+ onClicked: {
+ forceActiveFocus()
+ // XrayConfigModel.setXPadding(...)
+ }
+ Keys.onEnterPressed: clicked()
+ Keys.onReturnPressed: clicked()
+ }
+}
diff --git a/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml
new file mode 100644
index 000000000..e998e55f3
--- /dev/null
+++ b/client/ui/qml/Pages2/PageProtocolXrayXmuxSettings.qml
@@ -0,0 +1,219 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+import PageEnum 1.0
+import Style 1.0
+
+import "./"
+import "../Controls2"
+import "../Controls2/TextTypes"
+import "../Config"
+import "../Components"
+
+PageType {
+ id: root
+
+ // Temporary local state
+ property bool xmuxEnabled: true
+ property string maxConcurrencyMin: "0"
+ property string maxConcurrencyMax: "0"
+ property string maxConnectionsMin: "0"
+ property string maxConnectionsMax: "0"
+ property string cMaxReuseTimesMin: "0"
+ property string cMaxReuseTimesMax: "0"
+ property string hMaxRequestTimesMin: "0"
+ property string hMaxRequestTimesMax: "0"
+ property string hMaxReusableSecsMin: "0"
+ property string hMaxReusableSecsMax: "0"
+ property string hKeepAlivePeriod: ""
+
+ BackButtonType {
+ id: backButton
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: 20 + PageController.safeAreaTopMargin
+ }
+
+ FlickableType {
+ id: flickable
+ anchors.top: backButton.bottom
+ anchors.bottom: saveButton.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ contentHeight: mainColumn.implicitHeight
+
+ ColumnLayout {
+ id: mainColumn
+ width: flickable.width
+ spacing: 0
+
+ // ── Header ────────────────────────────────────────────────
+ Header2TextType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 0
+ Layout.bottomMargin: 24
+ text: qsTr("xmux")
+ }
+
+ // ── xmux master switcher ──────────────────────────────────
+ SwitcherType {
+ Layout.fillWidth: true
+ Layout.margins: 16
+ text: qsTr("xmux")
+ checked: root.xmuxEnabled
+ onToggled: root.xmuxEnabled = checked
+ }
+
+ DividerType {
+ }
+
+ // ── Min/Max pairs (only when enabled) ─────────────────────
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 0
+ enabled: root.xmuxEnabled
+
+ // maxConcurrency
+ CaptionTextType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 24
+ Layout.bottomMargin: 8
+ text: qsTr("maxConcurrency")
+ color: AmneziaStyle.color.mutedGray
+ }
+ MinMaxRowType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ minValue: root.maxConcurrencyMin
+ maxValue: root.maxConcurrencyMax
+ onMinChanged: root.maxConcurrencyMin = val
+ onMaxChanged: root.maxConcurrencyMax = val
+ }
+
+ // maxConnections
+ CaptionTextType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 16
+ Layout.bottomMargin: 8
+ text: qsTr("maxConnections")
+ color: AmneziaStyle.color.mutedGray
+ }
+ MinMaxRowType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ minValue: root.maxConnectionsMin
+ maxValue: root.maxConnectionsMax
+ onMinChanged: root.maxConnectionsMin = val
+ onMaxChanged: root.maxConnectionsMax = val
+ }
+
+ // cMaxReuseTimes
+ CaptionTextType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 16
+ Layout.bottomMargin: 8
+ text: qsTr("cMaxReuseTimes")
+ color: AmneziaStyle.color.mutedGray
+ }
+ MinMaxRowType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ minValue: root.cMaxReuseTimesMin
+ maxValue: root.cMaxReuseTimesMax
+ onMinChanged: root.cMaxReuseTimesMin = val
+ onMaxChanged: root.cMaxReuseTimesMax = val
+ }
+
+ // hMaxRequestTimes
+ CaptionTextType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 16
+ Layout.bottomMargin: 8
+ text: qsTr("hMaxRequestTimes")
+ color: AmneziaStyle.color.mutedGray
+ }
+ MinMaxRowType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ minValue: root.hMaxRequestTimesMin
+ maxValue: root.hMaxRequestTimesMax
+ onMinChanged: root.hMaxRequestTimesMin = val
+ onMaxChanged: root.hMaxRequestTimesMax = val
+ }
+
+ // hMaxReusableSecs
+ CaptionTextType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 16
+ Layout.bottomMargin: 8
+ text: qsTr("hMaxReusableSecs")
+ color: AmneziaStyle.color.mutedGray
+ }
+ MinMaxRowType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ minValue: root.hMaxReusableSecsMin
+ maxValue: root.hMaxReusableSecsMax
+ onMinChanged: root.hMaxReusableSecsMin = val
+ onMaxChanged: root.hMaxReusableSecsMax = val
+ }
+
+ // hKeepAlivePeriod — single field
+ TextFieldWithHeaderType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 16
+ headerText: qsTr("hKeepAlivePeriod")
+ textField.text: root.hKeepAlivePeriod
+ textField.validator: IntValidator {
+ bottom: 0
+ }
+ textField.onEditingFinished: root.hKeepAlivePeriod = textField.text
+ }
+ }
+
+ Item {
+ Layout.preferredHeight: 16
+ }
+ }
+ }
+
+ // ── Save button ───────────────────────────────────────────────────
+ BasicButtonType {
+ id: saveButton
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottomMargin: 16 + PageController.safeAreaBottomMargin
+ anchors.leftMargin: 16
+ anchors.rightMargin: 16
+
+ text: qsTr("Save")
+ onClicked: {
+ forceActiveFocus()
+ // XrayConfigModel.setXmux(...)
+ }
+ Keys.onEnterPressed: clicked()
+ Keys.onReturnPressed: clicked()
+ }
+}
diff --git a/client/ui/qml/qml.qrc b/client/ui/qml/qml.qrc
index f2a462630..1314d3324 100644
--- a/client/ui/qml/qml.qrc
+++ b/client/ui/qml/qml.qrc
@@ -77,6 +77,12 @@
Pages2/PageProtocolRaw.qml
Pages2/PageProtocolWireGuardSettings.qml
Pages2/PageProtocolXraySettings.qml
+
+ Pages2/PageProtocolXrayTransportSettings.qml
+ Pages2/PageProtocolXrayXmuxSettings.qml
+ Pages2/PageProtocolXrayXPaddingSettings.qml
+ Controls2/MinMaxRowType.qml
+
Pages2/PageServiceDnsSettings.qml
Pages2/PageServiceSftpSettings.qml
Pages2/PageServiceSocksProxySettings.qml