diff --git a/client/core/controllers/selfhosted/importController.cpp b/client/core/controllers/selfhosted/importController.cpp index c1c7503eb..57b14146c 100644 --- a/client/core/controllers/selfhosted/importController.cpp +++ b/client/core/controllers/selfhosted/importController.cpp @@ -11,8 +11,13 @@ #include #include #include +#include +#include +#include #include +#include + #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; diff --git a/client/core/controllers/selfhosted/importController.h b/client/core/controllers/selfhosted/importController.h index 3168f6f4a..6f020fcd3 100644 --- a/client/core/controllers/selfhosted/importController.h +++ b/client/core/controllers/selfhosted/importController.h @@ -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); diff --git a/client/core/controllers/serversController.cpp b/client/core/controllers/serversController.cpp index 1842775b8..5bdbcaad3 100644 --- a/client/core/controllers/serversController.cpp +++ b/client/core/controllers/serversController.cpp @@ -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; diff --git a/client/core/controllers/serversController.h b/client/core/controllers/serversController.h index 3b91e5558..61c97de09 100644 --- a/client/core/controllers/serversController.h +++ b/client/core/controllers/serversController.h @@ -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 getServers() const; diff --git a/client/core/models/selfhosted/nativeServerConfig.cpp b/client/core/models/selfhosted/nativeServerConfig.cpp index 34577ac29..95642238b 100644 --- a/client/core/models/selfhosted/nativeServerConfig.cpp +++ b/client/core/models/selfhosted/nativeServerConfig.cpp @@ -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; } diff --git a/client/core/models/selfhosted/nativeServerConfig.h b/client/core/models/selfhosted/nativeServerConfig.h index 13982eb8b..3f2c3f045 100644 --- a/client/core/models/selfhosted/nativeServerConfig.h +++ b/client/core/models/selfhosted/nativeServerConfig.h @@ -2,7 +2,9 @@ #define NATIVESERVERCONFIG_H #include +#include #include +#include #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 configString; + std::optional configName; + std::optional currentConfig; bool hasContainers() const; ContainerConfig containerConfig(DockerContainer container) const; diff --git a/client/core/models/serverConfig.cpp b/client/core/models/serverConfig.cpp index 7006fb07b..a08e539f7 100644 --- a/client/core/models/serverConfig.cpp +++ b/client/core/models/serverConfig.cpp @@ -120,6 +120,10 @@ bool ServerConfig::isApiConfig() const return isApiV1() || isApiV2(); } +bool ServerConfig::isXRayConfig() const { + return isNative() && std::get(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 { diff --git a/client/core/models/serverConfig.h b/client/core/models/serverConfig.h index 93ef1842d..7e396eaf0 100644 --- a/client/core/models/serverConfig.h +++ b/client/core/models/serverConfig.h @@ -57,6 +57,7 @@ struct ServerConfig { bool isApiV1() const; bool isApiV2() const; bool isApiConfig() const; + bool isXRayConfig() const; template T* as() { diff --git a/client/core/repositories/secureServersRepository.cpp b/client/core/repositories/secureServersRepository.cpp index 7107b8aa5..6e3e83744 100644 --- a/client/core/repositories/secureServersRepository.cpp +++ b/client/core/repositories/secureServersRepository.cpp @@ -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(); + + 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(); + 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(); + 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(); + 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(); + if (!xrayConfig->configName.has_value()) + return QJsonArray(); + + return xrayConfig->configName.value(); +} + ServerCredentials SecureServersRepository::serverCredentials(int index) const { ServerConfig config = server(index); diff --git a/client/core/repositories/secureServersRepository.h b/client/core/repositories/secureServersRepository.h index 03c876a71..a52c9b776 100644 --- a/client/core/repositories/secureServersRepository.h +++ b/client/core/repositories/secureServersRepository.h @@ -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; diff --git a/client/images/controls/arrow-down.svg b/client/images/controls/arrow-down.svg new file mode 100644 index 000000000..e5410c468 --- /dev/null +++ b/client/images/controls/arrow-down.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/images/controls/arrow-up.svg b/client/images/controls/arrow-up.svg new file mode 100644 index 000000000..f7b8f28d1 --- /dev/null +++ b/client/images/controls/arrow-up.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/images/images.qrc b/client/images/images.qrc index 6bbfb6308..504c8ee9c 100644 --- a/client/images/images.qrc +++ b/client/images/images.qrc @@ -9,8 +9,10 @@ controls/amnezia.svg controls/app.svg controls/archive-restore.svg + controls/arrow-down.svg controls/arrow-left.svg controls/arrow-right.svg + controls/arrow-up.svg controls/bug.svg controls/check.svg controls/chevron-down.svg diff --git a/client/ui/controllers/importUiController.cpp b/client/ui/controllers/importUiController.cpp index ce9b952c1..3ae7c4dc6 100644 --- a/client/ui/controllers/importUiController.cpp +++ b/client/ui/controllers/importUiController.cpp @@ -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; diff --git a/client/ui/controllers/importUiController.h b/client/ui/controllers/importUiController.h index 853539d05..f465203d3 100644 --- a/client/ui/controllers/importUiController.h +++ b/client/ui/controllers/importUiController.h @@ -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); diff --git a/client/ui/controllers/qml/pageController.h b/client/ui/controllers/qml/pageController.h index 603e8a8f4..f3cca3abc 100644 --- a/client/ui/controllers/qml/pageController.h +++ b/client/ui/controllers/qml/pageController.h @@ -44,6 +44,8 @@ namespace PageLoader PageSettingsApiNativeConfigs, PageSettingsApiDevices, PageSettingsApiSubscriptionKey, + PageSettingsXRayAvailableConfigs, + PageSettingsXRayServerInfo, PageSettingsKillSwitchExceptions, PageServiceSftpSettings, diff --git a/client/ui/controllers/serversUiController.cpp b/client/ui/controllers/serversUiController.cpp index ccafe0f4b..c2a200271 100644 --- a/client/ui/controllers/serversUiController.cpp +++ b/client/ui/controllers/serversUiController.cpp @@ -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()) { diff --git a/client/ui/controllers/serversUiController.h b/client/ui/controllers/serversUiController.h index 7d16362af..5899fd0ff 100644 --- a/client/ui/controllers/serversUiController.h +++ b/client/ui/controllers/serversUiController.h @@ -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; diff --git a/client/ui/models/serversModel.cpp b/client/ui/models/serversModel.cpp index 131d241f8..88bcbda45 100644 --- a/client/ui/models/serversModel.cpp +++ b/client/ui/models/serversModel.cpp @@ -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()->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(data(m_processedServerIndex, HasWriteAccessRole)); @@ -350,6 +364,8 @@ QHash ServersModel::roleNames() const roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired"; roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon"; + roles[IsXRayConfigSelectionAvailableRole] = "isXRayConfigSelectionAvailable"; + return roles; } diff --git a/client/ui/models/serversModel.h b/client/ui/models/serversModel.h index 9bff2eadb..051be48e8 100644 --- a/client/ui/models/serversModel.h +++ b/client/ui/models/serversModel.h @@ -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(); diff --git a/client/ui/qml/Components/ServersListView.qml b/client/ui/qml/Components/ServersListView.qml index b96dfb1d2..3e3b1f808 100644 --- a/client/ui/qml/Components/ServersListView.qml +++ b/client/ui/qml/Components/ServersListView.qml @@ -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) } diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 25794211c..db34f9ade 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -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 diff --git a/client/ui/qml/Pages2/PageSettingsServersList.qml b/client/ui/qml/Pages2/PageSettingsServersList.qml index c163b1eb9..297b561ed 100644 --- a/client/ui/qml/Pages2/PageSettingsServersList.qml +++ b/client/ui/qml/Pages2/PageSettingsServersList.qml @@ -97,6 +97,8 @@ PageType { } PageController.goToPage(PageEnum.PageSettingsApiServerInfo) + } else if (ServersModel.getProcessedServerData("isXRayConfigSelectionAvailable")) { + PageController.goToPage(PageEnum.PageSettingsXRayServerInfo) } else { PageController.goToPage(PageEnum.PageSettingsServerInfo) } diff --git a/client/ui/qml/Pages2/PageSettingsXRayAvailableConfigs.qml b/client/ui/qml/Pages2/PageSettingsXRayAvailableConfigs.qml new file mode 100644 index 000000000..016452103 --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsXRayAvailableConfigs.qml @@ -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] }) + } + } +} diff --git a/client/ui/qml/Pages2/PageSettingsXRayServerInfo.qml b/client/ui/qml/Pages2/PageSettingsXRayServerInfo.qml new file mode 100644 index 000000000..2c68e6854 --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsXRayServerInfo.qml @@ -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 + } +} diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index 1f3318933..39b3b2e8f 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -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) } } diff --git a/client/ui/qml/qml.qrc b/client/ui/qml/qml.qrc index f2a462630..27b144f7c 100644 --- a/client/ui/qml/qml.qrc +++ b/client/ui/qml/qml.qrc @@ -100,6 +100,8 @@ Pages2/PageSettingsServerServices.qml Pages2/PageSettingsServersList.qml Pages2/PageSettingsSplitTunneling.qml + Pages2/PageSettingsXRayAvailableConfigs.qml + Pages2/PageSettingsXRayServerInfo.qml Pages2/PageSettingsNewsNotifications.qml Pages2/PageSettingsNewsDetail.qml Pages2/PageProtocolAwgClientSettings.qml