diff --git a/client/ui/controllers/qml/pageController.h b/client/ui/controllers/qml/pageController.h
index 57362aa6a..4fc298d81 100644
--- a/client/ui/controllers/qml/pageController.h
+++ b/client/ui/controllers/qml/pageController.h
@@ -82,9 +82,12 @@ namespace PageLoader
PageDevMenu,
+ PageProtocolXrayConfigsSettings,
PageProtocolXrayTransportSettings,
PageProtocolXrayXmuxSettings,
PageProtocolXrayXPaddingSettings,
+ PageProtocolXrayFlowSettings,
+ PageProtocolXraySecuritySettings,
};
Q_ENUM_NS(PageEnum)
diff --git a/client/ui/qml/Pages2/PageProtocolXrayConfigsSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayConfigsSettings.qml
new file mode 100644
index 000000000..ef3e11c32
--- /dev/null
+++ b/client/ui/qml/Pages2/PageProtocolXrayConfigsSettings.qml
@@ -0,0 +1,217 @@
+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 model — will be replaced by real XrayConfigsModel
+ ListModel {
+ id: configsModel
+ ListElement {
+ configName: "XHTTP TLS Reality"; configDate: "24.02.2026 11:12"
+ }
+ ListElement {
+ configName: "RAW (TCP) TLS Reality"; configDate: "24.02.2026 11:14"
+ }
+ ListElement {
+ configName: "RAW (TCP) TLS Reality"; configDate: "24.02.2026 11:14"
+ }
+ ListElement {
+ configName: "RAW (TCP) TLS Reality"; configDate: "24.02.2026 11:15"
+ }
+ }
+
+ // Currently selected config for the drawer
+ property string selectedConfigName: ""
+ property int selectedConfigIndex: -1
+
+ BackButtonType {
+ id: backButton
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: 20 + PageController.safeAreaTopMargin
+ }
+
+ ListViewType {
+ id: listView
+ anchors.top: backButton.bottom
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+
+ header: ColumnLayout {
+ width: listView.width
+ spacing: 0
+
+ // ── Header ────────────────────────────────────────────────
+ Header2TextType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 0
+ Layout.bottomMargin: 24
+ text: qsTr("XRay Configurations")
+ wrapMode: Text.WordWrap
+ }
+
+ // ── Create config from current settings ───────────────────
+ LabelWithButtonType {
+ Layout.fillWidth: true
+ text: qsTr("Create configuration based on current settings")
+ textMaximumLineCount: 2
+ rightImageSource: "qrc:/images/controls/chevron-right.svg"
+ clickedFunction: function () {
+ // XrayConfigModel.createConfigFromCurrent()
+ }
+ }
+
+ DividerType {
+ }
+
+ // ── Export ────────────────────────────────────────────────
+ LabelWithButtonType {
+ Layout.fillWidth: true
+ text: qsTr("Export settings")
+ rightImageSource: "qrc:/images/controls/chevron-right.svg"
+ clickedFunction: function () {
+ // XrayConfigModel.exportSettings()
+ }
+ }
+
+ DividerType {
+ }
+
+ // ── Import ────────────────────────────────────────────────
+ LabelWithButtonType {
+ Layout.fillWidth: true
+ text: qsTr("Import settings")
+ descriptionText: qsTr("In JSON format")
+ rightImageSource: "qrc:/images/controls/chevron-right.svg"
+ clickedFunction: function () {
+ // XrayConfigModel.importSettings()
+ }
+ }
+
+ DividerType {
+ }
+
+ // ── Configurations section label ──────────────────────────
+ CaptionTextType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 24
+ Layout.bottomMargin: 8
+ text: qsTr("Configurations")
+ color: AmneziaStyle.color.mutedGray
+ }
+ }
+
+ model: configsModel
+
+ delegate: ColumnLayout {
+ width: listView.width
+ spacing: 0
+
+ LabelWithButtonType {
+ Layout.fillWidth: true
+
+ text: configName
+ descriptionText: configDate
+
+ rightImageSource: "qrc:/images/controls/more-vertical.svg"
+
+ clickedFunction: function () {
+ root.selectedConfigName = configName
+ root.selectedConfigIndex = index
+ configActionsDrawer.openTriggered()
+ }
+ }
+
+ DividerType {
+ }
+ }
+ }
+
+ // ── Per-config actions drawer ─────────────────────────────────────
+ DrawerType2 {
+ id: configActionsDrawer
+
+ parent: root
+ anchors.fill: parent
+ expandedHeight: root.height * 0.4
+
+ expandedStateContent: ColumnLayout {
+ id: drawerContent
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ spacing: 0
+
+ onImplicitHeightChanged: {
+ configActionsDrawer.expandedHeight = drawerContent.implicitHeight + 32
+ }
+
+ BackButtonType {
+ Layout.fillWidth: true
+ Layout.topMargin: 16
+ backButtonFunction: function () {
+ configActionsDrawer.closeTriggered()
+ }
+ }
+
+ Header2TextType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 8
+ Layout.bottomMargin: 16
+ text: root.selectedConfigName
+ wrapMode: Text.WordWrap
+ }
+
+ // ── Apply config ──────────────────────────────────────────
+ LabelWithButtonType {
+ Layout.fillWidth: true
+ text: qsTr("Apply configuration")
+ rightImageSource: "qrc:/images/controls/chevron-right.svg"
+ clickedFunction: function () {
+ configActionsDrawer.closeTriggered()
+ // XrayConfigModel.applyConfig(root.selectedConfigIndex)
+ }
+ }
+
+ DividerType {
+ }
+
+ // ── Delete config ─────────────────────────────────────────
+ LabelWithButtonType {
+ Layout.fillWidth: true
+ text: qsTr("Delete configuration")
+ textColor: AmneziaStyle.color.vibrantRed
+ clickedFunction: function () {
+ configActionsDrawer.closeTriggered()
+ // XrayConfigModel.deleteConfig(root.selectedConfigIndex)
+ }
+ }
+
+ DividerType {
+ }
+
+ Item {
+ Layout.preferredHeight: 16
+ }
+ }
+ }
+}
diff --git a/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml
new file mode 100644
index 000000000..1309b5315
--- /dev/null
+++ b/client/ui/qml/Pages2/PageProtocolXrayFlowSettings.qml
@@ -0,0 +1,109 @@
+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 role
+ property int selectedFlow: 1 // 0=Empty, 1=xtls-rprx-vision, 2=xtls-rprx-vision-udp443
+
+ 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
+
+ Header2TextType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 0
+ Layout.bottomMargin: 24
+ text: qsTr("Flow")
+ }
+
+ VerticalRadioButton {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: qsTr("Empty")
+ checked: root.selectedFlow === 0
+ onClicked: root.selectedFlow = 0
+ }
+
+ DividerType {
+ }
+
+ VerticalRadioButton {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: qsTr("xtls-rprx-vision")
+ checked: root.selectedFlow === 1
+ onClicked: root.selectedFlow = 1
+ }
+
+ DividerType {
+ }
+
+ VerticalRadioButton {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: qsTr("xtls-rprx-vision-udp443")
+ checked: root.selectedFlow === 2
+ onClicked: root.selectedFlow = 2
+ }
+
+ DividerType {
+ }
+
+ Item {
+ Layout.preferredHeight: 16
+ }
+ }
+ }
+
+ 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.setFlow(...)
+ }
+ Keys.onEnterPressed: clicked()
+ Keys.onReturnPressed: clicked()
+ }
+}
diff --git a/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml
new file mode 100644
index 000000000..377e359a8
--- /dev/null
+++ b/client/ui/qml/Pages2/PageProtocolXraySecuritySettings.qml
@@ -0,0 +1,316 @@
+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 selectedSecurity: 2 // 0=None, 1=TLS, 2=Reality
+
+ // Shared TLS + Reality fields
+ property string fingerprint: "Mozilla/5.0"
+ property string serverName: "cdn.example.com"
+
+ // TLS-only fields
+ property string alpn: "HTTP/2"
+
+ 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
+
+ Header2TextType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 0
+ Layout.bottomMargin: 24
+ text: qsTr("Security")
+ }
+
+ // ── Radio: None ───────────────────────────────────────────
+ VerticalRadioButton {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: qsTr("None")
+ checked: root.selectedSecurity === 0
+ onClicked: root.selectedSecurity = 0
+ }
+
+ DividerType {
+ }
+
+ // ── Radio: TLS ────────────────────────────────────────────
+ VerticalRadioButton {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: qsTr("TLS")
+ checked: root.selectedSecurity === 1
+ onClicked: root.selectedSecurity = 1
+ }
+
+ DividerType {
+ }
+
+ // ── Radio: Reality ────────────────────────────────────────
+ VerticalRadioButton {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: qsTr("Reality")
+ checked: root.selectedSecurity === 2
+ onClicked: root.selectedSecurity = 2
+ }
+
+ DividerType {
+ }
+
+ // ══════════════════════════════════════════════════════════
+ // TLS fields (ALPN + Fingerprint + SNI)
+ // ══════════════════════════════════════════════════════════
+ ColumnLayout {
+ visible: root.selectedSecurity === 1
+ Layout.fillWidth: true
+ spacing: 0
+
+ DropDownType {
+ id: tlsAlpnDropDown
+ Layout.fillWidth: true
+ Layout.topMargin: 16
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: root.alpn
+ descriptionText: qsTr("ALPN")
+ headerText: qsTr("ALPN")
+ drawerParent: root
+ listView: ListViewWithRadioButtonType {
+ rootWidth: root.width
+ model: ListModel {
+ ListElement {
+ name: "HTTP/2"
+ }
+ ListElement {
+ name: "HTTP/1.1"
+ }
+ ListElement {
+ name: "HTTP/2,HTTP/1.1"
+ }
+ }
+ clickedFunction: function () {
+ root.alpn = selectedText
+ tlsAlpnDropDown.text = selectedText
+ tlsAlpnDropDown.closeTriggered()
+ }
+ Component.onCompleted: {
+ for (var i = 0; i < model.count; i++) {
+ if (model.get(i).name === root.alpn) {
+ selectedIndex = i;
+ break
+ }
+ }
+ }
+ }
+ }
+
+ DropDownType {
+ id: tlsFingerprintDropDown
+ Layout.fillWidth: true
+ Layout.topMargin: 8
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: root.fingerprint
+ descriptionText: qsTr("Fingerprint")
+ headerText: qsTr("Fingerprint")
+ drawerParent: root
+ listView: ListViewWithRadioButtonType {
+ rootWidth: root.width
+ model: ListModel {
+ ListElement {
+ name: "Mozilla/5.0"
+ }
+ ListElement {
+ name: "Chrome"
+ }
+ ListElement {
+ name: "Firefox"
+ }
+ ListElement {
+ name: "Safari"
+ }
+ ListElement {
+ name: "iOS"
+ }
+ ListElement {
+ name: "Android"
+ }
+ ListElement {
+ name: "Edge"
+ }
+ ListElement {
+ name: "360"
+ }
+ ListElement {
+ name: "QQ"
+ }
+ ListElement {
+ name: "Random"
+ }
+ }
+ clickedFunction: function () {
+ root.fingerprint = selectedText
+ tlsFingerprintDropDown.text = selectedText
+ tlsFingerprintDropDown.closeTriggered()
+ }
+ Component.onCompleted: {
+ for (var i = 0; i < model.count; i++) {
+ if (model.get(i).name === root.fingerprint) {
+ selectedIndex = i;
+ break
+ }
+ }
+ }
+ }
+ }
+
+ TextFieldWithHeaderType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 8
+ headerText: qsTr("Server Name (SNI)")
+ textField.text: root.serverName
+ textField.onEditingFinished: root.serverName = textField.text
+ }
+ }
+
+ // ══════════════════════════════════════════════════════════
+ // Reality fields (Fingerprint + SNI)
+ // ══════════════════════════════════════════════════════════
+ ColumnLayout {
+ visible: root.selectedSecurity === 2
+ Layout.fillWidth: true
+ spacing: 0
+
+ DropDownType {
+ id: realityFingerprintDropDown
+ Layout.fillWidth: true
+ Layout.topMargin: 16
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ text: root.fingerprint
+ descriptionText: qsTr("Fingerprint")
+ headerText: qsTr("Fingerprint")
+ drawerParent: root
+ listView: ListViewWithRadioButtonType {
+ rootWidth: root.width
+ model: ListModel {
+ ListElement {
+ name: "Mozilla/5.0"
+ }
+ ListElement {
+ name: "Chrome"
+ }
+ ListElement {
+ name: "Firefox"
+ }
+ ListElement {
+ name: "Safari"
+ }
+ ListElement {
+ name: "iOS"
+ }
+ ListElement {
+ name: "Android"
+ }
+ ListElement {
+ name: "Edge"
+ }
+ ListElement {
+ name: "360"
+ }
+ ListElement {
+ name: "QQ"
+ }
+ ListElement {
+ name: "Random"
+ }
+ }
+ clickedFunction: function () {
+ root.fingerprint = selectedText
+ realityFingerprintDropDown.text = selectedText
+ realityFingerprintDropDown.closeTriggered()
+ }
+ Component.onCompleted: {
+ for (var i = 0; i < model.count; i++) {
+ if (model.get(i).name === root.fingerprint) {
+ selectedIndex = i;
+ break
+ }
+ }
+ }
+ }
+ }
+
+ TextFieldWithHeaderType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 8
+ headerText: qsTr("Server Name (SNI)")
+ textField.text: root.serverName
+ textField.onEditingFinished: root.serverName = textField.text
+ }
+ }
+
+ Item {
+ Layout.preferredHeight: 16
+ }
+ }
+ }
+
+ 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.setSecurity(...)
+ }
+ Keys.onEnterPressed: clicked()
+ Keys.onReturnPressed: clicked()
+ }
+}
diff --git a/client/ui/qml/Pages2/PageProtocolXraySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySettings.qml
index dd9455c9c..9739c1317 100644
--- a/client/ui/qml/Pages2/PageProtocolXraySettings.qml
+++ b/client/ui/qml/Pages2/PageProtocolXraySettings.qml
@@ -69,6 +69,9 @@ PageType {
implicitHeight: 40
image: "qrc:/images/controls/more-vertical.svg"
imageColor: AmneziaStyle.color.mutedGray
+ onClicked: function () {
+ PageController.goToPage(PageEnum.PageProtocolXrayConfigsSettings)
+ }
}
}
diff --git a/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml
index 093990fdb..c29136f4d 100644
--- a/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml
+++ b/client/ui/qml/Pages2/PageProtocolXrayTransportSettings.qml
@@ -35,6 +35,12 @@ PageType {
// Traffic Shaping
property string uplinkChunkSize: "0"
property string scMaxBufferedPosts: ""
+ property string scMaxEachPostBytesMin: "1"
+ property string scMaxEachPostBytesMax: "100"
+ property string scMinPostsIntervalMsMin: "100"
+ property string scMinPostsIntervalMsMax: "800"
+ property string scStreamUpServerSecsMin: "1"
+ property string scStreamUpServerSecsMax: "100"
// mKCP fields
property string mkcpTti: ""
property string mkcpUplinkCapacity: ""
@@ -603,41 +609,64 @@ PageType {
textField.onEditingFinished: root.scMaxBufferedPosts = textField.text
}
- // scMaxEachPostBytes → nav row
- LabelWithButtonType {
+ // scMaxEachPostBytes — min/max range
+ CaptionTextType {
Layout.fillWidth: true
- Layout.topMargin: 8
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 16
+ Layout.bottomMargin: 8
text: qsTr("scMaxEachPostBytes")
- descriptionText: qsTr("1—100")
- rightImageSource: "qrc:/images/controls/chevron-right.svg"
- clickedFunction: function () { /* navigate */
- }
+ color: AmneziaStyle.color.mutedGray
}
- DividerType {
+ MinMaxRowType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ minValue: root.scMaxEachPostBytesMin
+ maxValue: root.scMaxEachPostBytesMax
+ onMinChanged: root.scMaxEachPostBytesMin = val
+ onMaxChanged: root.scMaxEachPostBytesMax = val
}
- // scMinPostsIntervalMs → nav row
- LabelWithButtonType {
+ // scMinPostsIntervalMs — min/max range
+ CaptionTextType {
Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 16
+ Layout.bottomMargin: 8
text: qsTr("scMinPostsIntervalMs")
- descriptionText: qsTr("100—600ms")
- rightImageSource: "qrc:/images/controls/chevron-right.svg"
- clickedFunction: function () { /* navigate */
- }
+ color: AmneziaStyle.color.mutedGray
}
- DividerType {
+ MinMaxRowType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ minValue: root.scMinPostsIntervalMsMin
+ maxValue: root.scMinPostsIntervalMsMax
+ onMinChanged: root.scMinPostsIntervalMsMin = val
+ onMaxChanged: root.scMinPostsIntervalMsMax = val
}
- // scStreamUpServerSecs → nav row
- LabelWithButtonType {
+ // scStreamUpServerSecs — min/max range
+ CaptionTextType {
Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 16
+ Layout.bottomMargin: 8
text: qsTr("scStreamUpServerSecs")
- descriptionText: qsTr("1—100")
- rightImageSource: "qrc:/images/controls/chevron-right.svg"
- clickedFunction: function () { /* navigate */
- }
+ color: AmneziaStyle.color.mutedGray
}
- DividerType {
+ MinMaxRowType {
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ minValue: root.scStreamUpServerSecsMin
+ maxValue: root.scStreamUpServerSecsMax
+ onMinChanged: root.scStreamUpServerSecsMin = val
+ onMaxChanged: root.scStreamUpServerSecsMax = val
}
// ── Padding and multiplexing ──────────────────────────
diff --git a/client/ui/qml/qml.qrc b/client/ui/qml/qml.qrc
index 1314d3324..d70a531b6 100644
--- a/client/ui/qml/qml.qrc
+++ b/client/ui/qml/qml.qrc
@@ -78,6 +78,9 @@
Pages2/PageProtocolWireGuardSettings.qml
Pages2/PageProtocolXraySettings.qml
+ Pages2/PageProtocolXrayConfigsSettings.qml
+ Pages2/PageProtocolXrayFlowSettings.qml
+ Pages2/PageProtocolXraySecuritySettings.qml
Pages2/PageProtocolXrayTransportSettings.qml
Pages2/PageProtocolXrayXmuxSettings.qml
Pages2/PageProtocolXrayXPaddingSettings.qml