feat: implement local proxy settings UI and functionality

- Added new QML pages for managing local proxy settings and connection types.
- Updated SettingsController to handle local proxy enablement and port configuration.
- Enhanced server model to include processed server UUID for local proxy management.
This commit is contained in:
aiamnezia
2025-12-30 14:19:32 +04:00
parent 41ab51a5ef
commit 2a653f8876
9 changed files with 400 additions and 27 deletions

View File

@@ -253,6 +253,8 @@
<file>ui/qml/Components/AwgTextField.qml</file>
<file>ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml</file>
<file>ui/qml/Components/SmartScroll.qml</file>
<file>ui/qml/Pages2/PageSettingsConnectionType.qml</file>
<file>ui/qml/Pages2/PageSettingsLocalProxy.qml</file>
</qresource>
<qresource prefix="/countriesFlags">
<file>images/flagKit/ZW.svg</file>

View File

@@ -42,6 +42,8 @@ namespace PageLoader
PageSettingsApiDevices,
PageSettingsApiSubscriptionKey,
PageSettingsKillSwitchExceptions,
PageSettingsConnectionType,
PageSettingsLocalProxy,
PageServiceSftpSettings,
PageServiceTorWebsiteSettings,

View File

@@ -16,6 +16,11 @@
#include <AmneziaVPN-Swift.h>
#endif
namespace {
constexpr int kLocalProxyPortMin = 1024;
constexpr int kLocalProxyPortMax = 65535;
}
SettingsController::SettingsController(const QSharedPointer<ServersModel> &serversModel,
const QSharedPointer<ContainersModel> &containersModel,
const QSharedPointer<LanguageModel> &languageModel,
@@ -49,6 +54,8 @@ SettingsController::SettingsController(const QSharedPointer<ServersModel> &serve
m_isDevModeEnabled = m_settings->isDevGatewayEnv();
toggleDevGatewayEnv(m_isDevModeEnabled);
connect(m_settings.get(), &Settings::localProxySettingsChanged, this, &SettingsController::localProxySettingsUpdated);
}
QString getPlatformName()
@@ -523,3 +530,57 @@ void SettingsController::disableHomeAdLabel()
m_settings->disableHomeAdLabel();
emit isHomeAdLabelVisibleChanged(false);
}
bool SettingsController::isLocalProxyHttpEnabled() const
{
return m_settings->isLocalProxyHttpEnabled();
}
int SettingsController::localProxyPort() const
{
return static_cast<int>(m_settings->localProxyPort());
}
QString SettingsController::localProxyOwnerUuid() const
{
return m_settings->localProxyOwnerUuid();
}
bool SettingsController::setLocalProxyPort(int port)
{
if (port < kLocalProxyPortMin || port > kLocalProxyPortMax) {
return false;
}
if (m_settings->localProxyPort() == static_cast<quint16>(port)) {
return true;
}
m_settings->setLocalProxyPort(static_cast<quint16>(port));
return true;
}
bool SettingsController::enableLocalProxy(const QString &ownerUuid, int port)
{
if (port < kLocalProxyPortMin || port > kLocalProxyPortMax || ownerUuid.isEmpty()) {
return false;
}
if (m_settings->isLocalProxyHttpEnabled() && m_settings->localProxyOwnerUuid() != ownerUuid) {
return false;
}
m_settings->setLocalProxyOwnerUuid(ownerUuid);
setLocalProxyPort(port);
m_settings->setLocalProxyHttpEnabled(true);
return true;
}
void SettingsController::disableLocalProxy()
{
if (m_settings->isLocalProxyHttpEnabled()) {
m_settings->setLocalProxyHttpEnabled(false);
}
}

View File

