diff --git a/.gitignore b/.gitignore index a48886bfc..2340a7191 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,8 @@ deploy/build_64/* winbuild*.bat .cache/ .vscode/ - +.cursorignore +.cursor/ # Qt-es /.qmake.cache diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index 8c5ef4a59..c759b1c1f 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -1,5 +1,6 @@ #include "coreController.h" +#include #include #include #include @@ -41,6 +42,12 @@ void CoreController::initLocalProxy() m_proxyServer.reset(new ProxyServer(m_settings, this)); + QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() { + if (m_settings && m_settings->isLocalProxyHttpEnabled()) { + m_settings->setLocalProxyHttpEnabled(false); + } + }); + auto syncLocalProxy = [this]() { if (!m_proxyServer) { return; diff --git a/client/core/local-proxy/proxyserver.cpp b/client/core/local-proxy/proxyserver.cpp index 0f941de72..8a270f3ef 100644 --- a/client/core/local-proxy/proxyserver.cpp +++ b/client/core/local-proxy/proxyserver.cpp @@ -8,6 +8,7 @@ ProxyServer::ProxyServer(const std::shared_ptr &settings, QObject *par , m_settings(settings) , m_service(new ProxyService(settings, this)) { + m_lastRestartToken = m_settings ? m_settings->localProxyRestartToken() : 0; } ProxyServer::~ProxyServer() @@ -73,6 +74,7 @@ bool ProxyServer::syncSettings() } const quint16 newProxyPort = m_settings ? m_settings->localProxyPort() : 0; + const int restartToken = m_settings ? m_settings->localProxyRestartToken() : 0; const bool xrayRunning = m_service->isXrayRunning(); if (!xrayRunning) { @@ -80,15 +82,27 @@ bool ProxyServer::syncSettings() const bool started = startXrayProcess(); if (started) { m_currentProxyPort = newProxyPort; + m_lastRestartToken = restartToken; } return started; } + if (m_lastRestartToken != restartToken) { + qInfo() << "Local proxy: restarting Xray due to config change token"; + const bool restarted = m_service->restartXray(); + if (restarted) { + m_currentProxyPort = newProxyPort; + m_lastRestartToken = restartToken; + } + return restarted; + } + if (m_currentProxyPort != newProxyPort) { qInfo() << "Local proxy: proxy port changed from" << m_currentProxyPort << "to" << newProxyPort; const bool restarted = m_service->restartXray(); if (restarted) { m_currentProxyPort = newProxyPort; + m_lastRestartToken = restartToken; } return restarted; } diff --git a/client/core/local-proxy/proxyserver.h b/client/core/local-proxy/proxyserver.h index c6d40a035..c9ef0742d 100644 --- a/client/core/local-proxy/proxyserver.h +++ b/client/core/local-proxy/proxyserver.h @@ -32,4 +32,5 @@ private: bool m_isRunning {false}; quint16 m_currentApiPort {0}; quint16 m_currentProxyPort {0}; + int m_lastRestartToken {0}; }; \ No newline at end of file diff --git a/client/settings.cpp b/client/settings.cpp index c705a20af..3f40030db 100644 --- a/client/settings.cpp +++ b/client/settings.cpp @@ -3,6 +3,8 @@ #include "QCoreApplication" #include "QThread" +#include + #include "core/networkUtilities.h" #include "version.h" @@ -84,8 +86,15 @@ void Settings::removeServer(int index) if (index >= servers.size()) return; + const QString removedUuid = servers.at(index).toObject().value(config_key::server_uuid).toString(); servers.removeAt(index); setServersArray(servers); + + if (!removedUuid.isEmpty() && removedUuid == localProxyOwnerUuid()) { + m_settings.setValue("Conf/localProxyHttpEnabled", false); + m_settings.setValue("Conf/localProxyOwnerUuid", ""); + emit localProxySettingsChanged(); + } emit serverRemoved(index); } @@ -644,3 +653,16 @@ void Settings::setLocalProxyHttpEnabled(bool enabled) m_settings.setValue("Conf/localProxyHttpEnabled", enabled); emit localProxySettingsChanged(); } + +int Settings::localProxyRestartToken() const +{ + return m_settings.value("Conf/localProxyRestartToken", 0).toInt(); +} + +void Settings::bumpLocalProxyRestartToken() +{ + const int current = localProxyRestartToken(); + const int next = (current == std::numeric_limits::max()) ? 0 : (current + 1); + m_settings.setValue("Conf/localProxyRestartToken", next); + emit localProxySettingsChanged(); +} diff --git a/client/settings.h b/client/settings.h index d2978bcbe..4c10026f7 100644 --- a/client/settings.h +++ b/client/settings.h @@ -256,6 +256,8 @@ public: void setLocalProxyPortUserDefined(bool userDefined); bool isLocalProxyHttpEnabled() const; void setLocalProxyHttpEnabled(bool enabled); + int localProxyRestartToken() const; + void bumpLocalProxyRestartToken(); signals: void saveLogsChanged(bool enabled); diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index d55a74dbd..4a9812ecc 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -933,6 +933,7 @@ bool ApiConfigsController::importTrialFromGateway(const QString &email) bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName, bool reloadServiceConfig) { + const QString serverUuid = m_serversModel->getServerUuid(serverIndex); auto serverConfig = m_serversModel->getServerConfig(serverIndex); auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); @@ -994,6 +995,14 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const emit subscriptionRefreshNeeded(); } + + if (!serverUuid.isEmpty() + && m_settings + && m_settings->isLocalProxyHttpEnabled() + && m_settings->localProxyOwnerUuid() == serverUuid) { + m_settings->bumpLocalProxyRestartToken(); + } + if (reloadServiceConfig) { emit reloadServerFromApiFinished(tr("API config reloaded")); } else if (newCountryName.isEmpty()) { @@ -1030,6 +1039,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex) m_settings->isStrictKillSwitchEnabled()); auto serverConfig = m_serversModel->getServerConfig(serverIndex); + const QString serverUuid = m_serversModel->getServerUuid(serverIndex); auto installationUuid = m_settings->getInstallationUuid(true); QString serviceProtocol = serverConfig.value(configKey::protocol).toString(); @@ -1054,6 +1064,14 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex) } m_serversModel->editServer(serverConfig, serverIndex); + + if (!serverUuid.isEmpty() + && m_settings + && m_settings->isLocalProxyHttpEnabled() + && m_settings->localProxyOwnerUuid() == serverUuid) { + m_settings->bumpLocalProxyRestartToken(); + } + emit updateServerFromApiFinished(); return true; } else { diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index 5358789f6..8e1a2c032 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -1,231 +1,232 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts - -import Style 1.0 - -import "TextTypes" - -Item { - id: root - - property string headerText - property string headerTextDisabledColor: AmneziaStyle.color.charcoalGray - property string headerTextColor: AmneziaStyle.color.mutedGray - - property alias errorText: errorField.text - property bool clearErrorOnTextChanged: true - property bool checkEmptyText: false - property bool rightButtonClickedOnEnter: false - - property string buttonText - property string buttonImageSource - property var clickedFunc - - property alias textField: textField - property string textFieldTextColor: AmneziaStyle.color.paleGray - property string textFieldTextDisabledColor: AmneziaStyle.color.mutedGray - - property bool textFieldEditable: true - - property string borderColor: AmneziaStyle.color.slateGray - property string borderFocusedColor: AmneziaStyle.color.paleGray - - property string backgroundColor: AmneziaStyle.color.onyxBlack - property string backgroundDisabledColor: AmneziaStyle.color.transparent - property string bgBorderHoveredColor: AmneziaStyle.color.charcoalGray - - implicitWidth: content.implicitWidth - implicitHeight: content.implicitHeight - - Keys.onTabPressed: { - FocusController.nextKeyTabItem() - } - - Keys.onBacktabPressed: { - FocusController.previousKeyTabItem() - } - - Keys.onUpPressed: { - FocusController.nextKeyUpItem() - } - - Keys.onDownPressed: { - FocusController.nextKeyDownItem() - } - - ColumnLayout { - id: content - anchors.fill: parent - - Rectangle { - id: backgroud - Layout.fillWidth: true - Layout.preferredHeight: input.implicitHeight - color: root.enabled ? root.backgroundColor : root.backgroundDisabledColor - radius: 16 - border.color: getBackgroundBorderColor(root.borderColor) - border.width: 1 - - Behavior on border.color { - PropertyAnimation { duration: 200 } - } - - RowLayout { - id: input - anchors.fill: backgroud - ColumnLayout { - Layout.margins: 16 - LabelTextType { - text: root.headerText - color: root.enabled ? root.headerTextColor : root.headerTextDisabledColor - - visible: text !== "" - - Layout.fillWidth: true - } - - TextField { - id: textField - - property bool isFocusable: true - - Keys.onTabPressed: { - FocusController.nextKeyTabItem() - } - - Keys.onBacktabPressed: { - FocusController.previousKeyTabItem() - } - - enabled: root.textFieldEditable - color: root.enabled ? root.textFieldTextColor : root.textFieldTextDisabledColor - - inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhSensitiveData | Qt.ImhNoPredictiveText - - placeholderTextColor: AmneziaStyle.color.charcoalGray - - selectionColor: AmneziaStyle.color.richBrown - selectedTextColor: AmneziaStyle.color.paleGray - - font.pixelSize: 16 - font.weight: 400 - font.family: "PT Root UI VF" - - height: 24 - Layout.fillWidth: true - - topPadding: 0 - rightPadding: 0 - leftPadding: 0 - bottomPadding: 0 - - background: Rectangle { - anchors.fill: parent - color: root.backgroundDisabledColor - } - - onTextChanged: { - if (root.clearErrorOnTextChanged) { - root.errorText = "" - } - } - - onActiveFocusChanged: { - if (root.checkEmptyText && text === "") { - root.errorText = qsTr("The field can't be empty") - } - } - - ContextMenu.menu: ContextMenuType { - textObj: textField - } - - onFocusChanged: { - backgroud.border.color = getBackgroundBorderColor(root.borderColor) - } - } - } - } - } - - SmallTextType { - id: errorField - - text: root.errorText - visible: root.errorText !== "" - color: AmneziaStyle.color.vibrantRed - - Layout.fillWidth: true - } - } - - MouseArea { - anchors.fill: root - cursorShape: Qt.IBeamCursor - - hoverEnabled: true - - onPressed: function(mouse) { - textField.forceActiveFocus() - mouse.accepted = false - - backgroud.border.color = getBackgroundBorderColor(root.borderColor) - } - - onEntered: { - backgroud.border.color = getBackgroundBorderColor(bgBorderHoveredColor) - } - - - onExited: { - backgroud.border.color = getBackgroundBorderColor(root.borderColor) - } - } - - BasicButtonType { - visible: (root.buttonText !== "") || (root.buttonImageSource !== "") - - focusPolicy: Qt.NoFocus - text: root.buttonText - leftImageSource: root.buttonImageSource - - anchors.top: content.top - anchors.bottom: content.bottom - anchors.right: content.right - - height: content.implicitHeight - width: content.implicitHeight - squareLeftSide: true - - clickedFunc: function() { - if (root.clickedFunc && typeof root.clickedFunc === "function") { - root.clickedFunc() - } - } - } - - function getBackgroundBorderColor(noneFocusedColor) { - return textField.focus ? root.borderFocusedColor : noneFocusedColor - } - - Keys.onEnterPressed: { - if (root.rightButtonClickedOnEnter && root.clickedFunc && typeof root.clickedFunc === "function") { - clickedFunc() - } - - // if (KeyNavigation.tab) { - // KeyNavigation.tab.forceActiveFocus(); - // } - } - - Keys.onReturnPressed: { - if (root.rightButtonClickedOnEnter &&root.clickedFunc && typeof root.clickedFunc === "function") { - clickedFunc() - } - - // if (KeyNavigation.tab) { - // KeyNavigation.tab.forceActiveFocus(); - // } - } -} +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Style 1.0 + +import "TextTypes" + +Item { + id: root + + property string headerText + property string headerTextDisabledColor: AmneziaStyle.color.charcoalGray + property string headerTextColor: AmneziaStyle.color.mutedGray + + property alias errorText: errorField.text + property bool clearErrorOnTextChanged: true + property bool checkEmptyText: false + property bool rightButtonClickedOnEnter: false + + property string buttonText + property string buttonImageSource + property var clickedFunc + + property alias textField: textField + property string textFieldTextColor: AmneziaStyle.color.paleGray + property string textFieldTextDisabledColor: AmneziaStyle.color.mutedGray + + property bool textFieldEditable: true + + property string borderColor: AmneziaStyle.color.slateGray + property string borderFocusedColor: AmneziaStyle.color.paleGray + + property string backgroundColor: AmneziaStyle.color.onyxBlack + property string backgroundDisabledColor: AmneziaStyle.color.transparent + property string bgBorderHoveredColor: AmneziaStyle.color.charcoalGray + + implicitWidth: content.implicitWidth + implicitHeight: content.implicitHeight + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + ColumnLayout { + id: content + anchors.fill: parent + + Rectangle { + id: backgroud + Layout.fillWidth: true + Layout.preferredHeight: input.implicitHeight + color: root.enabled ? root.backgroundColor : root.backgroundDisabledColor + radius: 16 + border.color: getBackgroundBorderColor(root.borderColor) + border.width: 1 + + Behavior on border.color { + PropertyAnimation { duration: 200 } + } + + RowLayout { + id: input + anchors.fill: backgroud + ColumnLayout { + Layout.margins: 16 + LabelTextType { + text: root.headerText + color: root.enabled ? root.headerTextColor : root.headerTextDisabledColor + + visible: text !== "" + + Layout.fillWidth: true + } + + TextField { + id: textField + + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + enabled: root.textFieldEditable + color: root.enabled ? root.textFieldTextColor : root.textFieldTextDisabledColor + + inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhSensitiveData | Qt.ImhNoPredictiveText + + placeholderTextColor: AmneziaStyle.color.charcoalGray + + selectionColor: AmneziaStyle.color.richBrown + selectedTextColor: AmneziaStyle.color.paleGray + + font.pixelSize: 16 + font.weight: 400 + font.family: "PT Root UI VF" + + height: 24 + Layout.fillWidth: true + + topPadding: 0 + rightPadding: 0 + leftPadding: 0 + bottomPadding: 0 + + background: Rectangle { + anchors.fill: parent + color: root.backgroundDisabledColor + } + + onTextChanged: { + if (root.clearErrorOnTextChanged) { + root.errorText = "" + } + } + + onActiveFocusChanged: { + if (root.checkEmptyText && text === "") { + root.errorText = qsTr("The field can't be empty") + } + } + + ContextMenu.menu: ContextMenuType { + textObj: textField + } + + onFocusChanged: { + backgroud.border.color = getBackgroundBorderColor(root.borderColor) + } + } + } + } + } + + SmallTextType { + id: errorField + + text: root.errorText + visible: root.errorText !== "" + color: AmneziaStyle.color.vibrantRed + + Layout.fillWidth: true + } + } + + MouseArea { + anchors.fill: root + cursorShape: Qt.IBeamCursor + + hoverEnabled: true + + onPressed: function(mouse) { + textField.forceActiveFocus() + mouse.accepted = false + + backgroud.border.color = getBackgroundBorderColor(root.borderColor) + } + + onEntered: { + backgroud.border.color = getBackgroundBorderColor(bgBorderHoveredColor) + } + + + onExited: { + backgroud.border.color = getBackgroundBorderColor(root.borderColor) + } + } + + BasicButtonType { + visible: (root.buttonText !== "") || (root.buttonImageSource !== "") + parent: backgroud + + focusPolicy: Qt.NoFocus + text: root.buttonText + leftImageSource: root.buttonImageSource + + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + + height: parent.height + width: Math.max(height, implicitWidth) + squareLeftSide: true + + clickedFunc: function() { + if (root.clickedFunc && typeof root.clickedFunc === "function") { + root.clickedFunc() + } + } + } + + function getBackgroundBorderColor(noneFocusedColor) { + return textField.focus ? root.borderFocusedColor : noneFocusedColor + } + + Keys.onEnterPressed: { + if (root.rightButtonClickedOnEnter && root.clickedFunc && typeof root.clickedFunc === "function") { + clickedFunc() + } + + // if (KeyNavigation.tab) { + // KeyNavigation.tab.forceActiveFocus(); + // } + } + + Keys.onReturnPressed: { + if (root.rightButtonClickedOnEnter &&root.clickedFunc && typeof root.clickedFunc === "function") { + clickedFunc() + } + + // if (KeyNavigation.tab) { + // KeyNavigation.tab.forceActiveFocus(); + // } + } +} diff --git a/client/ui/qml/Pages2/PageSettingsConnectionType.qml b/client/ui/qml/Pages2/PageSettingsConnectionType.qml index 863a84eae..ee5bda192 100644 --- a/client/ui/qml/Pages2/PageSettingsConnectionType.qml +++ b/client/ui/qml/Pages2/PageSettingsConnectionType.qml @@ -64,7 +64,7 @@ PageType { rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { - PageController.goToPage(PageEnum.PageSettingsConnectionProtocols) + PageController.goToPage(PageEnum.PageSettingsServerProtocols) } } @@ -76,7 +76,7 @@ PageType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - visible: SettingsController.isLocalProxySupported + visible: SettingsController.isLocalProxySupported && ServersModel.processedServerIsPremium Layout.preferredHeight: visible ? implicitHeight : 0 text: qsTr("Local proxy") diff --git a/client/ui/qml/Pages2/PageSettingsLocalProxy.qml b/client/ui/qml/Pages2/PageSettingsLocalProxy.qml index 356ca9128..4e2e3ec5b 100644 --- a/client/ui/qml/Pages2/PageSettingsLocalProxy.qml +++ b/client/ui/qml/Pages2/PageSettingsLocalProxy.qml @@ -22,18 +22,11 @@ PageType { property int pendingStartAutoSelectedPort: -1 property bool pendingStartVpnWasActive: false - Component.onCompleted: root.syncSwitchState() - function getPortField() { var item = listView.itemAtIndex(0) return item !== null ? item.children[0] : null } - function getHeaderBlock() { - var headerItem = listView.headerItem - return headerItem && headerItem.children.length > 0 ? headerItem.children[0] : null - } - function computePortErrorText() { var portField = getPortField() if (portField === null) return "" @@ -54,28 +47,22 @@ PageType { return "" } - function syncSwitchState() { - setSwitcherChecked(SettingsController.isLocalProxyHttpEnabled) - } - - function setSwitcherChecked(value) { - var header = getHeaderBlock() - if (!header || header.switcher.checked === value) { - return - } - header.switcher.checked = value - } - function handleLocalProxyToggle(checked) { if (checked) { + if (!ServersModel.processedServerIsPremium) { + PageController.showNotificationMessage(qsTr("Local proxy is available only for Amnezia Premium")) + return + } const wasVpnActive = ConnectionController.isConnected || ConnectionController.isConnectionInProgress if (wasVpnActive) { ConnectionController.closeConnection() } - const serverUuid = ServersModel.processedServerUuid + let serverUuid = ServersModel.processedServerUuid + if (!serverUuid && ServersModel.defaultIndex !== undefined) { + serverUuid = ServersModel.getServerUuid(ServersModel.defaultIndex) + } if (!serverUuid) { - root.setSwitcherChecked(false) PageController.showNotificationMessage(qsTr("Unable to determine the current server")) return } @@ -83,14 +70,12 @@ PageType { if (SettingsController.isLocalProxyHttpEnabled && SettingsController.localProxyOwnerUuid && SettingsController.localProxyOwnerUuid !== serverUuid) { - root.setSwitcherChecked(false) PageController.showNotificationMessage(qsTr("Local proxy is already enabled for another server")) return } const requestedPort = SettingsController.localProxyPort if (requestedPort < root.localProxyPortMin || requestedPort > root.localProxyPortMax) { - root.setSwitcherChecked(false) PageController.showNotificationMessage(qsTr("Port must be between %1 and %2") .arg(root.localProxyPortMin) .arg(root.localProxyPortMax)) @@ -103,7 +88,6 @@ PageType { || requestedPort !== root.defaultLocalProxyPort) { PageController.showNotificationMessage(qsTr("Port %1 is already in use on this device. Choose another one") .arg(requestedPort)) - root.setSwitcherChecked(false) return } @@ -111,32 +95,28 @@ PageType { if (autoSelectedPort <= 0) { PageController.showNotificationMessage(qsTr("Port %1 is already in use on this device. Choose another one") .arg(requestedPort)) - root.setSwitcherChecked(false) return } } const portToEnable = autoSelectedPort > 0 ? autoSelectedPort : requestedPort if (!SettingsController.enableLocalProxy(serverUuid, portToEnable)) { - root.setSwitcherChecked(false) PageController.showNotificationMessage(qsTr("Failed to enable local proxy. Check the port (%1-%2).") .arg(root.localProxyPortMin) .arg(root.localProxyPortMax)) - root.syncSwitchState() return } root.pendingStartRequestedPort = requestedPort root.pendingStartAutoSelectedPort = autoSelectedPort root.pendingStartVpnWasActive = wasVpnActive startSuccessToastTimer.restart() - root.syncSwitchState() } else { startSuccessToastTimer.stop() root.pendingStartRequestedPort = -1 root.pendingStartAutoSelectedPort = -1 root.pendingStartVpnWasActive = false SettingsController.disableLocalProxy() - root.syncSwitchState() + PageController.showNotificationMessage(qsTr("Local proxy stopped")) } } @@ -174,14 +154,20 @@ PageType { Layout.rightMargin: 16 headerText: qsTr("Local Proxy") descriptionText: qsTr("Use a proxy to route selected apps (for example, the CensorTracker extension) through Amnezia Premium.") - showSwitcher: true - Component.onCompleted: root.syncSwitchState() + showSwitcher: ServersModel.processedServerIsPremium + switcher { + checked: SettingsController.isLocalProxyHttpEnabled + } switcherFunction: function(checked) { // Ignore UI sync toggles; react only to real state change intent. if (checked === SettingsController.isLocalProxyHttpEnabled) { return } root.handleLocalProxyToggle(checked) + // Keep checked declaratively linked after any user interaction path. + localProxyHeader.switcher.checked = Qt.binding(function() { + return SettingsController.isLocalProxyHttpEnabled + }) } } @@ -209,11 +195,10 @@ PageType { text: qsTr("Learn more") clickedFunc: function() { - const isRussian = LanguageModel.currentLanguageName === "Русский" - const learnMoreUrl = isRussian - ? "http://docs.amnezia.org/ru/documentation/instructions/local-proxy" - : "http://docs.amnezia.org/documentation/instructions/local-proxy" - Qt.openUrlExternally(learnMoreUrl) + const path = LanguageModel.currentLanguageName === "Русский" + ? "ru/documentation/instructions/local-proxy" + : "documentation/instructions/local-proxy" + Qt.openUrlExternally(LanguageModel.getCurrentDocsUrl(path)) } } } @@ -245,9 +230,8 @@ PageType { PageController.showNotificationMessage(qsTr("Copied: 127.0.0.1:%1").arg(portText)) } - textField.validator: IntValidator { - bottom: root.localProxyPortMin - top: root.localProxyPortMax + textField.validator: RegularExpressionValidator { + regularExpression: /^[0-9]{0,5}$/ } textField.leftPadding: portPrefix.implicitWidth textField.placeholderText: root.defaultLocalProxyPort.toString() @@ -256,7 +240,7 @@ PageType { function syncPortValue() { const port = SettingsController.localProxyPort const isValidPort = port >= root.localProxyPortMin && port <= root.localProxyPortMax - textField.text = (isValidPort && port !== root.defaultLocalProxyPort) ? port.toString() : "" + textField.text = isValidPort ? port.toString() : "" } function portValue() { @@ -309,6 +293,10 @@ PageType { enabled: true clickedFunc: function() { + if (SettingsController.isLocalProxyHttpEnabled) { + PageController.showNotificationMessage(qsTr("Disable Local Proxy to change the port")) + return + } const validationError = root.computePortErrorText() root.portValidationError = validationError if (validationError !== "") { @@ -361,7 +349,6 @@ PageType { target: SettingsController function onLocalProxySettingsUpdated() { - root.syncSwitchState() var portField = root.getPortField() if (portField !== null && !portField.textField.activeFocus) { portField.syncPortValue() @@ -374,7 +361,6 @@ PageType { root.pendingStartAutoSelectedPort = -1 root.pendingStartVpnWasActive = false PageController.showNotificationMessage(message) - root.syncSwitchState() } } @@ -382,7 +368,6 @@ PageType { target: ServersModel function onProcessedServerChanged() { - root.syncSwitchState() var portField = root.getPortField() if (portField !== null && !portField.textField.activeFocus) { portField.syncPortValue()