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