@@ -36,6 +36,9 @@ public:
Q_PROPERTY(int safeAreaTopMargin READ getSafeAreaTopMargin NOTIFY safeAreaTopMarginChanged)
Q_PROPERTY(int safeAreaBottomMargin READ getSafeAreaBottomMargin NOTIFY safeAreaBottomMarginChanged)
Q_PROPERTY(int imeHeight READ getImeHeight NOTIFY imeHeightChanged)
Q_PROPERTY(bool isLocalProxyHttpEnabled READ isLocalProxyHttpEnabled NOTIFY localProxySettingsUpdated)
Q_PROPERTY(int localProxyPort READ localProxyPort WRITE setLocalProxyPort NOTIFY localProxySettingsUpdated)
Q_PROPERTY(QString localProxyOwnerUuid READ localProxyOwnerUuid NOTIFY localProxySettingsUpdated)
public slots:
void toggleAmneziaDns(bool enable);
@@ -109,6 +112,13 @@ public slots:
bool isHomeAdLabelVisible();
void disableHomeAdLabel();
bool isLocalProxyHttpEnabled() const;
int localProxyPort() const;
QString localProxyOwnerUuid() const;
bool setLocalProxyPort(int port);
bool enableLocalProxy(const QString &ownerUuid, int port);
void disableLocalProxy();
signals:
void primaryDnsChanged();
void secondaryDnsChanged();
@@ -140,6 +150,7 @@ signals:
void isHomeAdLabelVisibleChanged(bool visible);
void startMinimizedChanged();
void localProxySettingsUpdated();
private:
QSharedPointer<ServersModel> m_serversModel;

View File

@@ -984,3 +984,9 @@ QString ServersModel::getServerUuid(int index) const
return QString();
return m_servers.at(index).toObject().value(config_key::server_uuid).toString();
}
QString ServersModel::getProcessedServerUuid() const
{
return getServerUuid(m_processedServerIndex);
}

View File

@@ -84,6 +84,7 @@ public:
Q_PROPERTY(int processedIndex READ getProcessedServerIndex WRITE setProcessedServerIndex NOTIFY processedServerIndexChanged)
Q_PROPERTY(bool processedServerIsPremium READ processedServerIsPremium NOTIFY processedServerChanged)
Q_PROPERTY(QString processedServerUuid READ getProcessedServerUuid NOTIFY processedServerChanged)
Q_PROPERTY(bool isAdVisible READ isAdVisible NOTIFY defaultServerIndexChanged)
Q_PROPERTY(QString adHeader READ adHeader NOTIFY defaultServerIndexChanged)
@@ -160,6 +161,8 @@ public slots:
QString adDescription();
QString getServerUuid(int index) const;
QString getProcessedServerUuid() const;
protected:
QHash<int, QByteArray> roleNames() const override;

View File

@@ -151,32 +151,6 @@ PageType {
readonly property bool isVisibleForAmneziaFree: ApiAccountInfoModel.data("isComponentVisible")
SwitcherType {
id: switcher
readonly property bool isVlessProtocol: ApiConfigsController.isVlessProtocol()
Layout.fillWidth: true
Layout.topMargin: 24
Layout.rightMargin: 16
Layout.leftMargin: 16
visible: ApiAccountInfoModel.data("isProtocolSelectionSupported")
text: qsTr("Use VLESS protocol")
checked: switcher.isVlessProtocol
onToggled: function() {
if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) {
PageController.showNotificationMessage(qsTr("Cannot change protocol during active connection"))
} else {
PageController.showBusyIndicator(true)
ApiConfigsController.setCurrentProtocol(switcher.isVlessProtocol ? "awg" : "vless")
ApiConfigsController.updateServiceFromGateway(ServersModel.processedIndex, "", "", true)
PageController.showBusyIndicator(false)
}
}
}
WarningType {
id: warning
@@ -201,11 +175,25 @@ PageType {
}
LabelWithButtonType {
id: vpnKey
id: connectionSwitcher
Layout.fillWidth: true
Layout.topMargin: warning.visible ? 16 : 32
text: qsTr("Connection")
descriptionText: qsTr("Protocol selection and local proxy setup")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
PageController.goToPage(PageEnum.PageSettingsConnectionType)
}
}
DividerType {}
LabelWithButtonType {
id: vpnKey
Layout.fillWidth: true
visible: footer.isVisibleForAmneziaFree
text: qsTr("Subscription Key")

View File

@@ -0,0 +1,93 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Config"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onActiveFocusChanged: {
if(backButton.enabled && backButton.activeFocus) {
listView.positionViewAtBeginning()
}
}
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
header: ColumnLayout {
width: listView.width
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("Connection")
}
}
model: 1
delegate: ColumnLayout {
width: listView.width
LabelWithButtonType {
id: vpnProtocolButton
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("VPN protocol")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
PageController.goToPage(PageEnum.PageSettingsConnectionProtocols)
}
}
DividerType {}
LabelWithButtonType {
id: localProxyButton
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Local proxy")
descriptionText: SettingsController.isLocalProxyHttpEnabled ? qsTr("Running: 127.0.0.1:%1").arg(SettingsController.localProxyPort || 0)
: qsTr("Disabled")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
PageController.goToPage(PageEnum.PageSettingsLocalProxy)
}
}
DividerType {}
}
}
}

