feat: add xray subscription link support

This commit is contained in:
vkamn
2026-05-15 16:32:47 +08:00
parent 009ca981d5
commit 476f16d027
27 changed files with 828 additions and 9 deletions
@@ -11,8 +11,13 @@
#include <QRegularExpressionMatch>
#include <QRegularExpressionMatchIterator>
#include <QUrl>
#include <QUrlQuery>
#include <QEventLoop>
#include <QTimer>
#include <algorithm>
#include <iostream>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
#include "core/utils/protocolEnum.h"
@@ -372,6 +377,206 @@ int ImportController::qrChunksTotal() const
return m_totalQrCodeChunksCount;
}
ImportController::ImportResult ImportController::importLink(const QUrl &url)
{
ImportResult result;
if (!url.isValid()) {
qWarning() << "Invalid URL:" << url;
result.errorCode = ErrorCode::ImportInvalidConfigError;
return result;
}
QNetworkAccessManager manager;
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
QNetworkReply *reply = manager.get(request);
QEventLoop loop;
QTimer timer;
timer.setSingleShot(true);
bool timedOut = false;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
QObject::connect(&timer, &QTimer::timeout, &loop, [&]() {
timedOut = true;
reply->abort();
loop.quit();
});
timer.start(10000);
loop.exec();
if (timedOut) {
qWarning() << "Request timed out";
reply->deleteLater();
result.errorCode = ErrorCode::ImportInvalidConfigError;
return result;
}
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "Network error:" << reply->errorString();
reply->deleteLater();
result.errorCode = ErrorCode::ImportInvalidConfigError;
return result;
}
QByteArray data = reply->readAll();
reply->deleteLater();
if (data.isEmpty()) {
qWarning() << "Empty response";
result.errorCode = ErrorCode::ImportInvalidConfigError;
return result;
}
QByteArray decoded;
QString text;
if (isValidBase64(data)) {
decoded = QByteArray::fromBase64(data);
text = QString::fromUtf8(decoded).trimmed();
} else {
data.replace('\r', "");
text = QString::fromUtf8(data).trimmed();
}
if (text.isEmpty()) {
qWarning() << "Decoded text is empty";
result.errorCode = ErrorCode::ImportInvalidConfigError;
return result;
}
QStringList configs = text.split('\n', Qt::SkipEmptyParts);
QJsonArray configStrings;
QJsonArray configNames;
for (const QString &cfg : configs) {
bool supported = true;
if (!(cfg.startsWith("vless://") || cfg.startsWith("vmess://") || cfg.startsWith("trojan://")
|| cfg.startsWith("ss://") || cfg.startsWith("ssd://"))) {
supported = false;
qWarning() << "Unknown protocol:" << cfg.left(20);
continue;
}
QUrl url(cfg);
QUrlQuery query(url);
QString name = QUrl::fromPercentEncoding(url.fragment().toUtf8());
name.isEmpty() ? name = "Unnamed" : name = "[" + name.replace(" /", "]");
if (!name.contains("v2ray") && supported) {
configStrings.append(cfg);
configNames.append(name);
} else {
qWarning() << "Config unsupported";
}
}
if (configStrings.isEmpty()) {
qWarning() << "No valid configs found";
result.errorCode = ErrorCode::ImportInvalidConfigError;
return result;
}
QString firstConfig = configStrings.first().toString();
result = extractConfigFromData(firstConfig);
QJsonObject serverConfig;
for (auto it = result.config.begin(); it != result.config.end(); ++it) {
serverConfig.insert(it.key(), it.value());
}
serverConfig.insert(configKey::description, m_appSettingsRepository->nextAvailableServerName());
serverConfig["xray_subscription_config"] = configStrings;
serverConfig["xray_subscription_config_name"] = configNames;
serverConfig["xray_subscription_config_current"] = 0;
result.config = serverConfig;
return result;
}
ImportController::ImportResult ImportController::editServerConfigWithData(QString data, int serverIndex, const QJsonObject& uiConfig)
{
ImportResult result = extractConfigFromData(data);
if (result.errorCode != ErrorCode::NoError)
return result;
const ServerConfig currentServerConfig = m_serversRepository->server(serverIndex);
QJsonObject editedConfig = result.config;
const QJsonObject currentConfig = currentServerConfig.toJson();
for (auto it = uiConfig.begin(); it != uiConfig.end(); ++it) {
editedConfig.insert(it.key(), it.value());
}
if (currentConfig.contains(configKey::description)) {
editedConfig.insert(configKey::description, currentConfig.value(configKey::description));
}
if (currentConfig.contains("xray_subscription_config")) {
editedConfig.insert("xray_subscription_config", currentConfig.value("xray_subscription_config"));
}
if (currentConfig.contains("xray_subscription_config_name")) {
editedConfig.insert("xray_subscription_config_name", currentConfig.value("xray_subscription_config_name"));
}
if (currentConfig.contains("xray_subscription_config_current")) {
editedConfig.insert("xray_subscription_config_current", currentConfig.value("xray_subscription_config_current"));
}
const ServerConfig finalServerConfig = ServerConfig::fromJson(editedConfig);
m_serversRepository->editServer(serverIndex, finalServerConfig);
result.config = editedConfig;
return result;
}
bool ImportController::isValidBase64(const QByteArray &input)
{
QByteArray data = input;
data = data.trimmed();
if (data.isEmpty())
return false;
static QRegularExpression base64Regex("^[A-Za-z0-9+/=_\\r\\n-]+$");
if (!base64Regex.match(QString::fromLatin1(data)).hasMatch())
return false;
data.replace("\r", "");
data.replace("\n", "");
if (data.size() % 4 != 0)
return false;
QByteArray decoded = QByteArray::fromBase64(data, QByteArray::Base64UrlEncoding);
if (decoded.isEmpty())
decoded = QByteArray::fromBase64(data);
return !decoded.isEmpty();
}
void ImportController::importConfig(const QJsonObject &config)
{
ServerCredentials credentials;
@@ -63,6 +63,10 @@ public:
int qrChunksReceived() const;
int qrChunksTotal() const;
ImportResult importLink(const QUrl &url);
ImportResult editServerConfigWithData(QString data, int serverIndex, const QJsonObject &uiConfig);
bool isValidBase64(const QByteArray &input);
void importConfig(const QJsonObject &config);
QJsonObject processNativeWireGuardConfig(const QJsonObject &config);
@@ -58,6 +58,31 @@ void ServersController::clearCachedProfile(int serverIndex, DockerContainer cont
m_serversRepository->clearLastConnectionConfig(serverIndex, container);
}
void ServersController::setCurrentConfigIndex(const int index)
{
m_serversRepository->setCurrentConfigIndex(index);
}
int ServersController::getCurrentConfigIndex() const
{
return m_serversRepository->getCurrentConfigIndex();
}
QString ServersController::getConfigString(const int index) const
{
return m_serversRepository->getConfigString(index);
}
QString ServersController::getConfigName(const int index) const
{
return m_serversRepository->getConfigName(index);
}
QJsonArray ServersController::getConfigNames() const
{
return m_serversRepository->getConfigNames();
}
QJsonArray ServersController::getServersArray() const
{
QJsonArray result;
@@ -64,6 +64,13 @@ public:
// Cache management
void clearCachedProfile(int serverIndex, DockerContainer container);
// XRay subscription config getters/setters
void setCurrentConfigIndex(int index);
int getCurrentConfigIndex() const;
QString getConfigString(const int index) const;
QString getConfigName(const int index) const;
QJsonArray getConfigNames() const;
// Getters
QJsonArray getServersArray() const;
QVector<ServerConfig> getServers() const;
@@ -58,6 +58,18 @@ QJsonObject NativeServerConfig::toJson() const
if (!dns2.isEmpty()) {
obj[configKey::dns2] = dns2;
}
if (configString) {
obj[QLatin1String("xray_subscription_config")] = configString.value();
}
if (configName) {
obj[QLatin1String("xray_subscription_config_name")] = configName.value();
}
if (currentConfig) {
obj[QLatin1String("xray_subscription_config_current")] = currentConfig.value();
}
return obj;
}
@@ -85,7 +97,16 @@ NativeServerConfig NativeServerConfig::fromJson(const QJsonObject& json)
config.dns1 = json.value(configKey::dns1).toString();
config.dns2 = json.value(configKey::dns2).toString();
if (json.contains(QLatin1String("xray_subscription_config")))
config.configString = json.value(QLatin1String("xray_subscription_config")).toArray();
if (json.contains(QLatin1String("xray_subscription_config_name")))
config.configName = json.value(QLatin1String("xray_subscription_config_name")).toArray();
if (json.contains(QLatin1String("xray_subscription_config_current")))
config.currentConfig = json.value(QLatin1String("xray_subscription_config_current")).toInt();
return config;
}
@@ -2,7 +2,9 @@
#define NATIVESERVERCONFIG_H
#include <QJsonObject>
#include <QJsonArray>
#include <QMap>
#include <optional>
#include "core/utils/containerEnum.h"
#include "core/utils/containers/containerUtils.h"
@@ -21,6 +23,10 @@ struct NativeServerConfig {
DockerContainer defaultContainer;
QString dns1;
QString dns2;
std::optional<QJsonArray> configString;
std::optional<QJsonArray> configName;
std::optional<int> currentConfig;
bool hasContainers() const;
ContainerConfig containerConfig(DockerContainer container) const;
+6 -2
View File
@@ -120,6 +120,10 @@ bool ServerConfig::isApiConfig() const
return isApiV1() || isApiV2();
}
bool ServerConfig::isXRayConfig() const {
return isNative() && std::get<NativeServerConfig>(data).configString.has_value();
}
QJsonObject ServerConfig::toJson() const
{
return std::visit([](const auto& v) { return v.toJson(); }, data);
@@ -150,7 +154,7 @@ ServerConfig ServerConfig::fromJson(const QJsonObject& json)
break;
}
}
if (hasThirdPartyConfig) {
return ServerConfig{NativeServerConfig::fromJson(json)};
} else {
@@ -186,7 +190,7 @@ ServerConfig ServerConfig::fromJson(const QJsonObject& json)
break;
}
}
if (hasThirdPartyConfig) {
return ServerConfig{NativeServerConfig::fromJson(json)};
} else {
+1
View File
@@ -57,6 +57,7 @@ struct ServerConfig {
bool isApiV1() const;
bool isApiV2() const;
bool isApiConfig() const;
bool isXRayConfig() const;
template<typename T>
T* as() {
@@ -176,6 +176,73 @@ void SecureServersRepository::clearLastConnectionConfig(int serverIndex, DockerC
setContainerConfig(serverIndex, container, containerCfg);
}
void SecureServersRepository::setCurrentConfigIndex(const int index)
{
ServerConfig serverConfig = server(m_defaultServerIndex);
NativeServerConfig *xrayConfig = serverConfig.as<NativeServerConfig>();
xrayConfig->currentConfig = index;
editServer(m_defaultServerIndex, serverConfig);
}
int SecureServersRepository::getCurrentConfigIndex() const
{
const ServerConfig serverConfig = server(m_defaultServerIndex);
if (!serverConfig.isXRayConfig())
return int();
const NativeServerConfig *xrayConfig = serverConfig.as<NativeServerConfig>();
if (!xrayConfig->currentConfig.has_value())
return int();
return xrayConfig->currentConfig.value();
}
QString SecureServersRepository::getConfigString(const int index) const
{
const ServerConfig serverConfig = server(m_defaultServerIndex);
if (!serverConfig.isXRayConfig())
return QString();
const NativeServerConfig *xrayConfig = serverConfig.as<NativeServerConfig>();
if (!xrayConfig->configString.has_value())
return QString();
if (index < 0 || index >= xrayConfig->configString.value().size())
return QString();
return xrayConfig->configString.value().at(index).toString();
}
QString SecureServersRepository::getConfigName(const int index) const
{
const ServerConfig serverConfig = server(m_defaultServerIndex);
if (!serverConfig.isXRayConfig())
return QString();
const NativeServerConfig *xrayConfig = serverConfig.as<NativeServerConfig>();
if (!xrayConfig->configName.has_value())
return QString();
if (index < 0 || index >= xrayConfig->configName.value().size())
return QString();
return xrayConfig->configName.value().at(index).toString();
}
QJsonArray SecureServersRepository::getConfigNames() const
{
const ServerConfig serverConfig = server(m_defaultServerIndex);
if (!serverConfig.isXRayConfig())
return QJsonArray();
const NativeServerConfig *xrayConfig = serverConfig.as<NativeServerConfig>();
if (!xrayConfig->configName.has_value())
return QJsonArray();
return xrayConfig->configName.value();
}
ServerCredentials SecureServersRepository::serverCredentials(int index) const
{
ServerConfig config = server(index);
@@ -35,6 +35,12 @@ public:
void setContainerConfig(int serverIndex, DockerContainer container, const ContainerConfig &config);
void clearLastConnectionConfig(int serverIndex, DockerContainer container);
void setCurrentConfigIndex(int index);
int getCurrentConfigIndex() const;
QString getConfigString(const int index) const;
QString getConfigName(const int index) const;
QJsonArray getConfigNames() const;
ServerCredentials serverCredentials(int index) const;
bool hasServerWithVpnKey(const QString &vpnKey) const;
bool hasServerWithCrc(quint16 crc) const;
+4
View File
@@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 3.75V14.25" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.25 9L9 14.25L3.75 9" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 327 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 14.25V3.75" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.75 9L9 3.75L14.25 9" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 326 B

+2
View File
@@ -9,8 +9,10 @@
<file>controls/amnezia.svg</file>
<file>controls/app.svg</file>
<file>controls/archive-restore.svg</file>
<file>controls/arrow-down.svg</file>
<file>controls/arrow-left.svg</file>
<file>controls/arrow-right.svg</file>
<file>controls/arrow-up.svg</file>
<file>controls/bug.svg</file>
<file>controls/check.svg</file>
<file>controls/chevron-down.svg</file>
@@ -31,6 +31,41 @@ ImportUiController::ImportUiController(ImportController* importController, QObje
connect(m_importController, &ImportController::restoreAppConfig, this, &ImportUiController::restoreAppConfig);
}
bool ImportUiController::importLink(const QUrl &url)
{
auto result = m_importController->importLink(url);
if (result.errorCode != ErrorCode::NoError) {
emit importErrorOccurred(result.errorCode, false);
return false;
}
m_config = result.config;
m_configFileName = result.configFileName;
m_maliciousWarningText = result.maliciousWarningText;
m_isNativeWireGuardConfig = result.isNativeWireGuardConfig;
return true;
}
bool ImportUiController::editServerConfigWithData(QString data, int serverIndex)
{
auto result = m_importController->editServerConfigWithData(data, serverIndex, m_config);
if (result.errorCode != ErrorCode::NoError) {
emit importErrorOccurred(result.errorCode, false);
return false;
}
m_config = result.config;
m_configFileName = result.configFileName;
m_maliciousWarningText = result.maliciousWarningText;
m_isNativeWireGuardConfig = result.isNativeWireGuardConfig;
return true;
}
bool ImportUiController::extractConfigFromFile(const QString &fileName)
{
QString data;
@@ -20,6 +20,8 @@ public:
public slots:
void importConfig();
void clearConfigFileName();
bool importLink(const QUrl &url);
bool editServerConfigWithData(QString data, int serverIndex);
bool extractConfigFromFile(const QString &fileName);
bool extractConfigFromData(QString data);
bool extractConfigFromQr(const QByteArray &data);
@@ -44,6 +44,8 @@ namespace PageLoader
PageSettingsApiNativeConfigs,
PageSettingsApiDevices,
PageSettingsApiSubscriptionKey,
PageSettingsXRayAvailableConfigs,
PageSettingsXRayServerInfo,
PageSettingsKillSwitchExceptions,
PageServiceSftpSettings,
+41 -1
View File
@@ -159,7 +159,11 @@ QString ServersUiController::getDefaultServerDescriptionCollapsed() const
if (server.isApiConfig()) {
return description;
}
if (server.isXRayConfig()) {
return getConfigName(getCurrentConfigIndex());
}
DockerContainer container = server.defaultContainer();
QString containerName = ContainerUtils::containerHumanNames().value(container);
QString protocolVersion;
@@ -210,6 +214,10 @@ QString ServersUiController::getDefaultServerDescriptionExpanded() const
if (server.isApiConfig()) {
return description;
}
if (server.isXRayConfig()) {
return getConfigName(getCurrentConfigIndex());
}
return description + server.hostName();
}
@@ -272,6 +280,13 @@ bool ServersUiController::isDefaultServerFromApi() const
|| configVersion == apiDefs::ConfigSource::AmneziaGateway;
}
bool ServersUiController::isDefaultServerContainXRayConfigs() const
{
int defaultIndex = getDefaultServerIndex();
const ServerConfig server = m_serversController->getServerConfig(defaultIndex);
return server.isXRayConfig();
}
int ServersUiController::getProcessedServerIndex() const
{
return m_processedServerIndex;
@@ -444,6 +459,31 @@ QString ServersUiController::adDescription() const
return QString();
}
void ServersUiController::setCurrentConfigIndex(const int index)
{
m_serversController->setCurrentConfigIndex(index);
}
int ServersUiController::getCurrentConfigIndex() const
{
return m_serversController->getCurrentConfigIndex();
}
QString ServersUiController::getConfigString(const int index) const
{
return m_serversController->getConfigString(index);
}
QString ServersUiController::getConfigName(const int index) const
{
return m_serversController->getConfigName(index);
}
QJsonArray ServersUiController::getConfigNames() const
{
return m_serversController->getConfigNames();
}
void ServersUiController::updateContainersModel()
{
if (m_processedServerIndex < 0 || m_processedServerIndex >= m_serversController->getServersCount()) {
@@ -26,6 +26,8 @@ class ServersUiController : public QObject
Q_PROPERTY(bool isDefaultServerDefaultContainerHasSplitTunneling READ isDefaultServerDefaultContainerHasSplitTunneling NOTIFY defaultServerIndexChanged)
Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIndexChanged)
Q_PROPERTY(bool isDefaultServerContainXRayConfigs READ isDefaultServerContainXRayConfigs NOTIFY defaultServerIndexChanged)
Q_PROPERTY(int processedIndex READ getProcessedServerIndex WRITE setProcessedServerIndex NOTIFY processedServerIndexChanged)
Q_PROPERTY(int processedContainerIndex READ getProcessedContainerIndex WRITE setProcessedContainerIndex NOTIFY processedContainerIndexChanged)
Q_PROPERTY(bool processedServerIsPremium READ processedServerIsPremium NOTIFY processedServerIndexChanged)
@@ -61,6 +63,7 @@ public slots:
QString getDefaultServerDescriptionExpanded() const;
bool isDefaultServerDefaultContainerHasSplitTunneling() const;
bool isDefaultServerFromApi() const;
bool isDefaultServerContainXRayConfigs() const;
int getProcessedServerIndex() const;
void setProcessedServerIndex(int index);
@@ -77,6 +80,12 @@ public slots:
bool isAdVisible() const;
QString adHeader() const;
QString adDescription() const;
void setCurrentConfigIndex(int index);
int getCurrentConfigIndex() const;
QString getConfigString(const int index) const;
QString getConfigName(const int index) const;
QJsonArray getConfigNames() const;
QStringList getAllInstalledServicesName(int serverIndex) const;
+16
View File
@@ -34,6 +34,10 @@ namespace
constexpr char publicKeyInfo[] = "public_key";
constexpr char expiresAt[] = "expires_at";
constexpr char xraySubscriptionConfig[] = "xray_subscription_config";
constexpr char xraySubscriptionConfigName[] = "xray_subscription_config_name";
constexpr char xraySubscriptionConfigCurrent[] = "xray_subscription_config_current";
}
QString normalizeVpnKey(const QString &vpnKey)
@@ -207,6 +211,11 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
}
return apiUtils::isSubscriptionExpiringSoon(apiConfig.subscription.endDate);
}
case IsXRayConfigSelectionAvailableRole: {
if (server.isXRayConfig()) {
return server.as<NativeServerConfig>()->configString.has_value();
}
}
}
return QVariant();
@@ -301,6 +310,11 @@ bool ServersModel::isDefaultServerFromApi()
|| data(m_defaultServerIndex, IsServerFromGatewayApiRole).toBool();
}
bool ServersModel::isDefaultServerContainXRayConfigs()
{
return data(m_defaultServerIndex, IsXRayConfigSelectionAvailableRole).toBool();
}
bool ServersModel::isProcessedServerHasWriteAccess()
{
return qvariant_cast<bool>(data(m_processedServerIndex, HasWriteAccessRole));
@@ -350,6 +364,8 @@ QHash<int, QByteArray> ServersModel::roleNames() const
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
roles[IsXRayConfigSelectionAvailableRole] = "isXRayConfigSelectionAvailable";
return roles;
}
+4
View File
@@ -46,6 +46,8 @@ public:
IsSubscriptionExpiredRole,
IsSubscriptionExpiringSoonRole,
IsXRayConfigSelectionAvailableRole,
HasAmneziaDns
};
@@ -60,6 +62,8 @@ public slots:
bool isDefaultServerCurrentlyProcessed();
bool isDefaultServerFromApi();
bool isDefaultServerContainXRayConfigs();
bool isProcessedServerHasWriteAccess();
bool isDefaultServerHasWriteAccess();
bool hasServerWithWriteAccess();
+3 -1
View File
@@ -68,7 +68,7 @@ ListViewType {
text: name
descriptionText: isServerFromGatewayApi && (isSubscriptionExpired || isSubscriptionExpiringSoon)
? (isSubscriptionExpired ? qsTr("Subscription expired. Please renew") : qsTr("Subscription expiring soon"))
: serverDescription
: (isXRayConfigSelectionAvailable ? ServersUiController.getConfigName(ServersUiController.getCurrentConfigIndex()) : serverDescription)
descriptionColor: isServerFromGatewayApi && (isSubscriptionExpired || isSubscriptionExpiringSoon)
? (isSubscriptionExpired ? AmneziaStyle.color.vibrantRed : AmneziaStyle.color.goldenApricot)
: AmneziaStyle.color.mutedGray
@@ -121,6 +121,8 @@ ListViewType {
PageController.goToPage(PageEnum.PageSettingsApiServerInfo)
}
} else if (ServersModel.getProcessedServerData("isXRayConfigSelectionAvailable")) {
PageController.goToPage(PageEnum.PageSettingsXRayAvailableConfigs)
} else {
PageController.goToPage(PageEnum.PageSettingsServerInfo)
}
+5 -3
View File
@@ -311,11 +311,11 @@ PageType {
objectName: "rowLayoutLabel"
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Layout.topMargin: 8
Layout.bottomMargin: drawer.isCollapsedStateActive ? 44 : ServersUiController.isDefaultServerFromApi ? 61 : 16
Layout.bottomMargin: drawer.isCollapsedStateActive ? 44 : (ServersUiController.isDefaultServerFromApi || ServersUiController.isDefaultServerContainXRayConfigs) ? 61 : 16
spacing: 0
BasicButtonType {
enabled: (ServersUiController.defaultServerImagePathCollapsed !== "") && drawer.isCollapsedStateActive
enabled: (ServersUiController.defaultServerImagePathCollapsed !== "" || ServersUiController.isDefaultServerContainXRayConfigs) && drawer.isCollapsedStateActive
hoverEnabled: enabled
implicitHeight: 36
@@ -359,6 +359,8 @@ PageType {
PageController.goToPage(PageEnum.PageSettingsApiServerInfo)
}
} else if (ServersModel.getProcessedServerData("isXRayConfigSelectionAvailable")) {
PageController.goToPage(PageEnum.PageSettingsXRayAvailableConfigs)
} else {
PageController.goToPage(PageEnum.PageSettingsServerInfo)
}
@@ -379,7 +381,7 @@ PageType {
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
spacing: 8
visible: !ServersUiController.isDefaultServerFromApi
visible: !ServersUiController.isDefaultServerFromApi && !ServersUiController.isDefaultServerContainXRayConfigs
DropDownType {
id: containersDropDown
@@ -97,6 +97,8 @@ PageType {
}
PageController.goToPage(PageEnum.PageSettingsApiServerInfo)
} else if (ServersModel.getProcessedServerData("isXRayConfigSelectionAvailable")) {
PageController.goToPage(PageEnum.PageSettingsXRayServerInfo)
} else {
PageController.goToPage(PageEnum.PageSettingsServerInfo)
}
@@ -0,0 +1,164 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import SortFilterProxyModel 0.2
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
property var processedServer
Connections {
target: ServersModel
function onProcessedServerChanged() {
root.processedServer = proxyServersModel.get(0)
}
}
SortFilterProxyModel {
id: proxyServersModel
objectName: "proxyServersModel"
sourceModel: ServersModel
filters: [
ValueFilter {
roleName: "isCurrentlyProcessed"
value: true
}
]
Component.onCompleted: {
root.processedServer = proxyServersModel.get(0)
}
}
ListViewType {
id: menuContent
anchors.fill: parent
model: xrayConfigs
currentIndex: 0
ButtonGroup {
id: containersRadioButtonGroup
}
header: ColumnLayout {
width: menuContent.width
spacing: 4
BackButtonType {
id: backButton
objectName: "backButton"
Layout.topMargin: 20 + PageController.safeAreaTopMargin
}
HeaderTypeWithButton {
id: headerContent
objectName: "headerContent"
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 10
actionButtonImage: "qrc:/images/controls/settings.svg"
headerText: root.processedServer.name
actionButtonFunction: function() {
PageController.goToPage(PageEnum.PageSettingsXRayServerInfo)
}
}
}
delegate: ColumnLayout {
id: content
width: menuContent.width
height: content.implicitHeight
RowLayout {
VerticalRadioButton {
id: containerRadioButton
Layout.fillWidth: true
Layout.leftMargin: 16
text: model.title
ButtonGroup.group: containersRadioButtonGroup
imageSource: "qrc:/images/controls/download.svg"
checked: index === ServersUiController.getCurrentConfigIndex()
checkable: !ConnectionController.isConnected
onClicked: {
if (ConnectionController.isConnectionInProgress) {
PageController.showNotificationMessage(qsTr("Unable change config while trying to make an active connection"))
return
}
if (ConnectionController.isConnected) {
PageController.showNotificationMessage(qsTr("Unable change config while there is an active connection"))
return
}
if (index !== ServersUiController.getCurrentConfigIndex()) {
PageController.showBusyIndicator(true)
ServersUiController.setCurrentConfigIndex(index)
ImportController.editServerConfigWithData(ServersUiController.getConfigString(index), ServersUiController.getProcessedServerIndex())
PageController.showBusyIndicator(false)
}
}
Keys.onEnterPressed: {
if (checkable) {
checked = true
}
containerRadioButton.clicked()
}
Keys.onReturnPressed: {
if (checkable) {
checked = true
}
containerRadioButton.clicked()
}
}
}
DividerType {
Layout.fillWidth: true
}
}
}
ListModel {
id: xrayConfigs
}
Component.onCompleted: {
xrayConfigs.clear()
const names = ServersUiController.getConfigNames()
for (let i = 0; i < names.length; ++i) {
xrayConfigs.append({ title: names[i] })
}
}
}
@@ -0,0 +1,172 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import SortFilterProxyModel 0.2
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
property var processedServer
Connections {
target: ServersModel
function onProcessedServerChanged() {
root.processedServer = proxyServersModel.get(0)
}
}
SortFilterProxyModel {
id: proxyServersModel
objectName: "proxyServersModel"
sourceModel: ServersModel
filters: [
ValueFilter {
roleName: "isCurrentlyProcessed"
value: true
}
]
Component.onCompleted: {
root.processedServer = proxyServersModel.get(0)
}
}
ListViewType {
id: listView
anchors.fill: parent
model: 1
header: ColumnLayout {
width: listView.width
spacing: 4
BackButtonType {
id: backButton
objectName: "backButton"
Layout.topMargin: 20 + PageController.safeAreaTopMargin
}
HeaderTypeWithButton {
id: headerContent
objectName: "headerContent"
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 10
actionButtonImage: "qrc:/images/controls/edit-3.svg"
headerText: root.processedServer.name
actionButtonFunction: function() {
serverNameEditDrawer.openTriggered()
}
}
}
footer: ColumnLayout {
id: footer
width: listView.width
spacing: 0
BasicButtonType {
id: resetButton
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 24
Layout.bottomMargin: 16
Layout.leftMargin: 8
implicitHeight: 32
defaultColor: "transparent"
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
textColor: AmneziaStyle.color.vibrantRed
text: qsTr("Reload config")
clickedFunc: function() {
var headerText = qsTr("Reload config?")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) {
PageController.showNotificationMessage(qsTr("Cannot reload config during active connection"))
} else {
PageController.showBusyIndicator(true)
InstallController.rebootProcessedServer()
PageController.showBusyIndicator(false)
}
}
var noButtonFunction = function() {
}
showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
BasicButtonType {
id: removeButton
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: 16
Layout.leftMargin: 8
implicitHeight: 32
defaultColor: "transparent"
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
textColor: AmneziaStyle.color.vibrantRed
text: qsTr("Remove from application")
clickedFunc: function() {
var headerText = qsTr("Remove from application?")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) {
PageController.showNotificationMessage(qsTr("Cannot remove server during active connection"))
} else {
PageController.showBusyIndicator(true)
InstallController.removeServer(ServersUiController.getProcessedServerIndex())
PageController.showBusyIndicator(false)
}
}
var noButtonFunction = function() {
}
showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
}
}
RenameServerDrawer {
id: serverNameEditDrawer
anchors.fill: parent
expandedHeight: parent.height * 0.35
serverNameText: root.processedServer.name
}
}
@@ -187,8 +187,19 @@ PageType {
text: qsTr("Continue")
function isValidUrl(text) {
try {
var u = new URL(text)
return u.protocol === "http:" || u.protocol === "https:"
} catch(e) {
return false
}
}
clickedFunc: function() {
if (ImportController.extractConfigFromData(textKey.textField.text)) {
if (isValidUrl(textKey.textField.text) && ImportController.importLink(textKey.textField.text)) {
PageController.goToPage(PageEnum.PageSetupWizardViewConfig)
}else if (ImportController.extractConfigFromData(textKey.textField.text)) {
PageController.goToPage(PageEnum.PageSetupWizardViewConfig)
}
}
+2
View File
@@ -100,6 +100,8 @@
<file>Pages2/PageSettingsServerServices.qml</file>
<file>Pages2/PageSettingsServersList.qml</file>
<file>Pages2/PageSettingsSplitTunneling.qml</file>
<file>Pages2/PageSettingsXRayAvailableConfigs.qml</file>
<file>Pages2/PageSettingsXRayServerInfo.qml</file>
<file>Pages2/PageSettingsNewsNotifications.qml</file>
<file>Pages2/PageSettingsNewsDetail.qml</file>
<file>Pages2/PageProtocolAwgClientSettings.qml</file>