From 578ec6d6a8bceed6a1b8acc428fa13337d36a8f9 Mon Sep 17 00:00:00 2001 From: dranik Date: Wed, 17 Jun 2026 12:20:31 +0300 Subject: [PATCH] checking the parallelism of lines --- .../qml/Pages2/PageServiceMtProxySettings.qml | 3029 ++++++++-------- .../qml/Pages2/PageServiceTelemtSettings.qml | 3099 +++++++++-------- 2 files changed, 3074 insertions(+), 3054 deletions(-) diff --git a/client/ui/qml/Pages2/PageServiceMtProxySettings.qml b/client/ui/qml/Pages2/PageServiceMtProxySettings.qml index 85efad224..532857649 100644 --- a/client/ui/qml/Pages2/PageServiceMtProxySettings.qml +++ b/client/ui/qml/Pages2/PageServiceMtProxySettings.qml @@ -408,1586 +408,1595 @@ PageType { anchors.fill: parent enabled: !root.pageBusy - BackButtonType { - id: backButton - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 20 + PageController.safeAreaTopMargin + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + PageController.safeAreaTopMargin - onFocusChanged: { - if (this.activeFocus) { - if (mainTabBar.currentIndex === 0) { - connectionListView.positionViewAtBeginning() - } else { - settingsListView.positionViewAtBeginning() - } - } - } - } - - ColumnLayout { - id: pageHeader - anchors.top: backButton.bottom - anchors.left: parent.left - anchors.right: parent.right - spacing: 0 - - BaseHeaderType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 24 - - headerText: qsTr("MTProxy settings") - descriptionLinkText: qsTr("Read more about this settings") - descriptionLinkUrl: "https://core.telegram.org/proxy" - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.topMargin: 8 - visible: root.mtProxyNetworkBlocked - text: qsTr("No internet connection. Connect to the internet to change MTProxy settings.") - color: AmneziaStyle.color.mutedGray - wrapMode: Text.WordWrap - font.pixelSize: 14 - } - } - - TabBar { - id: mainTabBar - anchors.top: pageHeader.bottom - anchors.left: parent.left - anchors.right: parent.right - width: parent.width - - background: Rectangle { - color: AmneziaStyle.color.transparent - Rectangle { - width: parent.width - height: 1 - anchors.bottom: parent.bottom - color: AmneziaStyle.color.slateGray - } - } - - TabButtonType { - text: qsTr("Connection") - isSelected: mainTabBar.currentIndex === 0 - } - TabButtonType { - text: qsTr("Settings") - isSelected: mainTabBar.currentIndex === 1 - } - } - - StackLayout { - id: tabContent - anchors.top: mainTabBar.bottom - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - currentIndex: mainTabBar.currentIndex - - ListViewType { - id: connectionListView - model: MtProxyConfigModel - - delegate: ColumnLayout { - width: connectionListView.width - spacing: 0 - - property int secretTabIndex: root.syncedSecretTabIndex - - function activeSecret() { - return root.mtProxyClientSecretForTabIndex(secret, root.syncedSecretTabIndex, - root.savedTlsDomain, MtProxyConfigModel.defaultTlsDomain()) - } - - function effectiveSecret() { - return activeSecret() - } - - function effectiveHost() { - return root.savedPublicHost !== "" ? root.savedPublicHost : ServersUiController.serverHostName(ServersUiController.processedServerId) - } - - function tmeLink() { - return "https://t.me/proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() - } - - function tgLink() { - return "tg://proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() - } - - CaptionTextType { - Layout.fillWidth: true - Layout.topMargin: 24 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - text: qsTr("Use Telegram connection link") - color: AmneziaStyle.color.mutedGray - } - - Rectangle { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - implicitHeight: linkRow.implicitHeight + 16 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 - - RowLayout { - id: linkRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 8 - spacing: 4 - - CaptionTextType { - Layout.fillWidth: true - text: secret !== "" ? tmeLink() : qsTr("Deploy MTProxy first") - color: secret !== "" ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray - elide: Text.ElideRight - maximumLineCount: 1 - font.pixelSize: 13 - } - - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/qr-code.svg" - imageColor: AmneziaStyle.color.paleGray - visible: secret !== "" - onClicked: { - ExportController.generateQrFromString(tmeLink()) - PageController.goToShareConnectionPage( - qsTr("Telegram connection link"), - qsTr("MTProxy connection link"), - "", "", "") - } - } - - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - visible: secret !== "" - onClicked: { - GC.copyToClipBoard(tmeLink()) - PageController.showNotificationMessage(qsTr("Copied")) - } - } - } - } - - Rectangle { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - implicitHeight: tgLinkRow.implicitHeight + 16 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 - visible: secret !== "" - - RowLayout { - id: tgLinkRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 8 - spacing: 4 - - CaptionTextType { - Layout.fillWidth: true - text: tgLink() - color: AmneziaStyle.color.goldenApricot - elide: Text.ElideRight - maximumLineCount: 1 - font.pixelSize: 13 - } - - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/qr-code.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: { - ExportController.generateQrFromString(tgLink()) - PageController.goToShareConnectionPage( - qsTr("Telegram connection link"), - qsTr("MTProxy connection link"), - "", "", "") - } - } - - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: { - GC.copyToClipBoard(tgLink()) - PageController.showNotificationMessage(qsTr("Copied")) - } - } - } - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - spacing: 4 - - CaptionTextType { - text: qsTr("Or enter the proxy details manually.") - color: AmneziaStyle.color.mutedGray - } - - CaptionTextType { - Layout.fillWidth: true - text: qsTr("How to do it") - color: AmneziaStyle.color.goldenApricot - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: Qt.openUrlExternally("https://core.telegram.org/proxy") - } - } - - Item { - Layout.fillWidth: true - } - } - - Rectangle { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 32 - implicitHeight: manualCol.implicitHeight + 8 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 - - ColumnLayout { - id: manualCol - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.topMargin: 8 - spacing: 0 - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 12 - Layout.rightMargin: 8 - Layout.bottomMargin: 8 - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - CaptionTextType { - text: qsTr("Host") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - } - CaptionTextType { - Layout.fillWidth: true - text: effectiveHost() - color: AmneziaStyle.color.paleGray - elide: Text.ElideRight - } - } - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: { GC.copyToClipBoard(effectiveHost()) - PageController.showNotificationMessage(qsTr("Copied")) } - } - } - - DividerType { - Layout.fillWidth: true - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 12 - Layout.rightMargin: 8 - Layout.topMargin: 8 - Layout.bottomMargin: 8 - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - CaptionTextType { - text: qsTr("Port") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - } - CaptionTextType { - Layout.fillWidth: true - text: port - color: AmneziaStyle.color.paleGray - } - } - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: { GC.copyToClipBoard(port) - PageController.showNotificationMessage(qsTr("Copied")) } - } - } - - DividerType { - Layout.fillWidth: true - } - - ButtonGroup { - id: secretTabGroup - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 12 - Layout.rightMargin: 8 - Layout.topMargin: 4 - Layout.bottomMargin: 8 - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - CaptionTextType { - text: qsTr("Secret") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - } - CaptionTextType { - Layout.fillWidth: true - text: activeSecret() - color: AmneziaStyle.color.paleGray - wrapMode: Text.WrapAnywhere - font.pixelSize: 13 - } - } - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: { GC.copyToClipBoard(activeSecret()) - PageController.showNotificationMessage(qsTr("Copied")) } - } - } - } - } - - LabelWithButtonType { - id: removeButton - Layout.fillWidth: true - Layout.bottomMargin: 24 - Layout.leftMargin: 0 - Layout.rightMargin: 16 - visible: ServersUiController.isProcessedServerHasWriteAccess() - text: qsTr("Delete MTProxy") - textColor: AmneziaStyle.color.vibrantRed - clickedFunction: function () { - var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) - var descriptionText = qsTr("The proxy will be stopped and all users will lose access.") - var yesButtonText = qsTr("Continue") - var noButtonText = qsTr("Cancel") - var yesButtonFunction = function () { - PageController.goToPage(PageEnum.PageDeinstalling) - InstallController.removeContainer(ServersUiController.processedServerId, ServersUiController.processedContainerIndex) - } - showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, function () { - }) - } - MouseArea { - anchors.fill: removeButton - cursorShape: Qt.PointingHandCursor - enabled: false + onFocusChanged: { + if (this.activeFocus) { + if (mainTabBar.currentIndex === 0) { + connectionListView.positionViewAtBeginning() + } else { + settingsListView.positionViewAtBeginning() } } } } - ListViewType { - id: settingsListView - model: MtProxyConfigModel - reuseItems: false + ColumnLayout { + id: pageHeader + anchors.top: backButton.bottom + anchors.left: parent.left + anchors.right: parent.right + spacing: 0 - delegate: ColumnLayout { - id: settingsRoot - width: settingsListView.width - spacing: 0 + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 24 - readonly property bool fieldsEditable: isEnabled && containerStatus === 1 && !root.pageBusy + headerText: qsTr("MTProxy settings") + descriptionLinkText: qsTr("Read more about this settings") + descriptionLinkUrl: "https://core.telegram.org/proxy" + } - function mtProxyActiveSecretForBaseHex(baseHex) { - return root.mtProxyClientSecretForTabIndex(baseHex, root.syncedSecretTabIndex, - root.savedTlsDomain, MtProxyConfigModel.defaultTlsDomain()) + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + visible: root.mtProxyNetworkBlocked + text: qsTr("No internet connection. Connect to the internet to change MTProxy settings.") + color: AmneziaStyle.color.mutedGray + wrapMode: Text.WordWrap + font.pixelSize: 14 + } + } + + TabBar { + id: mainTabBar + anchors.top: pageHeader.bottom + anchors.left: parent.left + anchors.right: parent.right + width: parent.width + + background: Rectangle { + color: AmneziaStyle.color.transparent + Rectangle { + width: parent.width + height: 1 + anchors.bottom: parent.bottom + color: AmneziaStyle.color.slateGray } + } - function mtProxyEffectiveHostForLinks() { - return root.savedPublicHost !== "" ? root.savedPublicHost : ServersUiController.serverHostName(ServersUiController.processedServerId) - } + TabButtonType { + text: qsTr("Connection") + isSelected: mainTabBar.currentIndex === 0 + } + TabButtonType { + text: qsTr("Settings") + isSelected: mainTabBar.currentIndex === 1 + } + } - function mtProxyTmeLinkForAdditional(baseHex) { - return "https://t.me/proxy?server=" + mtProxyEffectiveHostForLinks() + "&port=" + port + "&secret=" + mtProxyActiveSecretForBaseHex(baseHex) - } + StackLayout { + id: tabContent + anchors.top: mainTabBar.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + currentIndex: mainTabBar.currentIndex - function mtProxyTgLinkForAdditional(baseHex) { - return "tg://proxy?server=" + mtProxyEffectiveHostForLinks() + "&port=" + port + "&secret=" + mtProxyActiveSecretForBaseHex(baseHex) - } + ListViewType { + id: connectionListView + model: MtProxyConfigModel - function mtProxyIsAdditionalPersisted(hex) { - return root.mtProxyIsPersistedAdditionalHex(hex) - } - - function mtProxyCopyText(text) { - GC.copyToClipBoard(text) - PageController.showNotificationMessage(qsTr("Copied")) - } - - function mtProxyShareQr(link) { - ExportController.generateQrFromString(link) - PageController.goToShareConnectionPage(qsTr("Telegram connection link"), - qsTr("MTProxy connection link"), "", "", "") - } - - function mtProxyRemoveAdditionalSecret(idx) { - // Symmetric with "Add additional secret": only mutate the model here; the change - // is pushed to the server on Save (no immediate server update on add/remove). - MtProxyConfigModel.removeAdditionalSecret(idx) - } - - SwitcherType { - id: enableMtProxySwitch - Layout.fillWidth: true - Layout.topMargin: 24 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - text: qsTr("Enable MTProxy") - checked: isEnabled - enabled: containerStatus !== 0 && containerStatus !== 3 && !root.pageBusy - && !root.mtProxyNetworkBlocked - onToggled: function () { - if (checked !== isEnabled) { - previousEnabled = isEnabled - previousContainerStatus = containerStatus - root.previousSecret = secret - isEnabled = checked - isUpdating = true - if (checked) { - root.pendingUpdateAfterEnable = true - InstallController.setContainerEnabled(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, true) - } else { - InstallController.setContainerEnabled(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, false) - } - } - } - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - visible: !fieldsEditable && !root.pageBusy - text: (containerStatus === 1 || containerStatus === 2) - ? qsTr("Enable MTProxy to edit settings") - : (statusErrorCode !== 0 - ? qsTr("Settings locked: connection timed out (error code %1). Re-open the page to retry.").arg(statusErrorCode) - : qsTr("Cannot reach the server — settings are unavailable")) - color: AmneziaStyle.color.mutedGray - wrapMode: Text.WordWrap - } - - ColumnLayout { - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 * 2 - spacing: 4 - - CaptionTextType { - text: qsTr("Base secret") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - } - - RowLayout { - Layout.fillWidth: true - spacing: 8 - - CaptionTextType { - Layout.fillWidth: true - text: secret !== "" ? mtProxyActiveSecretForBaseHex(secret) : qsTr("Not generated") - color: secret !== "" ? AmneziaStyle.color.paleGray : AmneziaStyle.color.mutedGray - wrapMode: Text.WrapAnywhere - font.pixelSize: 14 - } - - ImageButtonType { - Layout.alignment: Qt.AlignTop - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/refresh-cw.svg" - imageColor: AmneziaStyle.color.paleGray - visible: ServersUiController.isProcessedServerHasWriteAccess() - enabled: fieldsEditable - onClicked: { - var secretSnapshot = secret - showQuestionDrawer( - qsTr("Generate new secret?"), - qsTr("All existing connection links will stop working. Users will need new links."), - qsTr("Generate"), - qsTr("Cancel"), - function () { - root.previousSecret = secretSnapshot - if (containerStatus === 1) { - isUpdating = true - MtProxyConfigModel.generateSecret() - root.mtProxyScheduleUpdate(false) - } else { - MtProxyConfigModel.generateSecret() - PageController.showNotificationMessage(qsTr("New secret saved. It will be applied when MTProxy is started.")) - } - }, - function () { - } - ) - } - } - } - } - - TextFieldWithHeaderType { - id: publicHostTextField - enabled: fieldsEditable - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 4 - headerText: qsTr("Public host / IP") - textField.placeholderText: ServersUiController.serverHostName(ServersUiController.processedServerId) - textField.text: publicHost - textField.maximumLength: 253 - textField.validator: PublicHostInputValidator { - } - textField.onTextChanged: { - var t = publicHostTextField.textField.text - if (MtProxyConfigModel.isPublicHostTypingIncomplete(t)) { - publicHostTextField.errorText = "" - } else if (!MtProxyConfigModel.isValidPublicHost(t)) { - publicHostTextField.errorText = qsTr("Enter a valid IP address or domain name") - } else { - publicHostTextField.errorText = "" - } - } - textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - if (!MtProxyConfigModel.isValidPublicHost(textField.text)) { - publicHostTextField.errorText = qsTr("Enter a valid IP address or domain name") - return - } - publicHostTextField.errorText = "" - if (textField.text !== publicHost) { - publicHost = textField.text - MtProxyConfigModel.setPublicHost(publicHost) - } - } - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 4 - visible: publicHostTextField.textField.text === "" - text: qsTr("Leave empty to use server IP automatically") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - wrapMode: Text.WordWrap - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 12 - visible: publicHostTextField.textField.text !== "" && - publicHostTextField.textField.text !== ServersUiController.serverHostName(ServersUiController.processedServerId) - text: qsTr("⚠ This overrides the server IP in connection links. Make sure this host/domain points to your server.") - color: AmneziaStyle.color.goldenApricot - font.pixelSize: 12 - wrapMode: Text.WordWrap - } - - TextFieldWithHeaderType { - id: portTextField - enabled: fieldsEditable - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - headerText: qsTr("Server port") - textField.placeholderText: MtProxyConfigModel.defaultPort() - textField.maximumLength: 5 - textField.inputMethodHints: Qt.ImhDigitsOnly - textField.validator: IntValidator { - bottom: 1 - top: 65535 - } - Component.onCompleted: { - var savedPort = port - textField.text = (savedPort === MtProxyConfigModel.defaultPort()) ? "" : savedPort - } - textField.onTextChanged: { - var cur = portTextField.textField.text - var clean = MtProxyConfigModel.sanitizePortFieldText(cur) - if (clean !== cur) { - textField.text = clean - textField.cursorPosition = clean.length - } - } - textField.onEditingFinished: { - textField.text = MtProxyConfigModel.sanitizePortFieldText(textField.text) - } - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 12 - visible: transportMode === "faketls" && portTextField.textField.text !== "443" && portTextField.textField.text !== "" - text: qsTr("FakeTLS may not work on ports other than 443") - color: AmneziaStyle.color.goldenApricot - font.pixelSize: 12 - wrapMode: Text.WordWrap - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - text: qsTr("The promoted channel is set in @MTProxyBot. Paste the proxy tag here: exactly 32 hexadecimal characters (0-9, A-F), as in the bot message — or leave empty.") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - wrapMode: Text.WordWrap - } - - TextFieldWithHeaderType { - id: tagTextField - enabled: fieldsEditable - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - headerText: qsTr("Promoted channel tag (optional)") - textField.placeholderText: qsTr("32 hex chars from @MTProxyBot (e.g. 3b7b2fa9…)") - textField.text: tag - textField.maximumLength: MtProxyConfigModel.mtProxyBotTagHexLength() - textField.onTextChanged: { - var cur = tagTextField.textField.text - var clean = MtProxyConfigModel.sanitizeMtProxyTagFieldText(cur) - if (clean !== cur) { - textField.text = clean - textField.cursorPosition = clean.length - return - } - var tt = tagTextField.textField.text - if (tt === "") { - tagTextField.errorText = "" - return - } - if (MtProxyConfigModel.isMtProxyTagTypingIncomplete(tt)) { - tagTextField.errorText = "" - return - } - if (!MtProxyConfigModel.isValidMtProxyTag(tt)) { - tagTextField.errorText = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F).") - return - } - tagTextField.errorText = "" - } - textField.onEditingFinished: { - var raw = textField.text.replace(/^\s+|\s+$/g, '') - var normalized = MtProxyConfigModel.sanitizeMtProxyTagFieldText(raw) - textField.text = normalized - if (!MtProxyConfigModel.isValidMtProxyTag(normalized)) { - tagTextField.errorText = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F). Leave empty if unused.") - return - } - tagTextField.errorText = "" - if (normalized !== tag) { - tag = normalized - MtProxyConfigModel.setTag(tag) - } - } - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - spacing: 4 - - CaptionTextType { - text: qsTr("Get a tag from") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - } - CaptionTextType { - text: "@MTProxyBot" - color: AmneziaStyle.color.goldenApricot - font.pixelSize: 12 - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: Qt.openUrlExternally("https://t.me/MTProxyBot") - } - } - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.topMargin: 16 * 2 - text: qsTr("Transport mode") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - } - - DropDownType { - id: transportModeDropDown - enabled: fieldsEditable - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - - drawerParent: root - drawerHeight: 0.4 - headerText: qsTr("Transport mode") - text: transportMode === "faketls" ? qsTr("FakeTLS") : qsTr("Standard MTProto") - - listView: Component { - ListViewType { - model: [qsTr("Standard MTProto"), qsTr("FakeTLS")] - delegate: LabelWithButtonType { - Layout.fillWidth: true - text: modelData - rightImageSource: { - var isCurrent = (index === 0 && transportMode === "standard") || - (index === 1 && transportMode === "faketls") - return isCurrent ? "qrc:/images/controls/check.svg" : "" - } - rightImageColor: AmneziaStyle.color.goldenApricot - clickedFunction: function () { - transportMode = (index === 0) ? "standard" : "faketls" - MtProxyConfigModel.setTransportMode(transportMode) - root.syncedSecretTabIndex = transportMode === "faketls" ? 1 : 0 - transportModeDropDown.closeTriggered() - } - } - } - } - } - - TextFieldWithHeaderType { - id: tlsDomainTextField - enabled: fieldsEditable - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - visible: transportMode === "faketls" - headerText: qsTr("FakeTLS domain") - textField.placeholderText: root.previousTlsDomain - textField.validator: RegularExpressionValidator { - regularExpression: /^[A-Za-z0-9.-]*$/ - } - Component.onCompleted: { - var savedDomain = tlsDomain - textField.text = (savedDomain === MtProxyConfigModel.defaultTlsDomain() || savedDomain === "") ? "" : savedDomain - } - textField.onTextChanged: { - var t = tlsDomainTextField.textField.text - if (t === "" || MtProxyConfigModel.isFakeTlsDomainTypingIncomplete(t) - || MtProxyConfigModel.isValidFakeTlsDomain(t)) { - tlsDomainTextField.errorText = "" - } else { - tlsDomainTextField.errorText = qsTr("Enter a valid domain name") - } - } - textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - var domainValue = textField.text === "" ? MtProxyConfigModel.defaultTlsDomain() : textField.text - if (!MtProxyConfigModel.isValidFakeTlsDomain(domainValue)) { - tlsDomainTextField.errorText = qsTr("Enter a valid domain name") - return - } - tlsDomainTextField.errorText = "" - if (domainValue !== tlsDomain) { - tlsDomain = domainValue - MtProxyConfigModel.setTlsDomain(tlsDomain) - } - } - } - - ColumnLayout { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - spacing: 4 - visible: transportMode === "faketls" - - CaptionTextType { - Layout.fillWidth: true - text: qsTr("The domain is encoded into the FakeTLS client secret (ee + base_secret + hex(domain)). It must support HTTPS / TLS 1.3.") - color: AmneziaStyle.color.mutedGray - wrapMode: Text.WordWrap - font.pixelSize: 12 - } - CaptionTextType { - Layout.fillWidth: true - text: qsTr("\u26a0 Changing the domain will invalidate all previously issued FakeTLS connection links.") - color: AmneziaStyle.color.goldenApricot - wrapMode: Text.WordWrap - font.pixelSize: 12 - } - } - - LabelWithButtonType { - id: advancedHeader - Layout.fillWidth: true - Layout.leftMargin: 0 - Layout.rightMargin: 16 - property bool expanded: false - text: qsTr("Advanced") - rightImageSource: expanded - ? "qrc:/images/controls/chevron-up.svg" - : "qrc:/images/controls/chevron-down.svg" - rightImageColor: AmneziaStyle.color.mutedGray - clickedFunction: function () { - expanded = !expanded - } - } - - ColumnLayout { - Layout.fillWidth: true + delegate: ColumnLayout { + width: connectionListView.width spacing: 0 - visible: advancedHeader.expanded - enabled: fieldsEditable + + property int secretTabIndex: root.syncedSecretTabIndex + + function activeSecret() { + return root.mtProxyClientSecretForTabIndex(secret, root.syncedSecretTabIndex, + root.savedTlsDomain, MtProxyConfigModel.defaultTlsDomain()) + } + + function effectiveSecret() { + return activeSecret() + } + + function effectiveHost() { + return root.savedPublicHost !== "" ? root.savedPublicHost : ServersUiController.serverHostName(ServersUiController.processedServerId) + } + + function tmeLink() { + return "https://t.me/proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() + } + + function tgLink() { + return "tg://proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() + } CaptionTextType { + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("Use Telegram connection link") + color: AmneziaStyle.color.mutedGray + } + + Rectangle { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - Layout.topMargin: 8 - Layout.bottomMargin: 4 - text: qsTr("Additional secrets") - color: AmneziaStyle.color.mutedGray + Layout.bottomMargin: 16 + implicitHeight: linkRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + RowLayout { + id: linkRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: secret !== "" ? tmeLink() : qsTr("Deploy MTProxy first") + color: secret !== "" ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + visible: secret !== "" + onClicked: { + ExportController.generateQrFromString(tmeLink()) + PageController.goToShareConnectionPage( + qsTr("Telegram connection link"), + qsTr("MTProxy connection link"), + "", "", "") + } + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + visible: secret !== "" + onClicked: { + GC.copyToClipBoard(tmeLink()) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + } } - CaptionTextType { + + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + implicitHeight: tgLinkRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + visible: secret !== "" + + RowLayout { + id: tgLinkRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: tgLink() + color: AmneziaStyle.color.goldenApricot + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { + ExportController.generateQrFromString(tgLink()) + PageController.goToShareConnectionPage( + qsTr("Telegram connection link"), + qsTr("MTProxy connection link"), + "", "", "") + } + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { + GC.copyToClipBoard(tgLink()) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + } + } + + RowLayout { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.bottomMargin: 8 - text: qsTr("Add extra secrets to allow gradual migration without disconnecting existing users.") - color: AmneziaStyle.color.charcoalGray - wrapMode: Text.WordWrap - font.pixelSize: 12 + spacing: 4 + + CaptionTextType { + text: qsTr("Or enter the proxy details manually.") + color: AmneziaStyle.color.mutedGray + } + + CaptionTextType { + Layout.fillWidth: true + text: qsTr("How to do it") + color: AmneziaStyle.color.goldenApricot + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally("https://core.telegram.org/proxy") + } + } + + Item { + Layout.fillWidth: true + } } - Repeater { - model: additionalSecrets - delegate: ColumnLayout { - id: addSecretDelegate - property bool linksExpanded: false - readonly property bool linksPanelAllowed: settingsRoot.mtProxyIsAdditionalPersisted(modelData) - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 32 + implicitHeight: manualCol.implicitHeight + 8 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + ColumnLayout { + id: manualCol + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: 8 spacing: 0 - onLinksPanelAllowedChanged: { - if (!linksPanelAllowed) { - linksExpanded = false - } - } - - Rectangle { + RowLayout { Layout.fillWidth: true - implicitHeight: collapsedBar.implicitHeight + 16 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 - - RowLayout { - id: collapsedBar - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 8 - spacing: 8 - - Item { + Layout.leftMargin: 12 + Layout.rightMargin: 8 + Layout.bottomMargin: 8 + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + CaptionTextType { + text: qsTr("Host") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { Layout.fillWidth: true - implicitHeight: Math.max(hexCaption.implicitHeight, 24) - - RowLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - spacing: 8 - - CaptionTextType { - id: hexCaption - Layout.fillWidth: true - text: settingsRoot.mtProxyActiveSecretForBaseHex(modelData) - color: AmneziaStyle.color.paleGray - elide: Text.ElideMiddle - font.pixelSize: 13 - } - - Image { - width: 24 - height: 24 - visible: addSecretDelegate.linksPanelAllowed - source: "qrc:/images/controls/chevron-down.svg" - sourceSize.width: 24 - sourceSize.height: 24 - rotation: addSecretDelegate.linksExpanded ? 180 : 0 - Behavior on rotation { NumberAnimation { duration: 150 } } - } - } - - MouseArea { - anchors.fill: parent - visible: addSecretDelegate.linksPanelAllowed - enabled: addSecretDelegate.linksPanelAllowed - cursorShape: Qt.PointingHandCursor - onClicked: addSecretDelegate.linksExpanded = !addSecretDelegate.linksExpanded - } - } - - ImageButtonType { - implicitWidth: 32 - implicitHeight: 32 - hoverEnabled: true - image: "qrc:/images/controls/trash.svg" - imageColor: AmneziaStyle.color.vibrantRed - onClicked: settingsRoot.mtProxyRemoveAdditionalSecret(index) + text: effectiveHost() + color: AmneziaStyle.color.paleGray + elide: Text.ElideRight } } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(effectiveHost()) + PageController.showNotificationMessage(qsTr("Copied")) } + } } - ColumnLayout { + DividerType { Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 8 Layout.topMargin: 8 - spacing: 8 - visible: addSecretDelegate.linksPanelAllowed && addSecretDelegate.linksExpanded - - CaptionTextType { + Layout.bottomMargin: 8 + ColumnLayout { Layout.fillWidth: true - text: qsTr("Use Telegram connection link") - color: AmneziaStyle.color.mutedGray - } - - Rectangle { - Layout.fillWidth: true - implicitHeight: expTmeRow.implicitHeight + 16 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 - - RowLayout { - id: expTmeRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 8 - spacing: 4 - - CaptionTextType { - Layout.fillWidth: true - text: settingsRoot.mtProxyTmeLinkForAdditional(modelData) - color: AmneziaStyle.color.goldenApricot - elide: Text.ElideRight - maximumLineCount: 1 - font.pixelSize: 13 - } - - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/qr-code.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: settingsRoot.mtProxyShareQr(settingsRoot.mtProxyTmeLinkForAdditional(modelData)) - } - - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: settingsRoot.mtProxyCopyText(settingsRoot.mtProxyTmeLinkForAdditional(modelData)) - } + spacing: 2 + CaptionTextType { + text: qsTr("Port") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: port + color: AmneziaStyle.color.paleGray } } - - Rectangle { - Layout.fillWidth: true - implicitHeight: expTgRow.implicitHeight + 16 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 - - RowLayout { - id: expTgRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 8 - spacing: 4 - - CaptionTextType { - Layout.fillWidth: true - text: settingsRoot.mtProxyTgLinkForAdditional(modelData) - color: AmneziaStyle.color.goldenApricot - elide: Text.ElideRight - maximumLineCount: 1 - font.pixelSize: 13 - } - - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/qr-code.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: settingsRoot.mtProxyShareQr(settingsRoot.mtProxyTgLinkForAdditional(modelData)) - } - - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: settingsRoot.mtProxyCopyText(settingsRoot.mtProxyTgLinkForAdditional(modelData)) - } - } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(port) + PageController.showNotificationMessage(qsTr("Copied")) } } } - } - } - BasicButtonType { - Layout.fillWidth: true - Layout.topMargin: 8 - - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - text: qsTr("Add additional secret") - clickedFunc: function () { - MtProxyConfigModel.addAdditionalSecret() - } - } - - DividerType { - Layout.fillWidth: true - Layout.bottomMargin: 8 - } - - LabelTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.bottomMargin: 4 - text: qsTr("Worker mode") - } - - ButtonGroup { - id: workerModeGroup - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 4 - spacing: 0 - visible: transportMode !== "faketls" - - HorizontalRadioButton { - Layout.fillWidth: true - text: qsTr("Auto") - ButtonGroup.group: workerModeGroup - checked: workersMode === "auto" - onClicked: { workersMode = "auto"; MtProxyConfigModel.setWorkersMode("auto") } - } - HorizontalRadioButton { - Layout.fillWidth: true - text: qsTr("Manual") - ButtonGroup.group: workerModeGroup - checked: workersMode === "manual" - onClicked: { workersMode = "manual"; MtProxyConfigModel.setWorkersMode("manual") } - } - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - visible: transportMode === "faketls" - text: qsTr("Workers are set to 0 automatically for FakeTLS mode.") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - wrapMode: Text.WordWrap - } - - TextFieldWithHeaderType { - id: workersTextField - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - visible: workersMode === "manual" && transportMode !== "faketls" - headerText: qsTr("Workers count") - textField.placeholderText: "2" - textField.text: workers - textField.maximumLength: 2 - textField.inputMethodHints: Qt.ImhDigitsOnly - textField.validator: IntValidator { - bottom: 0 - top: MtProxyConfigModel.maxWorkers() - } - textField.onTextChanged: { - var cur = workersTextField.textField.text - if (cur === "") { - return + DividerType { + Layout.fillWidth: true } - var n = parseInt(cur, 10) - var maxW = MtProxyConfigModel.maxWorkers() - if (isNaN(n) || n < 0) { n = 0 } - if (n > maxW) { n = maxW } - var clamped = String(n) - if (clamped !== cur) { - textField.text = clamped - textField.cursorPosition = clamped.length + + ButtonGroup { + id: secretTabGroup } - } - textField.onEditingFinished: { - var v = workersTextField.textField.text - if (v !== "") { - var m = parseInt(v, 10) - var maxW2 = MtProxyConfigModel.maxWorkers() - if (isNaN(m) || m < 0) { m = 0 } - if (m > maxW2) { m = maxW2 } - v = String(m) - textField.text = v + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 8 + Layout.topMargin: 4 + Layout.bottomMargin: 8 + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + CaptionTextType { + text: qsTr("Secret") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: activeSecret() + color: AmneziaStyle.color.paleGray + wrapMode: Text.WrapAnywhere + font.pixelSize: 13 + } + } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(activeSecret()) + PageController.showNotificationMessage(qsTr("Copied")) } + } } - if (v !== workers) { - workers = v - MtProxyConfigModel.setWorkers(workers) - } - } - } - - DividerType { - Layout.fillWidth: true - Layout.bottomMargin: 8 - } - - SwitcherType { - Layout.fillWidth: true - Layout.rightMargin: 16 - Layout.leftMargin: 16 - Layout.bottomMargin: 4 - text: qsTr("Server is behind NAT / Docker bridge") - descriptionText: qsTr("Enable if your server is not directly accessible from the internet, e.g. Docker or private network") - checked: natEnabled - onToggled: function () { - if (checked !== natEnabled) { - natEnabled = checked - MtProxyConfigModel.setNatEnabled(natEnabled) - } - } - } - - TextFieldWithHeaderType { - id: natInternalIpTextField - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - visible: natEnabled - headerText: qsTr("Internal IP") - textField.placeholderText: "172.17.0.2" - textField.text: natInternalIp - textField.maximumLength: 15 - textField.validator: RegularExpressionValidator { - regularExpression: root.natIpv4InputFormat - } - textField.onTextChanged: { - if (root.natIpv4FieldShowInvalidError(textField.text)) { - natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") - } else { - natInternalIpTextField.errorText = "" - } - } - textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - if (!MtProxyConfigModel.isValidOptionalIpv4(textField.text)) { - natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") - return - } - natInternalIpTextField.errorText = "" - if (textField.text !== natInternalIp) { - natInternalIp = textField.text - MtProxyConfigModel.setNatInternalIp(natInternalIp) - } - } - } - - TextFieldWithHeaderType { - id: natExternalIpTextField - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - visible: natEnabled - headerText: qsTr("External IP") - textField.placeholderText: "1.2.3.4" - textField.text: natExternalIp - textField.maximumLength: 15 - textField.validator: RegularExpressionValidator { - regularExpression: root.natIpv4InputFormat - } - textField.onTextChanged: { - if (root.natIpv4FieldShowInvalidError(textField.text)) { - natExternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") - } else { - natExternalIpTextField.errorText = "" - } - } - textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - if (!MtProxyConfigModel.isValidOptionalIpv4(textField.text)) { - natExternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") - return - } - natExternalIpTextField.errorText = "" - if (textField.text !== natExternalIp) { - natExternalIp = textField.text - MtProxyConfigModel.setNatExternalIp(natExternalIp) - } - } - } - } - - DividerType { - Layout.fillWidth: true - Layout.topMargin: 8 - } - - ColumnLayout { - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - spacing: 8 - visible: containerStatus === 1 - - RowLayout { - Layout.fillWidth: true - - Header2Type { - Layout.fillWidth: true - headerText: qsTr("Diagnostics") - } - - ImageButtonType { - implicitWidth: 32 - implicitHeight: 32 - image: "qrc:/images/controls/refresh-cw.svg" - imageColor: diagLoading ? AmneziaStyle.color.mutedGray : AmneziaStyle.color.paleGray - hoverEnabled: !diagLoading - enabled: !diagLoading - onClicked: { - diagLoading = true - InstallController.refreshContainerDiagnostics(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, parseInt(port)) - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 8 - Rectangle { - width: 8 - height: 8 - radius: 4 - color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray - } - CaptionTextType { - Layout.fillWidth: true - text: qsTr("Public port reachable") - color: AmneziaStyle.color.paleGray - } - CaptionTextType { - text: diagClientsConnected < 0 ? qsTr("—") : (diagPortReachable ? qsTr("Yes") : qsTr("No")) - color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 8 - Rectangle { - width: 8 - height: 8 - radius: 4 - color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray - } - CaptionTextType { - Layout.fillWidth: true - text: qsTr("Telegram upstream reachable") - color: AmneziaStyle.color.paleGray - } - CaptionTextType { - text: diagClientsConnected < 0 ? qsTr("—") : (diagTelegramReachable ? qsTr("Yes") : qsTr("No")) - color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 8 - Rectangle { - width: 8 - height: 8 - radius: 4 - color: diagClientsConnected >= 0 ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray - } - CaptionTextType { - Layout.fillWidth: true - text: qsTr("Clients connected") - color: AmneziaStyle.color.paleGray - } - CaptionTextType { - text: diagClientsConnected < 0 ? qsTr("—") : diagClientsConnected.toString() - color: AmneziaStyle.color.paleGray - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 8 - Rectangle { - width: 8 - height: 8 - radius: 4 - color: AmneziaStyle.color.mutedGray - } - CaptionTextType { - Layout.fillWidth: true - text: qsTr("Last config refresh") - color: AmneziaStyle.color.paleGray - } - CaptionTextType { - text: diagLastConfigRefresh !== "" ? diagLastConfigRefresh : qsTr("—") - color: AmneziaStyle.color.mutedGray } } LabelWithButtonType { + id: removeButton Layout.fillWidth: true - Layout.leftMargin: -16 - visible: diagStatsEndpoint !== "" - text: qsTr("Stats endpoint") - descriptionText: diagStatsEndpoint - descriptionOnTop: true - rightImageSource: "qrc:/images/controls/copy.svg" - rightImageColor: AmneziaStyle.color.paleGray + Layout.bottomMargin: 24 + Layout.leftMargin: 0 + Layout.rightMargin: 16 + visible: ServersUiController.isProcessedServerHasWriteAccess() + text: qsTr("Delete MTProxy") + textColor: AmneziaStyle.color.vibrantRed clickedFunction: function () { - GC.copyToClipBoard(diagStatsEndpoint) - PageController.showNotificationMessage(qsTr("Copied")) + var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) + var descriptionText = qsTr("The proxy will be stopped and all users will lose access.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + PageController.goToPage(PageEnum.PageDeinstalling) + InstallController.removeContainer(ServersUiController.processedServerId, ServersUiController.processedContainerIndex) + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, function () { + }) + } + MouseArea { + anchors.fill: removeButton + cursorShape: Qt.PointingHandCursor + enabled: false + } + } + } + } + + ListViewType { + id: settingsListView + model: MtProxyConfigModel + reuseItems: false + + delegate: ColumnLayout { + id: settingsRoot + width: settingsListView.width + spacing: 0 + + readonly property bool fieldsEditable: isEnabled && containerStatus === 1 && !root.pageBusy + + function mtProxyActiveSecretForBaseHex(baseHex) { + return root.mtProxyClientSecretForTabIndex(baseHex, root.syncedSecretTabIndex, + root.savedTlsDomain, MtProxyConfigModel.defaultTlsDomain()) + } + + function mtProxyEffectiveHostForLinks() { + return root.savedPublicHost !== "" ? root.savedPublicHost : ServersUiController.serverHostName(ServersUiController.processedServerId) + } + + function mtProxyTmeLinkForAdditional(baseHex) { + return "https://t.me/proxy?server=" + mtProxyEffectiveHostForLinks() + "&port=" + port + "&secret=" + mtProxyActiveSecretForBaseHex(baseHex) + } + + function mtProxyTgLinkForAdditional(baseHex) { + return "tg://proxy?server=" + mtProxyEffectiveHostForLinks() + "&port=" + port + "&secret=" + mtProxyActiveSecretForBaseHex(baseHex) + } + + function mtProxyIsAdditionalPersisted(hex) { + return root.mtProxyIsPersistedAdditionalHex(hex) + } + + function mtProxyCopyText(text) { + GC.copyToClipBoard(text) + PageController.showNotificationMessage(qsTr("Copied")) + } + + function mtProxyShareQr(link) { + ExportController.generateQrFromString(link) + PageController.goToShareConnectionPage(qsTr("Telegram connection link"), + qsTr("MTProxy connection link"), "", "", "") + } + + function mtProxyRemoveAdditionalSecret(idx) { + MtProxyConfigModel.removeAdditionalSecret(idx) + } + + SwitcherType { + id: enableMtProxySwitch + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + text: qsTr("Enable MTProxy") + checked: isEnabled + enabled: containerStatus !== 0 && containerStatus !== 3 && !root.pageBusy + && !root.mtProxyNetworkBlocked + onToggled: function () { + if (checked !== isEnabled) { + previousEnabled = isEnabled + previousContainerStatus = containerStatus + root.previousSecret = secret + isEnabled = checked + isUpdating = true + if (checked) { + root.pendingUpdateAfterEnable = true + InstallController.setContainerEnabled(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, true) + } else { + InstallController.setContainerEnabled(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, false) + } + } } } CaptionTextType { Layout.fillWidth: true - text: diagLoading ? qsTr("Refreshing…") : qsTr("Tap ↻ to refresh diagnostics") + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + visible: !fieldsEditable && !root.pageBusy + text: (containerStatus === 1 || containerStatus === 2) + ? qsTr("Enable MTProxy to edit settings") + : (statusErrorCode !== 0 + ? qsTr("Settings locked: connection timed out (error code %1). Re-open the page to retry.").arg(statusErrorCode) + : qsTr("Cannot reach the server — settings are unavailable")) color: AmneziaStyle.color.mutedGray - visible: diagClientsConnected < 0 + wrapMode: Text.WordWrap } - } - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.topMargin: 16 * 2 - Layout.bottomMargin: 24 - text: qsTr("If you change the settings, the proxy connection link will change. The old link will stop working.") - color: AmneziaStyle.color.mutedGray - wrapMode: Text.WordWrap - font.pixelSize: 12 - } + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 * 2 + spacing: 4 - BasicButtonType { - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.bottomMargin: 32 - Layout.rightMargin: 16 - Layout.leftMargin: 16 - visible: ServersUiController.isProcessedServerHasWriteAccess() - enabled: fieldsEditable && !root.mtProxyNetworkBlocked - text: qsTr("Save") - clickedFunc: function () { - if (root.mtProxyNetworkBlocked) { - PageController.showErrorMessage(qsTr("No internet connection. Connect to the internet to change MTProxy settings.")) - return + CaptionTextType { + text: qsTr("Base secret") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 } - publicHostTextField.errorText = "" - tagTextField.errorText = "" - tlsDomainTextField.errorText = "" - natInternalIpTextField.errorText = "" - natExternalIpTextField.errorText = "" - portTextField.errorText = "" - var portValue = portTextField.textField.text === "" - ? MtProxyConfigModel.defaultPort() - : portTextField.textField.text + RowLayout { + Layout.fillWidth: true + spacing: 8 - var errorLines = [] - var bullet = "- " - if (!portTextField.textField.acceptableInput && portTextField.textField.text !== "") { - var portErr = qsTr("The port must be in the range of 1 to 65535") - portTextField.errorText = portErr - errorLines.push(bullet + portErr) - } - if (!MtProxyConfigModel.isValidPublicHost(publicHostTextField.textField.text)) { - var hostErr = qsTr("Enter a valid IP address or domain name") - publicHostTextField.errorText = hostErr - errorLines.push(bullet + hostErr) - } - var tagNormalized = MtProxyConfigModel.sanitizeMtProxyTagFieldText(tagTextField.textField.text) - tagTextField.textField.text = tagNormalized - if (!MtProxyConfigModel.isValidMtProxyTag(tagNormalized)) { - var tagErr = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F), or leave empty.") - tagTextField.errorText = tagErr - errorLines.push(bullet + tagErr) - } - var domainValueForSave = tlsDomainTextField.textField.text === "" - ? MtProxyConfigModel.defaultTlsDomain() - : tlsDomainTextField.textField.text - if (!MtProxyConfigModel.isValidFakeTlsDomain(domainValueForSave)) { - var tlsErr = qsTr("Enter a valid domain name") - tlsDomainTextField.errorText = tlsErr - errorLines.push(bullet + tlsErr) - } - var natIpErr = qsTr("Enter a valid IPv4 address") - if (!MtProxyConfigModel.isValidOptionalIpv4(natInternalIpTextField.textField.text)) { - natInternalIpTextField.errorText = natIpErr - errorLines.push(bullet + qsTr("NAT internal IP: enter a valid IPv4 address")) - } - if (!MtProxyConfigModel.isValidOptionalIpv4(natExternalIpTextField.textField.text)) { - natExternalIpTextField.errorText = natIpErr - errorLines.push(bullet + qsTr("NAT external IP: enter a valid IPv4 address")) - } - if (errorLines.length > 0) { - PageController.showErrorMessage(errorLines.join("\n")) - return - } - MtProxyConfigModel.setPort(portValue) - MtProxyConfigModel.setTag(tagNormalized) - MtProxyConfigModel.setPublicHost(publicHostTextField.textField.text) - MtProxyConfigModel.setTransportMode(transportMode) - var domainValue = tlsDomainTextField.textField.text === "" - ? MtProxyConfigModel.defaultTlsDomain() - : tlsDomainTextField.textField.text - MtProxyConfigModel.setTlsDomain(domainValue) + CaptionTextType { + Layout.fillWidth: true + text: secret !== "" ? mtProxyActiveSecretForBaseHex(secret) : qsTr("Not generated") + color: secret !== "" ? AmneziaStyle.color.paleGray : AmneziaStyle.color.mutedGray + wrapMode: Text.WrapAnywhere + font.pixelSize: 14 + } - if (transportMode === "faketls") { - workers = "0" - MtProxyConfigModel.setWorkers("0") - } else { - MtProxyConfigModel.setWorkersMode(workersMode) - MtProxyConfigModel.setWorkers(workers) + ImageButtonType { + Layout.alignment: Qt.AlignTop + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/refresh-cw.svg" + imageColor: AmneziaStyle.color.paleGray + visible: ServersUiController.isProcessedServerHasWriteAccess() + enabled: fieldsEditable + onClicked: { + var secretSnapshot = secret + showQuestionDrawer( + qsTr("Generate new secret?"), + qsTr("All existing connection links will stop working. Users will need new links."), + qsTr("Generate"), + qsTr("Cancel"), + function () { + root.previousSecret = secretSnapshot + if (containerStatus === 1) { + isUpdating = true + MtProxyConfigModel.generateSecret() + root.mtProxyScheduleUpdate(false) + } else { + MtProxyConfigModel.generateSecret() + PageController.showNotificationMessage(qsTr("New secret saved. It will be applied when MTProxy is started.")) + } + }, + function () { + } + ) + } + } } - MtProxyConfigModel.setNatEnabled(natEnabled) - MtProxyConfigModel.setNatInternalIp(natInternalIpTextField.textField.text) - MtProxyConfigModel.setNatExternalIp(natExternalIpTextField.textField.text) + } - previousPort = port - previousTag = tag - previousPublicHost = publicHost - previousTransportMode = transportMode - previousTlsDomain = tlsDomain - previousWorkersMode = workersMode - previousWorkers = workers - previousNatEnabled = natEnabled - previousNatInternalIp = natInternalIp - previousNatExternalIp = natExternalIp - root.previousSecret = secret - isUpdating = true - root.mtProxyScheduleUpdate(false) + TextFieldWithHeaderType { + id: publicHostTextField + enabled: fieldsEditable + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + headerText: qsTr("Public host / IP") + textField.placeholderText: ServersUiController.serverHostName(ServersUiController.processedServerId) + textField.text: publicHost + textField.maximumLength: 253 + textField.validator: PublicHostInputValidator { + } + textField.onTextChanged: { + var t = publicHostTextField.textField.text + if (MtProxyConfigModel.isPublicHostTypingIncomplete(t)) { + publicHostTextField.errorText = "" + } else if (!MtProxyConfigModel.isValidPublicHost(t)) { + publicHostTextField.errorText = qsTr("Enter a valid IP address or domain name") + } else { + publicHostTextField.errorText = "" + } + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (!MtProxyConfigModel.isValidPublicHost(textField.text)) { + publicHostTextField.errorText = qsTr("Enter a valid IP address or domain name") + return + } + publicHostTextField.errorText = "" + if (textField.text !== publicHost) { + publicHost = textField.text + MtProxyConfigModel.setPublicHost(publicHost) + } + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + visible: publicHostTextField.textField.text === "" + text: qsTr("Leave empty to use server IP automatically") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 12 + visible: publicHostTextField.textField.text !== "" && + publicHostTextField.textField.text !== ServersUiController.serverHostName(ServersUiController.processedServerId) + text: qsTr("⚠ This overrides the server IP in connection links. Make sure this host/domain points to your server.") + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + TextFieldWithHeaderType { + id: portTextField + enabled: fieldsEditable + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + headerText: qsTr("Server port") + textField.placeholderText: MtProxyConfigModel.defaultPort() + textField.maximumLength: 5 + textField.inputMethodHints: Qt.ImhDigitsOnly + textField.validator: IntValidator { + bottom: 1 + top: 65535 + } + Component.onCompleted: { + var savedPort = port + textField.text = (savedPort === MtProxyConfigModel.defaultPort()) ? "" : savedPort + } + textField.onTextChanged: { + var cur = portTextField.textField.text + var clean = MtProxyConfigModel.sanitizePortFieldText(cur) + if (clean !== cur) { + textField.text = clean + textField.cursorPosition = clean.length + } + } + textField.onEditingFinished: { + textField.text = MtProxyConfigModel.sanitizePortFieldText(textField.text) + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 12 + visible: transportMode === "faketls" && portTextField.textField.text !== "443" && portTextField.textField.text !== "" + text: qsTr("FakeTLS may not work on ports other than 443") + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("The promoted channel is set in @MTProxyBot. Paste the proxy tag here: exactly 32 hexadecimal characters (0-9, A-F), as in the bot message — or leave empty.") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + TextFieldWithHeaderType { + id: tagTextField + enabled: fieldsEditable + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + headerText: qsTr("Promoted channel tag (optional)") + textField.placeholderText: qsTr("32 hex chars from @MTProxyBot (e.g. 3b7b2fa9…)") + textField.text: tag + textField.maximumLength: MtProxyConfigModel.mtProxyBotTagHexLength() + textField.onTextChanged: { + var cur = tagTextField.textField.text + var clean = MtProxyConfigModel.sanitizeMtProxyTagFieldText(cur) + if (clean !== cur) { + textField.text = clean + textField.cursorPosition = clean.length + return + } + var tt = tagTextField.textField.text + if (tt === "") { + tagTextField.errorText = "" + return + } + if (MtProxyConfigModel.isMtProxyTagTypingIncomplete(tt)) { + tagTextField.errorText = "" + return + } + if (!MtProxyConfigModel.isValidMtProxyTag(tt)) { + tagTextField.errorText = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F).") + return + } + tagTextField.errorText = "" + } + textField.onEditingFinished: { + var raw = textField.text.replace(/^\s+|\s+$/g, '') + var normalized = MtProxyConfigModel.sanitizeMtProxyTagFieldText(raw) + textField.text = normalized + if (!MtProxyConfigModel.isValidMtProxyTag(normalized)) { + tagTextField.errorText = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F). Leave empty if unused.") + return + } + tagTextField.errorText = "" + if (normalized !== tag) { + tag = normalized + MtProxyConfigModel.setTag(tag) + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + spacing: 4 + + CaptionTextType { + text: qsTr("Get a tag from") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + text: "@MTProxyBot" + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 12 + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally("https://t.me/MTProxyBot") + } + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 * 2 + text: qsTr("Transport mode") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + + DropDownType { + id: transportModeDropDown + enabled: fieldsEditable + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + + drawerParent: root + drawerHeight: 0.4 + headerText: qsTr("Transport mode") + text: transportMode === "faketls" ? qsTr("FakeTLS") : qsTr("Standard MTProto") + + listView: Component { + ListViewType { + model: [qsTr("Standard MTProto"), qsTr("FakeTLS")] + delegate: LabelWithButtonType { + Layout.fillWidth: true + text: modelData + rightImageSource: { + var isCurrent = (index === 0 && transportMode === "standard") || + (index === 1 && transportMode === "faketls") + return isCurrent ? "qrc:/images/controls/check.svg" : "" + } + rightImageColor: AmneziaStyle.color.goldenApricot + clickedFunction: function () { + transportMode = (index === 0) ? "standard" : "faketls" + MtProxyConfigModel.setTransportMode(transportMode) + root.syncedSecretTabIndex = transportMode === "faketls" ? 1 : 0 + transportModeDropDown.closeTriggered() + } + } + } + } + } + + TextFieldWithHeaderType { + id: tlsDomainTextField + enabled: fieldsEditable + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: transportMode === "faketls" + headerText: qsTr("FakeTLS domain") + textField.placeholderText: root.previousTlsDomain + textField.validator: RegularExpressionValidator { + regularExpression: /^[A-Za-z0-9.-]*$/ + } + Component.onCompleted: { + var savedDomain = tlsDomain + textField.text = (savedDomain === MtProxyConfigModel.defaultTlsDomain() || savedDomain === "") ? "" : savedDomain + } + textField.onTextChanged: { + var t = tlsDomainTextField.textField.text + if (t === "" || MtProxyConfigModel.isFakeTlsDomainTypingIncomplete(t) + || MtProxyConfigModel.isValidFakeTlsDomain(t)) { + tlsDomainTextField.errorText = "" + } else { + tlsDomainTextField.errorText = qsTr("Enter a valid domain name") + } + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + var domainValue = textField.text === "" ? MtProxyConfigModel.defaultTlsDomain() : textField.text + if (!MtProxyConfigModel.isValidFakeTlsDomain(domainValue)) { + tlsDomainTextField.errorText = qsTr("Enter a valid domain name") + return + } + tlsDomainTextField.errorText = "" + if (domainValue !== tlsDomain) { + tlsDomain = domainValue + MtProxyConfigModel.setTlsDomain(tlsDomain) + } + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + spacing: 4 + visible: transportMode === "faketls" + + CaptionTextType { + Layout.fillWidth: true + text: qsTr("The domain is encoded into the FakeTLS client secret (ee + base_secret + hex(domain)). It must support HTTPS / TLS 1.3.") + color: AmneziaStyle.color.mutedGray + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("\u26a0 Changing the domain will invalidate all previously issued FakeTLS connection links.") + color: AmneziaStyle.color.goldenApricot + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + } + + LabelWithButtonType { + id: advancedHeader + Layout.fillWidth: true + Layout.leftMargin: 0 + Layout.rightMargin: 16 + property bool expanded: false + text: qsTr("Advanced") + rightImageSource: expanded + ? "qrc:/images/controls/chevron-up.svg" + : "qrc:/images/controls/chevron-down.svg" + rightImageColor: AmneziaStyle.color.mutedGray + clickedFunction: function () { + expanded = !expanded + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + visible: advancedHeader.expanded + enabled: fieldsEditable + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + Layout.bottomMargin: 4 + text: qsTr("Additional secrets") + color: AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("Add extra secrets to allow gradual migration without disconnecting existing users.") + color: AmneziaStyle.color.charcoalGray + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + + Repeater { + model: additionalSecrets + delegate: ColumnLayout { + id: addSecretDelegate + property bool linksExpanded: false + readonly property bool linksPanelAllowed: settingsRoot.mtProxyIsAdditionalPersisted(modelData) + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + spacing: 0 + + onLinksPanelAllowedChanged: { + if (!linksPanelAllowed) { + linksExpanded = false + } + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: collapsedBar.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + RowLayout { + id: collapsedBar + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 8 + + Item { + Layout.fillWidth: true + implicitHeight: Math.max(hexCaption.implicitHeight, 24) + + RowLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 8 + + CaptionTextType { + id: hexCaption + Layout.fillWidth: true + text: settingsRoot.mtProxyActiveSecretForBaseHex(modelData) + color: AmneziaStyle.color.paleGray + elide: Text.ElideMiddle + font.pixelSize: 13 + } + + Image { + width: 24 + height: 24 + visible: addSecretDelegate.linksPanelAllowed + source: "qrc:/images/controls/chevron-down.svg" + sourceSize.width: 24 + sourceSize.height: 24 + rotation: addSecretDelegate.linksExpanded ? 180 : 0 + Behavior on rotation { + NumberAnimation { + duration: 150 + } + } + } + } + + MouseArea { + anchors.fill: parent + visible: addSecretDelegate.linksPanelAllowed + enabled: addSecretDelegate.linksPanelAllowed + cursorShape: Qt.PointingHandCursor + onClicked: addSecretDelegate.linksExpanded = !addSecretDelegate.linksExpanded + } + } + + ImageButtonType { + implicitWidth: 32 + implicitHeight: 32 + hoverEnabled: true + image: "qrc:/images/controls/trash.svg" + imageColor: AmneziaStyle.color.vibrantRed + onClicked: settingsRoot.mtProxyRemoveAdditionalSecret(index) + } + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: 8 + spacing: 8 + visible: addSecretDelegate.linksPanelAllowed && addSecretDelegate.linksExpanded + + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Use Telegram connection link") + color: AmneziaStyle.color.mutedGray + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: expTmeRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + RowLayout { + id: expTmeRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: settingsRoot.mtProxyTmeLinkForAdditional(modelData) + color: AmneziaStyle.color.goldenApricot + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: settingsRoot.mtProxyShareQr(settingsRoot.mtProxyTmeLinkForAdditional(modelData)) + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: settingsRoot.mtProxyCopyText(settingsRoot.mtProxyTmeLinkForAdditional(modelData)) + } + } + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: expTgRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + RowLayout { + id: expTgRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: settingsRoot.mtProxyTgLinkForAdditional(modelData) + color: AmneziaStyle.color.goldenApricot + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: settingsRoot.mtProxyShareQr(settingsRoot.mtProxyTgLinkForAdditional(modelData)) + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: settingsRoot.mtProxyCopyText(settingsRoot.mtProxyTgLinkForAdditional(modelData)) + } + } + } + } + } + } + + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 8 + + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + text: qsTr("Add additional secret") + clickedFunc: function () { + MtProxyConfigModel.addAdditionalSecret() + } + } + + DividerType { + Layout.fillWidth: true + Layout.bottomMargin: 8 + } + + LabelTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.bottomMargin: 4 + text: qsTr("Worker mode") + } + + ButtonGroup { + id: workerModeGroup + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + spacing: 0 + visible: transportMode !== "faketls" + + HorizontalRadioButton { + Layout.fillWidth: true + text: qsTr("Auto") + ButtonGroup.group: workerModeGroup + checked: workersMode === "auto" + onClicked: { workersMode = "auto"; MtProxyConfigModel.setWorkersMode("auto") } + } + HorizontalRadioButton { + Layout.fillWidth: true + text: qsTr("Manual") + ButtonGroup.group: workerModeGroup + checked: workersMode === "manual" + onClicked: { workersMode = "manual"; MtProxyConfigModel.setWorkersMode("manual") } + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + visible: transportMode === "faketls" + text: qsTr("Workers are set to 0 automatically for FakeTLS mode.") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + TextFieldWithHeaderType { + id: workersTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: workersMode === "manual" && transportMode !== "faketls" + headerText: qsTr("Workers count") + textField.placeholderText: "2" + textField.text: workers + textField.maximumLength: 2 + textField.inputMethodHints: Qt.ImhDigitsOnly + textField.validator: IntValidator { + bottom: 0 + top: MtProxyConfigModel.maxWorkers() + } + textField.onTextChanged: { + var cur = workersTextField.textField.text + if (cur === "") { + return + } + var n = parseInt(cur, 10) + var maxW = MtProxyConfigModel.maxWorkers() + if (isNaN(n) || n < 0) { + n = 0 + } + if (n > maxW) { + n = maxW + } + var clamped = String(n) + if (clamped !== cur) { + textField.text = clamped + textField.cursorPosition = clamped.length + } + } + textField.onEditingFinished: { + var v = workersTextField.textField.text + if (v !== "") { + var m = parseInt(v, 10) + var maxW2 = MtProxyConfigModel.maxWorkers() + if (isNaN(m) || m < 0) { + m = 0 + } + if (m > maxW2) { + m = maxW2 + } + v = String(m) + textField.text = v + } + if (v !== workers) { + workers = v + MtProxyConfigModel.setWorkers(workers) + } + } + } + + DividerType { + Layout.fillWidth: true + Layout.bottomMargin: 8 + } + + SwitcherType { + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 4 + text: qsTr("Server is behind NAT / Docker bridge") + descriptionText: qsTr("Enable if your server is not directly accessible from the internet, e.g. Docker or private network") + checked: natEnabled + onToggled: function () { + if (checked !== natEnabled) { + natEnabled = checked + MtProxyConfigModel.setNatEnabled(natEnabled) + } + } + } + + TextFieldWithHeaderType { + id: natInternalIpTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: natEnabled + headerText: qsTr("Internal IP") + textField.placeholderText: "172.17.0.2" + textField.text: natInternalIp + textField.maximumLength: 15 + textField.validator: RegularExpressionValidator { + regularExpression: root.natIpv4InputFormat + } + textField.onTextChanged: { + if (root.natIpv4FieldShowInvalidError(textField.text)) { + natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + } else { + natInternalIpTextField.errorText = "" + } + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (!MtProxyConfigModel.isValidOptionalIpv4(textField.text)) { + natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + return + } + natInternalIpTextField.errorText = "" + if (textField.text !== natInternalIp) { + natInternalIp = textField.text + MtProxyConfigModel.setNatInternalIp(natInternalIp) + } + } + } + + TextFieldWithHeaderType { + id: natExternalIpTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: natEnabled + headerText: qsTr("External IP") + textField.placeholderText: "1.2.3.4" + textField.text: natExternalIp + textField.maximumLength: 15 + textField.validator: RegularExpressionValidator { + regularExpression: root.natIpv4InputFormat + } + textField.onTextChanged: { + if (root.natIpv4FieldShowInvalidError(textField.text)) { + natExternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + } else { + natExternalIpTextField.errorText = "" + } + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (!MtProxyConfigModel.isValidOptionalIpv4(textField.text)) { + natExternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + return + } + natExternalIpTextField.errorText = "" + if (textField.text !== natExternalIp) { + natExternalIp = textField.text + MtProxyConfigModel.setNatExternalIp(natExternalIp) + } + } + } + } + + DividerType { + Layout.fillWidth: true + Layout.topMargin: 8 + } + + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + spacing: 8 + visible: containerStatus === 1 + + RowLayout { + Layout.fillWidth: true + + Header2Type { + Layout.fillWidth: true + headerText: qsTr("Diagnostics") + } + + ImageButtonType { + implicitWidth: 32 + implicitHeight: 32 + image: "qrc:/images/controls/refresh-cw.svg" + imageColor: diagLoading ? AmneziaStyle.color.mutedGray : AmneziaStyle.color.paleGray + hoverEnabled: !diagLoading + enabled: !diagLoading + onClicked: { + diagLoading = true + InstallController.refreshContainerDiagnostics(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, parseInt(port)) + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Public port reachable") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : (diagPortReachable ? qsTr("Yes") : qsTr("No")) + color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Telegram upstream reachable") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : (diagTelegramReachable ? qsTr("Yes") : qsTr("No")) + color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Clients connected") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : diagClientsConnected.toString() + color: AmneziaStyle.color.paleGray + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Last config refresh") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagLastConfigRefresh !== "" ? diagLastConfigRefresh : qsTr("—") + color: AmneziaStyle.color.mutedGray + } + } + + LabelWithButtonType { + Layout.fillWidth: true + Layout.leftMargin: -16 + visible: diagStatsEndpoint !== "" + text: qsTr("Stats endpoint") + descriptionText: diagStatsEndpoint + descriptionOnTop: true + rightImageSource: "qrc:/images/controls/copy.svg" + rightImageColor: AmneziaStyle.color.paleGray + clickedFunction: function () { + GC.copyToClipBoard(diagStatsEndpoint) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + + CaptionTextType { + Layout.fillWidth: true + text: diagLoading ? qsTr("Refreshing…") : qsTr("Tap ↻ to refresh diagnostics") + color: AmneziaStyle.color.mutedGray + visible: diagClientsConnected < 0 + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 * 2 + Layout.bottomMargin: 24 + text: qsTr("If you change the settings, the proxy connection link will change. The old link will stop working.") + color: AmneziaStyle.color.mutedGray + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.bottomMargin: 32 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + visible: ServersUiController.isProcessedServerHasWriteAccess() + enabled: fieldsEditable && !root.mtProxyNetworkBlocked + text: qsTr("Save") + clickedFunc: function () { + if (root.mtProxyNetworkBlocked) { + PageController.showErrorMessage(qsTr("No internet connection. Connect to the internet to change MTProxy settings.")) + return + } + publicHostTextField.errorText = "" + tagTextField.errorText = "" + tlsDomainTextField.errorText = "" + natInternalIpTextField.errorText = "" + natExternalIpTextField.errorText = "" + portTextField.errorText = "" + + var portValue = portTextField.textField.text === "" + ? MtProxyConfigModel.defaultPort() + : portTextField.textField.text + + var errorLines = [] + var bullet = "- " + if (!portTextField.textField.acceptableInput && portTextField.textField.text !== "") { + var portErr = qsTr("The port must be in the range of 1 to 65535") + portTextField.errorText = portErr + errorLines.push(bullet + portErr) + } + if (!MtProxyConfigModel.isValidPublicHost(publicHostTextField.textField.text)) { + var hostErr = qsTr("Enter a valid IP address or domain name") + publicHostTextField.errorText = hostErr + errorLines.push(bullet + hostErr) + } + var tagNormalized = MtProxyConfigModel.sanitizeMtProxyTagFieldText(tagTextField.textField.text) + tagTextField.textField.text = tagNormalized + if (!MtProxyConfigModel.isValidMtProxyTag(tagNormalized)) { + var tagErr = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F), or leave empty.") + tagTextField.errorText = tagErr + errorLines.push(bullet + tagErr) + } + var domainValueForSave = tlsDomainTextField.textField.text === "" + ? MtProxyConfigModel.defaultTlsDomain() + : tlsDomainTextField.textField.text + if (!MtProxyConfigModel.isValidFakeTlsDomain(domainValueForSave)) { + var tlsErr = qsTr("Enter a valid domain name") + tlsDomainTextField.errorText = tlsErr + errorLines.push(bullet + tlsErr) + } + var natIpErr = qsTr("Enter a valid IPv4 address") + if (!MtProxyConfigModel.isValidOptionalIpv4(natInternalIpTextField.textField.text)) { + natInternalIpTextField.errorText = natIpErr + errorLines.push(bullet + qsTr("NAT internal IP: enter a valid IPv4 address")) + } + if (!MtProxyConfigModel.isValidOptionalIpv4(natExternalIpTextField.textField.text)) { + natExternalIpTextField.errorText = natIpErr + errorLines.push(bullet + qsTr("NAT external IP: enter a valid IPv4 address")) + } + if (errorLines.length > 0) { + PageController.showErrorMessage(errorLines.join("\n")) + return + } + MtProxyConfigModel.setPort(portValue) + MtProxyConfigModel.setTag(tagNormalized) + MtProxyConfigModel.setPublicHost(publicHostTextField.textField.text) + MtProxyConfigModel.setTransportMode(transportMode) + var domainValue = tlsDomainTextField.textField.text === "" + ? MtProxyConfigModel.defaultTlsDomain() + : tlsDomainTextField.textField.text + MtProxyConfigModel.setTlsDomain(domainValue) + + if (transportMode === "faketls") { + workers = "0" + MtProxyConfigModel.setWorkers("0") + } else { + MtProxyConfigModel.setWorkersMode(workersMode) + MtProxyConfigModel.setWorkers(workers) + } + MtProxyConfigModel.setNatEnabled(natEnabled) + MtProxyConfigModel.setNatInternalIp(natInternalIpTextField.textField.text) + MtProxyConfigModel.setNatExternalIp(natExternalIpTextField.textField.text) + + previousPort = port + previousTag = tag + previousPublicHost = publicHost + previousTransportMode = transportMode + previousTlsDomain = tlsDomain + previousWorkersMode = workersMode + previousWorkers = workers + previousNatEnabled = natEnabled + previousNatInternalIp = natInternalIp + previousNatExternalIp = natExternalIp + root.previousSecret = secret + isUpdating = true + root.mtProxyScheduleUpdate(false) + } } } } } } - - } } diff --git a/client/ui/qml/Pages2/PageServiceTelemtSettings.qml b/client/ui/qml/Pages2/PageServiceTelemtSettings.qml index 8fb51e82e..2a73fc48b 100644 --- a/client/ui/qml/Pages2/PageServiceTelemtSettings.qml +++ b/client/ui/qml/Pages2/PageServiceTelemtSettings.qml @@ -43,35 +43,6 @@ PageType { property string savedTlsDomain: "" property string savedPublicHost: "" - readonly property var natIpv4InputFormat: /^(\d{1,3}\.){0,3}\d{0,3}$/ - - function natIpv4FieldShowInvalidError(text) { - var t = text ? String(text).replace(/^\s+|\s+$/g, '') : "" - if (t === "") - return false - if (TelemtConfigModel.isValidOptionalIpv4(t)) - return false - var parts = t.split('.') - var j - for (j = 0; j < parts.length; j++) { - if (parts[j].length > 3) - return true - } - if (parts.length > 4) - return true - if (t.indexOf('.') < 0 && t.length > 3) - return true - if (t.endsWith('.')) - return false - if (parts.length < 4) - return false - for (var i = 0; i < parts.length; i++) { - if (parts[i] === "") - return true - } - return true - } - onSavedTransportModeChanged: { if (savedTransportMode === "faketls") { root.syncedSecretTabIndex = 1 @@ -182,13 +153,6 @@ PageType { root.telemtScheduleContainerStatusRefresh() } - function telemtScheduleUpdate(closePage) { - var cp = closePage === undefined ? false : closePage - Qt.callLater(function () { - InstallController.updateServerConfig(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.Telemt, cp) - }) - } - property var telemtPersistedAdditionalHex: [] function telemtRefreshPersistedAdditionalSecrets() { @@ -210,6 +174,42 @@ PageType { return false } + readonly property var natIpv4InputFormat: /^(\d{1,3}\.){0,3}\d{0,3}$/ + + function telemtScheduleUpdate(closePage) { + var cp = closePage === undefined ? false : closePage + Qt.callLater(function () { + InstallController.updateServerConfig(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, ProtocolEnum.Telemt, cp) + }) + } + + function natIpv4FieldShowInvalidError(text) { + var t = text ? String(text).replace(/^\s+|\s+$/g, '') : "" + if (t === "") + return false + if (TelemtConfigModel.isValidOptionalIpv4(t)) + return false + var parts = t.split('.') + var j + for (j = 0; j < parts.length; j++) { + if (parts[j].length > 3) + return true + } + if (parts.length > 4) + return true + if (t.indexOf('.') < 0 && t.length > 3) + return true + if (t.endsWith('.')) + return false + if (parts.length < 4) + return false + for (var i = 0; i < parts.length; i++) { + if (parts[i] === "") + return true + } + return true + } + function statusText() { if (isCheckingStatus) { return qsTr("Checking...") @@ -408,1584 +408,1595 @@ PageType { anchors.fill: parent enabled: !root.pageBusy - BackButtonType { - id: backButton - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 20 + PageController.safeAreaTopMargin + BackButtonType { + id: backButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + PageController.safeAreaTopMargin - onFocusChanged: { - if (this.activeFocus) { - if (mainTabBar.currentIndex === 0) { - connectionListView.positionViewAtBeginning() - } else { - settingsListView.positionViewAtBeginning() - } - } - } - } - - ColumnLayout { - id: pageHeader - anchors.top: backButton.bottom - anchors.left: parent.left - anchors.right: parent.right - spacing: 0 - - BaseHeaderType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 24 - - headerText: qsTr("Telemt settings") - descriptionLinkText: qsTr("Read more about this settings") - descriptionLinkUrl: "https://github.com/telemt/telemt" - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.topMargin: 8 - visible: root.telemtNetworkBlocked - text: qsTr("No internet connection. Connect to the internet to change Telemt settings.") - color: AmneziaStyle.color.mutedGray - wrapMode: Text.WordWrap - font.pixelSize: 14 - } - } - - TabBar { - id: mainTabBar - anchors.top: pageHeader.bottom - anchors.left: parent.left - anchors.right: parent.right - width: parent.width - - background: Rectangle { - color: AmneziaStyle.color.transparent - Rectangle { - width: parent.width - height: 1 - anchors.bottom: parent.bottom - color: AmneziaStyle.color.slateGray - } - } - - TabButtonType { - text: qsTr("Connection") - isSelected: mainTabBar.currentIndex === 0 - } - TabButtonType { - text: qsTr("Settings") - isSelected: mainTabBar.currentIndex === 1 - } - } - - StackLayout { - id: tabContent - anchors.top: mainTabBar.bottom - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - currentIndex: mainTabBar.currentIndex - - ListViewType { - id: connectionListView - model: TelemtConfigModel - - delegate: ColumnLayout { - width: connectionListView.width - spacing: 0 - - property int secretTabIndex: root.syncedSecretTabIndex - - function activeSecret() { - return root.telemtClientSecretForTabIndex(secret, root.syncedSecretTabIndex, - root.savedTlsDomain, TelemtConfigModel.defaultTlsDomain()) - } - - function effectiveSecret() { - return activeSecret() - } - - function effectiveHost() { - return root.savedPublicHost !== "" ? root.savedPublicHost : ServersUiController.serverHostName(ServersUiController.processedServerId) - } - - function tmeLink() { - return "https://t.me/proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() - } - - function tgLink() { - return "tg://proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() - } - - CaptionTextType { - Layout.fillWidth: true - Layout.topMargin: 24 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - text: qsTr("Use Telegram connection link") - color: AmneziaStyle.color.mutedGray - } - - Rectangle { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - implicitHeight: linkRow.implicitHeight + 16 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 - - RowLayout { - id: linkRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 8 - spacing: 4 - - CaptionTextType { - Layout.fillWidth: true - text: secret !== "" ? tmeLink() : qsTr("Deploy Telemt first") - color: secret !== "" ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray - elide: Text.ElideRight - maximumLineCount: 1 - font.pixelSize: 13 - } - - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/qr-code.svg" - imageColor: AmneziaStyle.color.paleGray - visible: secret !== "" - onClicked: { - ExportController.generateQrFromString(tmeLink()) - PageController.goToShareConnectionPage( - qsTr("Telegram connection link"), - qsTr("Telemt connection link"), - "", "", "") - } - } - - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - visible: secret !== "" - onClicked: { - GC.copyToClipBoard(tmeLink()) - PageController.showNotificationMessage(qsTr("Copied")) - } - } - } - } - - Rectangle { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - implicitHeight: tgLinkRow.implicitHeight + 16 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 - visible: secret !== "" - - RowLayout { - id: tgLinkRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 8 - spacing: 4 - - CaptionTextType { - Layout.fillWidth: true - text: tgLink() - color: AmneziaStyle.color.goldenApricot - elide: Text.ElideRight - maximumLineCount: 1 - font.pixelSize: 13 - } - - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/qr-code.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: { - ExportController.generateQrFromString(tgLink()) - PageController.goToShareConnectionPage( - qsTr("Telegram connection link"), - qsTr("Telemt connection link"), - "", "", "") - } - } - - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: { - GC.copyToClipBoard(tgLink()) - PageController.showNotificationMessage(qsTr("Copied")) - } - } - } - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - spacing: 4 - - CaptionTextType { - text: qsTr("Or enter the proxy details manually.") - color: AmneziaStyle.color.mutedGray - } - - CaptionTextType { - Layout.fillWidth: true - text: qsTr("How to do it") - color: AmneziaStyle.color.goldenApricot - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: Qt.openUrlExternally("https://core.telegram.org/proxy") - } - } - - Item { - Layout.fillWidth: true - } - } - - Rectangle { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 32 - implicitHeight: manualCol.implicitHeight + 8 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 - - ColumnLayout { - id: manualCol - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.topMargin: 8 - spacing: 0 - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 12 - Layout.rightMargin: 8 - Layout.bottomMargin: 8 - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - CaptionTextType { - text: qsTr("Host") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - } - CaptionTextType { - Layout.fillWidth: true - text: effectiveHost() - color: AmneziaStyle.color.paleGray - elide: Text.ElideRight - } - } - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: { GC.copyToClipBoard(effectiveHost()) - PageController.showNotificationMessage(qsTr("Copied")) } - } - } - - DividerType { - Layout.fillWidth: true - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 12 - Layout.rightMargin: 8 - Layout.topMargin: 8 - Layout.bottomMargin: 8 - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - CaptionTextType { - text: qsTr("Port") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - } - CaptionTextType { - Layout.fillWidth: true - text: port - color: AmneziaStyle.color.paleGray - } - } - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: { GC.copyToClipBoard(port) - PageController.showNotificationMessage(qsTr("Copied")) } - } - } - - DividerType { - Layout.fillWidth: true - } - - ButtonGroup { - id: secretTabGroup - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 12 - Layout.rightMargin: 8 - Layout.topMargin: 4 - Layout.bottomMargin: 8 - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - CaptionTextType { - text: qsTr("Secret") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - } - CaptionTextType { - Layout.fillWidth: true - text: activeSecret() - color: AmneziaStyle.color.paleGray - wrapMode: Text.WrapAnywhere - font.pixelSize: 13 - } - } - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: { GC.copyToClipBoard(activeSecret()) - PageController.showNotificationMessage(qsTr("Copied")) } - } - } - } - } - - LabelWithButtonType { - id: removeButton - Layout.fillWidth: true - Layout.bottomMargin: 24 - Layout.leftMargin: 0 - Layout.rightMargin: 16 - visible: ServersUiController.isProcessedServerHasWriteAccess() - text: qsTr("Delete Telemt") - textColor: AmneziaStyle.color.vibrantRed - clickedFunction: function () { - var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) - var descriptionText = qsTr("The proxy will be stopped and all users will lose access.") - var yesButtonText = qsTr("Continue") - var noButtonText = qsTr("Cancel") - var yesButtonFunction = function () { - PageController.goToPage(PageEnum.PageDeinstalling) - InstallController.removeContainer(ServersUiController.processedServerId, ServersUiController.processedContainerIndex) - } - showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, function () { - }) - } - MouseArea { - anchors.fill: removeButton - cursorShape: Qt.PointingHandCursor - enabled: false + onFocusChanged: { + if (this.activeFocus) { + if (mainTabBar.currentIndex === 0) { + connectionListView.positionViewAtBeginning() + } else { + settingsListView.positionViewAtBeginning() } } } } - ListViewType { - id: settingsListView - model: TelemtConfigModel - reuseItems: false + ColumnLayout { + id: pageHeader + anchors.top: backButton.bottom + anchors.left: parent.left + anchors.right: parent.right + spacing: 0 - delegate: ColumnLayout { - id: settingsRoot - width: settingsListView.width - spacing: 0 + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 24 - readonly property bool fieldsEditable: isEnabled && containerStatus === 1 && !root.pageBusy + headerText: qsTr("Telemt settings") + descriptionLinkText: qsTr("Read more about this settings") + descriptionLinkUrl: "https://github.com/telemt/telemt" + } - function telemtActiveSecretForBaseHex(baseHex) { - return root.telemtClientSecretForTabIndex(baseHex, root.syncedSecretTabIndex, - root.savedTlsDomain, TelemtConfigModel.defaultTlsDomain()) + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + visible: root.telemtNetworkBlocked + text: qsTr("No internet connection. Connect to the internet to change Telemt settings.") + color: AmneziaStyle.color.mutedGray + wrapMode: Text.WordWrap + font.pixelSize: 14 + } + } + + TabBar { + id: mainTabBar + anchors.top: pageHeader.bottom + anchors.left: parent.left + anchors.right: parent.right + width: parent.width + + background: Rectangle { + color: AmneziaStyle.color.transparent + Rectangle { + width: parent.width + height: 1 + anchors.bottom: parent.bottom + color: AmneziaStyle.color.slateGray } + } - function telemtEffectiveHostForLinks() { - return root.savedPublicHost !== "" ? root.savedPublicHost : ServersUiController.serverHostName(ServersUiController.processedServerId) - } + TabButtonType { + text: qsTr("Connection") + isSelected: mainTabBar.currentIndex === 0 + } + TabButtonType { + text: qsTr("Settings") + isSelected: mainTabBar.currentIndex === 1 + } + } - function telemtTmeLinkForAdditional(baseHex) { - return "https://t.me/proxy?server=" + telemtEffectiveHostForLinks() + "&port=" + port + "&secret=" + telemtActiveSecretForBaseHex(baseHex) - } + StackLayout { + id: tabContent + anchors.top: mainTabBar.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + currentIndex: mainTabBar.currentIndex - function telemtTgLinkForAdditional(baseHex) { - return "tg://proxy?server=" + telemtEffectiveHostForLinks() + "&port=" + port + "&secret=" + telemtActiveSecretForBaseHex(baseHex) - } + ListViewType { + id: connectionListView + model: TelemtConfigModel - function telemtIsAdditionalPersisted(hex) { - return root.telemtIsPersistedAdditionalHex(hex) - } - - function telemtCopyText(text) { - GC.copyToClipBoard(text) - PageController.showNotificationMessage(qsTr("Copied")) - } - - function telemtShareQr(link) { - ExportController.generateQrFromString(link) - PageController.goToShareConnectionPage(qsTr("Telegram connection link"), - qsTr("Telemt connection link"), "", "", "") - } - - function telemtRemoveAdditionalSecret(idx) { - TelemtConfigModel.removeAdditionalSecret(idx) - } - - SwitcherType { - id: enableTelemtSwitch - Layout.fillWidth: true - Layout.topMargin: 24 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - text: qsTr("Enable Telemt") - checked: isEnabled - enabled: containerStatus !== 0 && containerStatus !== 3 && !root.pageBusy - && !root.telemtNetworkBlocked - onToggled: function () { - if (checked !== isEnabled) { - previousEnabled = isEnabled - previousContainerStatus = containerStatus - root.previousSecret = secret - isEnabled = checked - isUpdating = true - if (checked) { - root.pendingUpdateAfterEnable = true - InstallController.setContainerEnabled(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, true) - } else { - InstallController.setContainerEnabled(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, false) - } - } - } - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - visible: !fieldsEditable && !root.pageBusy - text: (containerStatus === 1 || containerStatus === 2) - ? qsTr("Enable Telemt to edit settings") - : (statusErrorCode !== 0 - ? qsTr("Settings locked: connection timed out (error code %1). Re-open the page to retry.").arg(statusErrorCode) - : qsTr("Cannot reach the server — settings are unavailable")) - color: AmneziaStyle.color.mutedGray - wrapMode: Text.WordWrap - } - - ColumnLayout { - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 * 2 - spacing: 4 - - CaptionTextType { - text: qsTr("Base secret") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - } - - RowLayout { - Layout.fillWidth: true - spacing: 8 - - CaptionTextType { - Layout.fillWidth: true - text: secret !== "" ? telemtActiveSecretForBaseHex(secret) : qsTr("Not generated") - color: secret !== "" ? AmneziaStyle.color.paleGray : AmneziaStyle.color.mutedGray - wrapMode: Text.WrapAnywhere - font.pixelSize: 14 - } - - ImageButtonType { - Layout.alignment: Qt.AlignTop - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/refresh-cw.svg" - imageColor: AmneziaStyle.color.paleGray - visible: ServersUiController.isProcessedServerHasWriteAccess() - enabled: fieldsEditable - onClicked: { - var secretSnapshot = secret - showQuestionDrawer( - qsTr("Generate new secret?"), - qsTr("All existing connection links will stop working. Users will need new links."), - qsTr("Generate"), - qsTr("Cancel"), - function () { - root.previousSecret = secretSnapshot - if (containerStatus === 1) { - isUpdating = true - TelemtConfigModel.generateSecret() - root.telemtScheduleUpdate(false) - } else { - TelemtConfigModel.generateSecret() - PageController.showNotificationMessage(qsTr("New secret saved. It will be applied when Telemt is started.")) - } - }, - function () { - } - ) - } - } - } - } - - TextFieldWithHeaderType { - id: publicHostTextField - enabled: fieldsEditable - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 4 - headerText: qsTr("Public host / IP") - textField.placeholderText: ServersUiController.serverHostName(ServersUiController.processedServerId) - textField.text: publicHost - textField.maximumLength: 253 - textField.validator: PublicHostInputValidator { - } - textField.onTextChanged: { - var t = publicHostTextField.textField.text - if (TelemtConfigModel.isPublicHostTypingIncomplete(t)) { - publicHostTextField.errorText = "" - } else if (!TelemtConfigModel.isValidPublicHost(t)) { - publicHostTextField.errorText = qsTr("Enter a valid IP address or domain name") - } else { - publicHostTextField.errorText = "" - } - } - textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - if (!TelemtConfigModel.isValidPublicHost(textField.text)) { - publicHostTextField.errorText = qsTr("Enter a valid IP address or domain name") - return - } - publicHostTextField.errorText = "" - if (textField.text !== publicHost) { - publicHost = textField.text - TelemtConfigModel.setPublicHost(publicHost) - } - } - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 4 - visible: publicHostTextField.textField.text === "" - text: qsTr("Leave empty to use server IP automatically") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - wrapMode: Text.WordWrap - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 12 - visible: publicHostTextField.textField.text !== "" && - publicHostTextField.textField.text !== ServersUiController.serverHostName(ServersUiController.processedServerId) - text: qsTr("⚠ This overrides the server IP in connection links. Make sure this host/domain points to your server.") - color: AmneziaStyle.color.goldenApricot - font.pixelSize: 12 - wrapMode: Text.WordWrap - } - - TextFieldWithHeaderType { - id: portTextField - enabled: fieldsEditable - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - headerText: qsTr("Server port") - textField.placeholderText: TelemtConfigModel.defaultPort() - textField.maximumLength: 5 - textField.inputMethodHints: Qt.ImhDigitsOnly - textField.validator: IntValidator { - bottom: 1 - top: 65535 - } - Component.onCompleted: { - var savedPort = port - textField.text = (savedPort === TelemtConfigModel.defaultPort()) ? "" : savedPort - } - textField.onTextChanged: { - var cur = portTextField.textField.text - var clean = TelemtConfigModel.sanitizePortFieldText(cur) - if (clean !== cur) { - textField.text = clean - textField.cursorPosition = clean.length - } - } - textField.onEditingFinished: { - textField.text = TelemtConfigModel.sanitizePortFieldText(textField.text) - } - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 12 - visible: transportMode === "faketls" && portTextField.textField.text !== "443" && portTextField.textField.text !== "" - text: qsTr("FakeTLS may not work on ports other than 443") - color: AmneziaStyle.color.goldenApricot - font.pixelSize: 12 - wrapMode: Text.WordWrap - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - text: qsTr("The promoted channel is set in @MTProxyBot. Paste the proxy tag here: exactly 32 hexadecimal characters (0-9, A-F), as in the bot message — or leave empty.") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - wrapMode: Text.WordWrap - } - - TextFieldWithHeaderType { - id: tagTextField - enabled: fieldsEditable - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - headerText: qsTr("Promoted channel tag (optional)") - textField.placeholderText: qsTr("32 hex chars from @MTProxyBot (e.g. 3b7b2fa9…)") - textField.text: tag - textField.maximumLength: TelemtConfigModel.mtProxyBotTagHexLength() - textField.onTextChanged: { - var cur = tagTextField.textField.text - var clean = TelemtConfigModel.sanitizeMtProxyTagFieldText(cur) - if (clean !== cur) { - textField.text = clean - textField.cursorPosition = clean.length - return - } - var tt = tagTextField.textField.text - if (tt === "") { - tagTextField.errorText = "" - return - } - if (TelemtConfigModel.isMtProxyTagTypingIncomplete(tt)) { - tagTextField.errorText = "" - return - } - if (!TelemtConfigModel.isValidMtProxyTag(tt)) { - tagTextField.errorText = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F).") - return - } - tagTextField.errorText = "" - } - textField.onEditingFinished: { - var raw = textField.text.replace(/^\s+|\s+$/g, '') - var normalized = TelemtConfigModel.sanitizeMtProxyTagFieldText(raw) - textField.text = normalized - if (!TelemtConfigModel.isValidMtProxyTag(normalized)) { - tagTextField.errorText = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F). Leave empty if unused.") - return - } - tagTextField.errorText = "" - if (normalized !== tag) { - tag = normalized - TelemtConfigModel.setTag(tag) - } - } - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - spacing: 4 - - CaptionTextType { - text: qsTr("Get a tag from") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - } - CaptionTextType { - text: "@MTProxyBot" - color: AmneziaStyle.color.goldenApricot - font.pixelSize: 12 - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: Qt.openUrlExternally("https://t.me/MTProxyBot") - } - } - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.topMargin: 16 * 2 - text: qsTr("Transport mode") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - } - - DropDownType { - id: transportModeDropDown - enabled: fieldsEditable - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - - drawerParent: root - drawerHeight: 0.4 - headerText: qsTr("Transport mode") - text: transportMode === "faketls" ? qsTr("FakeTLS") : qsTr("Standard MTProto") - - listView: Component { - ListViewType { - model: [qsTr("Standard MTProto"), qsTr("FakeTLS")] - delegate: LabelWithButtonType { - Layout.fillWidth: true - text: modelData - rightImageSource: { - var isCurrent = (index === 0 && transportMode === "standard") || - (index === 1 && transportMode === "faketls") - return isCurrent ? "qrc:/images/controls/check.svg" : "" - } - rightImageColor: AmneziaStyle.color.goldenApricot - clickedFunction: function () { - transportMode = (index === 0) ? "standard" : "faketls" - TelemtConfigModel.setTransportMode(transportMode) - root.syncedSecretTabIndex = transportMode === "faketls" ? 1 : 0 - transportModeDropDown.closeTriggered() - } - } - } - } - } - - TextFieldWithHeaderType { - id: tlsDomainTextField - enabled: fieldsEditable - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - visible: transportMode === "faketls" - headerText: qsTr("FakeTLS domain") - textField.placeholderText: root.previousTlsDomain - textField.validator: RegularExpressionValidator { - regularExpression: /^[A-Za-z0-9.-]*$/ - } - Component.onCompleted: { - var savedDomain = tlsDomain - textField.text = (savedDomain === TelemtConfigModel.defaultTlsDomain() || savedDomain === "") ? "" : savedDomain - } - textField.onTextChanged: { - var t = tlsDomainTextField.textField.text - if (t === "" || TelemtConfigModel.isFakeTlsDomainTypingIncomplete(t) - || TelemtConfigModel.isValidFakeTlsDomain(t)) { - tlsDomainTextField.errorText = "" - } else { - tlsDomainTextField.errorText = qsTr("Enter a valid domain name") - } - } - textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - var domainValue = textField.text === "" ? TelemtConfigModel.defaultTlsDomain() : textField.text - if (!TelemtConfigModel.isValidFakeTlsDomain(domainValue)) { - tlsDomainTextField.errorText = qsTr("Enter a valid domain name") - return - } - tlsDomainTextField.errorText = "" - if (domainValue !== tlsDomain) { - tlsDomain = domainValue - TelemtConfigModel.setTlsDomain(tlsDomain) - } - } - } - - ColumnLayout { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - spacing: 4 - visible: transportMode === "faketls" - - CaptionTextType { - Layout.fillWidth: true - text: qsTr("The domain is encoded into the FakeTLS client secret (ee + base_secret + hex(domain)). It must support HTTPS / TLS 1.3.") - color: AmneziaStyle.color.mutedGray - wrapMode: Text.WordWrap - font.pixelSize: 12 - } - CaptionTextType { - Layout.fillWidth: true - text: qsTr("\u26a0 Changing the domain will invalidate all previously issued FakeTLS connection links.") - color: AmneziaStyle.color.goldenApricot - wrapMode: Text.WordWrap - font.pixelSize: 12 - } - } - - LabelWithButtonType { - id: advancedHeader - Layout.fillWidth: true - Layout.leftMargin: 0 - Layout.rightMargin: 16 - property bool expanded: false - text: qsTr("Advanced") - rightImageSource: expanded - ? "qrc:/images/controls/chevron-up.svg" - : "qrc:/images/controls/chevron-down.svg" - rightImageColor: AmneziaStyle.color.mutedGray - clickedFunction: function () { - expanded = !expanded - } - } - - ColumnLayout { - Layout.fillWidth: true + delegate: ColumnLayout { + width: connectionListView.width spacing: 0 - visible: advancedHeader.expanded - enabled: fieldsEditable + + property int secretTabIndex: root.syncedSecretTabIndex + + function activeSecret() { + return root.telemtClientSecretForTabIndex(secret, root.syncedSecretTabIndex, + root.savedTlsDomain, TelemtConfigModel.defaultTlsDomain()) + } + + function effectiveSecret() { + return activeSecret() + } + + function effectiveHost() { + return root.savedPublicHost !== "" ? root.savedPublicHost : ServersUiController.serverHostName(ServersUiController.processedServerId) + } + + function tmeLink() { + return "https://t.me/proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() + } + + function tgLink() { + return "tg://proxy?server=" + effectiveHost() + "&port=" + port + "&secret=" + activeSecret() + } CaptionTextType { + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("Use Telegram connection link") + color: AmneziaStyle.color.mutedGray + } + + Rectangle { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - Layout.topMargin: 8 - Layout.bottomMargin: 4 - text: qsTr("Additional secrets") - color: AmneziaStyle.color.mutedGray + Layout.bottomMargin: 16 + implicitHeight: linkRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + RowLayout { + id: linkRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: secret !== "" ? tmeLink() : qsTr("Deploy Telemt first") + color: secret !== "" ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + visible: secret !== "" + onClicked: { + ExportController.generateQrFromString(tmeLink()) + PageController.goToShareConnectionPage( + qsTr("Telegram connection link"), + qsTr("Telemt connection link"), + "", "", "") + } + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + visible: secret !== "" + onClicked: { + GC.copyToClipBoard(tmeLink()) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + } } - CaptionTextType { + + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + implicitHeight: tgLinkRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + visible: secret !== "" + + RowLayout { + id: tgLinkRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: tgLink() + color: AmneziaStyle.color.goldenApricot + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { + ExportController.generateQrFromString(tgLink()) + PageController.goToShareConnectionPage( + qsTr("Telegram connection link"), + qsTr("Telemt connection link"), + "", "", "") + } + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { + GC.copyToClipBoard(tgLink()) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + } + } + + RowLayout { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.bottomMargin: 8 - text: qsTr("Add extra secrets to allow gradual migration without disconnecting existing users.") - color: AmneziaStyle.color.charcoalGray - wrapMode: Text.WordWrap - font.pixelSize: 12 + spacing: 4 + + CaptionTextType { + text: qsTr("Or enter the proxy details manually.") + color: AmneziaStyle.color.mutedGray + } + + CaptionTextType { + Layout.fillWidth: true + text: qsTr("How to do it") + color: AmneziaStyle.color.goldenApricot + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally("https://core.telegram.org/proxy") + } + } + + Item { + Layout.fillWidth: true + } } - Repeater { - model: additionalSecrets - delegate: ColumnLayout { - id: addSecretDelegate - property bool linksExpanded: false - readonly property bool linksPanelAllowed: settingsRoot.telemtIsAdditionalPersisted(modelData) - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 32 + implicitHeight: manualCol.implicitHeight + 8 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + ColumnLayout { + id: manualCol + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: 8 spacing: 0 - onLinksPanelAllowedChanged: { - if (!linksPanelAllowed) { - linksExpanded = false - } - } - - Rectangle { + RowLayout { Layout.fillWidth: true - implicitHeight: collapsedBar.implicitHeight + 16 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 - - RowLayout { - id: collapsedBar - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 8 - spacing: 8 - - Item { + Layout.leftMargin: 12 + Layout.rightMargin: 8 + Layout.bottomMargin: 8 + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + CaptionTextType { + text: qsTr("Host") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { Layout.fillWidth: true - implicitHeight: Math.max(hexCaption.implicitHeight, 24) - - RowLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - spacing: 8 - - CaptionTextType { - id: hexCaption - Layout.fillWidth: true - text: settingsRoot.telemtActiveSecretForBaseHex(modelData) - color: AmneziaStyle.color.paleGray - elide: Text.ElideMiddle - font.pixelSize: 13 - } - - Image { - width: 24 - height: 24 - visible: addSecretDelegate.linksPanelAllowed - source: "qrc:/images/controls/chevron-down.svg" - sourceSize.width: 24 - sourceSize.height: 24 - rotation: addSecretDelegate.linksExpanded ? 180 : 0 - Behavior on rotation { NumberAnimation { duration: 150 } } - } - } - - MouseArea { - anchors.fill: parent - visible: addSecretDelegate.linksPanelAllowed - enabled: addSecretDelegate.linksPanelAllowed - cursorShape: Qt.PointingHandCursor - onClicked: addSecretDelegate.linksExpanded = !addSecretDelegate.linksExpanded - } - } - - ImageButtonType { - implicitWidth: 32 - implicitHeight: 32 - hoverEnabled: true - image: "qrc:/images/controls/trash.svg" - imageColor: AmneziaStyle.color.vibrantRed - onClicked: settingsRoot.telemtRemoveAdditionalSecret(index) + text: effectiveHost() + color: AmneziaStyle.color.paleGray + elide: Text.ElideRight } } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(effectiveHost()) + PageController.showNotificationMessage(qsTr("Copied")) } + } } - ColumnLayout { + DividerType { Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 8 Layout.topMargin: 8 - spacing: 8 - visible: addSecretDelegate.linksPanelAllowed && addSecretDelegate.linksExpanded - - CaptionTextType { + Layout.bottomMargin: 8 + ColumnLayout { Layout.fillWidth: true - text: qsTr("Use Telegram connection link") - color: AmneziaStyle.color.mutedGray - } - - Rectangle { - Layout.fillWidth: true - implicitHeight: expTmeRow.implicitHeight + 16 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 - - RowLayout { - id: expTmeRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 8 - spacing: 4 - - CaptionTextType { - Layout.fillWidth: true - text: settingsRoot.telemtTmeLinkForAdditional(modelData) - color: AmneziaStyle.color.goldenApricot - elide: Text.ElideRight - maximumLineCount: 1 - font.pixelSize: 13 - } - - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/qr-code.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: settingsRoot.telemtShareQr(settingsRoot.telemtTmeLinkForAdditional(modelData)) - } - - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: settingsRoot.telemtCopyText(settingsRoot.telemtTmeLinkForAdditional(modelData)) - } + spacing: 2 + CaptionTextType { + text: qsTr("Port") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: port + color: AmneziaStyle.color.paleGray } } - - Rectangle { - Layout.fillWidth: true - implicitHeight: expTgRow.implicitHeight + 16 - color: AmneziaStyle.color.onyxBlack - radius: 8 - border.color: AmneziaStyle.color.slateGray - border.width: 1 - - RowLayout { - id: expTgRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 12 - anchors.rightMargin: 8 - spacing: 4 - - CaptionTextType { - Layout.fillWidth: true - text: settingsRoot.telemtTgLinkForAdditional(modelData) - color: AmneziaStyle.color.goldenApricot - elide: Text.ElideRight - maximumLineCount: 1 - font.pixelSize: 13 - } - - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/qr-code.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: settingsRoot.telemtShareQr(settingsRoot.telemtTgLinkForAdditional(modelData)) - } - - ImageButtonType { - implicitWidth: 36 - implicitHeight: 36 - hoverEnabled: true - image: "qrc:/images/controls/copy.svg" - imageColor: AmneziaStyle.color.paleGray - onClicked: settingsRoot.telemtCopyText(settingsRoot.telemtTgLinkForAdditional(modelData)) - } - } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(port) + PageController.showNotificationMessage(qsTr("Copied")) } } } - } - } - BasicButtonType { - Layout.fillWidth: true - Layout.topMargin: 8 - - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - text: qsTr("Add additional secret") - clickedFunc: function () { - TelemtConfigModel.addAdditionalSecret() - } - } - - DividerType { - Layout.fillWidth: true - Layout.bottomMargin: 8 - } - - LabelTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.bottomMargin: 4 - text: qsTr("Worker mode") - } - - ButtonGroup { - id: workerModeGroup - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 4 - spacing: 0 - visible: transportMode !== "faketls" - - HorizontalRadioButton { - Layout.fillWidth: true - text: qsTr("Auto") - ButtonGroup.group: workerModeGroup - checked: workersMode === "auto" - onClicked: { workersMode = "auto"; TelemtConfigModel.setWorkersMode("auto") } - } - HorizontalRadioButton { - Layout.fillWidth: true - text: qsTr("Manual") - ButtonGroup.group: workerModeGroup - checked: workersMode === "manual" - onClicked: { workersMode = "manual"; TelemtConfigModel.setWorkersMode("manual") } - } - } - - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - visible: transportMode === "faketls" - text: qsTr("Workers are set to 0 automatically for FakeTLS mode.") - color: AmneziaStyle.color.mutedGray - font.pixelSize: 12 - wrapMode: Text.WordWrap - } - - TextFieldWithHeaderType { - id: workersTextField - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - visible: workersMode === "manual" && transportMode !== "faketls" - headerText: qsTr("Workers count") - textField.placeholderText: "2" - textField.text: workers - textField.maximumLength: 2 - textField.inputMethodHints: Qt.ImhDigitsOnly - textField.validator: IntValidator { - bottom: 0 - top: TelemtConfigModel.maxWorkers() - } - textField.onTextChanged: { - var cur = workersTextField.textField.text - if (cur === "") { - return + DividerType { + Layout.fillWidth: true } - var n = parseInt(cur, 10) - var maxW = TelemtConfigModel.maxWorkers() - if (isNaN(n) || n < 0) { n = 0 } - if (n > maxW) { n = maxW } - var clamped = String(n) - if (clamped !== cur) { - textField.text = clamped - textField.cursorPosition = clamped.length + + ButtonGroup { + id: secretTabGroup } - } - textField.onEditingFinished: { - var v = workersTextField.textField.text - if (v !== "") { - var m = parseInt(v, 10) - var maxW2 = TelemtConfigModel.maxWorkers() - if (isNaN(m) || m < 0) { m = 0 } - if (m > maxW2) { m = maxW2 } - v = String(m) - textField.text = v + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 8 + Layout.topMargin: 4 + Layout.bottomMargin: 8 + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + CaptionTextType { + text: qsTr("Secret") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: activeSecret() + color: AmneziaStyle.color.paleGray + wrapMode: Text.WrapAnywhere + font.pixelSize: 13 + } + } + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: { GC.copyToClipBoard(activeSecret()) + PageController.showNotificationMessage(qsTr("Copied")) } + } } - if (v !== workers) { - workers = v - TelemtConfigModel.setWorkers(workers) - } - } - } - - DividerType { - Layout.fillWidth: true - Layout.bottomMargin: 8 - } - - SwitcherType { - Layout.fillWidth: true - Layout.rightMargin: 16 - Layout.leftMargin: 16 - Layout.bottomMargin: 4 - text: qsTr("Server is behind NAT / Docker bridge") - descriptionText: qsTr("Enable if your server is not directly accessible from the internet, e.g. Docker or private network") - checked: natEnabled - onToggled: function () { - if (checked !== natEnabled) { - natEnabled = checked - TelemtConfigModel.setNatEnabled(natEnabled) - } - } - } - - TextFieldWithHeaderType { - id: natInternalIpTextField - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - visible: natEnabled - headerText: qsTr("Internal IP") - textField.placeholderText: "172.17.0.2" - textField.text: natInternalIp - textField.maximumLength: 15 - textField.validator: RegularExpressionValidator { - regularExpression: root.natIpv4InputFormat - } - textField.onTextChanged: { - if (root.natIpv4FieldShowInvalidError(textField.text)) { - natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") - } else { - natInternalIpTextField.errorText = "" - } - } - textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - if (!TelemtConfigModel.isValidOptionalIpv4(textField.text)) { - natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") - return - } - natInternalIpTextField.errorText = "" - if (textField.text !== natInternalIp) { - natInternalIp = textField.text - TelemtConfigModel.setNatInternalIp(natInternalIp) - } - } - } - - TextFieldWithHeaderType { - id: natExternalIpTextField - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 16 - visible: natEnabled - headerText: qsTr("External IP") - textField.placeholderText: "1.2.3.4" - textField.text: natExternalIp - textField.maximumLength: 15 - textField.validator: RegularExpressionValidator { - regularExpression: root.natIpv4InputFormat - } - textField.onTextChanged: { - if (root.natIpv4FieldShowInvalidError(textField.text)) { - natExternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") - } else { - natExternalIpTextField.errorText = "" - } - } - textField.onEditingFinished: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - if (!TelemtConfigModel.isValidOptionalIpv4(textField.text)) { - natExternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") - return - } - natExternalIpTextField.errorText = "" - if (textField.text !== natExternalIp) { - natExternalIp = textField.text - TelemtConfigModel.setNatExternalIp(natExternalIp) - } - } - } - } - - DividerType { - Layout.fillWidth: true - Layout.topMargin: 8 - } - - ColumnLayout { - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.bottomMargin: 8 - spacing: 8 - visible: containerStatus === 1 - - RowLayout { - Layout.fillWidth: true - - Header2Type { - Layout.fillWidth: true - headerText: qsTr("Diagnostics") - } - - ImageButtonType { - implicitWidth: 32 - implicitHeight: 32 - image: "qrc:/images/controls/refresh-cw.svg" - imageColor: diagLoading ? AmneziaStyle.color.mutedGray : AmneziaStyle.color.paleGray - hoverEnabled: !diagLoading - enabled: !diagLoading - onClicked: { - diagLoading = true - InstallController.refreshContainerDiagnostics(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, parseInt(port)) - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 8 - Rectangle { - width: 8 - height: 8 - radius: 4 - color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray - } - CaptionTextType { - Layout.fillWidth: true - text: qsTr("Public port reachable") - color: AmneziaStyle.color.paleGray - } - CaptionTextType { - text: diagClientsConnected < 0 ? qsTr("—") : (diagPortReachable ? qsTr("Yes") : qsTr("No")) - color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 8 - Rectangle { - width: 8 - height: 8 - radius: 4 - color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray - } - CaptionTextType { - Layout.fillWidth: true - text: qsTr("Telegram upstream reachable") - color: AmneziaStyle.color.paleGray - } - CaptionTextType { - text: diagClientsConnected < 0 ? qsTr("—") : (diagTelegramReachable ? qsTr("Yes") : qsTr("No")) - color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 8 - Rectangle { - width: 8 - height: 8 - radius: 4 - color: diagClientsConnected >= 0 ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray - } - CaptionTextType { - Layout.fillWidth: true - text: qsTr("Clients connected") - color: AmneziaStyle.color.paleGray - } - CaptionTextType { - text: diagClientsConnected < 0 ? qsTr("—") : diagClientsConnected.toString() - color: AmneziaStyle.color.paleGray - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 8 - Rectangle { - width: 8 - height: 8 - radius: 4 - color: AmneziaStyle.color.mutedGray - } - CaptionTextType { - Layout.fillWidth: true - text: qsTr("Last config refresh") - color: AmneziaStyle.color.paleGray - } - CaptionTextType { - text: diagLastConfigRefresh !== "" ? diagLastConfigRefresh : qsTr("—") - color: AmneziaStyle.color.mutedGray } } LabelWithButtonType { + id: removeButton Layout.fillWidth: true - Layout.leftMargin: -16 - visible: diagStatsEndpoint !== "" - text: qsTr("Stats endpoint") - descriptionText: diagStatsEndpoint - descriptionOnTop: true - rightImageSource: "qrc:/images/controls/copy.svg" - rightImageColor: AmneziaStyle.color.paleGray + Layout.bottomMargin: 24 + Layout.leftMargin: 0 + Layout.rightMargin: 16 + visible: ServersUiController.isProcessedServerHasWriteAccess() + text: qsTr("Delete Telemt") + textColor: AmneziaStyle.color.vibrantRed clickedFunction: function () { - GC.copyToClipBoard(diagStatsEndpoint) - PageController.showNotificationMessage(qsTr("Copied")) + var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) + var descriptionText = qsTr("The proxy will be stopped and all users will lose access.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + var yesButtonFunction = function () { + PageController.goToPage(PageEnum.PageDeinstalling) + InstallController.removeContainer(ServersUiController.processedServerId, ServersUiController.processedContainerIndex) + } + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, function () { + }) + } + MouseArea { + anchors.fill: removeButton + cursorShape: Qt.PointingHandCursor + enabled: false + } + } + } + } + + ListViewType { + id: settingsListView + model: TelemtConfigModel + reuseItems: false + + delegate: ColumnLayout { + id: settingsRoot + width: settingsListView.width + spacing: 0 + + readonly property bool fieldsEditable: isEnabled && containerStatus === 1 && !root.pageBusy + + function telemtActiveSecretForBaseHex(baseHex) { + return root.telemtClientSecretForTabIndex(baseHex, root.syncedSecretTabIndex, + root.savedTlsDomain, TelemtConfigModel.defaultTlsDomain()) + } + + function telemtEffectiveHostForLinks() { + return root.savedPublicHost !== "" ? root.savedPublicHost : ServersUiController.serverHostName(ServersUiController.processedServerId) + } + + function telemtTmeLinkForAdditional(baseHex) { + return "https://t.me/proxy?server=" + telemtEffectiveHostForLinks() + "&port=" + port + "&secret=" + telemtActiveSecretForBaseHex(baseHex) + } + + function telemtTgLinkForAdditional(baseHex) { + return "tg://proxy?server=" + telemtEffectiveHostForLinks() + "&port=" + port + "&secret=" + telemtActiveSecretForBaseHex(baseHex) + } + + function telemtIsAdditionalPersisted(hex) { + return root.telemtIsPersistedAdditionalHex(hex) + } + + function telemtCopyText(text) { + GC.copyToClipBoard(text) + PageController.showNotificationMessage(qsTr("Copied")) + } + + function telemtShareQr(link) { + ExportController.generateQrFromString(link) + PageController.goToShareConnectionPage(qsTr("Telegram connection link"), + qsTr("Telemt connection link"), "", "", "") + } + + function telemtRemoveAdditionalSecret(idx) { + TelemtConfigModel.removeAdditionalSecret(idx) + } + + SwitcherType { + id: enableTelemtSwitch + Layout.fillWidth: true + Layout.topMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + text: qsTr("Enable Telemt") + checked: isEnabled + enabled: containerStatus !== 0 && containerStatus !== 3 && !root.pageBusy + && !root.telemtNetworkBlocked + onToggled: function () { + if (checked !== isEnabled) { + previousEnabled = isEnabled + previousContainerStatus = containerStatus + root.previousSecret = secret + isEnabled = checked + isUpdating = true + if (checked) { + root.pendingUpdateAfterEnable = true + InstallController.setContainerEnabled(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, true) + } else { + InstallController.setContainerEnabled(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, false) + } + } } } CaptionTextType { Layout.fillWidth: true - text: diagLoading ? qsTr("Refreshing…") : qsTr("Tap ↻ to refresh diagnostics") + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + visible: !fieldsEditable && !root.pageBusy + text: (containerStatus === 1 || containerStatus === 2) + ? qsTr("Enable Telemt to edit settings") + : (statusErrorCode !== 0 + ? qsTr("Settings locked: connection timed out (error code %1). Re-open the page to retry.").arg(statusErrorCode) + : qsTr("Cannot reach the server — settings are unavailable")) color: AmneziaStyle.color.mutedGray - visible: diagClientsConnected < 0 + wrapMode: Text.WordWrap } - } - CaptionTextType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.topMargin: 16 * 2 - Layout.bottomMargin: 24 - text: qsTr("If you change the settings, the proxy connection link will change. The old link will stop working.") - color: AmneziaStyle.color.mutedGray - wrapMode: Text.WordWrap - font.pixelSize: 12 - } + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 * 2 + spacing: 4 - BasicButtonType { - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.bottomMargin: 32 - Layout.rightMargin: 16 - Layout.leftMargin: 16 - visible: ServersUiController.isProcessedServerHasWriteAccess() - enabled: fieldsEditable && !root.telemtNetworkBlocked - text: qsTr("Save") - clickedFunc: function () { - if (root.telemtNetworkBlocked) { - PageController.showErrorMessage(qsTr("No internet connection. Connect to the internet to change Telemt settings.")) - return + CaptionTextType { + text: qsTr("Base secret") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 } - publicHostTextField.errorText = "" - tagTextField.errorText = "" - tlsDomainTextField.errorText = "" - natInternalIpTextField.errorText = "" - natExternalIpTextField.errorText = "" - portTextField.errorText = "" - var portValue = portTextField.textField.text === "" - ? TelemtConfigModel.defaultPort() - : portTextField.textField.text + RowLayout { + Layout.fillWidth: true + spacing: 8 - var errorLines = [] - var bullet = "- " - if (!portTextField.textField.acceptableInput && portTextField.textField.text !== "") { - var portErr = qsTr("The port must be in the range of 1 to 65535") - portTextField.errorText = portErr - errorLines.push(bullet + portErr) - } - if (!TelemtConfigModel.isValidPublicHost(publicHostTextField.textField.text)) { - var hostErr = qsTr("Enter a valid IP address or domain name") - publicHostTextField.errorText = hostErr - errorLines.push(bullet + hostErr) - } - var tagNormalized = TelemtConfigModel.sanitizeMtProxyTagFieldText(tagTextField.textField.text) - tagTextField.textField.text = tagNormalized - if (!TelemtConfigModel.isValidMtProxyTag(tagNormalized)) { - var tagErr = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F), or leave empty.") - tagTextField.errorText = tagErr - errorLines.push(bullet + tagErr) - } - var domainValueForSave = tlsDomainTextField.textField.text === "" - ? TelemtConfigModel.defaultTlsDomain() - : tlsDomainTextField.textField.text - if (!TelemtConfigModel.isValidFakeTlsDomain(domainValueForSave)) { - var tlsErr = qsTr("Enter a valid domain name") - tlsDomainTextField.errorText = tlsErr - errorLines.push(bullet + tlsErr) - } - var natIpErr = qsTr("Enter a valid IPv4 address") - if (!TelemtConfigModel.isValidOptionalIpv4(natInternalIpTextField.textField.text)) { - natInternalIpTextField.errorText = natIpErr - errorLines.push(bullet + qsTr("NAT internal IP: enter a valid IPv4 address")) - } - if (!TelemtConfigModel.isValidOptionalIpv4(natExternalIpTextField.textField.text)) { - natExternalIpTextField.errorText = natIpErr - errorLines.push(bullet + qsTr("NAT external IP: enter a valid IPv4 address")) - } - if (errorLines.length > 0) { - PageController.showErrorMessage(errorLines.join("\n")) - return - } - TelemtConfigModel.setPort(portValue) - TelemtConfigModel.setTag(tagNormalized) - TelemtConfigModel.setPublicHost(publicHostTextField.textField.text) - TelemtConfigModel.setTransportMode(transportMode) - var domainValue = tlsDomainTextField.textField.text === "" - ? TelemtConfigModel.defaultTlsDomain() - : tlsDomainTextField.textField.text - TelemtConfigModel.setTlsDomain(domainValue) + CaptionTextType { + Layout.fillWidth: true + text: secret !== "" ? telemtActiveSecretForBaseHex(secret) : qsTr("Not generated") + color: secret !== "" ? AmneziaStyle.color.paleGray : AmneziaStyle.color.mutedGray + wrapMode: Text.WrapAnywhere + font.pixelSize: 14 + } - if (transportMode === "faketls") { - workers = "0" - TelemtConfigModel.setWorkers("0") - } else { - TelemtConfigModel.setWorkersMode(workersMode) - TelemtConfigModel.setWorkers(workers) + ImageButtonType { + Layout.alignment: Qt.AlignTop + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/refresh-cw.svg" + imageColor: AmneziaStyle.color.paleGray + visible: ServersUiController.isProcessedServerHasWriteAccess() + enabled: fieldsEditable + onClicked: { + var secretSnapshot = secret + showQuestionDrawer( + qsTr("Generate new secret?"), + qsTr("All existing connection links will stop working. Users will need new links."), + qsTr("Generate"), + qsTr("Cancel"), + function () { + root.previousSecret = secretSnapshot + if (containerStatus === 1) { + isUpdating = true + TelemtConfigModel.generateSecret() + root.telemtScheduleUpdate(false) + } else { + TelemtConfigModel.generateSecret() + PageController.showNotificationMessage(qsTr("New secret saved. It will be applied when Telemt is started.")) + } + }, + function () { + } + ) + } + } } - TelemtConfigModel.setNatEnabled(natEnabled) - TelemtConfigModel.setNatInternalIp(natInternalIpTextField.textField.text) - TelemtConfigModel.setNatExternalIp(natExternalIpTextField.textField.text) + } - previousPort = port - previousTag = tag - previousPublicHost = publicHost - previousTransportMode = transportMode - previousTlsDomain = tlsDomain - previousWorkersMode = workersMode - previousWorkers = workers - previousNatEnabled = natEnabled - previousNatInternalIp = natInternalIp - previousNatExternalIp = natExternalIp - root.previousSecret = secret - isUpdating = true - root.telemtScheduleUpdate(false) + TextFieldWithHeaderType { + id: publicHostTextField + enabled: fieldsEditable + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + headerText: qsTr("Public host / IP") + textField.placeholderText: ServersUiController.serverHostName(ServersUiController.processedServerId) + textField.text: publicHost + textField.maximumLength: 253 + textField.validator: PublicHostInputValidator { + } + textField.onTextChanged: { + var t = publicHostTextField.textField.text + if (TelemtConfigModel.isPublicHostTypingIncomplete(t)) { + publicHostTextField.errorText = "" + } else if (!TelemtConfigModel.isValidPublicHost(t)) { + publicHostTextField.errorText = qsTr("Enter a valid IP address or domain name") + } else { + publicHostTextField.errorText = "" + } + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (!TelemtConfigModel.isValidPublicHost(textField.text)) { + publicHostTextField.errorText = qsTr("Enter a valid IP address or domain name") + return + } + publicHostTextField.errorText = "" + if (textField.text !== publicHost) { + publicHost = textField.text + TelemtConfigModel.setPublicHost(publicHost) + } + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + visible: publicHostTextField.textField.text === "" + text: qsTr("Leave empty to use server IP automatically") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 12 + visible: publicHostTextField.textField.text !== "" && + publicHostTextField.textField.text !== ServersUiController.serverHostName(ServersUiController.processedServerId) + text: qsTr("⚠ This overrides the server IP in connection links. Make sure this host/domain points to your server.") + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + TextFieldWithHeaderType { + id: portTextField + enabled: fieldsEditable + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + headerText: qsTr("Server port") + textField.placeholderText: TelemtConfigModel.defaultPort() + textField.maximumLength: 5 + textField.inputMethodHints: Qt.ImhDigitsOnly + textField.validator: IntValidator { + bottom: 1 + top: 65535 + } + Component.onCompleted: { + var savedPort = port + textField.text = (savedPort === TelemtConfigModel.defaultPort()) ? "" : savedPort + } + textField.onTextChanged: { + var cur = portTextField.textField.text + var clean = TelemtConfigModel.sanitizePortFieldText(cur) + if (clean !== cur) { + textField.text = clean + textField.cursorPosition = clean.length + } + } + textField.onEditingFinished: { + textField.text = TelemtConfigModel.sanitizePortFieldText(textField.text) + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 12 + visible: transportMode === "faketls" && portTextField.textField.text !== "443" && portTextField.textField.text !== "" + text: qsTr("FakeTLS may not work on ports other than 443") + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("The promoted channel is set in @MTProxyBot. Paste the proxy tag here: exactly 32 hexadecimal characters (0-9, A-F), as in the bot message — or leave empty.") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + TextFieldWithHeaderType { + id: tagTextField + enabled: fieldsEditable + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + headerText: qsTr("Promoted channel tag (optional)") + textField.placeholderText: qsTr("32 hex chars from @MTProxyBot (e.g. 3b7b2fa9…)") + textField.text: tag + textField.maximumLength: TelemtConfigModel.mtProxyBotTagHexLength() + textField.onTextChanged: { + var cur = tagTextField.textField.text + var clean = TelemtConfigModel.sanitizeMtProxyTagFieldText(cur) + if (clean !== cur) { + textField.text = clean + textField.cursorPosition = clean.length + return + } + var tt = tagTextField.textField.text + if (tt === "") { + tagTextField.errorText = "" + return + } + if (TelemtConfigModel.isMtProxyTagTypingIncomplete(tt)) { + tagTextField.errorText = "" + return + } + if (!TelemtConfigModel.isValidMtProxyTag(tt)) { + tagTextField.errorText = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F).") + return + } + tagTextField.errorText = "" + } + textField.onEditingFinished: { + var raw = textField.text.replace(/^\s+|\s+$/g, '') + var normalized = TelemtConfigModel.sanitizeMtProxyTagFieldText(raw) + textField.text = normalized + if (!TelemtConfigModel.isValidMtProxyTag(normalized)) { + tagTextField.errorText = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F). Leave empty if unused.") + return + } + tagTextField.errorText = "" + if (normalized !== tag) { + tag = normalized + TelemtConfigModel.setTag(tag) + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + spacing: 4 + + CaptionTextType { + text: qsTr("Get a tag from") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + CaptionTextType { + text: "@MTProxyBot" + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 12 + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Qt.openUrlExternally("https://t.me/MTProxyBot") + } + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 * 2 + text: qsTr("Transport mode") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + } + + DropDownType { + id: transportModeDropDown + enabled: fieldsEditable + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + + drawerParent: root + drawerHeight: 0.4 + headerText: qsTr("Transport mode") + text: transportMode === "faketls" ? qsTr("FakeTLS") : qsTr("Standard MTProto") + + listView: Component { + ListViewType { + model: [qsTr("Standard MTProto"), qsTr("FakeTLS")] + delegate: LabelWithButtonType { + Layout.fillWidth: true + text: modelData + rightImageSource: { + var isCurrent = (index === 0 && transportMode === "standard") || + (index === 1 && transportMode === "faketls") + return isCurrent ? "qrc:/images/controls/check.svg" : "" + } + rightImageColor: AmneziaStyle.color.goldenApricot + clickedFunction: function () { + transportMode = (index === 0) ? "standard" : "faketls" + TelemtConfigModel.setTransportMode(transportMode) + root.syncedSecretTabIndex = transportMode === "faketls" ? 1 : 0 + transportModeDropDown.closeTriggered() + } + } + } + } + } + + TextFieldWithHeaderType { + id: tlsDomainTextField + enabled: fieldsEditable + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: transportMode === "faketls" + headerText: qsTr("FakeTLS domain") + textField.placeholderText: root.previousTlsDomain + textField.validator: RegularExpressionValidator { + regularExpression: /^[A-Za-z0-9.-]*$/ + } + Component.onCompleted: { + var savedDomain = tlsDomain + textField.text = (savedDomain === TelemtConfigModel.defaultTlsDomain() || savedDomain === "") ? "" : savedDomain + } + textField.onTextChanged: { + var t = tlsDomainTextField.textField.text + if (t === "" || TelemtConfigModel.isFakeTlsDomainTypingIncomplete(t) + || TelemtConfigModel.isValidFakeTlsDomain(t)) { + tlsDomainTextField.errorText = "" + } else { + tlsDomainTextField.errorText = qsTr("Enter a valid domain name") + } + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + var domainValue = textField.text === "" ? TelemtConfigModel.defaultTlsDomain() : textField.text + if (!TelemtConfigModel.isValidFakeTlsDomain(domainValue)) { + tlsDomainTextField.errorText = qsTr("Enter a valid domain name") + return + } + tlsDomainTextField.errorText = "" + if (domainValue !== tlsDomain) { + tlsDomain = domainValue + TelemtConfigModel.setTlsDomain(tlsDomain) + } + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + spacing: 4 + visible: transportMode === "faketls" + + CaptionTextType { + Layout.fillWidth: true + text: qsTr("The domain is encoded into the FakeTLS client secret (ee + base_secret + hex(domain)). It must support HTTPS / TLS 1.3.") + color: AmneziaStyle.color.mutedGray + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("\u26a0 Changing the domain will invalidate all previously issued FakeTLS connection links.") + color: AmneziaStyle.color.goldenApricot + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + } + + LabelWithButtonType { + id: advancedHeader + Layout.fillWidth: true + Layout.leftMargin: 0 + Layout.rightMargin: 16 + property bool expanded: false + text: qsTr("Advanced") + rightImageSource: expanded + ? "qrc:/images/controls/chevron-up.svg" + : "qrc:/images/controls/chevron-down.svg" + rightImageColor: AmneziaStyle.color.mutedGray + clickedFunction: function () { + expanded = !expanded + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + visible: advancedHeader.expanded + enabled: fieldsEditable + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + Layout.bottomMargin: 4 + text: qsTr("Additional secrets") + color: AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + text: qsTr("Add extra secrets to allow gradual migration without disconnecting existing users.") + color: AmneziaStyle.color.charcoalGray + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + + Repeater { + model: additionalSecrets + delegate: ColumnLayout { + id: addSecretDelegate + property bool linksExpanded: false + readonly property bool linksPanelAllowed: settingsRoot.telemtIsAdditionalPersisted(modelData) + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + spacing: 0 + + onLinksPanelAllowedChanged: { + if (!linksPanelAllowed) { + linksExpanded = false + } + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: collapsedBar.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + RowLayout { + id: collapsedBar + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 8 + + Item { + Layout.fillWidth: true + implicitHeight: Math.max(hexCaption.implicitHeight, 24) + + RowLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 8 + + CaptionTextType { + id: hexCaption + Layout.fillWidth: true + text: settingsRoot.telemtActiveSecretForBaseHex(modelData) + color: AmneziaStyle.color.paleGray + elide: Text.ElideMiddle + font.pixelSize: 13 + } + + Image { + width: 24 + height: 24 + visible: addSecretDelegate.linksPanelAllowed + source: "qrc:/images/controls/chevron-down.svg" + sourceSize.width: 24 + sourceSize.height: 24 + rotation: addSecretDelegate.linksExpanded ? 180 : 0 + Behavior on rotation { + NumberAnimation { + duration: 150 + } + } + } + } + + MouseArea { + anchors.fill: parent + visible: addSecretDelegate.linksPanelAllowed + enabled: addSecretDelegate.linksPanelAllowed + cursorShape: Qt.PointingHandCursor + onClicked: addSecretDelegate.linksExpanded = !addSecretDelegate.linksExpanded + } + } + + ImageButtonType { + implicitWidth: 32 + implicitHeight: 32 + hoverEnabled: true + image: "qrc:/images/controls/trash.svg" + imageColor: AmneziaStyle.color.vibrantRed + onClicked: settingsRoot.telemtRemoveAdditionalSecret(index) + } + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: 8 + spacing: 8 + visible: addSecretDelegate.linksPanelAllowed && addSecretDelegate.linksExpanded + + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Use Telegram connection link") + color: AmneziaStyle.color.mutedGray + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: expTmeRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + RowLayout { + id: expTmeRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: settingsRoot.telemtTmeLinkForAdditional(modelData) + color: AmneziaStyle.color.goldenApricot + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: settingsRoot.telemtShareQr(settingsRoot.telemtTmeLinkForAdditional(modelData)) + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: settingsRoot.telemtCopyText(settingsRoot.telemtTmeLinkForAdditional(modelData)) + } + } + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: expTgRow.implicitHeight + 16 + color: AmneziaStyle.color.onyxBlack + radius: 8 + border.color: AmneziaStyle.color.slateGray + border.width: 1 + + RowLayout { + id: expTgRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 12 + anchors.rightMargin: 8 + spacing: 4 + + CaptionTextType { + Layout.fillWidth: true + text: settingsRoot.telemtTgLinkForAdditional(modelData) + color: AmneziaStyle.color.goldenApricot + elide: Text.ElideRight + maximumLineCount: 1 + font.pixelSize: 13 + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/qr-code.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: settingsRoot.telemtShareQr(settingsRoot.telemtTgLinkForAdditional(modelData)) + } + + ImageButtonType { + implicitWidth: 36 + implicitHeight: 36 + hoverEnabled: true + image: "qrc:/images/controls/copy.svg" + imageColor: AmneziaStyle.color.paleGray + onClicked: settingsRoot.telemtCopyText(settingsRoot.telemtTgLinkForAdditional(modelData)) + } + } + } + } + } + } + + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 8 + + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + text: qsTr("Add additional secret") + clickedFunc: function () { + TelemtConfigModel.addAdditionalSecret() + } + } + + DividerType { + Layout.fillWidth: true + Layout.bottomMargin: 8 + } + + LabelTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.bottomMargin: 4 + text: qsTr("Worker mode") + } + + ButtonGroup { + id: workerModeGroup + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 4 + spacing: 0 + visible: transportMode !== "faketls" + + HorizontalRadioButton { + Layout.fillWidth: true + text: qsTr("Auto") + ButtonGroup.group: workerModeGroup + checked: workersMode === "auto" + onClicked: { workersMode = "auto"; TelemtConfigModel.setWorkersMode("auto") } + } + HorizontalRadioButton { + Layout.fillWidth: true + text: qsTr("Manual") + ButtonGroup.group: workerModeGroup + checked: workersMode === "manual" + onClicked: { workersMode = "manual"; TelemtConfigModel.setWorkersMode("manual") } + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + visible: transportMode === "faketls" + text: qsTr("Workers are set to 0 automatically for FakeTLS mode.") + color: AmneziaStyle.color.mutedGray + font.pixelSize: 12 + wrapMode: Text.WordWrap + } + + TextFieldWithHeaderType { + id: workersTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: workersMode === "manual" && transportMode !== "faketls" + headerText: qsTr("Workers count") + textField.placeholderText: "2" + textField.text: workers + textField.maximumLength: 2 + textField.inputMethodHints: Qt.ImhDigitsOnly + textField.validator: IntValidator { + bottom: 0 + top: TelemtConfigModel.maxWorkers() + } + textField.onTextChanged: { + var cur = workersTextField.textField.text + if (cur === "") { + return + } + var n = parseInt(cur, 10) + var maxW = TelemtConfigModel.maxWorkers() + if (isNaN(n) || n < 0) { + n = 0 + } + if (n > maxW) { + n = maxW + } + var clamped = String(n) + if (clamped !== cur) { + textField.text = clamped + textField.cursorPosition = clamped.length + } + } + textField.onEditingFinished: { + var v = workersTextField.textField.text + if (v !== "") { + var m = parseInt(v, 10) + var maxW2 = TelemtConfigModel.maxWorkers() + if (isNaN(m) || m < 0) { + m = 0 + } + if (m > maxW2) { + m = maxW2 + } + v = String(m) + textField.text = v + } + if (v !== workers) { + workers = v + TelemtConfigModel.setWorkers(workers) + } + } + } + + DividerType { + Layout.fillWidth: true + Layout.bottomMargin: 8 + } + + SwitcherType { + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 4 + text: qsTr("Server is behind NAT / Docker bridge") + descriptionText: qsTr("Enable if your server is not directly accessible from the internet, e.g. Docker or private network") + checked: natEnabled + onToggled: function () { + if (checked !== natEnabled) { + natEnabled = checked + TelemtConfigModel.setNatEnabled(natEnabled) + } + } + } + + TextFieldWithHeaderType { + id: natInternalIpTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: natEnabled + headerText: qsTr("Internal IP") + textField.placeholderText: "172.17.0.2" + textField.text: natInternalIp + textField.maximumLength: 15 + textField.validator: RegularExpressionValidator { + regularExpression: root.natIpv4InputFormat + } + textField.onTextChanged: { + if (root.natIpv4FieldShowInvalidError(textField.text)) { + natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + } else { + natInternalIpTextField.errorText = "" + } + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (!TelemtConfigModel.isValidOptionalIpv4(textField.text)) { + natInternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + return + } + natInternalIpTextField.errorText = "" + if (textField.text !== natInternalIp) { + natInternalIp = textField.text + TelemtConfigModel.setNatInternalIp(natInternalIp) + } + } + } + + TextFieldWithHeaderType { + id: natExternalIpTextField + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + visible: natEnabled + headerText: qsTr("External IP") + textField.placeholderText: "1.2.3.4" + textField.text: natExternalIp + textField.maximumLength: 15 + textField.validator: RegularExpressionValidator { + regularExpression: root.natIpv4InputFormat + } + textField.onTextChanged: { + if (root.natIpv4FieldShowInvalidError(textField.text)) { + natExternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + } else { + natExternalIpTextField.errorText = "" + } + } + textField.onEditingFinished: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + if (!TelemtConfigModel.isValidOptionalIpv4(textField.text)) { + natExternalIpTextField.errorText = qsTr("Enter a valid IPv4 address") + return + } + natExternalIpTextField.errorText = "" + if (textField.text !== natExternalIp) { + natExternalIp = textField.text + TelemtConfigModel.setNatExternalIp(natExternalIp) + } + } + } + } + + DividerType { + Layout.fillWidth: true + Layout.topMargin: 8 + } + + ColumnLayout { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 8 + spacing: 8 + visible: containerStatus === 1 + + RowLayout { + Layout.fillWidth: true + + Header2Type { + Layout.fillWidth: true + headerText: qsTr("Diagnostics") + } + + ImageButtonType { + implicitWidth: 32 + implicitHeight: 32 + image: "qrc:/images/controls/refresh-cw.svg" + imageColor: diagLoading ? AmneziaStyle.color.mutedGray : AmneziaStyle.color.paleGray + hoverEnabled: !diagLoading + enabled: !diagLoading + onClicked: { + diagLoading = true + InstallController.refreshContainerDiagnostics(ServersUiController.processedServerId, ServersUiController.processedContainerIndex, parseInt(port)) + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Public port reachable") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : (diagPortReachable ? qsTr("Yes") : qsTr("No")) + color: diagClientsConnected >= 0 ? (diagPortReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Telegram upstream reachable") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : (diagTelegramReachable ? qsTr("Yes") : qsTr("No")) + color: diagClientsConnected >= 0 ? (diagTelegramReachable ? AmneziaStyle.color.paleGray : AmneziaStyle.color.vibrantRed) : AmneziaStyle.color.mutedGray + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: diagClientsConnected >= 0 ? AmneziaStyle.color.goldenApricot : AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Clients connected") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagClientsConnected < 0 ? qsTr("—") : diagClientsConnected.toString() + color: AmneziaStyle.color.paleGray + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Rectangle { + width: 8 + height: 8 + radius: 4 + color: AmneziaStyle.color.mutedGray + } + CaptionTextType { + Layout.fillWidth: true + text: qsTr("Last config refresh") + color: AmneziaStyle.color.paleGray + } + CaptionTextType { + text: diagLastConfigRefresh !== "" ? diagLastConfigRefresh : qsTr("—") + color: AmneziaStyle.color.mutedGray + } + } + + LabelWithButtonType { + Layout.fillWidth: true + Layout.leftMargin: -16 + visible: diagStatsEndpoint !== "" + text: qsTr("Stats endpoint") + descriptionText: diagStatsEndpoint + descriptionOnTop: true + rightImageSource: "qrc:/images/controls/copy.svg" + rightImageColor: AmneziaStyle.color.paleGray + clickedFunction: function () { + GC.copyToClipBoard(diagStatsEndpoint) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + + CaptionTextType { + Layout.fillWidth: true + text: diagLoading ? qsTr("Refreshing…") : qsTr("Tap ↻ to refresh diagnostics") + color: AmneziaStyle.color.mutedGray + visible: diagClientsConnected < 0 + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 * 2 + Layout.bottomMargin: 24 + text: qsTr("If you change the settings, the proxy connection link will change. The old link will stop working.") + color: AmneziaStyle.color.mutedGray + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + + BasicButtonType { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.bottomMargin: 32 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + visible: ServersUiController.isProcessedServerHasWriteAccess() + enabled: fieldsEditable && !root.telemtNetworkBlocked + text: qsTr("Save") + clickedFunc: function () { + if (root.telemtNetworkBlocked) { + PageController.showErrorMessage(qsTr("No internet connection. Connect to the internet to change Telemt settings.")) + return + } + publicHostTextField.errorText = "" + tagTextField.errorText = "" + tlsDomainTextField.errorText = "" + natInternalIpTextField.errorText = "" + natExternalIpTextField.errorText = "" + portTextField.errorText = "" + + var portValue = portTextField.textField.text === "" + ? TelemtConfigModel.defaultPort() + : portTextField.textField.text + + var errorLines = [] + var bullet = "- " + if (!portTextField.textField.acceptableInput && portTextField.textField.text !== "") { + var portErr = qsTr("The port must be in the range of 1 to 65535") + portTextField.errorText = portErr + errorLines.push(bullet + portErr) + } + if (!TelemtConfigModel.isValidPublicHost(publicHostTextField.textField.text)) { + var hostErr = qsTr("Enter a valid IP address or domain name") + publicHostTextField.errorText = hostErr + errorLines.push(bullet + hostErr) + } + var tagNormalized = TelemtConfigModel.sanitizeMtProxyTagFieldText(tagTextField.textField.text) + tagTextField.textField.text = tagNormalized + if (!TelemtConfigModel.isValidMtProxyTag(tagNormalized)) { + var tagErr = qsTr("Proxy tag must be exactly 32 hexadecimal characters (0-9, A-F), or leave empty.") + tagTextField.errorText = tagErr + errorLines.push(bullet + tagErr) + } + var domainValueForSave = tlsDomainTextField.textField.text === "" + ? TelemtConfigModel.defaultTlsDomain() + : tlsDomainTextField.textField.text + if (!TelemtConfigModel.isValidFakeTlsDomain(domainValueForSave)) { + var tlsErr = qsTr("Enter a valid domain name") + tlsDomainTextField.errorText = tlsErr + errorLines.push(bullet + tlsErr) + } + var natIpErr = qsTr("Enter a valid IPv4 address") + if (!TelemtConfigModel.isValidOptionalIpv4(natInternalIpTextField.textField.text)) { + natInternalIpTextField.errorText = natIpErr + errorLines.push(bullet + qsTr("NAT internal IP: enter a valid IPv4 address")) + } + if (!TelemtConfigModel.isValidOptionalIpv4(natExternalIpTextField.textField.text)) { + natExternalIpTextField.errorText = natIpErr + errorLines.push(bullet + qsTr("NAT external IP: enter a valid IPv4 address")) + } + if (errorLines.length > 0) { + PageController.showErrorMessage(errorLines.join("\n")) + return + } + TelemtConfigModel.setPort(portValue) + TelemtConfigModel.setTag(tagNormalized) + TelemtConfigModel.setPublicHost(publicHostTextField.textField.text) + TelemtConfigModel.setTransportMode(transportMode) + var domainValue = tlsDomainTextField.textField.text === "" + ? TelemtConfigModel.defaultTlsDomain() + : tlsDomainTextField.textField.text + TelemtConfigModel.setTlsDomain(domainValue) + + if (transportMode === "faketls") { + workers = "0" + TelemtConfigModel.setWorkers("0") + } else { + TelemtConfigModel.setWorkersMode(workersMode) + TelemtConfigModel.setWorkers(workers) + } + TelemtConfigModel.setNatEnabled(natEnabled) + TelemtConfigModel.setNatInternalIp(natInternalIpTextField.textField.text) + TelemtConfigModel.setNatExternalIp(natExternalIpTextField.textField.text) + + previousPort = port + previousTag = tag + previousPublicHost = publicHost + previousTransportMode = transportMode + previousTlsDomain = tlsDomain + previousWorkersMode = workersMode + previousWorkers = workers + previousNatEnabled = natEnabled + previousNatInternalIp = natInternalIp + previousNatExternalIp = natExternalIp + root.previousSecret = secret + isUpdating = true + root.telemtScheduleUpdate(false) + } } } } } } - - } }