fix: fix local proxy settings and restart logic

- Added local proxy restart token management in Settings.
- Implemented logic to handle local proxy state on application quit.
- Updated ProxyServer to restart based on configuration changes.
- Enhanced API configuration updates to bump restart token when necessary.
- Improved UI components to reflect local proxy availability and state.
- Added new error handling and notifications for local proxy operations.
This commit is contained in:
aiamnezia
2026-04-13 07:14:42 +04:00
parent 850b8ea03b
commit be692001b0
10 changed files with 328 additions and 277 deletions

3
.gitignore vendored
View File

@@ -10,7 +10,8 @@ deploy/build_64/*
winbuild*.bat
.cache/
.vscode/
.cursorignore
.cursor/
# Qt-es
/.qmake.cache

View File

@@ -1,5 +1,6 @@
#include "coreController.h"
#include <QCoreApplication>
#include <QDirIterator>
#include <QDebug>
#include <QTranslator>
@@ -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;

View File

@@ -8,6 +8,7 @@ ProxyServer::ProxyServer(const std::shared_ptr<Settings> &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;
}

View File

@@ -32,4 +32,5 @@ private:
bool m_isRunning {false};
quint16 m_currentApiPort {0};
quint16 m_currentProxyPort {0};
int m_lastRestartToken {0};
};

View File

@@ -3,6 +3,8 @@
#include "QCoreApplication"
#include "QThread"
#include <limits>
#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<int>::max()) ? 0 : (current + 1);
m_settings.setValue("Conf/localProxyRestartToken", next);
emit localProxySettingsChanged();
}

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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();
// }
}
}

View File

@@ -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")

View File

@@ -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()