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