View File

@@ -0,0 +1,207 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
PageType {
id: root
readonly property int localProxyPortMin: 1024
readonly property int localProxyPortMax: 65535
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onActiveFocusChanged: {
if (activeFocus) {
listView.positionViewAtBeginning()
}
}
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
header: ColumnLayout {
width: listView.width
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("Local proxy")
}
}
model: 1 // fake model to force the ListView to be created without a model
delegate: ColumnLayout {
width: listView.width
spacing: 16
SwitcherType {
id: localProxySwitch
Layout.fillWidth: true
Layout.margins: 16
property string statusText: ""
text: qsTr("Enable local proxy")
descriptionText: statusText
checked: SettingsController.isLocalProxyHttpEnabled
function computeStatusText() {
statusText = SettingsController.isLocalProxyHttpEnabled
? qsTr("Running: 127.0.0.1:%1").arg(SettingsController.localProxyPort || 0)
: qsTr("Disabled")
}
function syncState() {
computeStatusText()
if (checked !== SettingsController.isLocalProxyHttpEnabled) {
checked = SettingsController.isLocalProxyHttpEnabled
}
}
Component.onCompleted: syncState()
onToggled: function() {
if (checked) {
const serverUuid = ServersModel.processedServerUuid
if (!serverUuid) {
checked = false
PageController.showNotificationMessage(qsTr("Unable to determine the current server"))
return
}
if (SettingsController.isLocalProxyHttpEnabled
&& SettingsController.localProxyOwnerUuid
&& SettingsController.localProxyOwnerUuid !== serverUuid) {
checked = false
PageController.showNotificationMessage(qsTr("Local proxy is already enabled for another server"))
return
}
const requestedPort = portField.portValue()
if (requestedPort < root.localProxyPortMin || requestedPort > root.localProxyPortMax) {
checked = false
PageController.showNotificationMessage(qsTr("Port must be between %1 and %2")
.arg(root.localProxyPortMin)
.arg(root.localProxyPortMax))
return
}
if (!SettingsController.enableLocalProxy(serverUuid, requestedPort)) {
checked = false
PageController.showNotificationMessage(qsTr("Failed to enable local proxy. Check the port (%1-%2).")
.arg(root.localProxyPortMin)
.arg(root.localProxyPortMax))
}
} else {
SettingsController.disableLocalProxy()
}
}
}
DividerType {}
TextFieldWithHeaderType {
id: portField
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("HTTP API port")
enabled: true
textField.validator: IntValidator {
bottom: root.localProxyPortMin
top: root.localProxyPortMax
}
function syncPortValue() {
const port = SettingsController.localProxyPort
textField.text = (port >= root.localProxyPortMin && port <= root.localProxyPortMax) ? port.toString() : ""
}
function portValue() {
const value = parseInt(textField.text)
return isNaN(value) ? -1 : value
}
Component.onCompleted: syncPortValue()
textField.onEditingFinished: {
const value = portField.portValue()
if (value < root.localProxyPortMin || value > root.localProxyPortMax) {
PageController.showNotificationMessage(qsTr("Port must be between %1 and %2")
.arg(root.localProxyPortMin)
.arg(root.localProxyPortMax))
portField.syncPortValue()
return
}
if (!SettingsController.setLocalProxyPort(value)) {
PageController.showNotificationMessage(qsTr("Failed to save port. Valid range: %1-%2")
.arg(root.localProxyPortMin)
.arg(root.localProxyPortMax))
}
portField.syncPortValue()
}
}
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("HTTP API controls Xray via /api/v1/up and /api/v1/down. SOCKS inbound stays on port 10808.")
}
}
}
Connections {
target: SettingsController
function onLocalProxySettingsUpdated() {
localProxySwitch.syncState()
if (!portField.textField.activeFocus) {
portField.syncPortValue()
}
}
}
Connections {
target: ServersModel
function onProcessedServerChanged() {
localProxySwitch.syncState()
if (!portField.textField.activeFocus) {
portField.syncPortValue()
}
}
}
}