mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-10-24 03:57:10 +09:00
Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
051f4ca6ef | ||
|
|
69d87e70ac | ||
|
|
b7ea6c498d | ||
|
|
8ad00d533e | ||
|
|
b4c5c381ba | ||
|
|
3c25dfce55 | ||
|
|
d8fa1f5161 | ||
|
|
6960287ef5 | ||
|
|
1386336f10 | ||
|
|
c482fa51a7 | ||
|
|
f49fa1ac60 | ||
|
|
34101a91d9 | ||
|
|
972b4e1274 | ||
|
|
df47fdcae0 | ||
|
|
bc5ae6e410 | ||
|
|
0d38b22162 | ||
|
|
4773fb0821 | ||
|
|
a91e1a20f0 | ||
|
|
24b74fcb3e | ||
|
|
ed3deef2dd | ||
|
|
185c85e183 | ||
|
|
bb7cf69cfc | ||
|
|
5ba704ffcf | ||
|
|
1b47d79f26 | ||
|
|
a5cb5679ae | ||
|
|
6aefc6be47 | ||
|
|
378b30f553 | ||
|
|
812ee905f4 | ||
|
|
b16d603435 | ||
|
|
c107c01cab | ||
|
|
80ff2e0676 | ||
|
|
4b6f09d55f | ||
|
|
c6958d8b70 |
No files matched your search
@@ -88,7 +88,7 @@ jobs:
|
||||
host: 'linux'
|
||||
target: 'desktop'
|
||||
arch: 'linux_gcc_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
tools: 'tools_ifw'
|
||||
@@ -209,7 +209,7 @@ jobs:
|
||||
host: 'windows'
|
||||
target: 'desktop'
|
||||
arch: 'win64_msvc2022_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
tools: 'tools_ifw'
|
||||
@@ -528,7 +528,7 @@ jobs:
|
||||
host: 'mac'
|
||||
target: 'desktop'
|
||||
arch: 'clang_64'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
set-env: 'true'
|
||||
@@ -671,7 +671,7 @@ jobs:
|
||||
version: ${{ env.QT_VERSION }}
|
||||
host: 'mac'
|
||||
target: 'desktop'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia qthttpserver qtwebsockets'
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia'
|
||||
arch: 'clang_64'
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: 'true'
|
||||
@@ -1057,6 +1057,14 @@ jobs:
|
||||
- ConnectionTest
|
||||
- OpenSSLLibraryTest
|
||||
- GatewayServiceImportTest
|
||||
- UiNewsModelAndControllerTest
|
||||
- UiServicesModelAndControllerTest
|
||||
- UiSubscriptionControllerTest
|
||||
- UiApiAccountInfoModelTest
|
||||
- ApiBenefitsModelTest
|
||||
- ApiCountryModelTest
|
||||
- ApiCountryListModelTest
|
||||
- ApiDevicesModelTest
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
@@ -1077,6 +1085,7 @@ jobs:
|
||||
THIRD_PARTY_VLESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VLESS_IMPORT_DATA }}
|
||||
THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA }}
|
||||
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}
|
||||
AMNEZIA_TEST_API_KEY: ${{ secrets.AMNEZIA_TEST_API_KEY }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -1105,7 +1114,7 @@ jobs:
|
||||
host: linux
|
||||
target: desktop
|
||||
arch: linux_gcc_64
|
||||
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
|
||||
modules: qtremoteobjects qt5compat qtshadertools
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: true
|
||||
aqtversion: '==3.3.0'
|
||||
@@ -1181,7 +1190,7 @@ jobs:
|
||||
host: linux
|
||||
target: desktop
|
||||
arch: linux_gcc_64
|
||||
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
|
||||
modules: qtremoteobjects qt5compat qtshadertools
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: true
|
||||
aqtversion: '==3.3.0'
|
||||
@@ -1247,6 +1256,14 @@ jobs:
|
||||
- ConnectionTest
|
||||
- OpenSSLLibraryTest
|
||||
- GatewayServiceImportTest
|
||||
- UiNewsModelAndControllerTest
|
||||
- UiServicesModelAndControllerTest
|
||||
- UiSubscriptionControllerTest
|
||||
- UiApiAccountInfoModelTest
|
||||
- ApiBenefitsModelTest
|
||||
- ApiCountryModelTest
|
||||
- ApiCountryListModelTest
|
||||
- ApiDevicesModelTest
|
||||
|
||||
runs-on: windows-latest
|
||||
|
||||
@@ -1266,7 +1283,8 @@ jobs:
|
||||
THIRD_PARTY_TROJAN_IMPORT_DATA: ${{ secrets.THIRD_PARTY_TROJAN_IMPORT_DATA }}
|
||||
THIRD_PARTY_VLESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VLESS_IMPORT_DATA }}
|
||||
THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA }}
|
||||
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}
|
||||
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}|
|
||||
AMNEZIA_TEST_API_KEY: ${{ secrets.AMNEZIA_TEST_API_KEY }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -1303,7 +1321,7 @@ jobs:
|
||||
host: windows
|
||||
target: desktop
|
||||
arch: win64_msvc2022_64
|
||||
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
|
||||
modules: qtremoteobjects qt5compat qtshadertools
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: true
|
||||
|
||||
@@ -1354,6 +1372,14 @@ jobs:
|
||||
- ConnectionTest
|
||||
- OpenSSLLibraryTest
|
||||
- GatewayServiceImportTest
|
||||
- UiNewsModelAndControllerTest
|
||||
- UiServicesModelAndControllerTest
|
||||
- UiSubscriptionControllerTest
|
||||
- UiApiAccountInfoModelTest
|
||||
- ApiBenefitsModelTest
|
||||
- ApiCountryModelTest
|
||||
- ApiCountryListModelTest
|
||||
- ApiDevicesModelTest
|
||||
|
||||
runs-on: macos-15
|
||||
|
||||
@@ -1374,6 +1400,7 @@ jobs:
|
||||
THIRD_PARTY_VLESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VLESS_IMPORT_DATA }}
|
||||
THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA }}
|
||||
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}
|
||||
AMNEZIA_TEST_API_KEY: ${{ secrets.AMNEZIA_TEST_API_KEY }}
|
||||
|
||||
steps:
|
||||
- name: Setup Xcode
|
||||
@@ -1413,7 +1440,7 @@ jobs:
|
||||
host: mac
|
||||
target: desktop
|
||||
arch: clang_64
|
||||
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
|
||||
modules: qtremoteobjects qt5compat qtshadertools
|
||||
dir: ${{ runner.temp }}
|
||||
set-env: true
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ add_definitions(-DFREE_V2_ENDPOINT="$ENV{FREE_V2_ENDPOINT}")
|
||||
add_definitions(-DPREM_V1_ENDPOINT="$ENV{PREM_V1_ENDPOINT}")
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
set(PACKAGES ${PACKAGES} Widgets HttpServer)
|
||||
set(PACKAGES ${PACKAGES} Widgets)
|
||||
endif()
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS ${PACKAGES})
|
||||
@@ -60,7 +60,7 @@ if(TARGET Qt6::CorePrivate)
|
||||
endif()
|
||||
|
||||
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
|
||||
set(LIBS ${LIBS} Qt6::Widgets Qt6::HttpServer)
|
||||
set(LIBS ${LIBS} Qt6::Widgets)
|
||||
endif()
|
||||
|
||||
qt_standard_project_setup()
|
||||
|
||||
@@ -129,11 +129,6 @@ void AmneziaApplication::init()
|
||||
win->setPersistentSceneGraph(true);
|
||||
win->setPersistentGraphics(true);
|
||||
#endif
|
||||
#ifdef Q_OS_IOS
|
||||
auto pageController = m_coreController->pageController();
|
||||
pageController->setSafeAreaMargins(win->safeAreaMargins());
|
||||
QObject::connect(win, &QWindow::safeAreaMarginsChanged, pageController, &PageController::setSafeAreaMargins);
|
||||
#endif
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
win->show();
|
||||
#else
|
||||
|
||||
@@ -44,7 +44,6 @@ set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/controllers/selfhosted/exportController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/connectionController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/settingsController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/localProxyController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/logsController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.h
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/storePurchaseController.h
|
||||
@@ -70,9 +69,6 @@ set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/countryCatalog.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/gatewayPayloadBuilder.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/localProxyConstants.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/portUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/xrayConfigUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/swiftBridge.h
|
||||
${CLIENT_ROOT_DIR}/core/utils/utilities.h
|
||||
@@ -134,7 +130,6 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/controllers/selfhosted/exportController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/connectionController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/settingsController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/localProxyController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/logsController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.cpp
|
||||
${CLIENT_ROOT_DIR}/core/controllers/api/storePurchaseController.cpp
|
||||
@@ -162,8 +157,6 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/countryCatalog.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/api/gatewayPayloadBuilder.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/portUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/localProxy/xrayConfigUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/serverConfigUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.cpp
|
||||
${CLIENT_ROOT_DIR}/core/utils/utilities.cpp
|
||||
|
||||
@@ -362,33 +362,6 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
|
||||
return applyUpdatedServiceConfig(serverId, serviceProtocol, protocolData, responseBody);
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::fetchServiceConfig(const QString &serverId, const QString &serviceProtocol,
|
||||
QJsonObject &serverConfigJson)
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
ProtocolData protocolData = generateProtocolData(serviceProtocol);
|
||||
|
||||
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
|
||||
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
|
||||
.addField(apiDefs::key::serviceType, apiV2->serviceType())
|
||||
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
|
||||
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
|
||||
.addField(apiDefs::key::authData, apiV2->authData.toJson())
|
||||
.build();
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, apiV2->apiConfig.isTestPurchase);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
return extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
|
||||
}
|
||||
|
||||
void SubscriptionController::markSubscriptionExpiredByServer(const QString &serverId, const ApiV2ServerConfig &apiV2)
|
||||
{
|
||||
ApiV2ServerConfig expiredApiV2 = apiV2;
|
||||
|
||||
@@ -45,8 +45,6 @@ public:
|
||||
ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &email);
|
||||
|
||||
ErrorCode fetchServiceConfig(const QString &serverId, const QString &serviceProtocol, QJsonObject &serverConfigJson);
|
||||
|
||||
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
CaptchaInfo *captchaInfoOut = nullptr, ProtocolData *usedProtocolDataOut = nullptr);
|
||||
|
||||
|
||||
@@ -179,7 +179,6 @@ void CoreController::initCoreControllers()
|
||||
m_importCoreController = new ImportController(m_serversRepository, m_appSettingsRepository, this);
|
||||
m_connectionController = new ConnectionController(m_serversRepository, m_appSettingsRepository, m_vpnConnection.get(), this);
|
||||
m_settingsController = new SettingsController(m_serversRepository, m_appSettingsRepository, this);
|
||||
m_localProxyController = new LocalProxyController(m_serversRepository, m_appSettingsRepository, m_subscriptionController, this);
|
||||
}
|
||||
|
||||
void CoreController::initControllers()
|
||||
@@ -213,9 +212,6 @@ void CoreController::initControllers()
|
||||
m_settingsUiController = new SettingsUiController(m_settingsController, m_serversController, this);
|
||||
setQmlContextProperty("SettingsController", m_settingsUiController);
|
||||
|
||||
m_localProxyUiController = new LocalProxyUiController(m_localProxyController, this);
|
||||
setQmlContextProperty("LocalProxyController", m_localProxyUiController);
|
||||
|
||||
m_logsUiController = new LogsUiController(m_settingsController, this);
|
||||
setQmlContextProperty("LogsController", m_logsUiController);
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
#include "ui/controllers/selfhosted/installUiController.h"
|
||||
#include "ui/controllers/qml/pageController.h"
|
||||
#include "ui/controllers/settingsUiController.h"
|
||||
#include "ui/controllers/localProxyUiController.h"
|
||||
#include "ui/controllers/logsUiController.h"
|
||||
#include "ui/controllers/serversUiController.h"
|
||||
#include "ui/controllers/ipSplitTunnelingUiController.h"
|
||||
@@ -45,7 +44,6 @@
|
||||
#include "core/controllers/api/newsController.h"
|
||||
#include "core/controllers/selfhosted/installController.h"
|
||||
#include "core/controllers/settingsController.h"
|
||||
#include "core/controllers/localProxyController.h"
|
||||
#include "core/controllers/connectionController.h"
|
||||
#include "core/controllers/updateController.h"
|
||||
|
||||
@@ -177,7 +175,6 @@ private:
|
||||
ImportController* m_importCoreController;
|
||||
ExportUiController* m_exportUiController;
|
||||
SettingsUiController* m_settingsUiController;
|
||||
LocalProxyUiController* m_localProxyUiController;
|
||||
LogsUiController* m_logsUiController;
|
||||
ServersUiController* m_serversUiController;
|
||||
IpSplitTunnelingUiController* m_ipSplitTunnelingUiController;
|
||||
@@ -207,7 +204,6 @@ private:
|
||||
ExportController* m_exportController;
|
||||
ConnectionController* m_connectionController;
|
||||
SettingsController* m_settingsController;
|
||||
LocalProxyController* m_localProxyController;
|
||||
|
||||
ContainersModel* m_containersModel;
|
||||
ContainersModel* m_defaultServerContainersModel;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "coreSignalHandlers.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
#include <QtConcurrent>
|
||||
|
||||
@@ -33,7 +32,6 @@
|
||||
#include "core/controllers/selfhosted/installController.h"
|
||||
#include "core/controllers/selfhosted/exportController.h"
|
||||
#include "core/controllers/connectionController.h"
|
||||
#include "core/controllers/localProxyController.h"
|
||||
#include "ui/models/clientManagementModel.h"
|
||||
#include "ui/controllers/api/apiNewsUiController.h"
|
||||
#include "ui/models/containersModel.h"
|
||||
@@ -90,7 +88,6 @@ void CoreSignalHandlers::initAllHandlers()
|
||||
initIosSettingsHandler();
|
||||
initNotificationHandler();
|
||||
initUpdateFoundHandler();
|
||||
initLocalProxyHandler();
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initErrorMessagesHandler()
|
||||
@@ -497,12 +494,3 @@ void CoreSignalHandlers::initUpdateFoundHandler()
|
||||
});
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initLocalProxyHandler()
|
||||
{
|
||||
connect(m_coreController->m_connectionController, &ConnectionController::openConnectionRequested, m_coreController->m_localProxyController,
|
||||
&LocalProxyController::onVpnConnectionRequested);
|
||||
connect(m_coreController->m_localProxyController, &LocalProxyController::stoppedByVpn, m_coreController->m_pageController,
|
||||
&PageController::showNotificationMessage);
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, m_coreController->m_localProxyController,
|
||||
&LocalProxyController::disable);
|
||||
}
|
||||
@@ -43,7 +43,6 @@ private:
|
||||
void initIosSettingsHandler();
|
||||
void initNotificationHandler();
|
||||
void initUpdateFoundHandler();
|
||||
void initLocalProxyHandler();
|
||||
|
||||
CoreController* m_coreController;
|
||||
};
|
||||
|
||||
@@ -5,13 +5,11 @@
|
||||
#include <QFutureWatcher>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QMetaObject>
|
||||
#include <QThread>
|
||||
#include <QUrl>
|
||||
#include <QtConcurrent/QtConcurrentRun>
|
||||
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/utils/constants/apiConstants.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
|
||||
@@ -34,9 +32,6 @@ namespace
|
||||
constexpr QLatin1String s3FallbackEndpoints("s3_fallback_endpoints");
|
||||
constexpr QLatin1String isDevEnvironment("is_dev_environment");
|
||||
constexpr QLatin1String requestTimeoutMsecs("request_timeout_msecs");
|
||||
constexpr QLatin1String proxyRequestTimeoutMsecs("proxy_request_timeout_msecs");
|
||||
constexpr QLatin1String proxyStorageTimeoutMsecs("proxy_storage_timeout_msecs");
|
||||
constexpr QLatin1String proxyHealthTimeoutMsecs("proxy_health_timeout_msecs");
|
||||
}
|
||||
|
||||
namespace agwOptionsKey
|
||||
@@ -45,11 +40,6 @@ namespace
|
||||
constexpr QLatin1String userCountryCode("user_country_code");
|
||||
}
|
||||
|
||||
namespace agwStateKey
|
||||
{
|
||||
constexpr QLatin1String workingProxy("working_proxy");
|
||||
}
|
||||
|
||||
constexpr QLatin1String agwStateCacheKey("agw_state");
|
||||
constexpr QLatin1String agwTracePrefix("agw trace:");
|
||||
constexpr QLatin1String agwTraceFailoverMarker("suspicious");
|
||||
@@ -81,7 +71,6 @@ GatewayController::GatewayController(const QString &gatewayEndpoint, const bool
|
||||
const bool isStrictKillSwitchEnabled, SecureAppSettingsRepository *appSettingsRepository,
|
||||
QObject *parent)
|
||||
: QObject(parent),
|
||||
m_gatewayEndpoint(gatewayEndpoint),
|
||||
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled),
|
||||
m_appSettingsRepository(appSettingsRepository)
|
||||
{
|
||||
@@ -104,11 +93,13 @@ GatewayController::GatewayController(const QString &gatewayEndpoint, const bool
|
||||
config[agwConfigKey::s3FallbackEndpoints] = QJsonArray::fromStringList(fallbackEndpoints);
|
||||
config[agwConfigKey::isDevEnvironment] = isDevEnvironment;
|
||||
config[agwConfigKey::requestTimeoutMsecs] = requestTimeoutMsecs;
|
||||
config[agwConfigKey::proxyRequestTimeoutMsecs] = apiDefs::proxyRequestTimeoutMsecs;
|
||||
config[agwConfigKey::proxyStorageTimeoutMsecs] = apiDefs::proxyStorageTimeoutMsecs;
|
||||
config[agwConfigKey::proxyHealthTimeoutMsecs] = apiDefs::proxyHealthTimeoutMsecs;
|
||||
|
||||
m_client = agw_client_create(QJsonDocument(config).toJson(QJsonDocument::Compact).constData(), nullptr);
|
||||
agw_callbacks callbacks {};
|
||||
callbacks.struct_size = sizeof(agw_callbacks);
|
||||
callbacks.on_before_request = &GatewayController::onBeforeRequest;
|
||||
callbacks.on_before_request_user_data = this;
|
||||
|
||||
m_client = agw_client_create(QJsonDocument(config).toJson(QJsonDocument::Compact).constData(), &callbacks);
|
||||
if (m_client == 0) {
|
||||
qCritical() << "GatewayController: failed to create gateway client (missing key or endpoint?)";
|
||||
return;
|
||||
@@ -146,7 +137,6 @@ amnezia::ErrorCode GatewayController::post(const QString &endpoint, const QJsonO
|
||||
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
|
||||
{
|
||||
prepareRequest();
|
||||
return QtConcurrent::run(
|
||||
[this, endpoint, apiPayload]() -> QPair<amnezia::ErrorCode, QByteArray> { return executePost(endpoint, apiPayload); });
|
||||
}
|
||||
@@ -200,55 +190,44 @@ amnezia::ErrorCode GatewayController::mapResultCode(const int code, const QByteA
|
||||
}
|
||||
}
|
||||
|
||||
void GatewayController::prepareRequest()
|
||||
void GatewayController::onBeforeRequest(const char *host, void *userData)
|
||||
{
|
||||
auto *controller = static_cast<GatewayController *>(userData);
|
||||
const QString hostString = QString::fromUtf8(host);
|
||||
if (QThread::currentThread() == controller->thread()) {
|
||||
controller->handleBeforeRequest(hostString);
|
||||
} else {
|
||||
QMetaObject::invokeMethod(
|
||||
controller, [controller, hostString]() { controller->handleBeforeRequest(hostString); },
|
||||
Qt::BlockingQueuedConnection);
|
||||
}
|
||||
}
|
||||
|
||||
void GatewayController::handleBeforeRequest(const QString &host)
|
||||
{
|
||||
#ifdef Q_OS_IOS
|
||||
Q_UNUSED(host);
|
||||
IosController::Instance()->requestInetAccess();
|
||||
QThread::msleep(10);
|
||||
#endif
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
if (!m_isStrictKillSwitchEnabled) {
|
||||
return;
|
||||
}
|
||||
QStringList ips;
|
||||
for (const QString &host : requestHosts()) {
|
||||
if (m_isStrictKillSwitchEnabled) {
|
||||
const QString ip = NetworkUtilities::getIPAddress(host);
|
||||
if (!ip.isEmpty()) {
|
||||
ips.append(ip);
|
||||
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(QStringList { ip });
|
||||
if (!reply.waitForFinished(1000) || !reply.returnValue()) {
|
||||
qWarning() << "GatewayController::handleBeforeRequest(): failed to add killswitch exception for" << host;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (ips.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(ips);
|
||||
if (!reply.waitForFinished(1000) || !reply.returnValue()) {
|
||||
qWarning() << "GatewayController::prepareRequest(): Failed to execute remote addKillSwitchAllowedRange call";
|
||||
}
|
||||
});
|
||||
#else
|
||||
Q_UNUSED(host);
|
||||
#endif
|
||||
}
|
||||
|
||||
QStringList GatewayController::requestHosts() const
|
||||
{
|
||||
QStringList hosts;
|
||||
if (m_client != 0) {
|
||||
char *state = agw_export_state(m_client);
|
||||
if (state != nullptr) {
|
||||
const QString workingProxy = QJsonDocument::fromJson(QByteArray(state)).object().value(agwStateKey::workingProxy).toString();
|
||||
agw_string_free(state);
|
||||
if (!workingProxy.isEmpty()) {
|
||||
hosts.append(QUrl(workingProxy).host());
|
||||
}
|
||||
}
|
||||
}
|
||||
hosts.append(QUrl(m_gatewayEndpoint).host());
|
||||
hosts.removeAll(QString());
|
||||
hosts.removeDuplicates();
|
||||
return hosts;
|
||||
}
|
||||
|
||||
void GatewayController::persistState()
|
||||
{
|
||||
if (m_client == 0 || m_appSettingsRepository == nullptr) {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include "core/utils/errorCodes.h"
|
||||
|
||||
@@ -29,14 +28,13 @@ public:
|
||||
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
|
||||
|
||||
private:
|
||||
void prepareRequest();
|
||||
QStringList requestHosts() const;
|
||||
static void onBeforeRequest(const char *host, void *userData);
|
||||
void handleBeforeRequest(const QString &host);
|
||||
|
||||
QPair<amnezia::ErrorCode, QByteArray> executePost(const QString &endpoint, const QJsonObject &apiPayload);
|
||||
static amnezia::ErrorCode mapResultCode(const int code, const QByteArray &responseBody);
|
||||
void persistState();
|
||||
|
||||
QString m_gatewayEndpoint;
|
||||
bool m_isStrictKillSwitchEnabled = false;
|
||||
SecureAppSettingsRepository *m_appSettingsRepository = nullptr;
|
||||
|
||||
|
||||
@@ -1,525 +0,0 @@
|
||||
#include "localProxyController.h"
|
||||
|
||||
#include "core/controllers/api/subscriptionController.h"
|
||||
#include "core/repositories/secureAppSettingsRepository.h"
|
||||
#include "core/repositories/secureServersRepository.h"
|
||||
#include "core/utils/api/apiUtils.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/localProxy/localProxyConstants.h"
|
||||
#include "core/utils/localProxy/portUtils.h"
|
||||
#include "core/utils/localProxy/xrayConfigUtils.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonParseError>
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
#include "core/utils/ipcClient.h"
|
||||
|
||||
#include <QHostAddress>
|
||||
#include <QHttpServer>
|
||||
#include <QHttpServerResponse>
|
||||
#include <QTcpServer>
|
||||
#endif
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
LocalProxyController::LocalProxyController(SecureServersRepository *serversRepository,
|
||||
SecureAppSettingsRepository *appSettingsRepository,
|
||||
SubscriptionController *subscriptionController, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_serversRepository(serversRepository),
|
||||
m_appSettingsRepository(appSettingsRepository),
|
||||
m_subscriptionController(subscriptionController)
|
||||
{
|
||||
connect(m_serversRepository, &SecureServersRepository::serverEdited, this, &LocalProxyController::onServerEdited);
|
||||
connect(m_serversRepository, &SecureServersRepository::serverRemoved, this,
|
||||
[this](const QString &serverId, int) { onServerRemoved(serverId); });
|
||||
}
|
||||
|
||||
LocalProxyController::~LocalProxyController()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
bool LocalProxyController::isEnabled() const
|
||||
{
|
||||
return m_isEnabled;
|
||||
}
|
||||
|
||||
int LocalProxyController::port() const
|
||||
{
|
||||
return m_appSettingsRepository->localProxyPort();
|
||||
}
|
||||
|
||||
QString LocalProxyController::ownerId() const
|
||||
{
|
||||
return m_appSettingsRepository->localProxyOwnerId();
|
||||
}
|
||||
|
||||
bool LocalProxyController::isPortUserDefined() const
|
||||
{
|
||||
return m_appSettingsRepository->isLocalProxyPortUserDefined();
|
||||
}
|
||||
|
||||
int LocalProxyController::activePort() const
|
||||
{
|
||||
return m_activePort;
|
||||
}
|
||||
|
||||
bool LocalProxyController::setPort(int port)
|
||||
{
|
||||
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool isPortChanged = m_appSettingsRepository->localProxyPort() != static_cast<quint16>(port);
|
||||
if (isPortChanged) {
|
||||
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(port));
|
||||
}
|
||||
m_appSettingsRepository->setLocalProxyPortUserDefined(true);
|
||||
emit stateChanged();
|
||||
|
||||
if (isPortChanged && m_isEnabled) {
|
||||
start();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::isPortBusy(int port) const
|
||||
{
|
||||
return !portUtils::isPortAvailable(port);
|
||||
}
|
||||
|
||||
bool LocalProxyController::enable(const QString &ownerId, int port)
|
||||
{
|
||||
if (!isSupported() || ownerId.isEmpty() || port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_isEnabled && m_appSettingsRepository->localProxyOwnerId() != ownerId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!portUtils::isPortAvailable(port) && m_appSettingsRepository->isLocalProxyPortUserDefined()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository->localProxyPort() != static_cast<quint16>(port)) {
|
||||
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(port));
|
||||
}
|
||||
|
||||
m_appSettingsRepository->setLocalProxyOwnerId(ownerId);
|
||||
m_isEnabled = true;
|
||||
emit stateChanged();
|
||||
|
||||
start();
|
||||
return true;
|
||||
}
|
||||
|
||||
void LocalProxyController::disable()
|
||||
{
|
||||
if (!m_isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Local proxy is disabled";
|
||||
m_isEnabled = false;
|
||||
stop();
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
void LocalProxyController::start()
|
||||
{
|
||||
if (!startApi(localProxy::apiPort)) {
|
||||
disableWithError(tr("Local proxy failed to start. Check if the port is available"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!syncXray()) {
|
||||
disableWithError(tr("Couldn’t start the proxy due to an internal error. Try restarting the app"));
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Local proxy is running on 127.0.0.1:" << m_apiPort;
|
||||
}
|
||||
|
||||
void LocalProxyController::onServerEdited(const QString &serverId)
|
||||
{
|
||||
if (m_apiPort == 0 || m_appSettingsRepository->localProxyOwnerId() != serverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Owner server edited, restarting Xray";
|
||||
if (restartXray()) {
|
||||
m_appliedProxyPort = m_appSettingsRepository->localProxyPort();
|
||||
} else {
|
||||
disableWithError(tr("Couldn’t start the proxy due to an internal error. Try restarting the app"));
|
||||
}
|
||||
}
|
||||
|
||||
void LocalProxyController::onServerRemoved(const QString &serverId)
|
||||
{
|
||||
if (m_appSettingsRepository->localProxyOwnerId() != serverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_appSettingsRepository->setLocalProxyOwnerId(QString());
|
||||
if (m_isEnabled) {
|
||||
m_isEnabled = false;
|
||||
stop();
|
||||
}
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
void LocalProxyController::onVpnConnectionRequested()
|
||||
{
|
||||
if (!m_isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
disable();
|
||||
emit stoppedByVpn(tr("Local proxy stopped because VPN was turned on"));
|
||||
}
|
||||
|
||||
void LocalProxyController::stop()
|
||||
{
|
||||
stopXray();
|
||||
stopApi();
|
||||
m_appliedProxyPort = 0;
|
||||
}
|
||||
|
||||
bool LocalProxyController::syncXray()
|
||||
{
|
||||
const quint16 proxyPort = m_appSettingsRepository->localProxyPort();
|
||||
|
||||
bool synced;
|
||||
if (!isXrayEngineRunning()) {
|
||||
synced = startXray();
|
||||
} else if (m_appliedProxyPort != proxyPort) {
|
||||
qDebug() << "Proxy port changed from" << m_appliedProxyPort << "to" << proxyPort;
|
||||
synced = restartXray();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (synced) {
|
||||
m_appliedProxyPort = proxyPort;
|
||||
}
|
||||
return synced;
|
||||
}
|
||||
|
||||
bool LocalProxyController::startXray()
|
||||
{
|
||||
if (isXrayEngineRunning()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto config = buildXrayConfig();
|
||||
if (!config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!startXrayEngine(config->serializedConfig)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
qDebug() << "Xray started on port" << config->proxyPort;
|
||||
setActivePort(config->proxyPort);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::stopXray()
|
||||
{
|
||||
if (!stopXrayEngine()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setActivePort(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::restartXray()
|
||||
{
|
||||
qDebug() << "Restarting Xray with updated config";
|
||||
|
||||
if (!stopXray()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return startXray();
|
||||
}
|
||||
|
||||
void LocalProxyController::disableWithError(const QString &message)
|
||||
{
|
||||
qWarning() << message;
|
||||
|
||||
m_isEnabled = false;
|
||||
stop();
|
||||
emit stateChanged();
|
||||
emit startFailed(message);
|
||||
}
|
||||
|
||||
void LocalProxyController::setActivePort(int port)
|
||||
{
|
||||
if (m_activePort == port) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_activePort = port;
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
int LocalProxyController::runningProxyPort() const
|
||||
{
|
||||
return isXrayEngineRunning() ? m_activePort : 0;
|
||||
}
|
||||
|
||||
std::optional<LocalProxyController::XrayConfig> LocalProxyController::buildXrayConfig() const
|
||||
{
|
||||
const QString ownerId = m_appSettingsRepository->localProxyOwnerId();
|
||||
if (ownerId.isEmpty()) {
|
||||
qWarning() << "Local proxy owner server id is not configured";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto ownerServer = m_serversRepository->serverJsonById(ownerId);
|
||||
if (!ownerServer) {
|
||||
qWarning() << "Owner server with id" << ownerId << "not found";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!apiUtils::isPremiumServer(*ownerServer)) {
|
||||
qWarning() << "Server" << ownerServer->value(configKey::name).toString() << "is not premium, local proxy is unavailable";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto serializedConfig = xrayConfigUtils::extractLastConfig(*ownerServer);
|
||||
if (!serializedConfig) {
|
||||
serializedConfig = fetchXrayConfigFromGateway(ownerId);
|
||||
if (!serializedConfig) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(serializedConfig->toUtf8(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
qWarning() << "Failed to parse Xray config JSON:" << parseError.errorString();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto proxyPort = selectProxyPort();
|
||||
if (!proxyPort) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QJsonObject xrayConfig = doc.object();
|
||||
if (!xrayConfigUtils::setInboundPort(xrayConfig, *proxyPort)) {
|
||||
qWarning() << "Failed to override local proxy inbound port, using original config";
|
||||
return XrayConfig { *serializedConfig, 0 };
|
||||
}
|
||||
|
||||
return XrayConfig { QString::fromUtf8(QJsonDocument(xrayConfig).toJson(QJsonDocument::Compact)), *proxyPort };
|
||||
}
|
||||
|
||||
std::optional<int> LocalProxyController::selectProxyPort() const
|
||||
{
|
||||
int port = m_appSettingsRepository->localProxyPort();
|
||||
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
|
||||
port = localProxy::defaultProxyPort;
|
||||
}
|
||||
|
||||
if (portUtils::waitForPort(port, localProxy::portReleaseWaitMs)) {
|
||||
return port;
|
||||
}
|
||||
|
||||
if (m_appSettingsRepository->isLocalProxyPortUserDefined()) {
|
||||
qWarning() << "Local proxy port" << port << "is already in use";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<int> freePort;
|
||||
if (port != localProxy::defaultProxyPort && portUtils::isPortAvailable(localProxy::defaultProxyPort)) {
|
||||
freePort = localProxy::defaultProxyPort;
|
||||
} else {
|
||||
freePort = portUtils::findFirstAvailablePort(localProxy::defaultProxyPort + 1, localProxy::proxyPortMax);
|
||||
}
|
||||
|
||||
if (!freePort) {
|
||||
qWarning() << "No available local proxy port in range" << localProxy::defaultProxyPort + 1 << "-"
|
||||
<< localProxy::proxyPortMax;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
qDebug() << "Local proxy port" << port << "is busy, using" << *freePort;
|
||||
return freePort;
|
||||
}
|
||||
|
||||
std::optional<QString> LocalProxyController::fetchXrayConfigFromGateway(const QString &serverId) const
|
||||
{
|
||||
QJsonObject serverConfig;
|
||||
const ErrorCode errorCode = m_subscriptionController->fetchServiceConfig(serverId, configKey::vless, serverConfig);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
qWarning() << "Gateway request failed with error code" << static_cast<int>(errorCode);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto serializedConfig = xrayConfigUtils::extractLastConfig(serverConfig);
|
||||
if (!serializedConfig) {
|
||||
qWarning() << "Gateway response lacks Xray last_config payload";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
qDebug() << "Fetched Xray config from gateway";
|
||||
return serializedConfig;
|
||||
}
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
|
||||
bool LocalProxyController::isSupported() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::startApi(quint16 port)
|
||||
{
|
||||
if (m_httpServer && m_apiPort == port) {
|
||||
return true;
|
||||
}
|
||||
|
||||
stopApi();
|
||||
|
||||
const auto proxyPortValue = [this]() {
|
||||
const int port = runningProxyPort();
|
||||
return port > 0 ? QJsonValue(port) : QJsonValue::Null;
|
||||
};
|
||||
|
||||
auto httpServer = new QHttpServer(this);
|
||||
httpServer->route("/api/v1/up", QHttpServerRequest::Method::Post, [this, proxyPortValue] {
|
||||
const bool started = startXray();
|
||||
return QHttpServerResponse(QJsonObject { { "status", started ? "ok" : "error" }, { "proxyPort", proxyPortValue() } });
|
||||
});
|
||||
httpServer->route("/api/v1/down", QHttpServerRequest::Method::Post, [this] {
|
||||
const bool stopped = stopXray();
|
||||
return QHttpServerResponse(QJsonObject { { "status", stopped ? "ok" : "error" } });
|
||||
});
|
||||
httpServer->route("/api/v1/ping", QHttpServerRequest::Method::Get, [proxyPortValue] {
|
||||
return QHttpServerResponse(QJsonObject { { "status", "ok" }, { "proxyPort", proxyPortValue() } });
|
||||
});
|
||||
|
||||
auto tcpServer = new QTcpServer(httpServer);
|
||||
if (!tcpServer->listen(QHostAddress::LocalHost, port) || !httpServer->bind(tcpServer)) {
|
||||
qWarning() << "Failed to start HTTP API server on port" << port;
|
||||
delete httpServer;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_httpServer = httpServer;
|
||||
m_apiPort = port;
|
||||
qDebug() << "HTTP API server is running on localhost:" << port;
|
||||
return true;
|
||||
}
|
||||
|
||||
void LocalProxyController::stopApi()
|
||||
{
|
||||
delete m_httpServer;
|
||||
m_httpServer = nullptr;
|
||||
m_apiPort = 0;
|
||||
}
|
||||
|
||||
bool LocalProxyController::startXrayEngine(const QString &config)
|
||||
{
|
||||
if (config.trimmed().isEmpty()) {
|
||||
qWarning() << "Xray config content is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
const qint64 token = IpcClient::withInterface(
|
||||
[&](QSharedPointer<IpcInterfaceReplica> iface) -> qint64 {
|
||||
auto xrayStart = iface->xrayStartOwned(config);
|
||||
if (!xrayStart.waitForFinished()) {
|
||||
return 0;
|
||||
}
|
||||
return xrayStart.returnValue();
|
||||
},
|
||||
[]() -> qint64 { return 0; });
|
||||
|
||||
if (token == 0) {
|
||||
qWarning() << "Failed to start Xray via IPC";
|
||||
return false;
|
||||
}
|
||||
|
||||
m_xrayToken = token;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::stopXrayEngine()
|
||||
{
|
||||
if (m_xrayToken == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool stopped = IpcClient::withInterface(
|
||||
[this](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto xrayStop = iface->xrayStopOwned(m_xrayToken);
|
||||
return xrayStop.waitForFinished() && xrayStop.returnValue();
|
||||
},
|
||||
[]() { return false; });
|
||||
|
||||
if (!stopped) {
|
||||
qWarning() << "Failed to stop Xray via IPC";
|
||||
return false;
|
||||
}
|
||||
|
||||
m_xrayToken = 0;
|
||||
qDebug() << "Xray stopped";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::isXrayEngineRunning() const
|
||||
{
|
||||
if (m_xrayToken == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return IpcClient::withInterface(
|
||||
[this](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto currentToken = iface->xrayCurrentToken();
|
||||
return currentToken.waitForFinished() && currentToken.returnValue() == m_xrayToken;
|
||||
},
|
||||
[]() { return false; });
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
bool LocalProxyController::isSupported() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LocalProxyController::startApi(quint16)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void LocalProxyController::stopApi()
|
||||
{
|
||||
}
|
||||
|
||||
bool LocalProxyController::startXrayEngine(const QString &)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LocalProxyController::stopXrayEngine()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalProxyController::isXrayEngineRunning() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,84 +0,0 @@
|
||||
#ifndef LOCALPROXYCONTROLLER_H
|
||||
#define LOCALPROXYCONTROLLER_H
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
class QHttpServer;
|
||||
class SecureServersRepository;
|
||||
class SecureAppSettingsRepository;
|
||||
class SubscriptionController;
|
||||
|
||||
class LocalProxyController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
LocalProxyController(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
|
||||
SubscriptionController *subscriptionController, QObject *parent = nullptr);
|
||||
~LocalProxyController();
|
||||
|
||||
bool isSupported() const;
|
||||
bool isEnabled() const;
|
||||
int port() const;
|
||||
QString ownerId() const;
|
||||
bool isPortUserDefined() const;
|
||||
int activePort() const;
|
||||
|
||||
bool setPort(int port);
|
||||
bool isPortBusy(int port) const;
|
||||
bool enable(const QString &ownerId, int port);
|
||||
void disable();
|
||||
|
||||
public slots:
|
||||
void onServerEdited(const QString &serverId);
|
||||
void onServerRemoved(const QString &serverId);
|
||||
void onVpnConnectionRequested();
|
||||
|
||||
signals:
|
||||
void stateChanged();
|
||||
void startFailed(const QString &message);
|
||||
void stoppedByVpn(const QString &message);
|
||||
|
||||
private:
|
||||
struct XrayConfig
|
||||
{
|
||||
QString serializedConfig;
|
||||
int proxyPort = 0;
|
||||
};
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
bool syncXray();
|
||||
bool startXray();
|
||||
bool stopXray();
|
||||
bool restartXray();
|
||||
void disableWithError(const QString &message);
|
||||
void setActivePort(int port);
|
||||
int runningProxyPort() const;
|
||||
|
||||
std::optional<XrayConfig> buildXrayConfig() const;
|
||||
std::optional<int> selectProxyPort() const;
|
||||
std::optional<QString> fetchXrayConfigFromGateway(const QString &serverId) const;
|
||||
|
||||
bool startApi(quint16 port);
|
||||
void stopApi();
|
||||
bool startXrayEngine(const QString &config);
|
||||
bool stopXrayEngine();
|
||||
bool isXrayEngineRunning() const;
|
||||
|
||||
SecureServersRepository *m_serversRepository;
|
||||
SecureAppSettingsRepository *m_appSettingsRepository;
|
||||
SubscriptionController *m_subscriptionController;
|
||||
|
||||
bool m_isEnabled = false;
|
||||
QHttpServer *m_httpServer = nullptr;
|
||||
quint16 m_apiPort = 0;
|
||||
qint64 m_xrayToken = 0;
|
||||
quint16 m_appliedProxyPort = 0;
|
||||
int m_activePort = 0;
|
||||
};
|
||||
|
||||
#endif // LOCALPROXYCONTROLLER_H
|
||||
@@ -13,7 +13,6 @@
|
||||
#include "core/controllers/gatewayController.h"
|
||||
#include "core/utils/api/gatewayPayloadBuilder.h"
|
||||
#include "core/utils/appUiConfig.h"
|
||||
#include "core/utils/constants/apiConstants.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/selfhosted/scriptsRegistry.h"
|
||||
|
||||
@@ -141,7 +140,7 @@ void UpdateController::checkForUpdates()
|
||||
|
||||
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(),
|
||||
m_appSettingsRepository->isDevGatewayEnv(),
|
||||
apiDefs::requestTimeoutMsecs,
|
||||
7000,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(),
|
||||
m_appSettingsRepository);
|
||||
|
||||
|
||||
@@ -61,10 +61,6 @@ void OpenVpnProtocol::cleanupResources()
|
||||
|
||||
void OpenVpnProtocol::stop()
|
||||
{
|
||||
if (guardStop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "OpenVpnProtocol::stop()";
|
||||
|
||||
const bool wasActive = m_connectionState == Vpn::ConnectionState::Preparing
|
||||
@@ -183,8 +179,6 @@ void OpenVpnProtocol::updateRouteGateway(QString line)
|
||||
|
||||
ErrorCode OpenVpnProtocol::start()
|
||||
{
|
||||
resetStopGuard();
|
||||
|
||||
cleanupResources();
|
||||
|
||||
if (!QFileInfo::exists(configPath())) {
|
||||
|
||||
@@ -27,20 +27,6 @@ VpnProtocol::VpnProtocol(const QJsonObject &configuration, QObject *parent)
|
||||
connect(m_timeoutTimer, &QTimer::timeout, this, &VpnProtocol::onTimeout);
|
||||
}
|
||||
|
||||
bool VpnProtocol::guardStop()
|
||||
{
|
||||
if (m_stopped) {
|
||||
return true;
|
||||
}
|
||||
m_stopped = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void VpnProtocol::resetStopGuard()
|
||||
{
|
||||
m_stopped = false;
|
||||
}
|
||||
|
||||
void VpnProtocol::setLastError(ErrorCode lastError)
|
||||
{
|
||||
m_lastError = lastError;
|
||||
|
||||
@@ -89,11 +89,6 @@ protected:
|
||||
void startTimeoutTimer();
|
||||
void stopTimeoutTimer();
|
||||
|
||||
bool guardStop();
|
||||
void resetStopGuard();
|
||||
|
||||
bool m_stopped = false;
|
||||
|
||||
Vpn::ConnectionState m_connectionState;
|
||||
|
||||
QString m_routeGateway;
|
||||
|
||||
@@ -50,10 +50,8 @@ WireguardProtocol::~WireguardProtocol()
|
||||
|
||||
void WireguardProtocol::stop()
|
||||
{
|
||||
if (guardStop()) {
|
||||
return;
|
||||
}
|
||||
stopMzImpl();
|
||||
return;
|
||||
}
|
||||
|
||||
ErrorCode WireguardProtocol::startMzImpl()
|
||||
@@ -77,6 +75,5 @@ ErrorCode WireguardProtocol::stopMzImpl()
|
||||
|
||||
ErrorCode WireguardProtocol::start()
|
||||
{
|
||||
resetStopGuard();
|
||||
return startMzImpl();
|
||||
}
|
||||
@@ -67,8 +67,6 @@ XrayProtocol::~XrayProtocol()
|
||||
|
||||
ErrorCode XrayProtocol::start()
|
||||
{
|
||||
resetStopGuard();
|
||||
|
||||
qDebug() << "XrayProtocol::start()";
|
||||
|
||||
// Inject SOCKS5 auth into the inbound before starting xray.
|
||||
@@ -94,12 +92,11 @@ ErrorCode XrayProtocol::start()
|
||||
|
||||
return IpcClient::withInterface(
|
||||
[&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto xrayStart = iface->xrayStartOwned(xrayConfigStr);
|
||||
if (!xrayStart.waitForFinished() || xrayStart.returnValue() == 0) {
|
||||
auto xrayStart = iface->xrayStart(xrayConfigStr);
|
||||
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
|
||||
qCritical() << "Failed to start xray";
|
||||
return ErrorCode::XrayExecutableCrashed;
|
||||
}
|
||||
m_xrayToken = xrayStart.returnValue();
|
||||
return startTun2Socks();
|
||||
},
|
||||
[]() { return ErrorCode::AmneziaServiceConnectionFailed; });
|
||||
@@ -107,13 +104,9 @@ ErrorCode XrayProtocol::start()
|
||||
|
||||
void XrayProtocol::stop()
|
||||
{
|
||||
if (guardStop()) {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "XrayProtocol::stop()";
|
||||
|
||||
IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto disableKillSwitch = iface->disableKillSwitch();
|
||||
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
|
||||
qWarning() << "Failed to disable killswitch";
|
||||
@@ -130,10 +123,9 @@ void XrayProtocol::stop()
|
||||
if (!deleteTun.waitForFinished() || !deleteTun.returnValue())
|
||||
qWarning() << "Failed to delete tun";
|
||||
|
||||
auto xrayStop = iface->xrayStopOwned(m_xrayToken);
|
||||
auto xrayStop = iface->xrayStop();
|
||||
if (!xrayStop.waitForFinished() || !xrayStop.returnValue())
|
||||
qWarning() << "Failed to stop xray";
|
||||
m_xrayToken = 0;
|
||||
});
|
||||
|
||||
if (m_tun2socksProcess) {
|
||||
|
||||
@@ -38,10 +38,6 @@ private:
|
||||
int m_tun2socksRetryCount = 0;
|
||||
static constexpr int maxTun2SocksRetries = 5;
|
||||
static constexpr int tun2socksRetryDelayMs = 400;
|
||||
|
||||
// Owner token of the shared xray engine; a late stop with a stale token
|
||||
// won't kill an engine restarted by another consumer (e.g. the local proxy).
|
||||
qint64 m_xrayToken = 0;
|
||||
};
|
||||
|
||||
#endif // XRAYPROTOCOL_H
|
||||
@@ -504,33 +504,3 @@ void SecureAppSettingsRepository::setXraySavedConfigs(const QByteArray &data)
|
||||
{
|
||||
setValue("Xray/savedConfigs", data);
|
||||
}
|
||||
|
||||
QString SecureAppSettingsRepository::localProxyOwnerId() const
|
||||
{
|
||||
return value("Conf/localProxyOwnerId", "").toString();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setLocalProxyOwnerId(const QString &serverId)
|
||||
{
|
||||
setValue("Conf/localProxyOwnerId", serverId);
|
||||
}
|
||||
|
||||
quint16 SecureAppSettingsRepository::localProxyPort() const
|
||||
{
|
||||
return static_cast<quint16>(value("Conf/localProxyPort", 10808).toUInt());
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setLocalProxyPort(quint16 port)
|
||||
{
|
||||
setValue("Conf/localProxyPort", port);
|
||||
}
|
||||
|
||||
bool SecureAppSettingsRepository::isLocalProxyPortUserDefined() const
|
||||
{
|
||||
return value("Conf/localProxyPortUserDefined", false).toBool();
|
||||
}
|
||||
|
||||
void SecureAppSettingsRepository::setLocalProxyPortUserDefined(bool userDefined)
|
||||
{
|
||||
setValue("Conf/localProxyPortUserDefined", userDefined);
|
||||
}
|
||||
@@ -105,13 +105,6 @@ public:
|
||||
QByteArray xraySavedConfigs() const;
|
||||
void setXraySavedConfigs(const QByteArray &data);
|
||||
|
||||
QString localProxyOwnerId() const;
|
||||
void setLocalProxyOwnerId(const QString &serverId);
|
||||
quint16 localProxyPort() const;
|
||||
void setLocalProxyPort(quint16 port);
|
||||
bool isLocalProxyPortUserDefined() const;
|
||||
void setLocalProxyPortUserDefined(bool userDefined);
|
||||
|
||||
signals:
|
||||
void appLanguageChanged(QLocale locale);
|
||||
void allowedDnsServersChanged(const QStringList &servers);
|
||||
|
||||
@@ -323,15 +323,6 @@ serverConfigUtils::ConfigType SecureServersRepository::serverKind(const QString
|
||||
return serverConfigUtils::configTypeFromJson(withoutStorageServerId(it.value()));
|
||||
}
|
||||
|
||||
std::optional<QJsonObject> SecureServersRepository::serverJsonById(const QString &serverId) const
|
||||
{
|
||||
const auto it = m_serverJsonById.constFind(serverId);
|
||||
if (it == m_serverJsonById.constEnd()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return it.value();
|
||||
}
|
||||
|
||||
std::optional<SelfHostedAdminServerConfig> SecureServersRepository::selfHostedAdminConfig(const QString &serverId) const
|
||||
{
|
||||
const auto it = m_serverJsonById.constFind(serverId);
|
||||
|
||||
@@ -31,8 +31,6 @@ public:
|
||||
void removeServer(const QString &serverId);
|
||||
serverConfigUtils::ConfigType serverKind(const QString &serverId) const;
|
||||
|
||||
std::optional<QJsonObject> serverJsonById(const QString &serverId) const;
|
||||
|
||||
std::optional<SelfHostedAdminServerConfig> selfHostedAdminConfig(const QString &serverId) const;
|
||||
std::optional<SelfHostedUserServerConfig> selfHostedUserConfig(const QString &serverId) const;
|
||||
std::optional<NativeServerConfig> nativeConfig(const QString &serverId) const;
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
namespace apiDefs
|
||||
{
|
||||
|
||||
constexpr int requestTimeoutMsecs = 6 * 1000;
|
||||
constexpr int proxyRequestTimeoutMsecs = 6 * 1000;
|
||||
constexpr int proxyStorageTimeoutMsecs = 6 * 1000;
|
||||
constexpr int proxyHealthTimeoutMsecs = 1000;
|
||||
constexpr int requestTimeoutMsecs = 12 * 1000; // 12 secs
|
||||
|
||||
} // namespace apiDefs
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#ifndef LOCALPROXYDEFS_H
|
||||
#define LOCALPROXYDEFS_H
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
namespace amnezia::localProxy
|
||||
{
|
||||
constexpr int defaultProxyPort = 10808;
|
||||
constexpr int proxyPortMin = 1024;
|
||||
constexpr int proxyPortMax = 65535;
|
||||
constexpr quint16 apiPort = 49490;
|
||||
constexpr int portReleaseWaitMs = 800;
|
||||
}
|
||||
|
||||
#endif // LOCALPROXYDEFS_H
|
||||
@@ -1,51 +0,0 @@
|
||||
#include "portUtils.h"
|
||||
|
||||
#include "localProxyConstants.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QHostAddress>
|
||||
#include <QTcpServer>
|
||||
#include <QThread>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
bool portUtils::isPortAvailable(int port)
|
||||
{
|
||||
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QTcpServer server;
|
||||
const bool success = server.listen(QHostAddress::LocalHost, static_cast<quint16>(port));
|
||||
server.close();
|
||||
return success;
|
||||
}
|
||||
|
||||
bool portUtils::waitForPort(int port, int timeoutMs)
|
||||
{
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
while (!isPortAvailable(port)) {
|
||||
if (timer.elapsed() >= timeoutMs) {
|
||||
return false;
|
||||
}
|
||||
QThread::msleep(200);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<int> portUtils::findFirstAvailablePort(int startPort, int endPort)
|
||||
{
|
||||
startPort = qMax(startPort, localProxy::proxyPortMin);
|
||||
endPort = qMin(endPort, localProxy::proxyPortMax);
|
||||
|
||||
for (int port = startPort; port <= endPort; ++port) {
|
||||
if (isPortAvailable(port)) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
#ifndef PORTUTILS_H
|
||||
#define PORTUTILS_H
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace portUtils
|
||||
{
|
||||
bool isPortAvailable(int port);
|
||||
bool waitForPort(int port, int timeoutMs);
|
||||
std::optional<int> findFirstAvailablePort(int startPort, int endPort);
|
||||
}
|
||||
|
||||
#endif // PORTUTILS_H
|
||||
@@ -1,47 +0,0 @@
|
||||
#include "xrayConfigUtils.h"
|
||||
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/containers/containerUtils.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
std::optional<QString> xrayConfigUtils::extractLastConfig(const QJsonObject &serverConfig)
|
||||
{
|
||||
const QJsonArray containers = serverConfig.value(configKey::containers).toArray();
|
||||
const QString targetContainer = ContainerUtils::containerToString(DockerContainer::Xray);
|
||||
|
||||
for (const QJsonValue &value : containers) {
|
||||
const QJsonObject container = value.toObject();
|
||||
if (container.value(configKey::container).toString() != targetContainer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QJsonObject proto = container.value(QString(configKey::xray)).toObject();
|
||||
const QString serialized = proto.value(configKey::lastConfig).toString();
|
||||
if (!serialized.isEmpty()) {
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool xrayConfigUtils::setInboundPort(QJsonObject &xrayConfig, int port)
|
||||
{
|
||||
if (!xrayConfig.value(QLatin1String("inbounds")).isArray()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonArray inbounds = xrayConfig.value(QLatin1String("inbounds")).toArray();
|
||||
if (inbounds.isEmpty() || !inbounds.at(0).isObject()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject firstInbound = inbounds.at(0).toObject();
|
||||
firstInbound.insert(QLatin1String("port"), port);
|
||||
inbounds[0] = firstInbound;
|
||||
xrayConfig.insert(QLatin1String("inbounds"), inbounds);
|
||||
return true;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
#ifndef XRAYCONFIGUTILS_H
|
||||
#define XRAYCONFIGUTILS_H
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
namespace xrayConfigUtils
|
||||
{
|
||||
std::optional<QString> extractLastConfig(const QJsonObject &serverConfig);
|
||||
bool setInboundPort(QJsonObject &xrayConfig, int port);
|
||||
}
|
||||
|
||||
#endif // XRAYCONFIGUTILS_H
|
||||
@@ -0,0 +1,14 @@
|
||||
<svg width="24" height="24" viewBox="0 0 74 74" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4_34)">
|
||||
<path d="M55.5 12.3333H18.5C15.0942 12.3333 12.3333 15.0943 12.3333 18.5V55.5C12.3333 58.9058 15.0942 61.6667 18.5 61.6667H55.5C58.9057 61.6667 61.6666 58.9058 61.6666 55.5V18.5C61.6666 15.0943 58.9057 12.3333 55.5 12.3333Z" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21.5833 24.6667H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21.5833 37H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21.5833 49.3333H40.0833" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="61.5" cy="12.5" r="15" fill="#FBB36B" stroke="#1C1D21" stroke-width="5"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4_34">
|
||||
<rect width="74" height="74" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 982 B |
@@ -1,3 +1,8 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 1C20.7957 1 21.5585 1.3163 22.1211 1.87891C22.6837 2.44152 23 3.20435 23 4V20C23 20.7957 22.6837 21.5585 22.1211 22.1211C21.5585 22.6837 20.7957 23 20 23H4C3.9647 23 3.92987 22.9977 3.89551 22.9941C3.13785 22.9677 2.41681 22.659 1.87891 22.1211C1.3163 21.5585 1 20.7957 1 20V11C1 9.34772 2.34772 8 4 8H5V4C5 3.20435 5.3163 2.44152 5.87891 1.87891C6.44152 1.3163 7.20435 1 8 1H20ZM4 10C3.45228 10 3 10.4523 3 11V20C3 20.2652 3.10543 20.5195 3.29297 20.707C3.48051 20.8946 3.73478 21 4 21C4.26522 21 4.51949 20.8946 4.70703 20.707C4.89457 20.5195 5 20.2652 5 20V10H4ZM8 3C7.73478 3 7.4805 3.10543 7.29297 3.29297C7.10543 3.4805 7 3.73478 7 4V20C7 20.3444 6.93703 20.6814 6.82422 21H20C20.2652 21 20.5195 20.8946 20.707 20.707C20.8946 20.5195 21 20.2652 21 20V4C21 3.73478 20.8946 3.48051 20.707 3.29297C20.5195 3.10543 20.2652 3 20 3H8ZM15 17C15.5523 17 16 17.4477 16 18C16 18.5523 15.5523 19 15 19H10C9.44772 19 9 18.5523 9 18C9 17.4477 9.44771 17 10 17H15ZM18 13C18.5523 13 19 13.4477 19 14C19 14.5523 18.5523 15 18 15H10C9.44771 15 9 14.5523 9 14C9 13.4477 9.44771 13 10 13H18ZM18 5C18.5523 5 19 5.44772 19 6V10C19 10.5523 18.5523 11 18 11H10C9.44771 11 9 10.5523 9 10V6C9 5.44772 9.44771 5 10 5H18ZM11 9H17V7H11V9Z" fill="#E4E4E7"/>
|
||||
</svg>
|
||||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#CBCAC8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Основа газеты -->
|
||||
<rect x="4" y="4" width="16" height="16" rx="2"/>
|
||||
<!-- Линии текста -->
|
||||
<line x1="7" y1="8" x2="17" y2="8"/>
|
||||
<line x1="7" y1="12" x2="17" y2="12"/>
|
||||
<line x1="7" y1="16" x2="13" y2="16"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 410 B |
@@ -48,6 +48,7 @@
|
||||
<file>controls/map-pin.svg</file>
|
||||
<file>controls/more-vertical.svg</file>
|
||||
<file>controls/news.svg</file>
|
||||
<file>controls/news-unread.svg</file>
|
||||
<file>controls/unread-dot.svg</file>
|
||||
<file>controls/plus.svg</file>
|
||||
<file>controls/qr-code.svg</file>
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
<false/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
@@ -67,7 +68,7 @@
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Dark</string>
|
||||
<string>Light</string>
|
||||
<key>com.wireguard.ios.app_group_id</key>
|
||||
<string>${BUILD_IOS_GROUP_IDENTIFIER}</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
add_executable(test_ui_api_services_model_and_controller
|
||||
testUiApiServicesModelAndController.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_ui_api_services_model_and_controller PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_ui_news_model_and_controller
|
||||
testUiNewsModelAndController.cpp
|
||||
)
|
||||
@@ -25,8 +16,78 @@ target_link_libraries(test_gateway_service_import PRIVATE
|
||||
test_common
|
||||
)
|
||||
|
||||
add_test(NAME UiApiServicesModelAndControllerTest COMMAND test_ui_api_services_model_and_controller)
|
||||
add_executable(test_ui_services_model_and_controller
|
||||
testUiServicesModelAndController.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_ui_services_model_and_controller PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_ui_subscription_controller
|
||||
testSubscriptionUiController.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_ui_subscription_controller PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_ui_api_account_info_model
|
||||
testUiApiAccountInfoModel.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_ui_api_account_info_model PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_api_benefits_model
|
||||
testApiBenefitsModel.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_api_benefits_model PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_api_country_model
|
||||
testApiCountryModel.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_api_country_model PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_api_country_list_model
|
||||
testApiCountryListModel.cpp
|
||||
${CLIENT_ROOT_DIR}/resources/countries.qrc
|
||||
)
|
||||
|
||||
target_link_libraries(test_api_country_list_model PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_executable(test_api_devices_model
|
||||
testApiDevicesModel.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_api_devices_model PRIVATE
|
||||
Qt6::Test
|
||||
test_common
|
||||
)
|
||||
|
||||
add_test(NAME UiNewsModelAndControllerTest COMMAND test_ui_news_model_and_controller)
|
||||
add_test(NAME GatewayServiceImportTest COMMAND test_gateway_service_import)
|
||||
add_test(NAME UiServicesModelAndControllerTest COMMAND test_ui_services_model_and_controller)
|
||||
add_test(NAME UiSubscriptionControllerTest COMMAND test_ui_subscription_controller)
|
||||
add_test(NAME UiApiAccountInfoModelTest COMMAND test_ui_api_account_info_model)
|
||||
add_test(NAME ApiBenefitsModelTest COMMAND test_api_benefits_model)
|
||||
add_test(NAME ApiCountryModelTest COMMAND test_api_country_model)
|
||||
add_test(NAME ApiCountryListModelTest COMMAND test_api_country_list_model)
|
||||
add_test(NAME ApiDevicesModelTest COMMAND test_api_devices_model)
|
||||
|
||||
amnezia_copy_runtime_dlls()
|
||||
@@ -0,0 +1,213 @@
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include "ui/models/api/apiBenefitsModel.h"
|
||||
|
||||
class TestApiBenefitsModel : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void initTestCase();
|
||||
void initialState();
|
||||
void updateModel();
|
||||
void updateModelClearsOnEmptyArray();
|
||||
void updateModelSkipsInvalidItems();
|
||||
void updateModelSkipsItemsWithoutTitleAndBody();
|
||||
void updateModelTrimsLinkedBody();
|
||||
void updateModelPreservesQrcIcon();
|
||||
void updateModelUsesFallbackIcon();
|
||||
void dataReturnsInvalidForInvalidIndex();
|
||||
void dataReturnsInvalidForUnknownRole();
|
||||
void roleNames();
|
||||
void clear();
|
||||
};
|
||||
|
||||
void TestApiBenefitsModel::initTestCase()
|
||||
{
|
||||
qRegisterMetaType<QModelIndex>();
|
||||
}
|
||||
|
||||
void TestApiBenefitsModel::initialState()
|
||||
{
|
||||
ApiBenefitsModel model;
|
||||
|
||||
QCOMPARE(model.rowCount(), 0);
|
||||
QCOMPARE(model.rowCount(QModelIndex()), 0);
|
||||
}
|
||||
|
||||
void TestApiBenefitsModel::updateModel()
|
||||
{
|
||||
ApiBenefitsModel model;
|
||||
|
||||
const QJsonArray benefits = { QJsonObject { { "icon", "globe-2" },
|
||||
{ "title", "Global access" },
|
||||
{ "body", "Access servers worldwide" },
|
||||
{ "link", false } },
|
||||
QJsonObject { { "icon", "smartphone" },
|
||||
{ "title", "Multiple devices" },
|
||||
{ "body", "Connect your devices" },
|
||||
{ "link", true } } };
|
||||
|
||||
QSignalSpy aboutToResetSpy(&model, &QAbstractItemModel::modelAboutToBeReset);
|
||||
QSignalSpy resetSpy(&model, &QAbstractItemModel::modelReset);
|
||||
|
||||
model.updateModel(benefits);
|
||||
|
||||
QCOMPARE(aboutToResetSpy.count(), 1);
|
||||
QCOMPARE(resetSpy.count(), 1);
|
||||
QCOMPARE(model.rowCount(), 2);
|
||||
|
||||
const QModelIndex firstIndex = model.index(0, 0);
|
||||
QVERIFY(firstIndex.isValid());
|
||||
|
||||
QCOMPARE(model.rowCount(firstIndex), 0);
|
||||
QCOMPARE(model.data(firstIndex, ApiBenefitsModel::IconRole).toString(), QStringLiteral("qrc:/images/controls/globe-2.svg"));
|
||||
QCOMPARE(model.data(firstIndex, ApiBenefitsModel::TitleRole).toString(), QStringLiteral("Global access"));
|
||||
QCOMPARE(model.data(firstIndex, ApiBenefitsModel::BodyRole).toString(), QStringLiteral("Access servers worldwide"));
|
||||
QCOMPARE(model.data(firstIndex, ApiBenefitsModel::LinkRole).toBool(), false);
|
||||
|
||||
const QModelIndex secondIndex = model.index(1, 0);
|
||||
QVERIFY(secondIndex.isValid());
|
||||
|
||||
QCOMPARE(model.data(secondIndex, ApiBenefitsModel::IconRole).toString(), QStringLiteral("qrc:/images/controls/smartphone.svg"));
|
||||
QCOMPARE(model.data(secondIndex, ApiBenefitsModel::TitleRole).toString(), QStringLiteral("Multiple devices"));
|
||||
QCOMPARE(model.data(secondIndex, ApiBenefitsModel::BodyRole).toString(), QStringLiteral("Connect your devices"));
|
||||
QCOMPARE(model.data(secondIndex, ApiBenefitsModel::LinkRole).toBool(), true);
|
||||
}
|
||||
|
||||
void TestApiBenefitsModel::updateModelClearsOnEmptyArray()
|
||||
{
|
||||
ApiBenefitsModel model;
|
||||
|
||||
model.updateModel({ QJsonObject { { "title", "Benefit" }, { "body", "Description" } } });
|
||||
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
|
||||
QSignalSpy resetSpy(&model, &QAbstractItemModel::modelReset);
|
||||
|
||||
model.updateModel(QJsonArray { });
|
||||
|
||||
QCOMPARE(resetSpy.count(), 1);
|
||||
QCOMPARE(model.rowCount(), 0);
|
||||
}
|
||||
|
||||
void TestApiBenefitsModel::updateModelSkipsInvalidItems()
|
||||
{
|
||||
ApiBenefitsModel model;
|
||||
|
||||
const QJsonArray benefits = { QJsonObject { { "title", "Valid benefit" }, { "body", "Valid description" } },
|
||||
QJsonValue(QStringLiteral("invalid item")), QJsonValue(42), QJsonValue(false),
|
||||
QJsonValue(QJsonArray { }) };
|
||||
|
||||
model.updateModel(benefits);
|
||||
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QCOMPARE(model.data(model.index(0, 0), ApiBenefitsModel::TitleRole).toString(), QStringLiteral("Valid benefit"));
|
||||
}
|
||||
|
||||
void TestApiBenefitsModel::updateModelSkipsItemsWithoutTitleAndBody()
|
||||
{
|
||||
ApiBenefitsModel model;
|
||||
|
||||
const QJsonArray benefits = { QJsonObject { }, QJsonObject { { "title", "" }, { "body", "" } },
|
||||
QJsonObject { { "title", "Title only" } }, QJsonObject { { "body", "Body only" } } };
|
||||
|
||||
model.updateModel(benefits);
|
||||
|
||||
QCOMPARE(model.rowCount(), 2);
|
||||
QCOMPARE(model.data(model.index(0, 0), ApiBenefitsModel::TitleRole).toString(), QStringLiteral("Title only"));
|
||||
QCOMPARE(model.data(model.index(1, 0), ApiBenefitsModel::BodyRole).toString(), QStringLiteral("Body only"));
|
||||
}
|
||||
|
||||
void TestApiBenefitsModel::updateModelTrimsLinkedBody()
|
||||
{
|
||||
ApiBenefitsModel model;
|
||||
|
||||
model.updateModel({ QJsonObject { { "title", "Link" }, { "body", " https://example.com \n" }, { "link", true } },
|
||||
QJsonObject { { "title", "Text" }, { "body", " Description " }, { "link", false } } });
|
||||
|
||||
QCOMPARE(model.data(model.index(0, 0), ApiBenefitsModel::BodyRole).toString(), QStringLiteral("https://example.com"));
|
||||
QCOMPARE(model.data(model.index(1, 0), ApiBenefitsModel::BodyRole).toString(), QStringLiteral(" Description "));
|
||||
}
|
||||
|
||||
void TestApiBenefitsModel::updateModelPreservesQrcIcon()
|
||||
{
|
||||
ApiBenefitsModel model;
|
||||
|
||||
model.updateModel({ QJsonObject { { "title", "Benefit" }, { "body", "Description" }, { "icon", "qrc:/custom/icon.svg" } } });
|
||||
|
||||
QCOMPARE(model.data(model.index(0, 0), ApiBenefitsModel::IconRole).toString(), QStringLiteral("qrc:/custom/icon.svg"));
|
||||
}
|
||||
|
||||
void TestApiBenefitsModel::updateModelUsesFallbackIcon()
|
||||
{
|
||||
ApiBenefitsModel model;
|
||||
|
||||
model.updateModel({ QJsonObject { { "title", "Benefit" }, { "body", "Description" }, { "icon", "unknown-icon" } } });
|
||||
|
||||
QCOMPARE(model.data(model.index(0, 0), ApiBenefitsModel::IconRole).toString(), QStringLiteral("qrc:/images/controls/info.svg"));
|
||||
}
|
||||
|
||||
void TestApiBenefitsModel::dataReturnsInvalidForInvalidIndex()
|
||||
{
|
||||
ApiBenefitsModel model;
|
||||
|
||||
model.updateModel({ QJsonObject { { "title", "Benefit" }, { "body", "Description" } } });
|
||||
|
||||
QVERIFY(!model.data(QModelIndex(), ApiBenefitsModel::TitleRole).isValid());
|
||||
QVERIFY(!model.data(model.index(-1, 0), ApiBenefitsModel::TitleRole).isValid());
|
||||
QVERIFY(!model.data(model.index(1, 0), ApiBenefitsModel::TitleRole).isValid());
|
||||
QVERIFY(!model.data(model.index(0, 1), ApiBenefitsModel::TitleRole).isValid());
|
||||
}
|
||||
|
||||
void TestApiBenefitsModel::dataReturnsInvalidForUnknownRole()
|
||||
{
|
||||
ApiBenefitsModel model;
|
||||
|
||||
model.updateModel({ QJsonObject { { "title", "Benefit" }, { "body", "Description" } } });
|
||||
|
||||
const QModelIndex index = model.index(0, 0);
|
||||
|
||||
QVERIFY(!model.data(index, Qt::DisplayRole).isValid());
|
||||
QVERIFY(!model.data(index, ApiBenefitsModel::LinkRole + 1).isValid());
|
||||
}
|
||||
|
||||
void TestApiBenefitsModel::roleNames()
|
||||
{
|
||||
ApiBenefitsModel model;
|
||||
|
||||
const QHash<int, QByteArray> roles = model.roleNames();
|
||||
|
||||
QCOMPARE(roles.value(ApiBenefitsModel::IconRole), QByteArray("icon"));
|
||||
QCOMPARE(roles.value(ApiBenefitsModel::TitleRole), QByteArray("title"));
|
||||
QCOMPARE(roles.value(ApiBenefitsModel::BodyRole), QByteArray("body"));
|
||||
QCOMPARE(roles.value(ApiBenefitsModel::LinkRole), QByteArray("link"));
|
||||
}
|
||||
|
||||
void TestApiBenefitsModel::clear()
|
||||
{
|
||||
ApiBenefitsModel model;
|
||||
|
||||
model.updateModel({ QJsonObject { { "title", "Benefit 1" }, { "body", "Description 1" } },
|
||||
QJsonObject { { "title", "Benefit 2" }, { "body", "Description 2" } } });
|
||||
|
||||
QSignalSpy aboutToResetSpy(&model, &QAbstractItemModel::modelAboutToBeReset);
|
||||
QSignalSpy resetSpy(&model, &QAbstractItemModel::modelReset);
|
||||
|
||||
model.clear();
|
||||
|
||||
QCOMPARE(aboutToResetSpy.count(), 1);
|
||||
QCOMPARE(resetSpy.count(), 1);
|
||||
QCOMPARE(model.rowCount(), 0);
|
||||
|
||||
model.clear();
|
||||
|
||||
QCOMPARE(model.rowCount(), 0);
|
||||
QCOMPARE(resetSpy.count(), 2);
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestApiBenefitsModel)
|
||||
#include "testApiBenefitsModel.moc"
|
||||
@@ -0,0 +1,165 @@
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include "ui/models/api/apiCountryListModel.h"
|
||||
|
||||
class TestApiCountryListModel : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void initialState();
|
||||
void dataReturnsInvalidForInvalidIndex();
|
||||
void searchTextChanges();
|
||||
void searchTextChangedSignal();
|
||||
void clearSearch();
|
||||
void sortModeChanges();
|
||||
void invalidSortModeFallsBackToByRegion();
|
||||
void favoritesAreSortedAndDeduplicated();
|
||||
void collapsedSectionsAreStored();
|
||||
void sanitizeSearchFieldText();
|
||||
};
|
||||
|
||||
void TestApiCountryListModel::initialState()
|
||||
{
|
||||
ApiCountryListModel model(nullptr, QStringLiteral("test"));
|
||||
|
||||
QCOMPARE(model.rowCount(), 0);
|
||||
QCOMPARE(model.rowCount(QModelIndex()), 0);
|
||||
QVERIFY(!model.hasResults());
|
||||
QVERIFY(!model.isSearchActive());
|
||||
QVERIFY(model.searchText().isEmpty());
|
||||
QCOMPARE(model.sortMode(), ApiCountryListModel::ByRegion);
|
||||
QVERIFY(model.isGrouped());
|
||||
QCOMPARE(model.listId(), QStringLiteral("test"));
|
||||
QCOMPARE(model.activeUseCaseId(), QStringLiteral("all"));
|
||||
QCOMPARE(model.favoritesLimit(), 15);
|
||||
}
|
||||
|
||||
void TestApiCountryListModel::dataReturnsInvalidForInvalidIndex()
|
||||
{
|
||||
ApiCountryListModel model(nullptr, QStringLiteral("test"));
|
||||
|
||||
QVERIFY(!model.data(QModelIndex(), Qt::DisplayRole).isValid());
|
||||
QVERIFY(!model.data(model.index(-1, 0), ApiCountryListModel::CountryNameRole).isValid());
|
||||
QVERIFY(!model.data(model.index(0, 0), ApiCountryListModel::CountryNameRole).isValid());
|
||||
}
|
||||
|
||||
void TestApiCountryListModel::searchTextChanges()
|
||||
{
|
||||
ApiCountryListModel model(nullptr, QStringLiteral("test"));
|
||||
|
||||
model.setSearchText(QStringLiteral("Estonia"));
|
||||
|
||||
QCOMPARE(model.searchText(), QStringLiteral("Estonia"));
|
||||
QVERIFY(model.isSearchActive());
|
||||
QCOMPARE(model.rowCount(), 0);
|
||||
QVERIFY(!model.hasResults());
|
||||
|
||||
model.setSearchText(QStringLiteral(" "));
|
||||
|
||||
QVERIFY(!model.isSearchActive());
|
||||
QCOMPARE(model.searchText(), QStringLiteral(" "));
|
||||
}
|
||||
|
||||
void TestApiCountryListModel::searchTextChangedSignal()
|
||||
{
|
||||
ApiCountryListModel model(nullptr, QStringLiteral("test"));
|
||||
QSignalSpy spy(&model, &ApiCountryListModel::searchTextChanged);
|
||||
|
||||
QVERIFY(spy.isValid());
|
||||
|
||||
model.setSearchText(QStringLiteral("Estonia"));
|
||||
QCOMPARE(spy.count(), 1);
|
||||
|
||||
model.setSearchText(QStringLiteral("Estonia"));
|
||||
QCOMPARE(spy.count(), 1);
|
||||
|
||||
model.setSearchText(QStringLiteral("Latvia"));
|
||||
QCOMPARE(spy.count(), 2);
|
||||
}
|
||||
|
||||
void TestApiCountryListModel::clearSearch()
|
||||
{
|
||||
ApiCountryListModel model(nullptr, QStringLiteral("test"));
|
||||
|
||||
model.setSearchText(QStringLiteral("Estonia"));
|
||||
QVERIFY(model.isSearchActive());
|
||||
|
||||
QSignalSpy spy(&model, &ApiCountryListModel::searchTextChanged);
|
||||
QVERIFY(spy.isValid());
|
||||
|
||||
model.clearSearch();
|
||||
|
||||
QVERIFY(model.searchText().isEmpty());
|
||||
QVERIFY(!model.isSearchActive());
|
||||
QCOMPARE(spy.count(), 1);
|
||||
|
||||
model.clearSearch();
|
||||
QCOMPARE(spy.count(), 1);
|
||||
}
|
||||
|
||||
void TestApiCountryListModel::sortModeChanges()
|
||||
{
|
||||
ApiCountryListModel model(nullptr, QStringLiteral("test"));
|
||||
|
||||
QSignalSpy sortSpy(&model, &ApiCountryListModel::sortModeChanged);
|
||||
QSignalSpy groupingSpy(&model, &ApiCountryListModel::groupingChanged);
|
||||
|
||||
QVERIFY(sortSpy.isValid());
|
||||
QVERIFY(groupingSpy.isValid());
|
||||
|
||||
model.setSortMode(ApiCountryListModel::Alphabetical);
|
||||
|
||||
QCOMPARE(model.sortMode(), ApiCountryListModel::Alphabetical);
|
||||
QVERIFY(!model.isGrouped());
|
||||
QCOMPARE(sortSpy.count(), 1);
|
||||
QCOMPARE(groupingSpy.count(), 1);
|
||||
|
||||
model.setSortMode(ApiCountryListModel::Alphabetical);
|
||||
|
||||
QCOMPARE(sortSpy.count(), 1);
|
||||
QCOMPARE(groupingSpy.count(), 1);
|
||||
}
|
||||
|
||||
void TestApiCountryListModel::invalidSortModeFallsBackToByRegion()
|
||||
{
|
||||
ApiCountryListModel model(nullptr, QStringLiteral("test"));
|
||||
|
||||
model.setSortMode(ApiCountryListModel::Alphabetical);
|
||||
model.setSortMode(999);
|
||||
|
||||
QCOMPARE(model.sortMode(), ApiCountryListModel::ByRegion);
|
||||
QVERIFY(model.isGrouped());
|
||||
}
|
||||
|
||||
void TestApiCountryListModel::favoritesAreSortedAndDeduplicated()
|
||||
{
|
||||
ApiCountryListModel model(nullptr, QStringLiteral("test"));
|
||||
|
||||
model.setFavorites({ QStringLiteral("US"), QStringLiteral("DE"), QStringLiteral("US"), QString() });
|
||||
|
||||
QCOMPARE(model.favorites(), QStringList({ QStringLiteral("DE"), QStringLiteral("US") }));
|
||||
}
|
||||
|
||||
void TestApiCountryListModel::collapsedSectionsAreStored()
|
||||
{
|
||||
ApiCountryListModel model(nullptr, QStringLiteral("test"));
|
||||
|
||||
model.setCollapsedSections({ QStringLiteral("europe"), QStringLiteral("asia"), QStringLiteral("europe"), QString() });
|
||||
|
||||
QCOMPARE(model.collapsedSections(), QStringList({ QStringLiteral("asia"), QStringLiteral("europe") }));
|
||||
}
|
||||
|
||||
void TestApiCountryListModel::sanitizeSearchFieldText()
|
||||
{
|
||||
ApiCountryListModel model(nullptr, QStringLiteral("test"));
|
||||
|
||||
QCOMPARE(model.sanitizeSearchFieldText(QStringLiteral("Estonia")), QStringLiteral("Estonia"));
|
||||
QCOMPARE(model.sanitizeSearchFieldText(QStringLiteral("Hello, World! 123")), QStringLiteral("Hello World "));
|
||||
QCOMPARE(model.sanitizeSearchFieldText(QStringLiteral("")), QString());
|
||||
QCOMPARE(model.sanitizeSearchFieldText(QStringLiteral("123")), QString());
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestApiCountryListModel)
|
||||
#include "testApiCountryListModel.moc"
|
||||
@@ -0,0 +1,190 @@
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "ui/models/api/apiCountryModel.h"
|
||||
|
||||
class TestApiCountryModel : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
ApiCountryModel m_model;
|
||||
|
||||
static QJsonObject makeCountry(const QString &name, const QString &code, const QString &localizedCode)
|
||||
{
|
||||
return { { apiDefs::key::serverCountryName, name },
|
||||
{ apiDefs::key::serverCountryCode, code },
|
||||
{ apiDefs::key::serverCountryCodeL10n, localizedCode } };
|
||||
}
|
||||
|
||||
static QJsonObject makeIssuedConfig(const QString &countryCode, const QString &sourceType,
|
||||
const QString &lastDownloaded, const QString &workerLastUpdated)
|
||||
{
|
||||
return { { apiDefs::key::serverCountryCode, countryCode },
|
||||
{ apiDefs::key::sourceType, sourceType },
|
||||
{ apiDefs::key::lastDownloaded, lastDownloaded },
|
||||
{ apiDefs::key::workerLastUpdated, workerLastUpdated },
|
||||
{ apiDefs::key::installationUuid, "test-installation" },
|
||||
{ apiDefs::key::osVersion, "test-os" } };
|
||||
}
|
||||
|
||||
private slots:
|
||||
void init()
|
||||
{
|
||||
m_model.updateIssuedConfigsInfo(QJsonArray {});
|
||||
m_model.updateModel(QJsonArray {}, QString {}, QString {});
|
||||
}
|
||||
|
||||
void testInitialState()
|
||||
{
|
||||
QCOMPARE(m_model.rowCount(), 0);
|
||||
QCOMPARE(m_model.getCurrentIndex(), -1);
|
||||
QVERIFY(m_model.getCurrentCountryCode().isEmpty());
|
||||
QVERIFY(m_model.getUserCountryCode().isEmpty());
|
||||
QVERIFY(!m_model.hasExpiredWorkerConfigs());
|
||||
QVERIFY(!m_model.data(m_model.index(0, 0), ApiCountryModel::CountryNameRole).isValid());
|
||||
}
|
||||
|
||||
void testUpdateModel()
|
||||
{
|
||||
const QJsonArray countries { makeCountry("Germany", "DE", "de"), makeCountry("Estonia", "EE", "ee"),
|
||||
makeCountry("United States", "US", "us") };
|
||||
|
||||
QSignalSpy currentIndexSpy(&m_model, &ApiCountryModel::currentIndexChanged);
|
||||
|
||||
m_model.updateModel(countries, "EE", "us");
|
||||
|
||||
QCOMPARE(m_model.rowCount(), 3);
|
||||
QCOMPARE(m_model.getCurrentIndex(), 1);
|
||||
QCOMPARE(m_model.getCurrentCountryCode(), QString("EE"));
|
||||
QCOMPARE(m_model.getUserCountryCode(), QString("US"));
|
||||
|
||||
QCOMPARE(currentIndexSpy.count(), 1);
|
||||
QCOMPARE(currentIndexSpy.constFirst().constFirst().toInt(), 1);
|
||||
|
||||
const QModelIndex germanyIndex = m_model.index(0, 0);
|
||||
const QModelIndex estoniaIndex = m_model.index(1, 0);
|
||||
const QModelIndex unitedStatesIndex = m_model.index(2, 0);
|
||||
|
||||
QVERIFY(germanyIndex.isValid());
|
||||
QCOMPARE(m_model.data(germanyIndex, ApiCountryModel::CountryNameRole).toString(), QString("Germany"));
|
||||
QCOMPARE(m_model.data(germanyIndex, ApiCountryModel::CountryCodeRole).toString(), QString("DE"));
|
||||
|
||||
QCOMPARE(m_model.data(estoniaIndex, ApiCountryModel::CountryNameRole).toString(), QString("Estonia"));
|
||||
QCOMPARE(m_model.data(estoniaIndex, ApiCountryModel::CountryCodeRole).toString(), QString("EE"));
|
||||
|
||||
QCOMPARE(m_model.data(unitedStatesIndex, ApiCountryModel::CountryNameRole).toString(), QString("United States"));
|
||||
QCOMPARE(m_model.data(unitedStatesIndex, ApiCountryModel::CountryCodeRole).toString(), QString("US"));
|
||||
}
|
||||
|
||||
void testCurrentCountryNotFound()
|
||||
{
|
||||
const QJsonArray countries { makeCountry("Germany", "DE", "de"), makeCountry("Estonia", "EE", "ee") };
|
||||
|
||||
m_model.updateModel(countries, "FI", "ee");
|
||||
|
||||
QCOMPARE(m_model.rowCount(), 2);
|
||||
QCOMPARE(m_model.getCurrentIndex(), -1);
|
||||
QCOMPARE(m_model.getCurrentCountryCode(), QString("FI"));
|
||||
QCOMPARE(m_model.getUserCountryCode(), QString("EE"));
|
||||
}
|
||||
|
||||
void testCountryImageCodeRole()
|
||||
{
|
||||
m_model.updateModel(QJsonArray { makeCountry("Estonia", "EE", "ee") }, "EE", "EE");
|
||||
|
||||
const QModelIndex index = m_model.index(0, 0);
|
||||
|
||||
QCOMPARE(m_model.data(index, ApiCountryModel::CountryImageCodeRole).toString(), QString("EE"));
|
||||
}
|
||||
|
||||
void testCountryImageCodeFallback()
|
||||
{
|
||||
m_model.updateModel(QJsonArray { makeCountry("Estonia", "EE", "") }, "EE", "EE");
|
||||
|
||||
QCOMPARE(m_model.data(m_model.index(0, 0), ApiCountryModel::CountryImageCodeRole).toString(), QString("EE"));
|
||||
}
|
||||
|
||||
void testInvalidIndexesAndRoles()
|
||||
{
|
||||
m_model.updateModel(QJsonArray { makeCountry("Estonia", "EE", "ee") }, "EE", "EE");
|
||||
|
||||
QVERIFY(!m_model.data(QModelIndex(), ApiCountryModel::CountryNameRole).isValid());
|
||||
QVERIFY(!m_model.data(m_model.index(-1, 0), ApiCountryModel::CountryNameRole).isValid());
|
||||
QVERIFY(!m_model.data(m_model.index(1, 0), ApiCountryModel::CountryNameRole).isValid());
|
||||
QVERIFY(!m_model.data(m_model.index(0, 0), Qt::UserRole + 100).isValid());
|
||||
}
|
||||
|
||||
void testUpdateIssuedConfigsInfo()
|
||||
{
|
||||
m_model.updateModel(QJsonArray { makeCountry("Estonia", "EE", "ee"), makeCountry("Germany", "DE", "de") }, "EE", "EE");
|
||||
|
||||
const QJsonArray issuedConfigs {
|
||||
// Worker timestamp equals download timestamp: not expired.
|
||||
makeIssuedConfig("EE", "country_config", "2026-10-10T10:00:00", "2026-10-10T10:00:00"),
|
||||
|
||||
// Worker timestamp is newer than the download timestamp: expired.
|
||||
makeIssuedConfig("DE", "country_config", "2026-10-09T10:00:00", "2026-10-10T10:00:00"),
|
||||
|
||||
// Non-country configurations must be ignored.
|
||||
makeIssuedConfig("EE", "other_config", "2026-10-01T10:00:00", "2026-10-20T10:00:00")
|
||||
};
|
||||
|
||||
m_model.updateIssuedConfigsInfo(issuedConfigs);
|
||||
|
||||
const QModelIndex estoniaIndex = m_model.index(0, 0);
|
||||
const QModelIndex germanyIndex = m_model.index(1, 0);
|
||||
|
||||
QVERIFY(m_model.data(estoniaIndex, ApiCountryModel::IsIssuedRole).toBool());
|
||||
QVERIFY(!m_model.data(estoniaIndex, ApiCountryModel::IsWorkerExpiredRole).toBool());
|
||||
|
||||
QVERIFY(m_model.data(germanyIndex, ApiCountryModel::IsIssuedRole).toBool());
|
||||
QVERIFY(m_model.data(germanyIndex, ApiCountryModel::IsWorkerExpiredRole).toBool());
|
||||
|
||||
QVERIFY(m_model.hasExpiredWorkerConfigs());
|
||||
}
|
||||
|
||||
void testNonCountryConfigIsIgnored()
|
||||
{
|
||||
m_model.updateModel(QJsonArray { makeCountry("Estonia", "EE", "ee") }, "EE", "EE");
|
||||
|
||||
m_model.updateIssuedConfigsInfo(QJsonArray { makeIssuedConfig("EE", "other_config", "2026-10-01T10:00:00", "2026-10-20T10:00:00") });
|
||||
|
||||
const QModelIndex index = m_model.index(0, 0);
|
||||
|
||||
QVERIFY(!m_model.data(index, ApiCountryModel::IsIssuedRole).toBool());
|
||||
QVERIFY(!m_model.data(index, ApiCountryModel::IsWorkerExpiredRole).toBool());
|
||||
QVERIFY(!m_model.hasExpiredWorkerConfigs());
|
||||
}
|
||||
|
||||
void testCountryWithoutIssuedConfig()
|
||||
{
|
||||
m_model.updateModel(QJsonArray { makeCountry("Estonia", "EE", "ee") }, "EE", "EE");
|
||||
|
||||
m_model.updateIssuedConfigsInfo(QJsonArray { });
|
||||
|
||||
const QModelIndex index = m_model.index(0, 0);
|
||||
|
||||
QVERIFY(!m_model.data(index, ApiCountryModel::IsIssuedRole).toBool());
|
||||
QVERIFY(!m_model.data(index, ApiCountryModel::IsWorkerExpiredRole).toBool());
|
||||
QVERIFY(!m_model.hasExpiredWorkerConfigs());
|
||||
}
|
||||
|
||||
void testUpdateModelResetsCountries()
|
||||
{
|
||||
m_model.updateModel(QJsonArray { makeCountry("Germany", "DE", "de"), makeCountry("Estonia", "EE", "ee") }, "EE", "EE");
|
||||
m_model.updateModel(QJsonArray { makeCountry("Finland", "FI", "fi") }, "FI", "FI");
|
||||
|
||||
QCOMPARE(m_model.rowCount(), 1);
|
||||
QCOMPARE(m_model.getCurrentIndex(), 0);
|
||||
QCOMPARE(m_model.getCurrentCountryCode(), QString("FI"));
|
||||
QCOMPARE(m_model.getUserCountryCode(), QString("FI"));
|
||||
QCOMPARE(m_model.data(m_model.index(0, 0), ApiCountryModel::CountryNameRole).toString(), QString("Finland"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestApiCountryModel)
|
||||
#include "testApiCountryModel.moc"
|
||||
@@ -0,0 +1,158 @@
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "ui/models/api/apiDevicesModel.h"
|
||||
|
||||
class TestApiDevicesModel : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
static QJsonObject createDevice(const QString &installationUuid, const QString &sourceType,
|
||||
const QString &osVersion, const QString &countryCode, const QString &lastDownloaded)
|
||||
{
|
||||
return { { apiDefs::key::installationUuid, installationUuid },
|
||||
{ apiDefs::key::sourceType, sourceType },
|
||||
{ apiDefs::key::osVersion, osVersion },
|
||||
{ apiDefs::key::serverCountryCode, countryCode },
|
||||
{ apiDefs::key::lastDownloaded, lastDownloaded },
|
||||
{ apiDefs::key::workerLastUpdated, QStringLiteral("2026-10-01T12:00:00Z") } };
|
||||
}
|
||||
|
||||
private slots:
|
||||
void testEmptyModel()
|
||||
{
|
||||
ApiDevicesModel model;
|
||||
|
||||
QCOMPARE(model.rowCount(), 0);
|
||||
QCOMPARE(model.rowCount(QModelIndex()), 0);
|
||||
|
||||
const QJsonArray devices { createDevice(QStringLiteral("device-1"), QStringLiteral("gateway_account"),
|
||||
QStringLiteral("Windows 11"), QStringLiteral("EE"),
|
||||
QStringLiteral("2026-10-10T12:00:00Z")) };
|
||||
|
||||
model.updateModel(devices, QStringLiteral("device-1"));
|
||||
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
|
||||
const QModelIndex index = model.index(0, 0);
|
||||
QVERIFY(index.isValid());
|
||||
}
|
||||
|
||||
void testUpdateModel()
|
||||
{
|
||||
ApiDevicesModel model;
|
||||
|
||||
const QString currentUuid = QStringLiteral("device-1");
|
||||
|
||||
QJsonArray devices {
|
||||
createDevice(QStringLiteral("device-1"), QStringLiteral("gateway_account"), QStringLiteral("Windows 11"), QStringLiteral("EE"), QStringLiteral("2026-10-10T12:00:00Z")),
|
||||
createDevice(QStringLiteral("device-2"), QStringLiteral("gateway_account"), QStringLiteral("Android 15"), QStringLiteral("DE"), QStringLiteral("2026-10-11T12:00:00Z"))
|
||||
};
|
||||
|
||||
model.updateModel(devices, currentUuid);
|
||||
|
||||
QCOMPARE(model.rowCount(), 2);
|
||||
|
||||
const QModelIndex firstIndex = model.index(0, 0);
|
||||
const QModelIndex secondIndex = model.index(1, 0);
|
||||
|
||||
QVERIFY(firstIndex.isValid());
|
||||
QVERIFY(secondIndex.isValid());
|
||||
|
||||
QCOMPARE(model.data(firstIndex, ApiDevicesModel::OsVersionRole).toString(), QStringLiteral("Windows 11"));
|
||||
QCOMPARE(model.data(firstIndex, ApiDevicesModel::SupportTagRole).toString(), QStringLiteral("device-1"));
|
||||
QCOMPARE(model.data(firstIndex, ApiDevicesModel::CountryCodeRole).toString(), QStringLiteral("EE"));
|
||||
|
||||
QCOMPARE(model.data(secondIndex, ApiDevicesModel::OsVersionRole).toString(), QStringLiteral("Android 15"));
|
||||
QCOMPARE(model.data(secondIndex, ApiDevicesModel::SupportTagRole).toString(), QStringLiteral("device-2"));
|
||||
QCOMPARE(model.data(secondIndex, ApiDevicesModel::CountryCodeRole).toString(), QStringLiteral("DE"));
|
||||
}
|
||||
|
||||
void testCurrentDeviceRole()
|
||||
{
|
||||
ApiDevicesModel model;
|
||||
|
||||
QJsonArray devices {
|
||||
createDevice(QStringLiteral("device-1"), QStringLiteral("gateway_account"), QStringLiteral("Windows 11"), QStringLiteral("EE"), QStringLiteral("2026-10-10T12:00:00Z")),
|
||||
createDevice(QStringLiteral("device-2"), QStringLiteral("gateway_account"), QStringLiteral("Android 15"), QStringLiteral("DE"), QStringLiteral("2026-10-11T12:00:00Z"))
|
||||
};
|
||||
|
||||
model.updateModel(devices, QStringLiteral("device-2"));
|
||||
|
||||
QCOMPARE(model.rowCount(), 2);
|
||||
QCOMPARE(model.data(model.index(0, 0), ApiDevicesModel::IsCurrentDeviceRole).toBool(), false);
|
||||
QCOMPARE(model.data(model.index(1, 0), ApiDevicesModel::IsCurrentDeviceRole).toBool(), true);
|
||||
}
|
||||
|
||||
void testLastUpdateRole()
|
||||
{
|
||||
ApiDevicesModel model;
|
||||
|
||||
const QString lastDownloaded = QStringLiteral("2026-10-10T12:00:00Z");
|
||||
|
||||
model.updateModel(QJsonArray { createDevice(QStringLiteral("device-1"), QStringLiteral("gateway_account"), QStringLiteral("Windows 11"), QStringLiteral("EE"), lastDownloaded) }, QStringLiteral("device-1"));
|
||||
|
||||
const QModelIndex index = model.index(0, 0);
|
||||
QVERIFY(index.isValid());
|
||||
|
||||
const QString expected = QDateTime::fromString(lastDownloaded, Qt::ISODate).toLocalTime().toString(QStringLiteral("d MMM yyyy"));
|
||||
|
||||
QCOMPARE(model.data(index, ApiDevicesModel::LastUpdateRole).toString(), expected);
|
||||
}
|
||||
|
||||
void testFiltersNonGatewayDevices()
|
||||
{
|
||||
ApiDevicesModel model;
|
||||
|
||||
QJsonArray devices {
|
||||
createDevice(QStringLiteral("gateway-1"), QStringLiteral("gateway_account"), QStringLiteral("Windows 11"), QStringLiteral("EE"), QStringLiteral("2026-10-10T12:00:00Z")),
|
||||
createDevice(QStringLiteral("other-1"), QStringLiteral("other_source"), QStringLiteral("Linux"), QStringLiteral("DE"), QStringLiteral("2026-10-11T12:00:00Z")),
|
||||
createDevice(QStringLiteral("gateway-2"), QStringLiteral("gateway_account"), QStringLiteral("Android 15"), QStringLiteral("FI"), QStringLiteral("2026-10-12T12:00:00Z"))
|
||||
};
|
||||
|
||||
model.updateModel(devices, QStringLiteral("gateway-2"));
|
||||
|
||||
QCOMPARE(model.rowCount(), 2);
|
||||
QCOMPARE(model.data(model.index(0, 0), ApiDevicesModel::SupportTagRole).toString(), QStringLiteral("gateway-1"));
|
||||
QCOMPARE(model.data(model.index(1, 0), ApiDevicesModel::SupportTagRole).toString(), QStringLiteral("gateway-2"));
|
||||
QCOMPARE(model.data(model.index(1, 0), ApiDevicesModel::IsCurrentDeviceRole).toBool(), true);
|
||||
}
|
||||
|
||||
void testEmptyUpdateClearsModel()
|
||||
{
|
||||
ApiDevicesModel model;
|
||||
|
||||
QJsonArray devices { createDevice(QStringLiteral("device-1"), QStringLiteral("gateway_account"), QStringLiteral("Windows 11"), QStringLiteral("EE"), QStringLiteral("2026-10-10T12:00:00Z")) };
|
||||
|
||||
model.updateModel(devices, QStringLiteral("device-1"));
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
|
||||
model.updateModel(QJsonArray { }, QString { });
|
||||
|
||||
QCOMPARE(model.rowCount(), 0);
|
||||
}
|
||||
|
||||
void testInvalidIndexes()
|
||||
{
|
||||
ApiDevicesModel model;
|
||||
|
||||
const QModelIndex invalidIndex;
|
||||
QVERIFY(!model.data(invalidIndex, ApiDevicesModel::OsVersionRole).isValid());
|
||||
|
||||
QJsonArray devices { createDevice(QStringLiteral("device-1"), QStringLiteral("gateway_account"), QStringLiteral("Windows 11"), QStringLiteral("EE"), QStringLiteral("2026-10-10T12:00:00Z")) };
|
||||
|
||||
model.updateModel(devices, QStringLiteral("device-1"));
|
||||
|
||||
QVERIFY(!model.data(model.index(0, 1), ApiDevicesModel::OsVersionRole).isValid());
|
||||
QVERIFY(!model.data(model.index(-1, 0), ApiDevicesModel::OsVersionRole).isValid());
|
||||
QVERIFY(!model.data(model.index(1, 0), ApiDevicesModel::OsVersionRole).isValid());
|
||||
QVERIFY(!model.data(model.index(0, 0), Qt::DisplayRole).isValid());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestApiDevicesModel)
|
||||
#include "testApiDevicesModel.moc"
|
||||
@@ -0,0 +1,108 @@
|
||||
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
#include <QUuid>
|
||||
|
||||
#include "utils/testCoreController.h"
|
||||
#include "utils/testUtils.h"
|
||||
|
||||
#include "secureQSettings.h"
|
||||
#include "ui/controllers/api/subscriptionUiController.h"
|
||||
#include "vpnConnection.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestSubscriptionUiController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
TestCoreController *m_coreController = nullptr;
|
||||
SecureQSettings *m_settings = nullptr;
|
||||
SubscriptionUiController *m_subscriptionUiController = nullptr;
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
const QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
const auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
|
||||
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
|
||||
|
||||
m_subscriptionUiController = m_coreController->findChild<SubscriptionUiController *>();
|
||||
|
||||
QVERIFY2(m_subscriptionUiController != nullptr, "SubscriptionUiController should be created by CoreController");
|
||||
}
|
||||
|
||||
void cleanupTestCase()
|
||||
{
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
|
||||
m_subscriptionUiController = nullptr;
|
||||
}
|
||||
|
||||
void init()
|
||||
{
|
||||
m_settings->clearSettings();
|
||||
QVERIFY2(m_subscriptionUiController != nullptr, "SubscriptionUiController should be available");
|
||||
}
|
||||
|
||||
void testInitialState()
|
||||
{
|
||||
QCOMPARE(m_subscriptionUiController->property("qrCodesCount").toInt(), 0);
|
||||
QCOMPARE(m_subscriptionUiController->property("qrCodes").toList().size(), 0);
|
||||
QVERIFY(m_subscriptionUiController->property("vpnKey").toString().isEmpty());
|
||||
|
||||
QVERIFY(!m_subscriptionUiController->isCaptchaAwaitingUser());
|
||||
}
|
||||
|
||||
void testExportVpnKeyWithEmptyFileName()
|
||||
{
|
||||
QSignalSpy errorSpy(m_subscriptionUiController, &SubscriptionUiController::errorOccurred);
|
||||
|
||||
QVERIFY(errorSpy.isValid());
|
||||
|
||||
const bool result = m_subscriptionUiController->exportVpnKey(QStringLiteral("unknown-server"), QString());
|
||||
|
||||
QVERIFY2(!result, "Export should fail when the file name is empty");
|
||||
QCOMPARE(errorSpy.count(), 1);
|
||||
|
||||
QCOMPARE(m_subscriptionUiController->property("vpnKey").toString(), QString());
|
||||
QCOMPARE(m_subscriptionUiController->property("qrCodesCount").toInt(), 0);
|
||||
}
|
||||
|
||||
void testGetRenewalLinkWithEmptyServerId()
|
||||
{
|
||||
QSignalSpy errorSpy(m_subscriptionUiController, &SubscriptionUiController::errorOccurred);
|
||||
|
||||
QVERIFY(errorSpy.isValid());
|
||||
|
||||
m_subscriptionUiController->getRenewalLink(QString());
|
||||
|
||||
QCOMPARE(errorSpy.count(), 1);
|
||||
QCOMPARE(errorSpy.at(0).at(0).value<ErrorCode>(), ErrorCode::InternalError);
|
||||
}
|
||||
|
||||
void testCurrentActivePlanInfoWithoutActivePurchase()
|
||||
{
|
||||
const QVariantMap info = m_subscriptionUiController->currentActivePlanInfo();
|
||||
|
||||
QVERIFY(info.contains(QStringLiteral("hasActivePlan")));
|
||||
QVERIFY2(!info.value(QStringLiteral("hasActivePlan")).toBool(), "No active store purchase should be reported");
|
||||
}
|
||||
|
||||
void testErrorSignalIsAvailable()
|
||||
{
|
||||
QSignalSpy errorSpy(m_subscriptionUiController, &SubscriptionUiController::errorOccurred);
|
||||
|
||||
QVERIFY(errorSpy.isValid());
|
||||
QCOMPARE(errorSpy.count(), 0);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestSubscriptionUiController)
|
||||
#include "testSubscriptionUiController.moc"
|
||||
@@ -0,0 +1,146 @@
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "ui/models/api/apiAccountInfoModel.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
QJsonObject makeAccountInfo()
|
||||
{
|
||||
return { { apiDefs::key::activeDeviceCount, 2 },
|
||||
{ apiDefs::key::maxDeviceCount, 5 },
|
||||
{ apiDefs::key::subscriptionEndDate, "2099-12-31T23:59:59Z" },
|
||||
{ apiDefs::key::subscriptionDescription, "Premium subscription" },
|
||||
{ apiDefs::key::isRenewalAvailable, true },
|
||||
{ apiDefs::key::availableCountries, QJsonArray { "ru", "de" } },
|
||||
{ apiDefs::key::issuedConfigs, QJsonArray { } },
|
||||
{ apiDefs::key::supportInfo,
|
||||
QJsonObject { { apiDefs::key::telegram, "https://t.me/test_support" },
|
||||
{ apiDefs::key::email, "support@example.org" },
|
||||
{ apiDefs::key::billingEmail, "billing@example.org" },
|
||||
{ apiDefs::key::websiteName, "Example" },
|
||||
{ apiDefs::key::website, "https://example.org" } } } };
|
||||
}
|
||||
|
||||
QJsonObject makePremiumServerConfig()
|
||||
{
|
||||
QJsonObject apiConfig;
|
||||
apiConfig[apiDefs::key::isInAppPurchase] = false;
|
||||
apiConfig[apiDefs::key::serviceType] = "amnezia-premium";
|
||||
|
||||
QJsonObject config;
|
||||
config[configKey::configVersion] = serverConfigUtils::ConfigSource::AmneziaGateway;
|
||||
config[apiDefs::key::apiConfig] = apiConfig;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class TestUiApiAccountInfoModel : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void testInitialState()
|
||||
{
|
||||
ApiAccountInfoModel model;
|
||||
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QVERIFY(!model.data(QModelIndex(), ApiAccountInfoModel::SubscriptionStatusRole).isValid());
|
||||
|
||||
const QModelIndex invalidIndex = model.index(1, 0);
|
||||
QVERIFY(!model.data(invalidIndex, ApiAccountInfoModel::SubscriptionStatusRole).isValid());
|
||||
}
|
||||
|
||||
void testUpdateModel()
|
||||
{
|
||||
ApiAccountInfoModel model;
|
||||
|
||||
QSignalSpy modelResetSpy(&model, &QAbstractItemModel::modelReset);
|
||||
|
||||
model.updateModel(makeAccountInfo(), makePremiumServerConfig());
|
||||
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QCOMPARE(modelResetSpy.count(), 1);
|
||||
|
||||
const QModelIndex index = model.index(0, 0);
|
||||
QVERIFY(index.isValid());
|
||||
|
||||
QCOMPARE(model.data(index, ApiAccountInfoModel::ConnectedDevicesRole).toString(), QStringLiteral("2 out of 5"));
|
||||
QCOMPARE(model.data(index, ApiAccountInfoModel::ServiceDescriptionRole).toString(), QStringLiteral("Premium subscription"));
|
||||
QCOMPARE(model.data(index, ApiAccountInfoModel::IsSubscriptionRenewalAvailableRole).toBool(), true);
|
||||
QCOMPARE(model.data(index, ApiAccountInfoModel::IsInAppPurchaseRole).toBool(), false);
|
||||
QCOMPARE(model.data(index, ApiAccountInfoModel::IsComponentVisibleRole).toBool(), true);
|
||||
QCOMPARE(model.data(QStringLiteral("serviceDescription")).toString(), QStringLiteral("Premium subscription"));
|
||||
}
|
||||
|
||||
void testSupportLinks()
|
||||
{
|
||||
ApiAccountInfoModel model;
|
||||
model.updateModel(makeAccountInfo(), makePremiumServerConfig());
|
||||
|
||||
QCOMPARE(model.getTelegramBotLink(), QStringLiteral("https://t.me/test_support"));
|
||||
QCOMPARE(model.getEmailLink(), QStringLiteral("support@example.org"));
|
||||
QCOMPARE(model.getBillingEmailLink(), QStringLiteral("billing@example.org"));
|
||||
QCOMPARE(model.getSiteLink(), QStringLiteral("Example"));
|
||||
QCOMPARE(model.getFullSiteLink(), QStringLiteral("https://example.org"));
|
||||
}
|
||||
|
||||
void testAvailableCountriesAndIssuedConfigs()
|
||||
{
|
||||
ApiAccountInfoModel model;
|
||||
model.updateModel(makeAccountInfo(), makePremiumServerConfig());
|
||||
|
||||
QCOMPARE(model.getAvailableCountries(), QJsonArray({ "ru", "de" }));
|
||||
QCOMPARE(model.getIssuedConfigsInfo(), QJsonArray { });
|
||||
}
|
||||
|
||||
void testMissingOptionalFields()
|
||||
{
|
||||
ApiAccountInfoModel model;
|
||||
model.updateModel(QJsonObject { }, makePremiumServerConfig());
|
||||
|
||||
const QModelIndex index = model.index(0, 0);
|
||||
QVERIFY(index.isValid());
|
||||
|
||||
QCOMPARE(model.data(index, ApiAccountInfoModel::ConnectedDevicesRole).toString(), QStringLiteral("0 out of 0"));
|
||||
QCOMPARE(model.data(index, ApiAccountInfoModel::ServiceDescriptionRole).toString(), QString());
|
||||
QCOMPARE(model.data(index, ApiAccountInfoModel::IsSubscriptionRenewalAvailableRole).toBool(), false);
|
||||
QCOMPARE(model.data(index, ApiAccountInfoModel::IsInAppPurchaseRole).toBool(), false);
|
||||
|
||||
QVERIFY(model.getTelegramBotLink().isEmpty());
|
||||
QVERIFY(model.getEmailLink().isEmpty());
|
||||
QVERIFY(model.getBillingEmailLink().isEmpty());
|
||||
QVERIFY(model.getSiteLink().isEmpty());
|
||||
QVERIFY(model.getFullSiteLink().isEmpty());
|
||||
}
|
||||
|
||||
void testUpdateModelResetsData()
|
||||
{
|
||||
ApiAccountInfoModel model;
|
||||
model.updateModel(makeAccountInfo(), makePremiumServerConfig());
|
||||
|
||||
QCOMPARE(model.data(model.index(0, 0), ApiAccountInfoModel::ConnectedDevicesRole).toString(), QStringLiteral("2 out of 5"));
|
||||
|
||||
QJsonObject updatedInfo = makeAccountInfo();
|
||||
updatedInfo[apiDefs::key::activeDeviceCount] = 1;
|
||||
updatedInfo[apiDefs::key::maxDeviceCount] = 3;
|
||||
updatedInfo[apiDefs::key::subscriptionDescription] = "Updated subscription";
|
||||
|
||||
model.updateModel(updatedInfo, makePremiumServerConfig());
|
||||
|
||||
QCOMPARE(model.data(model.index(0, 0), ApiAccountInfoModel::ConnectedDevicesRole).toString(), QStringLiteral("1 out of 3"));
|
||||
QCOMPARE(model.data(model.index(0, 0), ApiAccountInfoModel::ServiceDescriptionRole).toString(), QStringLiteral("Updated subscription"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestUiApiAccountInfoModel)
|
||||
#include "testUiApiAccountInfoModel.moc"
|
||||
@@ -1,97 +0,0 @@
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
#include <QUuid>
|
||||
|
||||
#include "utils/testCoreController.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "secureQSettings.h"
|
||||
#include "vpnConnection.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
class TestUiApiServicesModelAndController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
TestCoreController *m_coreController;
|
||||
SecureQSettings *m_settings;
|
||||
|
||||
// TODO: add env vars for api
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
|
||||
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase()
|
||||
{
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init()
|
||||
{
|
||||
m_settings->clearSettings();
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
|
||||
}
|
||||
}
|
||||
|
||||
void testRolesAndSignals()
|
||||
{
|
||||
QSignalSpy errorOccurredSpy(m_coreController->m_servicesCatalogUiController, &ServicesCatalogUiController::errorOccurred);
|
||||
|
||||
/* TODO:
|
||||
m_coreController->m_servicesCatalogUiController->fillAvailableServices();
|
||||
QVERIFY(errorOccurredSpy.count() == 0, "errorOccurred signal should not be emitted");
|
||||
|
||||
QModelIndex serviceModelIndex = m_coreController->m_apiServicesModel->index(0, 0);
|
||||
QVERIFY2(serviceModelIndex.isValid(), "Service model index should be valid");
|
||||
|
||||
auto serviceName = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::NameRole);
|
||||
QCOMPARE(serviceName, );
|
||||
|
||||
auto serviceCardDescription = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::CardDescriptionRole);
|
||||
QCOMPARE(serviceCardDescription, );
|
||||
|
||||
auto isServiceAvailable = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::IsServiceAvailableRole);
|
||||
QCOMPARE(isServiceAvailable, );
|
||||
|
||||
auto serviceSpeed = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::SpeedRole);
|
||||
QCOMPARE(serviceSpeed, );
|
||||
|
||||
auto serviceTimeLimit = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::TimeLimitRole);
|
||||
QCOMPARE(serviceTimeLimit, );
|
||||
|
||||
auto serviceRegion = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::RegionRole);
|
||||
QCOMPARE(serviceRegion, );
|
||||
|
||||
auto serviceFeatures = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::FeaturesRole);
|
||||
QCOMPARE(serviceFeatures, );
|
||||
|
||||
auto servicePrice = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::PriceRole);
|
||||
QCOMPARE(servicePrice, );
|
||||
|
||||
auto serviceEndDate = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::EndDateRole);
|
||||
QCOMPARE(serviceEndDate, );
|
||||
|
||||
auto serviceOrder = m_coreController->m_apiServicesModel->data(serviceModelIndex, ApiServicesModel::OrderRole);
|
||||
QCOMPARE(serviceOrder, );
|
||||
*/
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestUiApiServicesModelAndController)
|
||||
#include "testUiApiServicesModelAndController.moc"
|
||||
@@ -1,17 +1,92 @@
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
#include <QUuid>
|
||||
#include <QProcessEnvironment>
|
||||
|
||||
#include "utils/testCoreController.h"
|
||||
#include "utils/testUtils.h"
|
||||
#include "core/models/serverDescription.h"
|
||||
#include "core/controllers/api/subscriptionController.h"
|
||||
#include "core/models/api/apiV2ServerConfig.h"
|
||||
#include "core/utils/constants/apiKeys.h"
|
||||
#include "core/utils/constants/configKeys.h"
|
||||
#include "core/utils/serverConfigUtils.h"
|
||||
|
||||
#include "ui/controllers/api/apiNewsUiController.h"
|
||||
#include "ui/models/newsModel.h"
|
||||
|
||||
#include "amneziaApplication.h"
|
||||
#include "secureQSettings.h"
|
||||
#include "vpnConnection.h"
|
||||
|
||||
using namespace amnezia;
|
||||
using namespace amnezia::test;
|
||||
|
||||
namespace
|
||||
{
|
||||
QJsonObject gatewayServerConfig()
|
||||
{
|
||||
QJsonObject lastConfig;
|
||||
lastConfig[configKey::hostName] = QStringLiteral("10.0.0.1");
|
||||
lastConfig[configKey::clientPrivKey] = QStringLiteral("test-private-key");
|
||||
lastConfig[configKey::persistentKeepAlive] = QStringLiteral("25-35");
|
||||
|
||||
QJsonObject awgConfig;
|
||||
awgConfig[configKey::lastConfig] = QString(QJsonDocument(lastConfig).toJson(QJsonDocument::Compact));
|
||||
awgConfig[configKey::port] = QStringLiteral("35333");
|
||||
|
||||
QJsonObject awgContainer;
|
||||
awgContainer[configKey::container] = QStringLiteral("amnezia-awg");
|
||||
awgContainer[QString(configKey::awg)] = awgConfig;
|
||||
|
||||
QJsonObject config;
|
||||
config[configKey::name] = QStringLiteral("Amnezia Premium");
|
||||
config[configKey::description] = QStringLiteral("Premium service");
|
||||
config[configKey::hostName] = QStringLiteral("gateway.example.org");
|
||||
config[configKey::configVersion] = serverConfigUtils::ConfigSource::AmneziaGateway;
|
||||
// config[configKey::formatVersion] = serverConfigUtils::currentConfigFormatVersion;
|
||||
config[configKey::defaultContainer] = QStringLiteral("amnezia-awg");
|
||||
config[configKey::containers] = QJsonArray { awgContainer };
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
ApiV2ServerConfig gatewayTestSubscription(const QString &apiKey)
|
||||
{
|
||||
ApiV2ServerConfig config = ApiV2ServerConfig::fromJson(gatewayServerConfig());
|
||||
|
||||
config.apiConfig.serviceType = QStringLiteral("amnezia-premium");
|
||||
config.apiConfig.serviceProtocol = QString(configKey::awg);
|
||||
config.apiConfig.userCountryCode = QStringLiteral("ru");
|
||||
config.apiConfig.vpnKey = QStringLiteral("vpn://stored-key");
|
||||
config.authData.apiKey = apiKey;
|
||||
config.crc = 42;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
QString envValue(const char *name)
|
||||
{
|
||||
return QProcessEnvironment::systemEnvironment().value(QString::fromLatin1(name)).trimmed();
|
||||
}
|
||||
|
||||
QJsonObject makeNewsItem(const QString &id, const QString &title, const QString &content, const QString ×tamp)
|
||||
{
|
||||
return { { "id", id }, { "title", title }, { "content", content }, { "timestamp", timestamp } };
|
||||
}
|
||||
|
||||
QJsonArray makeNews()
|
||||
{
|
||||
return { makeNewsItem("news-1", "First news", "First content", "2026-09-29T10:00:00Z"),
|
||||
makeNewsItem("news-2", "Second news", "Second content", "2026-09-28T10:00:00Z"),
|
||||
makeNewsItem("news-3", "Third news", "Third content", "2026-09-27T10:00:00Z") };
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class TestUiNewsModelAndController : public QObject
|
||||
{
|
||||
@@ -21,15 +96,13 @@ private:
|
||||
TestCoreController *m_coreController;
|
||||
SecureQSettings *m_settings;
|
||||
|
||||
// TODO: add env vars for api
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
const QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
const auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
|
||||
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
@@ -44,54 +117,184 @@ private slots:
|
||||
void init()
|
||||
{
|
||||
m_settings->clearSettings();
|
||||
|
||||
m_coreController->m_serversRepository->clearServers();
|
||||
m_coreController->m_serversRepository->invalidateCache();
|
||||
|
||||
if (m_coreController->m_serversModel) {
|
||||
m_coreController->m_serversModel->updateModel(QVector<ServerDescription>(), QString{});
|
||||
}
|
||||
|
||||
m_coreController->m_newsModel->setNewsList({});
|
||||
}
|
||||
|
||||
void testRolesAndSignals()
|
||||
{
|
||||
NewsModel *model = m_coreController->m_newsModel;
|
||||
model->setNewsList(makeNews());
|
||||
|
||||
QSignalSpy hasUnreadChangedSpy(model, &NewsModel::hasUnreadChanged);
|
||||
QSignalSpy processedIndexChangedSpy(model, &NewsModel::processedIndexChanged);
|
||||
|
||||
QCOMPARE(model->rowCount(), 3);
|
||||
QVERIFY(model->hasUnread());
|
||||
|
||||
const QModelIndex index = model->index(0, 0);
|
||||
|
||||
QVERIFY2(index.isValid(), "News model index should be valid");
|
||||
QCOMPARE(model->data(index, NewsModel::IdRole).toString(), QString("news-1"));
|
||||
QCOMPARE(model->data(index, NewsModel::TitleRole).toString(), QString("First news"));
|
||||
QCOMPARE(model->data(index, NewsModel::ContentRole).toString(), QString("First content"));
|
||||
|
||||
const QDateTime timestamp = QDateTime::fromString(model->data(index, NewsModel::TimestampRole).toString(), Qt::ISODate);
|
||||
|
||||
QVERIFY(timestamp.isValid());
|
||||
|
||||
const QDateTime expectedTimestamp = QDateTime::fromString("2026-09-29T10:00:00Z", Qt::ISODate).toLocalTime();
|
||||
|
||||
QCOMPARE(timestamp, expectedTimestamp);
|
||||
QCOMPARE(model->data(index, NewsModel::IsReadRole).toBool(), false);
|
||||
QCOMPARE(model->data(index, NewsModel::IsProcessedRole).toBool(), false);
|
||||
|
||||
model->setProcessedIndex(0);
|
||||
|
||||
QCOMPARE(model->processedIndex(), 0);
|
||||
QCOMPARE(processedIndexChangedSpy.count(), 1);
|
||||
QCOMPARE(model->data(index, NewsModel::IsProcessedRole).toBool(), true);
|
||||
|
||||
model->markAsRead(0);
|
||||
|
||||
QVERIFY(model->data(index, NewsModel::IsReadRole).toBool());
|
||||
QCOMPARE(hasUnreadChangedSpy.count(), 1);
|
||||
QVERIFY(model->hasUnread());
|
||||
|
||||
model->markAsRead(0);
|
||||
|
||||
QCOMPARE(hasUnreadChangedSpy.count(), 1);
|
||||
QVERIFY(model->data(index, NewsModel::IsReadRole).toBool());
|
||||
|
||||
model->markAsRead(-1);
|
||||
model->markAsRead(model->rowCount());
|
||||
|
||||
QCOMPARE(hasUnreadChangedSpy.count(), 1);
|
||||
QVERIFY(model->hasUnread());
|
||||
}
|
||||
|
||||
void testSorting()
|
||||
{
|
||||
NewsModel *model = m_coreController->m_newsModel;
|
||||
model->setNewsList({ makeNewsItem("old", "Old", "Old content", "2026-09-27T10:00:00Z"),
|
||||
makeNewsItem("new", "New", "New content", "2026-09-29T10:00:00Z"),
|
||||
makeNewsItem("middle", "Middle", "Middle content", "2026-09-28T10:00:00Z") });
|
||||
|
||||
QCOMPARE(model->rowCount(), 3);
|
||||
QCOMPARE(model->data(model->index(0, 0), NewsModel::IdRole).toString(), QString("new"));
|
||||
QCOMPARE(model->data(model->index(1, 0), NewsModel::IdRole).toString(), QString("middle"));
|
||||
QCOMPARE(model->data(model->index(2, 0), NewsModel::IdRole).toString(), QString("old"));
|
||||
}
|
||||
|
||||
void testInvalidItemsAreIgnored()
|
||||
{
|
||||
NewsModel *model = m_coreController->m_newsModel;
|
||||
model->setNewsList(
|
||||
{ QJsonValue("invalid"),
|
||||
QJsonObject { { "title", "No id" }, { "content", "Content" }, { "timestamp", "2026-09-29T10:00:00Z" } },
|
||||
makeNewsItem("valid", "Valid", "Valid content", "2026-09-29T10:00:00Z") });
|
||||
|
||||
QCOMPARE(model->rowCount(), 1);
|
||||
QCOMPARE(model->data(model->index(0, 0), NewsModel::IdRole).toString(), QString("valid"));
|
||||
}
|
||||
|
||||
void testReadStatePersistence()
|
||||
{
|
||||
const QJsonArray news { makeNewsItem("persist-1", "First", "Content 1", "2026-09-29T10:00:00Z"),
|
||||
makeNewsItem("persist-2", "Second", "Content 2", "2026-09-28T10:00:00Z") };
|
||||
|
||||
NewsModel *model = m_coreController->m_newsModel;
|
||||
model->setNewsList(news);
|
||||
model->markAsRead(0);
|
||||
|
||||
const QStringList savedIds = m_coreController->m_appSettingsRepository->getReadNewsIds();
|
||||
|
||||
QVERIFY(savedIds.contains(QStringLiteral("persist-1")));
|
||||
|
||||
NewsModel restoredModel(m_coreController->m_appSettingsRepository);
|
||||
restoredModel.setNewsList(news);
|
||||
|
||||
QCOMPARE(restoredModel.data(restoredModel.index(0, 0), NewsModel::IsReadRole).toBool(), true);
|
||||
QCOMPARE(restoredModel.data(restoredModel.index(1, 0), NewsModel::IsReadRole).toBool(), false);
|
||||
}
|
||||
|
||||
void testFetchNewsWithoutServers()
|
||||
{
|
||||
QSignalSpy fetchNewsFinishedSpy(m_coreController->m_apiNewsUiController, &ApiNewsUiController::fetchNewsFinished);
|
||||
QSignalSpy errorOccurredSpy(m_coreController->m_apiNewsUiController, &ApiNewsUiController::errorOccurred);
|
||||
QSignalSpy processedIndexChangedSpy(m_coreController->m_newsModel, &NewsModel::processedIndexChanged);
|
||||
QSignalSpy hasUnreadChangedSpy(m_coreController->m_newsModel, &NewsModel::hasUnreadChanged);
|
||||
|
||||
/* TODO:
|
||||
m_coreController->m_apiNewsUiController->fetchNews(false);
|
||||
QVERIFY(errorOccurredSpy.count() == 0, "errorOccurred signal should not be emitted");
|
||||
QVERIFY(fetchNewsFinishedSpy.count() == 1, "fetchNewsFinished signal should be emitted");
|
||||
|
||||
m_coreController->m_newsModel->updateModel();
|
||||
QVERIFY(hasUnreadChangedSpy.count() == 1, "hasUnreadChanged signal should be emitted");
|
||||
QTRY_COMPARE_WITH_TIMEOUT(fetchNewsFinishedSpy.count(), 1, 1000);
|
||||
QCOMPARE(errorOccurredSpy.count(), 0);
|
||||
QCOMPARE(m_coreController->m_newsModel->rowCount(), 0);
|
||||
}
|
||||
|
||||
QModelIndex newsModelIndex = m_coreController->m_newsModel->index(0, 0);
|
||||
QVERIFY2(newsModelIndex.isValid(), "News model index should be valid");
|
||||
void testFetchNewsFromApi()
|
||||
{
|
||||
const QString apiKey = envValue("AMNEZIA_TEST_API_KEY");
|
||||
|
||||
auto newsId = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::IdRole);
|
||||
QCOMPARE(newsId, );
|
||||
if (apiKey.isEmpty()) {
|
||||
QSKIP("AMNEZIA_TEST_API_KEY is not set, skipping live gateway test");
|
||||
}
|
||||
|
||||
auto newsTitle = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::TitleRole);
|
||||
QCOMPARE(newsTitle, );
|
||||
m_settings->setValue("Conf/installationUuid", QUuid::createUuidV5(QUuid(), QByteArrayLiteral("amnezia-news-api-test")).toString(QUuid::WithoutBraces));
|
||||
|
||||
auto newsContent = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::ContentRole);
|
||||
QCOMPARE(newsContent, );
|
||||
const ApiV2ServerConfig seed = gatewayTestSubscription(apiKey);
|
||||
const QString serverId = m_coreController->m_serversRepository->addServer(QString(), seed.toJson(), serverConfigUtils::configTypeFromJson(seed.toJson()));
|
||||
|
||||
auto newsTimestamp = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::TimestampRole);
|
||||
QCOMPARE(newsTimestamp, );
|
||||
QCOMPARE(m_coreController->m_serversRepository->serversCount(), 1);
|
||||
|
||||
auto newsIsRead = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::IsReadRole);
|
||||
QCOMPARE(newsIsRead, false);
|
||||
SubscriptionController::CaptchaInfo captchaInfo;
|
||||
|
||||
auto newsIsProcessed = m_coreController->m_newsModel->data(newsModelIndex, NewsModel::IsProcessedRole);
|
||||
QCOMPARE(newsIsProcessed, );
|
||||
const ErrorCode updateError = m_coreController->m_subscriptionController->updateServiceFromGateway(serverId, QStringLiteral("de"), /*isConnectEvent=*/false, &captchaInfo, nullptr);
|
||||
|
||||
m_coreController->m_newsModel->markAsRead(0);
|
||||
? m_coreController->m_newsModel->updateModel(); ?
|
||||
QVERIFY(hasUnreadChangedSpy.count() == 2, "hasUnreadChanged signal should be emitted");
|
||||
QCOMPARE(newsIsRead, true);
|
||||
*/
|
||||
if (updateError == ErrorCode::ApiCaptchaRequiredError) {
|
||||
QSKIP("Gateway requested a captcha, skipping live gateway test");
|
||||
}
|
||||
|
||||
QVERIFY2(updateError == ErrorCode::NoError, qPrintable(QStringLiteral("Gateway update failed: %1").arg(static_cast<int>(updateError))));
|
||||
|
||||
QCOMPARE(m_coreController->m_serversRepository->serversCount(), 1);
|
||||
|
||||
QSignalSpy fetchNewsFinishedSpy(m_coreController->m_apiNewsUiController, &ApiNewsUiController::fetchNewsFinished);
|
||||
QSignalSpy errorOccurredSpy(m_coreController->m_apiNewsUiController, &ApiNewsUiController::errorOccurred);
|
||||
|
||||
m_coreController->m_apiNewsUiController->fetchNews(false);
|
||||
|
||||
QTRY_COMPARE_WITH_TIMEOUT(fetchNewsFinishedSpy.count(), 1, 10000);
|
||||
QCOMPARE(errorOccurredSpy.count(), 0);
|
||||
QVERIFY(m_coreController->m_newsModel->rowCount() > 0);
|
||||
|
||||
const QModelIndex index = m_coreController->m_newsModel->index(0, 0);
|
||||
|
||||
QVERIFY(index.isValid());
|
||||
QVERIFY(!m_coreController->m_newsModel->data(index, NewsModel::IdRole).toString().isEmpty());
|
||||
QVERIFY(!m_coreController->m_newsModel->data(index, NewsModel::TitleRole).toString().isEmpty());
|
||||
QVERIFY(!m_coreController->m_newsModel->data(index, NewsModel::ContentRole).toString().isEmpty());
|
||||
|
||||
const QDateTime timestamp = QDateTime::fromString(m_coreController->m_newsModel->data(index, NewsModel::TimestampRole).toString(), Qt::ISODate);
|
||||
|
||||
QVERIFY(timestamp.isValid());
|
||||
QCOMPARE(m_coreController->m_newsModel->data(index, NewsModel::IsReadRole).toBool(), false);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestUiNewsModelAndController)
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
// CoreController accesses the gateway through amnApp->networkManager().
|
||||
// The application instance must be an AmneziaApplication.
|
||||
AmneziaApplication app(argc, argv);
|
||||
|
||||
TestUiNewsModelAndController tc;
|
||||
QTEST_SET_MAIN_SOURCE_PATH
|
||||
return QTest::qExec(&tc, argc, argv);
|
||||
}
|
||||
#include "testUiNewsModelAndController.moc"
|
||||
@@ -0,0 +1,310 @@
|
||||
#include <QDateTime>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
#include <QUuid>
|
||||
|
||||
#include "utils/testCoreController.h"
|
||||
#include "utils/testUtils.h"
|
||||
|
||||
#include "secureQSettings.h"
|
||||
#include "vpnConnection.h"
|
||||
|
||||
#include "ui/models/api/apiServicesModel.h"
|
||||
|
||||
using namespace amnezia;
|
||||
using namespace amnezia::test;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
QJsonObject makeServiceItem(const QString &name,
|
||||
const QString &cardDescription,
|
||||
const QString &description,
|
||||
const QString &type,
|
||||
const QString &protocol,
|
||||
const bool isAvailable,
|
||||
const QString &price,
|
||||
const QString &endDate,
|
||||
const QString &termsOfUseUrl,
|
||||
const QString &privacyPolicyUrl,
|
||||
const QJsonArray &subscriptionPlans = {})
|
||||
{
|
||||
return {
|
||||
{ "service_info",
|
||||
QJsonObject {
|
||||
{ "id", type + "-info" },
|
||||
{ "name", name }
|
||||
}
|
||||
},
|
||||
{ "service_type", type },
|
||||
{ "service_protocol", protocol },
|
||||
{ "is_available", isAvailable },
|
||||
{ "service_description",
|
||||
QJsonObject {
|
||||
{ "service_name", name },
|
||||
{ "card_description", cardDescription },
|
||||
{ "description", description },
|
||||
{ "terms_of_use_url", termsOfUseUrl },
|
||||
{ "privacy_policy_url", privacyPolicyUrl },
|
||||
{ "subscription_plans", subscriptionPlans },
|
||||
{ "min_price_label", price }
|
||||
}
|
||||
},
|
||||
{ "subscription",
|
||||
QJsonObject {
|
||||
{ "end_date", endDate }
|
||||
}
|
||||
},
|
||||
{ "available_countries",
|
||||
QJsonArray {
|
||||
"EE",
|
||||
"FI"
|
||||
}
|
||||
},
|
||||
{ "store_endpoint", "https://example.com/store" }
|
||||
};
|
||||
}
|
||||
|
||||
QJsonObject makeServicesResponse()
|
||||
{
|
||||
return {
|
||||
{ "user_country_code", "EE" },
|
||||
{ "services",
|
||||
QJsonArray {
|
||||
makeServiceItem("Amnezia Premium",
|
||||
"Premium service",
|
||||
"Premium service description",
|
||||
"amnezia-premium",
|
||||
"xray",
|
||||
true,
|
||||
"$19.99",
|
||||
"2026-10-29T10:00:00Z",
|
||||
"https://example.com/terms",
|
||||
"https://example.com/privacy",
|
||||
QJsonArray {
|
||||
QJsonObject {
|
||||
{ "id", "monthly" },
|
||||
{ "price", "$19.99" }
|
||||
}
|
||||
}),
|
||||
|
||||
makeServiceItem("Amnezia Free",
|
||||
"Free service",
|
||||
"Free service description",
|
||||
"amnezia-free",
|
||||
"wireguard",
|
||||
true,
|
||||
"$0",
|
||||
"",
|
||||
"https://example.com/terms",
|
||||
"https://example.com/privacy")
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class TestUiServicesModelAndController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
TestCoreController *m_coreController;
|
||||
SecureQSettings *m_settings;
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
const QString testOrg = "AmneziaVPN-Test-" + QUuid::createUuid().toString();
|
||||
m_settings = new SecureQSettings(testOrg, "amnezia-client", nullptr, false);
|
||||
|
||||
const auto vpnConnection = QSharedPointer<VpnConnection>::create(nullptr, nullptr);
|
||||
|
||||
m_coreController = new TestCoreController(vpnConnection, m_settings, nullptr, this);
|
||||
}
|
||||
|
||||
void cleanupTestCase()
|
||||
{
|
||||
m_settings->clearSettings();
|
||||
delete m_coreController;
|
||||
delete m_settings;
|
||||
}
|
||||
|
||||
void init()
|
||||
{
|
||||
m_settings->clearSettings();
|
||||
m_coreController->m_apiServicesModel->updateModel(QJsonObject {});
|
||||
}
|
||||
|
||||
void testRolesAndSignals()
|
||||
{
|
||||
ApiServicesModel *model = m_coreController->m_apiServicesModel;
|
||||
|
||||
QSignalSpy selectionChangedSpy(model, &ApiServicesModel::serviceSelectionChanged);
|
||||
|
||||
model->updateModel(makeServicesResponse());
|
||||
|
||||
QCOMPARE(model->rowCount(), 2);
|
||||
QCOMPARE(selectionChangedSpy.count(), 1);
|
||||
|
||||
const QModelIndex firstIndex = model->index(0, 0);
|
||||
|
||||
QCOMPARE(model->data(firstIndex, ApiServicesModel::NameRole).toString(), QString("Amnezia Premium"));
|
||||
QCOMPARE(model->data(firstIndex, ApiServicesModel::CardDescriptionRole).toString(), QString("Premium service"));
|
||||
QCOMPARE(model->data(firstIndex, ApiServicesModel::ServiceDescriptionRole).toString(), QString("Premium service description"));
|
||||
QCOMPARE(model->data(firstIndex, ApiServicesModel::IsServiceAvailableRole).toBool(), true);
|
||||
QCOMPARE(model->data(firstIndex, ApiServicesModel::IsPremiumRole).toBool(), true);
|
||||
QCOMPARE(model->data(firstIndex, ApiServicesModel::HasSubscriptionPlansRole).toBool(), true);
|
||||
QCOMPARE(model->data(firstIndex, ApiServicesModel::PriceRole).toString(), QString("$19.99"));
|
||||
QCOMPARE(model->data(firstIndex, ApiServicesModel::TermsOfUseUrlRole).toString(),QString("https://example.com/terms"));
|
||||
QCOMPARE(model->data(firstIndex, ApiServicesModel::PrivacyPolicyUrlRole).toString(), QString("https://example.com/privacy"));
|
||||
QCOMPARE(model->data(firstIndex, ApiServicesModel::ShowRecommendedRole).toBool(), true);
|
||||
QCOMPARE(model->data(firstIndex, ApiServicesModel::OrderRole).toInt(), 0);
|
||||
}
|
||||
|
||||
void testServiceSelection()
|
||||
{
|
||||
ApiServicesModel *model = m_coreController->m_apiServicesModel;
|
||||
model->updateModel(makeServicesResponse());
|
||||
|
||||
QCOMPARE(model->serviceIndexForType("amnezia-premium"), 0);
|
||||
QCOMPARE(model->serviceIndexForType("amnezia-free"), 1);
|
||||
QCOMPARE(model->serviceIndexForType("unknown"), -1);
|
||||
|
||||
QSignalSpy selectionChangedSpy(model, &ApiServicesModel::serviceSelectionChanged);
|
||||
|
||||
const QJsonArray expectedCountries { "EE", "FI" };
|
||||
|
||||
model->setServiceIndex(1);
|
||||
|
||||
QCOMPARE(selectionChangedSpy.count(), 1);
|
||||
QCOMPARE(model->getSelectedServiceName(), QString("Amnezia Free"));
|
||||
QCOMPARE(model->getSelectedServiceType(), QString("amnezia-free"));
|
||||
QCOMPARE(model->getSelectedServiceProtocol(), QString("wireguard"));
|
||||
QCOMPARE(model->getSelectedServiceCountries(), expectedCountries);
|
||||
QCOMPARE(model->getCountryCode(), QString("EE"));
|
||||
QCOMPARE(model->getStoreEndpoint(), QString("https://example.com/store"));
|
||||
}
|
||||
|
||||
void testSelectedServiceData()
|
||||
{
|
||||
ApiServicesModel *model = m_coreController->m_apiServicesModel;
|
||||
model->updateModel(makeServicesResponse());
|
||||
|
||||
QCOMPARE(model->getSelectedServiceData("name").toString(), QString("Amnezia Premium"));
|
||||
QCOMPARE(model->getSelectedServiceData("cardDescription").toString(), QString("Premium service"));
|
||||
QCOMPARE(model->getSelectedServiceData("serviceDescription").toString(), QString("Premium service description"));
|
||||
QCOMPARE(model->getSelectedServiceData("isServiceAvailable").toBool(), true);
|
||||
QCOMPARE(model->getSelectedServiceData("isPremium").toBool(), true);
|
||||
QCOMPARE(model->getSelectedServiceData("hasSubscriptionPlans").toBool(), true);
|
||||
QCOMPARE(model->getSelectedServiceData("price").toString(), QString("$19.99"));
|
||||
QCOMPARE(model->getSelectedServiceData("termsOfUseUrl").toString(), QString("https://example.com/terms"));
|
||||
QCOMPARE(model->getSelectedServiceData("privacyPolicyUrl").toString(), QString("https://example.com/privacy"));
|
||||
QVERIFY(!model->getSelectedServiceData("unknownRole").isValid());
|
||||
}
|
||||
|
||||
void testEndDate()
|
||||
{
|
||||
ApiServicesModel *model = m_coreController->m_apiServicesModel;
|
||||
model->updateModel(makeServicesResponse());
|
||||
|
||||
const QString endDate = model->data(model->index(0, 0), ApiServicesModel::EndDateRole).toString();
|
||||
const QString expected = QDateTime::fromString("2026-10-29T10:00:00Z", Qt::ISODate).toLocalTime().toString("d MMM yyyy");
|
||||
|
||||
QCOMPARE(endDate, expected);
|
||||
}
|
||||
|
||||
void testFreeServiceUnavailable()
|
||||
{
|
||||
ApiServicesModel *model = m_coreController->m_apiServicesModel;
|
||||
|
||||
QJsonArray services { makeServiceItem("Amnezia Free", "Free service", "Free service description", "amnezia-free",
|
||||
"wireguard", false, "$0", "", "https://example.com/terms", "https://example.com/privacy") };
|
||||
|
||||
QJsonObject data { { "services", services } };
|
||||
|
||||
model->updateModel(data);
|
||||
|
||||
const QModelIndex index = model->index(0, 0);
|
||||
|
||||
QCOMPARE(model->data(index, ApiServicesModel::IsServiceAvailableRole).toBool(), false);
|
||||
QCOMPARE(model->data(index, ApiServicesModel::IsPremiumRole).toBool(), false);
|
||||
QCOMPARE(model->data(index, ApiServicesModel::ShowRecommendedRole).toBool(), false);
|
||||
QCOMPARE(model->data(index, ApiServicesModel::OrderRole).toInt(), 1);
|
||||
|
||||
const QString description = model->data(index, ApiServicesModel::CardDescriptionRole).toString();
|
||||
|
||||
QVERIFY(description.contains("Not available in your region"));
|
||||
}
|
||||
|
||||
void testInvalidModelIndex()
|
||||
{
|
||||
ApiServicesModel *model = m_coreController->m_apiServicesModel;
|
||||
model->updateModel(makeServicesResponse());
|
||||
|
||||
QVERIFY(!model->data(QModelIndex(), ApiServicesModel::NameRole).isValid());
|
||||
QVERIFY(!model->data(model->index(-1, 0), ApiServicesModel::NameRole).isValid());
|
||||
QVERIFY(!model->data(model->index(model->rowCount(), 0), ApiServicesModel::NameRole).isValid());
|
||||
}
|
||||
|
||||
void testEmptyResponse()
|
||||
{
|
||||
ApiServicesModel *model = m_coreController->m_apiServicesModel;
|
||||
|
||||
QSignalSpy selectionChangedSpy(model, &ApiServicesModel::serviceSelectionChanged);
|
||||
|
||||
model->updateModel(QJsonObject {
|
||||
{ "user_country_code", "EE" },
|
||||
{ "services", QJsonArray {} }
|
||||
});
|
||||
|
||||
QCOMPARE(model->rowCount(), 1);
|
||||
QCOMPARE(selectionChangedSpy.count(), 1);
|
||||
}
|
||||
|
||||
void testServicesWithoutSubscriptionPlans()
|
||||
{
|
||||
ApiServicesModel *model = m_coreController->m_apiServicesModel;
|
||||
model->updateModel(makeServicesResponse());
|
||||
|
||||
const QModelIndex index = model->index(1, 0);
|
||||
|
||||
QCOMPARE(model->data(index, ApiServicesModel::HasSubscriptionPlansRole).toBool(), false);
|
||||
}
|
||||
|
||||
void testControllerAccessors()
|
||||
{
|
||||
ApiServicesModel *model = m_coreController->m_apiServicesModel;
|
||||
model->updateModel(makeServicesResponse());
|
||||
model->setServiceIndex(0);
|
||||
|
||||
ServicesCatalogUiController *controller = m_coreController->m_servicesCatalogUiController;
|
||||
|
||||
const QJsonObject expectedServiceInfo { { "id", "amnezia-premium-info" }, { "name", "Amnezia Premium" } };
|
||||
|
||||
QCOMPARE(controller->getSelectedServiceInfo(), expectedServiceInfo);
|
||||
QCOMPARE(controller->getSelectedServiceType(), model->getSelectedServiceType());
|
||||
QCOMPARE(controller->getSelectedServiceProtocol(), model->getSelectedServiceProtocol());
|
||||
QCOMPARE(controller->getSelectedServiceName(), model->getSelectedServiceName());
|
||||
QCOMPARE(controller->getSelectedServiceCountries(), model->getSelectedServiceCountries());
|
||||
QCOMPARE(controller->getCountryCode(), model->getCountryCode());
|
||||
QCOMPARE(controller->getStoreEndpoint(), model->getStoreEndpoint());
|
||||
QCOMPARE(controller->getSelectedServiceData("name"), model->getSelectedServiceData("name"));
|
||||
QCOMPARE(controller->getSelectedServiceData("isPremium"), model->getSelectedServiceData("isPremium"));
|
||||
}
|
||||
|
||||
void testControllerErrorSignal()
|
||||
{
|
||||
ServicesCatalogUiController *controller = m_coreController->m_servicesCatalogUiController;
|
||||
|
||||
QSignalSpy errorSpy(controller, &ServicesCatalogUiController::errorOccurred);
|
||||
|
||||
QVERIFY(errorSpy.isValid());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestUiServicesModelAndController)
|
||||
#include "testUiServicesModelAndController.moc"
|
||||
@@ -5289,8 +5289,8 @@ Create one from the current settings.</source>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="133"/>
|
||||
<source>Amnezia Hosting - VPN servers without complicated settings</source>
|
||||
<translation>Amnezia Hosting - خوادم VPN بلا إعدادات معقّدة</translation>
|
||||
<source>Amnezia hosting. VPN servers without complicated settings and headaches</source>
|
||||
<translation>استضافة Amnezia. خوادم VPN بلا إعدادات معقّدة ولا صداع</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
|
||||
|
||||
@@ -5276,8 +5276,8 @@ Cree una a partir de los ajustes actuales.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="133"/>
|
||||
<source>Amnezia Hosting - VPN servers without complicated settings</source>
|
||||
<translation>Amnezia Hosting - servidores VPN sin ajustes complicados</translation>
|
||||
<source>Amnezia hosting. VPN servers without complicated settings and headaches</source>
|
||||
<translation>Hosting de Amnezia. Servidores VPN sin ajustes complicados ni quebraderos de cabeza</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
|
||||
|
||||
@@ -5273,8 +5273,8 @@ Create one from the current settings.</source>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="133"/>
|
||||
<source>Amnezia Hosting - VPN servers without complicated settings</source>
|
||||
<translation>Amnezia Hosting - سرورهای VPN بدون تنظیمات پیچیده</translation>
|
||||
<source>Amnezia hosting. VPN servers without complicated settings and headaches</source>
|
||||
<translation>هاستینگ Amnezia. سرورهای VPN بدون تنظیمات پیچیده و دردسر</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
|
||||
|
||||
@@ -5276,8 +5276,8 @@ Créez-en une à partir des paramètres actuels.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="133"/>
|
||||
<source>Amnezia Hosting - VPN servers without complicated settings</source>
|
||||
<translation>Amnezia Hosting - des serveurs VPN sans réglages compliqués</translation>
|
||||
<source>Amnezia hosting. VPN servers without complicated settings and headaches</source>
|
||||
<translation>Hébergement Amnezia. Des serveurs VPN sans réglages compliqués ni prise de tête</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
|
||||
|
||||
@@ -5276,8 +5276,8 @@ Create one from the current settings.</source>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="133"/>
|
||||
<source>Amnezia Hosting - VPN servers without complicated settings</source>
|
||||
<translation>Amnezia Hosting - जटिल सेटिंग्स के बिना VPN सर्वर</translation>
|
||||
<source>Amnezia hosting. VPN servers without complicated settings and headaches</source>
|
||||
<translation>Amnezia होस्टिंग। जटिल सेटिंग्स और झंझट के बिना VPN सर्वर</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
|
||||
|
||||
@@ -5273,8 +5273,8 @@ Create one from the current settings.</source>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="133"/>
|
||||
<source>Amnezia Hosting - VPN servers without complicated settings</source>
|
||||
<translation>Amnezia Hosting - 복잡한 설정 없는 VPN 서버</translation>
|
||||
<source>Amnezia hosting. VPN servers without complicated settings and headaches</source>
|
||||
<translation>Amnezia 호스팅. 복잡한 설정도 번거로움도 없는 VPN 서버</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
|
||||
|
||||
@@ -5273,8 +5273,8 @@ Create one from the current settings.</source>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="133"/>
|
||||
<source>Amnezia Hosting - VPN servers without complicated settings</source>
|
||||
<translation>Amnezia Hosting - ရှုပ်ထွေးသော ဆက်တင်များ မလိုသော VPN ဆာဗာများ</translation>
|
||||
<source>Amnezia hosting. VPN servers without complicated settings and headaches</source>
|
||||
<translation>Amnezia hosting။ ရှုပ်ထွေးသော ဆက်တင်များနှင့် ခေါင်းကိုက်စရာများ မလိုသော VPN ဆာဗာများ</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
|
||||
|
||||
@@ -5704,8 +5704,8 @@ Create one from the current settings.</source>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="133"/>
|
||||
<source>Amnezia Hosting - VPN servers without complicated settings</source>
|
||||
<translation>Amnezia Hosting - VPN-серверы без сложных настроек</translation>
|
||||
<source>Amnezia hosting. VPN servers without complicated settings and headaches</source>
|
||||
<translation>Хостинг Amnezia. VPN-серверы без сложных настроек и головной боли</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
|
||||
|
||||
@@ -5273,8 +5273,8 @@ Create one from the current settings.</source>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="133"/>
|
||||
<source>Amnezia Hosting - VPN servers without complicated settings</source>
|
||||
<translation>Amnezia Hosting - เซิร์ฟเวอร์ VPN ที่ไม่ต้องตั้งค่ายุ่งยาก</translation>
|
||||
<source>Amnezia hosting. VPN servers without complicated settings and headaches</source>
|
||||
<translation>Amnezia hosting เซิร์ฟเวอร์ VPN ที่ไม่ต้องตั้งค่ายากและไม่ต้องกังวล</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
|
||||
|
||||
@@ -5279,8 +5279,8 @@ Create one from the current settings.</source>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="133"/>
|
||||
<source>Amnezia Hosting - VPN servers without complicated settings</source>
|
||||
<translation>Amnezia Hosting - VPN-сервери без складних налаштувань</translation>
|
||||
<source>Amnezia hosting. VPN servers without complicated settings and headaches</source>
|
||||
<translation>Хостинг Amnezia. VPN-сервери без складних налаштувань і зайвого клопоту</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
|
||||
|
||||
@@ -5276,8 +5276,8 @@ Create one from the current settings.</source>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="133"/>
|
||||
<source>Amnezia Hosting - VPN servers without complicated settings</source>
|
||||
<translation>Amnezia Hosting - پیچیدہ ترتیبات کے بغیر VPN سرورز</translation>
|
||||
<source>Amnezia hosting. VPN servers without complicated settings and headaches</source>
|
||||
<translation>Amnezia ہوسٹنگ۔ پیچیدہ ترتیبات اور جھنجھٹ کے بغیر VPN سرورز</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
|
||||
|
||||
@@ -5273,8 +5273,8 @@ Create one from the current settings.</source>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="133"/>
|
||||
<source>Amnezia Hosting - VPN servers without complicated settings</source>
|
||||
<translation>Amnezia Hosting - 无需复杂设置的 VPN 服务器</translation>
|
||||
<source>Amnezia hosting. VPN servers without complicated settings and headaches</source>
|
||||
<translation>Amnezia 托管服务。无需复杂设置、省心的 VPN 服务器</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
#include "localProxyUiController.h"
|
||||
|
||||
LocalProxyUiController::LocalProxyUiController(LocalProxyController *localProxyController, QObject *parent)
|
||||
: QObject(parent), m_localProxyController(localProxyController)
|
||||
{
|
||||
connect(m_localProxyController, &LocalProxyController::stateChanged, this, &LocalProxyUiController::stateChanged);
|
||||
connect(m_localProxyController, &LocalProxyController::startFailed, this, &LocalProxyUiController::startFailed);
|
||||
}
|
||||
|
||||
bool LocalProxyUiController::isSupported() const
|
||||
{
|
||||
return m_localProxyController->isSupported();
|
||||
}
|
||||
|
||||
bool LocalProxyUiController::isEnabled() const
|
||||
{
|
||||
return m_localProxyController->isEnabled();
|
||||
}
|
||||
|
||||
int LocalProxyUiController::port() const
|
||||
{
|
||||
return m_localProxyController->port();
|
||||
}
|
||||
|
||||
QString LocalProxyUiController::ownerId() const
|
||||
{
|
||||
return m_localProxyController->ownerId();
|
||||
}
|
||||
|
||||
int LocalProxyUiController::activePort() const
|
||||
{
|
||||
return m_localProxyController->activePort();
|
||||
}
|
||||
|
||||
bool LocalProxyUiController::isPortUserDefined() const
|
||||
{
|
||||
return m_localProxyController->isPortUserDefined();
|
||||
}
|
||||
|
||||
bool LocalProxyUiController::setPort(int port)
|
||||
{
|
||||
return m_localProxyController->setPort(port);
|
||||
}
|
||||
|
||||
bool LocalProxyUiController::isPortBusy(int port) const
|
||||
{
|
||||
return m_localProxyController->isPortBusy(port);
|
||||
}
|
||||
|
||||
bool LocalProxyUiController::enable(const QString &ownerId, int port)
|
||||
{
|
||||
return m_localProxyController->enable(ownerId, port);
|
||||
}
|
||||
|
||||
void LocalProxyUiController::disable()
|
||||
{
|
||||
m_localProxyController->disable();
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
#ifndef LOCALPROXYUICONTROLLER_H
|
||||
#define LOCALPROXYUICONTROLLER_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "core/controllers/localProxyController.h"
|
||||
|
||||
class LocalProxyUiController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LocalProxyUiController(LocalProxyController *localProxyController, QObject *parent = nullptr);
|
||||
|
||||
Q_PROPERTY(bool isSupported READ isSupported CONSTANT)
|
||||
Q_PROPERTY(bool isEnabled READ isEnabled NOTIFY stateChanged)
|
||||
Q_PROPERTY(int port READ port NOTIFY stateChanged)
|
||||
Q_PROPERTY(QString ownerId READ ownerId NOTIFY stateChanged)
|
||||
Q_PROPERTY(int activePort READ activePort NOTIFY stateChanged)
|
||||
|
||||
public slots:
|
||||
bool isSupported() const;
|
||||
bool isEnabled() const;
|
||||
int port() const;
|
||||
QString ownerId() const;
|
||||
int activePort() const;
|
||||
bool isPortUserDefined() const;
|
||||
|
||||
bool setPort(int port);
|
||||
bool isPortBusy(int port) const;
|
||||
bool enable(const QString &ownerId, int port);
|
||||
void disable();
|
||||
|
||||
signals:
|
||||
void stateChanged();
|
||||
void startFailed(const QString &message);
|
||||
|
||||
private:
|
||||
LocalProxyController *m_localProxyController;
|
||||
};
|
||||
|
||||
#endif // LOCALPROXYUICONTROLLER_H
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
#include <QGuiApplication>
|
||||
#include <QInputMethod>
|
||||
#else
|
||||
#include <QApplication>
|
||||
#endif
|
||||
@@ -42,10 +41,6 @@ PageController::PageController(ServersController* serversController, SettingsCon
|
||||
});
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
connect(QGuiApplication::inputMethod(), &QInputMethod::visibleChanged, this, &PageController::safeAreaBottomMarginChanged);
|
||||
#endif
|
||||
|
||||
#if defined Q_OS_MACX
|
||||
connect(this, &PageController::raiseMainWindow, []() {
|
||||
setDockIconVisible(true);
|
||||
@@ -196,7 +191,7 @@ int PageController::getSafeAreaTopMargin()
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
return m_safeAreaMargins.top();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int PageController::getSafeAreaBottomMargin()
|
||||
@@ -212,24 +207,7 @@ int PageController::getSafeAreaBottomMargin()
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
#ifdef Q_OS_IOS
|
||||
if (QGuiApplication::inputMethod()->isVisible()) {
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
return m_safeAreaMargins.bottom();
|
||||
}
|
||||
|
||||
void PageController::setSafeAreaMargins(const QMargins &margins)
|
||||
{
|
||||
const QMargins previous = m_safeAreaMargins;
|
||||
m_safeAreaMargins = margins;
|
||||
if (previous.top() != margins.top()) {
|
||||
emit safeAreaTopMarginChanged();
|
||||
}
|
||||
if (previous.bottom() != margins.bottom()) {
|
||||
emit safeAreaBottomMarginChanged();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int PageController::getImeHeight()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#ifndef PAGECONTROLLER_H
|
||||
#define PAGECONTROLLER_H
|
||||
|
||||
#include <QMargins>
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
|
||||
@@ -49,8 +48,6 @@ public slots:
|
||||
int getSafeAreaBottomMargin();
|
||||
int getImeHeight();
|
||||
|
||||
void setSafeAreaMargins(const QMargins &margins);
|
||||
|
||||
private slots:
|
||||
void onShowErrorMessage(amnezia::ErrorCode errorCode);
|
||||
|
||||
@@ -101,8 +98,6 @@ private:
|
||||
mutable int m_cachedNavigationBarHeight = -1;
|
||||
mutable bool m_cachedEdgeToEdgeEnabled = false;
|
||||
mutable bool m_edgeToEdgeCached = false;
|
||||
|
||||
QMargins m_safeAreaMargins;
|
||||
int m_imeHeight = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Shapes
|
||||
|
||||
import Style 1.0
|
||||
@@ -42,47 +43,27 @@ Button {
|
||||
implicitWidth: 190
|
||||
implicitHeight: 190
|
||||
|
||||
Item {
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
|
||||
visible: root.inProgress
|
||||
opacity: 0.1
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
Item {
|
||||
id: diskSource
|
||||
ShapePath {
|
||||
fillColor: AmneziaStyle.color.transparent
|
||||
strokeColor: AmneziaStyle.color.borderEmphasis
|
||||
strokeWidth: 1
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: -d.glowMargin
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: AmneziaStyle.color.transparent
|
||||
strokeColor: d.ringColor
|
||||
strokeWidth: 1
|
||||
|
||||
PathAngleArc {
|
||||
centerX: diskSource.width / 2
|
||||
centerY: diskSource.height / 2
|
||||
radiusX: d.ringRadius
|
||||
radiusY: d.ringRadius
|
||||
startAngle: 0
|
||||
sweepAngle: 360
|
||||
}
|
||||
}
|
||||
PathAngleArc {
|
||||
centerX: root.width / 2
|
||||
centerY: root.height / 2
|
||||
radiusX: d.ringRadius
|
||||
radiusY: d.ringRadius
|
||||
startAngle: 0
|
||||
sweepAngle: 360
|
||||
}
|
||||
}
|
||||
|
||||
ShadowType {
|
||||
anchors.fill: diskSource
|
||||
z: -1
|
||||
|
||||
source: diskSource
|
||||
color: AmneziaStyle.color.shadowGlow
|
||||
blur: 6
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -117,12 +98,16 @@ Button {
|
||||
}
|
||||
}
|
||||
|
||||
SelectionGlowType {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
MultiEffect {
|
||||
anchors.fill: ringSource
|
||||
|
||||
source: ringSource
|
||||
color: d.ringColor
|
||||
shadowEnabled: true
|
||||
shadowColor: d.ringColor
|
||||
shadowBlur: 0.6
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 0
|
||||
blurMax: 16
|
||||
}
|
||||
|
||||
RotationAnimator on rotation {
|
||||
|
||||
@@ -24,7 +24,6 @@ EmptyStateType {
|
||||
|
||||
ButtonType {
|
||||
visible: root.isSearchResult || root.categoryName !== ""
|
||||
size: ButtonType.Medium
|
||||
variant: ButtonType.Text
|
||||
text: qsTr("Show all")
|
||||
|
||||
|
||||
@@ -1,123 +1,27 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Style 1.0
|
||||
|
||||
import "../Controls2"
|
||||
|
||||
ItemDelegate {
|
||||
ListItemType {
|
||||
id: root
|
||||
|
||||
property bool favorite: false
|
||||
property string countryFlagCode
|
||||
property string subtitle
|
||||
property int maximumLineCount: 2
|
||||
property bool showSeparator: true
|
||||
|
||||
readonly property bool rowHovered: root.enabled && root.hovered && !favoriteButton.hovered
|
||||
|
||||
signal favoriteToggled()
|
||||
|
||||
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
|
||||
implicitContentWidth + leftPadding + rightPadding)
|
||||
implicitHeight: Math.max(56, implicitContentHeight + topPadding + bottomPadding)
|
||||
|
||||
padding: 12
|
||||
verticalPadding: 4
|
||||
leftPadding: 16
|
||||
spacing: 12
|
||||
leadingSlotWidth: 24
|
||||
icon.source: root.countryFlagCode !== "" ? "qrc:/countriesFlags/images/flagKit/" + root.countryFlagCode + ".svg" : ""
|
||||
icon.width: 21
|
||||
icon.height: 15
|
||||
icon.color: AmneziaStyle.color.transparent
|
||||
|
||||
hoverEnabled: true
|
||||
focusPolicy: Qt.TabFocus
|
||||
opacity: enabled ? 1.0 : 0.4
|
||||
FavoriteButton {
|
||||
favorite: root.favorite
|
||||
|
||||
Accessible.name: root.text
|
||||
Accessible.description: root.subtitle
|
||||
|
||||
Keys.onEnterPressed: root.click()
|
||||
Keys.onReturnPressed: root.click()
|
||||
|
||||
background: Item {
|
||||
implicitHeight: 56
|
||||
|
||||
HoverHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: root.enabled
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
anchors.bottomMargin: 4
|
||||
|
||||
radius: 16
|
||||
color: {
|
||||
if (root.enabled && root.down) {
|
||||
return AmneziaStyle.color.bgStateLayerPressed
|
||||
}
|
||||
return root.rowHovered ? AmneziaStyle.color.bgStateLayer : AmneziaStyle.color.transparent
|
||||
}
|
||||
border.width: root.enabled && root.down || root.highlighted ? 1 : 0
|
||||
border.color: AmneziaStyle.color.borderBold
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 150 }
|
||||
}
|
||||
|
||||
FocusRingType {
|
||||
radius: 16
|
||||
visible: root.visualFocus
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
height: 1
|
||||
visible: root.showSeparator
|
||||
color: AmneziaStyle.color.borderSoft
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: root.spacing
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 24
|
||||
Layout.preferredHeight: 48
|
||||
|
||||
visible: root.countryFlagCode !== ""
|
||||
|
||||
Image {
|
||||
anchors.centerIn: parent
|
||||
width: 21
|
||||
height: 15
|
||||
|
||||
source: root.countryFlagCode !== "" ? "qrc:/countriesFlags/images/flagKit/" + root.countryFlagCode + ".svg" : ""
|
||||
sourceSize: Qt.size(21, 15)
|
||||
}
|
||||
}
|
||||
|
||||
ListItemLabelType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 4
|
||||
Layout.topMargin: 10
|
||||
Layout.bottomMargin: 10
|
||||
|
||||
title: root.text
|
||||
subtitle: root.subtitle
|
||||
maximumLineCount: root.maximumLineCount
|
||||
}
|
||||
|
||||
FavoriteButton {
|
||||
id: favoriteButton
|
||||
|
||||
favorite: root.favorite
|
||||
|
||||
onClicked: root.favoriteToggled()
|
||||
}
|
||||
onClicked: root.favoriteToggled()
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ ItemDelegate {
|
||||
anchors.fill: parent
|
||||
|
||||
radius: parent.radius
|
||||
color: root.down ? AmneziaStyle.color.bgStateLayerPressed : AmneziaStyle.color.bgStateLayer
|
||||
color: AmneziaStyle.color.bgStateLayer
|
||||
opacity: root.enabled && (root.down || root.hovered) ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
|
||||
@@ -60,7 +60,6 @@ SheetType {
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
size: ButtonType.Medium
|
||||
variant: ButtonType.Text
|
||||
|
||||
text: qsTr("Support")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
import Style 1.0
|
||||
|
||||
@@ -17,11 +17,8 @@ Item {
|
||||
anchors.fill: parent
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: backdropMask
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1.0
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -46,14 +43,10 @@ Item {
|
||||
visible: false
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
FastBlur {
|
||||
anchors.fill: parent
|
||||
source: backdrop
|
||||
autoPaddingEnabled: false
|
||||
blurEnabled: true
|
||||
blurMax: 64
|
||||
blur: root.blurRadius <= 20 ? 0.73 : 0.9
|
||||
blurMultiplier: root.blurRadius <= 20 ? 0 : 0.5
|
||||
radius: root.blurRadius
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +57,6 @@ Item {
|
||||
anchors.fill: parent
|
||||
radius: root.radius
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
||||
@@ -59,7 +59,7 @@ AbstractButton {
|
||||
anchors.fill: parent
|
||||
|
||||
radius: 16
|
||||
color: !root.muted && root.enabled && (root.hovered || root.down) ? (root.down ? AmneziaStyle.color.bgStateLayerPressed : AmneziaStyle.color.bgStateLayer)
|
||||
color: !root.muted && root.enabled && (root.hovered || root.down) ? AmneziaStyle.color.bgStateLayer
|
||||
: AmneziaStyle.color.transparent
|
||||
|
||||
Behavior on color {
|
||||
@@ -95,73 +95,67 @@ AbstractButton {
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 0
|
||||
spacing: root.spacing
|
||||
|
||||
RowLayout {
|
||||
IconImage {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.preferredWidth: root.icon.width
|
||||
Layout.preferredHeight: root.icon.height
|
||||
|
||||
visible: root.icon.source.toString() !== ""
|
||||
source: root.icon.source
|
||||
sourceSize: Qt.size(root.icon.width, root.icon.height)
|
||||
color: root.icon.color
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: root.subtitle !== "" || root.footnote !== "" ? Qt.AlignTop : Qt.AlignVCenter
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
spacing: root.spacing
|
||||
spacing: 4
|
||||
|
||||
IconImage {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.preferredWidth: root.icon.width
|
||||
Layout.preferredHeight: root.icon.height
|
||||
H3TextType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: root.icon.source.toString() !== ""
|
||||
source: root.icon.source
|
||||
sourceSize: Qt.size(root.icon.width, root.icon.height)
|
||||
color: root.icon.color
|
||||
visible: text !== ""
|
||||
text: root.text
|
||||
color: root.muted ? AmneziaStyle.color.textTertiary : AmneziaStyle.color.textPrimary
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
SmallTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
spacing: 4
|
||||
visible: text !== "" && !root.muted
|
||||
text: root.subtitle
|
||||
textFormat: Text.RichText
|
||||
|
||||
H3TextType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: text !== ""
|
||||
text: root.text
|
||||
color: root.muted ? AmneziaStyle.color.textTertiary : AmneziaStyle.color.textPrimary
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
}
|
||||
|
||||
SmallTextType {
|
||||
Layout.fillWidth: true
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: text !== "" && !root.muted
|
||||
text: root.subtitle
|
||||
textFormat: Text.RichText
|
||||
visible: text !== "" && root.muted
|
||||
text: root.subtitle
|
||||
textFormat: Text.RichText
|
||||
color: AmneziaStyle.color.textQuaternary
|
||||
}
|
||||
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
}
|
||||
SmallTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
|
||||
CaptionTextType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: text !== "" && root.muted
|
||||
text: root.subtitle
|
||||
textFormat: Text.RichText
|
||||
color: AmneziaStyle.color.textQuaternary
|
||||
}
|
||||
|
||||
SmallTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
|
||||
visible: text !== ""
|
||||
text: root.footnote
|
||||
color: root.muted ? AmneziaStyle.color.textQuaternary : AmneziaStyle.color.textTertiary
|
||||
}
|
||||
visible: text !== ""
|
||||
text: root.footnote
|
||||
color: root.muted ? AmneziaStyle.color.textQuaternary : AmneziaStyle.color.textTertiary
|
||||
}
|
||||
}
|
||||
|
||||
IconButtonType {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.leftMargin: -root.spacing
|
||||
|
||||
icon.source: "qrc:/images/controls/chevron-right.svg"
|
||||
icon.color: AmneziaStyle.color.iconDefault
|
||||
|
||||
@@ -34,14 +34,13 @@ Button {
|
||||
id: d
|
||||
|
||||
readonly property bool small: root.size === ButtonType.Small
|
||||
readonly property bool textVariant: root.variant === ButtonType.Text
|
||||
readonly property bool outlined: root.variant === ButtonType.Secondary || root.variant === ButtonType.Destructive
|
||||
readonly property bool active: root.down || root.hovered
|
||||
|
||||
readonly property int height: root.size === ButtonType.Large ? 48 : (root.size === ButtonType.Medium ? 40 : 36)
|
||||
readonly property int radius: small ? 12 : (root.size === ButtonType.Medium && !textVariant ? 14 : 16)
|
||||
readonly property int iconSize: small || (textVariant && root.size === ButtonType.Medium) ? 20 : 24
|
||||
readonly property int gap: root.size === ButtonType.Large || textVariant ? 4 : 2
|
||||
readonly property int radius: root.size === ButtonType.Large ? 16 : (root.size === ButtonType.Medium ? 14 : 12)
|
||||
readonly property int iconSize: small ? 20 : 24
|
||||
readonly property int gap: root.size === ButtonType.Large ? 4 : 2
|
||||
|
||||
readonly property color backgroundColor: {
|
||||
switch (root.variant) {
|
||||
|
||||
@@ -22,14 +22,14 @@ Button {
|
||||
radius: 16
|
||||
|
||||
color: root.checked ? AmneziaStyle.color.bgSelected : AmneziaStyle.color.bgSurfaceAlt
|
||||
border.width: root.checked || (root.hovered && !root.down) ? 1 : 0
|
||||
border.color: root.checked ? AmneziaStyle.color.accentLight : AmneziaStyle.color.borderBold
|
||||
border.width: root.checked ? 1 : 0
|
||||
border.color: AmneziaStyle.color.accentLight
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
|
||||
radius: parent.radius
|
||||
visible: root.down
|
||||
visible: root.down || root.hovered
|
||||
color: AmneziaStyle.color.bgStateLayer
|
||||
}
|
||||
|
||||
|
||||
@@ -46,10 +46,7 @@ Button {
|
||||
if (root.filled) {
|
||||
return root.hovered || root.down ? AmneziaStyle.color.bgSurfaceInverseAlt : AmneziaStyle.color.bgSurfaceInverse
|
||||
}
|
||||
if (root.down) {
|
||||
return AmneziaStyle.color.bgStateLayerPressed
|
||||
}
|
||||
return root.hovered ? AmneziaStyle.color.bgStateLayer : AmneziaStyle.color.transparent
|
||||
return root.hovered || root.down ? AmneziaStyle.color.bgStateLayer : AmneziaStyle.color.transparent
|
||||
}
|
||||
border.width: !root.filled && root.down ? 1 : 0
|
||||
border.color: AmneziaStyle.color.borderMedium
|
||||
|
||||
@@ -13,7 +13,7 @@ Button {
|
||||
property string imageSource
|
||||
|
||||
implicitWidth: root.imageSource !== "" ? 48 : Math.max(83, implicitContentWidth + leftPadding + rightPadding)
|
||||
implicitHeight: 56
|
||||
implicitHeight: 64
|
||||
|
||||
horizontalPadding: 12
|
||||
verticalPadding: 8
|
||||
|
||||
@@ -7,23 +7,35 @@ Item {
|
||||
id: root
|
||||
|
||||
property T.AbstractButton control: null
|
||||
property bool selected: false
|
||||
property bool showSeparator: true
|
||||
property color separatorColor: AmneziaStyle.color.borderSoft
|
||||
|
||||
readonly property bool active: root.control !== null && root.control.enabled
|
||||
readonly property bool pressed: root.active && root.control.down
|
||||
readonly property bool hovered: root.active && root.control.hovered
|
||||
|
||||
implicitHeight: 56
|
||||
implicitHeight: 72
|
||||
|
||||
HoverHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: root.active
|
||||
}
|
||||
|
||||
Item {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
anchors.bottomMargin: 4
|
||||
|
||||
radius: 16
|
||||
color: root.hovered || root.pressed ? AmneziaStyle.color.bgStateLayer : AmneziaStyle.color.transparent
|
||||
border.width: root.pressed || root.selected ? 1 : 0
|
||||
border.color: AmneziaStyle.color.borderBold
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 150 }
|
||||
}
|
||||
|
||||
FocusRingType {
|
||||
radius: 16
|
||||
visible: root.control !== null && root.control.visualFocus
|
||||
|
||||
@@ -14,11 +14,10 @@ ColumnLayout {
|
||||
property color titleColor: AmneziaStyle.color.textPrimary
|
||||
property color subtitleColor: AmneziaStyle.color.textTertiary
|
||||
property int maximumLineCount: 2
|
||||
property int subtitleLineHeight: 20
|
||||
property real textOpacity: 1.0
|
||||
property bool dense: false
|
||||
|
||||
spacing: 0
|
||||
spacing: 4
|
||||
|
||||
SmallTextType {
|
||||
Layout.fillWidth: true
|
||||
@@ -69,6 +68,5 @@ ColumnLayout {
|
||||
text: root.subtitle
|
||||
color: root.subtitleColor
|
||||
opacity: root.textOpacity
|
||||
lineHeight: root.subtitleLineHeight + LanguageUiController.getLineHeightAppend()
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import QtQuick
|
||||
|
||||
import Style 1.0
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool hovered: false
|
||||
property bool pressed: false
|
||||
|
||||
width: 48
|
||||
height: 48
|
||||
|
||||
radius: 12
|
||||
color: {
|
||||
if (root.pressed) {
|
||||
return AmneziaStyle.color.bgStateLayerPressed
|
||||
}
|
||||
return root.hovered ? AmneziaStyle.color.bgStateLayer : AmneziaStyle.color.transparent
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 150 }
|
||||
}
|
||||
}
|
||||
@@ -22,19 +22,14 @@ ItemDelegate {
|
||||
property bool trailingContentVisible: trailingSlot.children.length > 0
|
||||
property color separatorColor: AmneziaStyle.color.borderSoft
|
||||
property int leadingSlotWidth: 48
|
||||
property bool showIconBadge: false
|
||||
|
||||
readonly property bool rowHovered: root.interactive && root.enabled && root.hovered && !trailingHover.hovered
|
||||
readonly property bool rowPressed: root.interactive && root.enabled && root.down
|
||||
|
||||
default property alias trailingContent: trailingSlot.data
|
||||
|
||||
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
|
||||
implicitContentWidth + leftPadding + rightPadding)
|
||||
implicitHeight: Math.max(root.dense ? 72 : 56, implicitContentHeight + topPadding + bottomPadding)
|
||||
implicitHeight: Math.max(72, implicitContentHeight + topPadding + bottomPadding)
|
||||
|
||||
padding: 12
|
||||
verticalPadding: 4
|
||||
spacing: 12
|
||||
|
||||
hoverEnabled: root.interactive
|
||||
@@ -53,6 +48,7 @@ ItemDelegate {
|
||||
|
||||
background: ListItemBackgroundType {
|
||||
control: root.interactive ? root : null
|
||||
selected: root.highlighted
|
||||
showSeparator: root.showSeparator
|
||||
separatorColor: root.separatorColor
|
||||
}
|
||||
@@ -74,22 +70,12 @@ ItemDelegate {
|
||||
source: root.icon.source
|
||||
sourceSize: Qt.size(root.icon.width, root.icon.height)
|
||||
color: root.icon.color
|
||||
|
||||
DotBadgeType {
|
||||
anchors.horizontalCenter: parent.right
|
||||
anchors.verticalCenter: parent.top
|
||||
|
||||
visible: root.showIconBadge
|
||||
color: AmneziaStyle.color.accentDefault
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListItemLabelType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 4
|
||||
Layout.topMargin: 10
|
||||
Layout.bottomMargin: 10
|
||||
|
||||
overline: root.overline
|
||||
dense: root.dense
|
||||
@@ -105,6 +91,12 @@ ItemDelegate {
|
||||
if (root.locked) {
|
||||
return AmneziaStyle.color.textSecondary
|
||||
}
|
||||
if (root.interactive && root.enabled && root.down || root.highlighted) {
|
||||
return AmneziaStyle.color.textTertiary
|
||||
}
|
||||
if (root.enabled && root.hovered) {
|
||||
return AmneziaStyle.color.textSecondary
|
||||
}
|
||||
return AmneziaStyle.color.textPrimary
|
||||
}
|
||||
}
|
||||
@@ -125,10 +117,6 @@ ItemDelegate {
|
||||
|
||||
spacing: 0
|
||||
visible: root.trailingContentVisible
|
||||
|
||||
HoverHandler {
|
||||
id: trailingHover
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -137,13 +125,6 @@ ItemDelegate {
|
||||
|
||||
visible: root.trailingIconSource !== ""
|
||||
|
||||
ListItemStateLayerType {
|
||||
anchors.centerIn: parent
|
||||
|
||||
hovered: root.rowHovered
|
||||
pressed: root.rowPressed
|
||||
}
|
||||
|
||||
IconImage {
|
||||
anchors.centerIn: parent
|
||||
width: 24
|
||||
|
||||
@@ -19,14 +19,9 @@ Popup {
|
||||
color: AmneziaStyle.color.bgOverlay
|
||||
}
|
||||
|
||||
property Item blurSource
|
||||
|
||||
background: BackdropBlurType {
|
||||
sourceItem: root.blurSource
|
||||
blurRadius: 20
|
||||
background: Rectangle {
|
||||
radius: 16
|
||||
tintColor: Qt.alpha(AmneziaStyle.color.bgOverlay, 0.75)
|
||||
borderColor: AmneziaStyle.color.transparent
|
||||
color: AmneziaStyle.color.bgOverlay
|
||||
}
|
||||
|
||||
contentItem: SpinnerType {
|
||||
|
||||
@@ -59,7 +59,7 @@ Item {
|
||||
y: 4
|
||||
|
||||
visible: root.toolbarVisible
|
||||
spacing: 16
|
||||
spacing: 8
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,33 +57,11 @@ Popup {
|
||||
}
|
||||
}
|
||||
|
||||
background: Item {
|
||||
Rectangle {
|
||||
id: panelShadowSource
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
radius: 14
|
||||
color: AmneziaStyle.color.bgSurface
|
||||
visible: false
|
||||
}
|
||||
|
||||
ShadowType {
|
||||
anchors.fill: panelShadowSource
|
||||
source: panelShadowSource
|
||||
verticalOffset: 8
|
||||
blur: 24
|
||||
color: AmneziaStyle.color.bgOverlay
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
|
||||
radius: 14
|
||||
color: AmneziaStyle.color.bgSurface
|
||||
border.width: 1
|
||||
border.color: AmneziaStyle.color.borderSoft
|
||||
}
|
||||
background: Rectangle {
|
||||
radius: 14
|
||||
color: AmneziaStyle.color.bgSurface
|
||||
border.width: 1
|
||||
border.color: AmneziaStyle.color.borderSoft
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
|
||||
import Style 1.0
|
||||
|
||||
@@ -24,12 +25,15 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SelectionGlowType {
|
||||
anchors.fill: parent
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: dot
|
||||
source: dot
|
||||
visible: root.checked
|
||||
color: AmneziaStyle.color.accentLight
|
||||
shadowEnabled: true
|
||||
shadowColor: AmneziaStyle.color.accentLight
|
||||
shadowBlur: 0.6
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
||||
@@ -18,15 +18,11 @@ RadioDelegate {
|
||||
|
||||
default property alias trailingContent: trailingSlot.data
|
||||
|
||||
readonly property bool rowHovered: root.enabled && root.hovered && !trailingHover.hovered
|
||||
readonly property bool rowPressed: root.enabled && root.down
|
||||
|
||||
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
|
||||
implicitContentWidth + leftPadding + rightPadding)
|
||||
implicitHeight: Math.max(56, implicitContentHeight + topPadding + bottomPadding)
|
||||
implicitHeight: Math.max(72, implicitContentHeight + topPadding + bottomPadding)
|
||||
|
||||
padding: 12
|
||||
verticalPadding: 4
|
||||
leftPadding: 12 + 48 + spacing
|
||||
spacing: 12
|
||||
|
||||
@@ -56,13 +52,6 @@ RadioDelegate {
|
||||
width: 48
|
||||
height: 48
|
||||
|
||||
ListItemStateLayerType {
|
||||
anchors.centerIn: parent
|
||||
|
||||
hovered: root.rowHovered
|
||||
pressed: root.rowPressed
|
||||
}
|
||||
|
||||
RadioIndicatorType {
|
||||
anchors.centerIn: parent
|
||||
visible: root.icon.source.toString() === ""
|
||||
@@ -87,8 +76,6 @@ RadioDelegate {
|
||||
ListItemLabelType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 4
|
||||
Layout.topMargin: 10
|
||||
Layout.bottomMargin: 10
|
||||
|
||||
title: root.text
|
||||
subtitle: root.subtitle
|
||||
@@ -99,6 +86,12 @@ RadioDelegate {
|
||||
if (root.locked) {
|
||||
return AmneziaStyle.color.textSecondary
|
||||
}
|
||||
if (root.enabled && root.down) {
|
||||
return AmneziaStyle.color.textTertiary
|
||||
}
|
||||
if (root.enabled && root.hovered) {
|
||||
return AmneziaStyle.color.textSecondary
|
||||
}
|
||||
return AmneziaStyle.color.textPrimary
|
||||
}
|
||||
}
|
||||
@@ -119,10 +112,6 @@ RadioDelegate {
|
||||
|
||||
spacing: 0
|
||||
visible: children.length > 0
|
||||
|
||||
HoverHandler {
|
||||
id: trailingHover
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ TextField {
|
||||
implicitHeight: 44
|
||||
|
||||
leftPadding: 52
|
||||
rightPadding: clearButton.visible ? 52 : 16
|
||||
rightPadding: clearButton.visible ? clearButton.width + 4 : 16
|
||||
topPadding: 10
|
||||
bottomPadding: 10
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
@@ -111,7 +111,6 @@ TextField {
|
||||
id: clearButton
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 16 - (width - 24) / 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
implicitWidth: Qt.platform.os === "android" ? 48 : 44
|
||||
|
||||
@@ -35,7 +35,7 @@ RadioButton {
|
||||
anchors.fill: parent
|
||||
|
||||
radius: parent.radius
|
||||
color: root.down ? AmneziaStyle.color.bgStateLayerPressed : AmneziaStyle.color.bgStateLayer
|
||||
color: AmneziaStyle.color.bgStateLayer
|
||||
opacity: !root.checked && root.enabled && (root.hovered || root.down) ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
|
||||
@@ -12,11 +12,10 @@ RadioDelegate {
|
||||
|
||||
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
|
||||
implicitContentWidth + leftPadding + rightPadding)
|
||||
implicitHeight: Math.max(56, implicitContentHeight + topPadding + bottomPadding)
|
||||
implicitHeight: Math.max(72, implicitContentHeight + topPadding + bottomPadding)
|
||||
|
||||
padding: 12
|
||||
leftPadding: 16
|
||||
verticalPadding: 14
|
||||
|
||||
hoverEnabled: true
|
||||
focusPolicy: Qt.StrongFocus
|
||||
@@ -32,13 +31,13 @@ RadioDelegate {
|
||||
|
||||
background: Item {
|
||||
implicitWidth: 358
|
||||
implicitHeight: 56
|
||||
implicitHeight: 72
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
|
||||
radius: 16
|
||||
color: root.enabled && !root.checked && (root.hovered || root.down) ? (root.down ? AmneziaStyle.color.bgStateLayerPressed : AmneziaStyle.color.bgStateLayer)
|
||||
color: root.enabled && !root.checked && (root.hovered || root.down) ? AmneziaStyle.color.bgStateLayer
|
||||
: AmneziaStyle.color.transparent
|
||||
border.width: root.checked || root.down ? 1 : 0
|
||||
border.color: root.checked ? AmneziaStyle.color.accentLight : AmneziaStyle.color.borderBold
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property Item source
|
||||
property color color
|
||||
|
||||
Repeater {
|
||||
model: [[24, 1.0, 0], [24, 1.0, 0], [16, 1.0, 1]]
|
||||
|
||||
MultiEffect {
|
||||
required property var modelData
|
||||
|
||||
x: root.source ? root.source.x : 0
|
||||
y: root.source ? root.source.y : 0
|
||||
width: root.source ? root.source.width : 0
|
||||
height: root.source ? root.source.height : 0
|
||||
|
||||
source: root.source
|
||||
shadowEnabled: true
|
||||
shadowColor: root.color
|
||||
blurMax: modelData[0]
|
||||
shadowBlur: modelData[1]
|
||||
blurMultiplier: modelData[2]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
|
||||
import Style 1.0
|
||||
|
||||
MultiEffect {
|
||||
id: root
|
||||
|
||||
property real blur: 0
|
||||
property color color: AmneziaStyle.color.shadowLayer1
|
||||
property real horizontalOffset: 0
|
||||
property real verticalOffset: 0
|
||||
|
||||
readonly property var blurParams: {
|
||||
if (root.blur <= 3) {
|
||||
return [12, 0.75]
|
||||
}
|
||||
if (root.blur <= 4) {
|
||||
return [12, 1.0]
|
||||
}
|
||||
if (root.blur <= 6) {
|
||||
return [20, 1.0]
|
||||
}
|
||||
if (root.blur <= 8) {
|
||||
return [48, 0.5]
|
||||
}
|
||||
if (root.blur <= 15) {
|
||||
return [48, 0.75]
|
||||
}
|
||||
return [48, 1.0]
|
||||
}
|
||||
|
||||
shadowEnabled: true
|
||||
shadowColor: root.color
|
||||
shadowHorizontalOffset: root.horizontalOffset
|
||||
shadowVerticalOffset: root.verticalOffset
|
||||
blurMax: root.blurParams[0]
|
||||
shadowBlur: root.blurParams[1]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
import Style 1.0
|
||||
|
||||
@@ -81,33 +81,22 @@ Popup {
|
||||
visible: false
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: spreadShadowSource
|
||||
|
||||
x: -3
|
||||
y: -3 + 4
|
||||
width: parent.width + 6
|
||||
height: parent.height + 6
|
||||
|
||||
radius: 15
|
||||
DropShadow {
|
||||
anchors.fill: shadowSource
|
||||
source: shadowSource
|
||||
verticalOffset: 4
|
||||
radius: 8
|
||||
samples: 17
|
||||
spread: 0.3
|
||||
color: AmneziaStyle.color.shadowLayer1
|
||||
visible: false
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: spreadShadowSource
|
||||
|
||||
source: spreadShadowSource
|
||||
blurEnabled: true
|
||||
blurMax: 48
|
||||
blur: 0.5
|
||||
}
|
||||
|
||||
ShadowType {
|
||||
DropShadow {
|
||||
anchors.fill: shadowSource
|
||||
source: shadowSource
|
||||
verticalOffset: 1
|
||||
blur: 3
|
||||
radius: 3
|
||||
samples: 7
|
||||
color: AmneziaStyle.color.shadowLayer2
|
||||
}
|
||||
|
||||
@@ -127,7 +116,7 @@ Popup {
|
||||
property real offsetX: 0
|
||||
property real offsetY: 0
|
||||
|
||||
implicitHeight: Math.max(content.implicitHeight, actionButton.visible ? 40 : 0)
|
||||
implicitHeight: Math.max(content.implicitHeight, actionButton.visible ? 48 : 0)
|
||||
|
||||
transform: Translate {
|
||||
x: swipe.offsetX
|
||||
@@ -199,7 +188,7 @@ Popup {
|
||||
visible: root.actionText !== "" || root.closable
|
||||
|
||||
implicitWidth: actionLabel.implicitWidth + 28
|
||||
implicitHeight: 40
|
||||
implicitHeight: 48
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
|
||||
@@ -35,8 +35,7 @@ Item {
|
||||
|
||||
BodyTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.topMargin: lineCount === 1 ? Math.max(0, (switcher.implicitHeight - lineHeight) / 2) : 0
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
text: root.text
|
||||
color: AmneziaStyle.color.textPrimary
|
||||
@@ -53,7 +52,7 @@ Item {
|
||||
SwitchType {
|
||||
id: switcher
|
||||
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Accessible.name: root.text
|
||||
Accessible.description: root.descriptionText
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Style 1.0
|
||||
@@ -56,12 +57,16 @@ Switch {
|
||||
color: root.checked ? AmneziaStyle.color.bgStateLayerAccent : AmneziaStyle.color.bgStateLayer
|
||||
}
|
||||
|
||||
SelectionGlowType {
|
||||
anchors.fill: parent
|
||||
MultiEffect {
|
||||
anchors.fill: knob
|
||||
|
||||
source: knob
|
||||
visible: root.checked
|
||||
color: AmneziaStyle.color.accentLight
|
||||
shadowEnabled: true
|
||||
shadowColor: AmneziaStyle.color.accentLight
|
||||
shadowBlur: 0.6
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
||||
@@ -17,7 +17,6 @@ Item {
|
||||
property bool passwordToggle: false
|
||||
property bool passwordRevealed: false
|
||||
property bool clearable: false
|
||||
property string trailingIconSource
|
||||
|
||||
property string buttonText
|
||||
property string buttonImageSource
|
||||
@@ -30,8 +29,6 @@ Item {
|
||||
|
||||
readonly property bool hasError: root.errorText !== ""
|
||||
|
||||
signal trailingClicked()
|
||||
|
||||
implicitWidth: content.implicitWidth
|
||||
implicitHeight: content.implicitHeight
|
||||
|
||||
@@ -57,12 +54,12 @@ Item {
|
||||
id: textField
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: root.headerText !== "" ? 64 : 56
|
||||
Layout.preferredHeight: 64
|
||||
|
||||
leftPadding: 16
|
||||
rightPadding: actions.visible ? actions.width + 16 : 16
|
||||
topPadding: root.headerText !== "" ? 28 : 16
|
||||
bottomPadding: root.headerText !== "" ? 12 : 16
|
||||
topPadding: root.headerText !== "" ? 28 : 20
|
||||
bottomPadding: root.headerText !== "" ? 12 : 20
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
color: AmneziaStyle.color.textPrimary
|
||||
@@ -149,7 +146,7 @@ Item {
|
||||
anchors.rightMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
visible: root.clearable || root.passwordToggle || root.trailingIconSource !== ""
|
||||
visible: clearButton.visible || eyeButton.visible
|
||||
|
||||
IconButtonType {
|
||||
id: clearButton
|
||||
@@ -177,18 +174,6 @@ Item {
|
||||
|
||||
onClicked: root.passwordRevealed = !root.passwordRevealed
|
||||
}
|
||||
|
||||
IconButtonType {
|
||||
id: trailingButton
|
||||
|
||||
implicitWidth: 40
|
||||
implicitHeight: 40
|
||||
|
||||
visible: root.trailingIconSource !== ""
|
||||
icon.source: root.trailingIconSource
|
||||
|
||||
onClicked: root.trailingClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,13 @@ import Style 1.0
|
||||
Text {
|
||||
id: root
|
||||
|
||||
readonly property real halfLeading: (root.lineHeight - fontMetrics.height) / 2
|
||||
readonly property real overflow: Math.max(0, Math.ceil(fontMetrics.height) - root.lineHeight)
|
||||
readonly property real halfLeading: Math.max(0, (root.lineHeight - fontMetrics.height) / 2)
|
||||
|
||||
lineHeight: 14 + LanguageUiController.getLineHeightAppend()
|
||||
lineHeightMode: Text.FixedHeight
|
||||
|
||||
topPadding: root.halfLeading
|
||||
bottomPadding: -root.halfLeading - root.overflow
|
||||
bottomPadding: -root.halfLeading
|
||||
|
||||
color: AmneziaStyle.color.textTertiary
|
||||
font.pixelSize: 12
|
||||
|
||||
@@ -22,7 +22,6 @@ QtObject {
|
||||
readonly property color bgPanelAlt: '#52525B'
|
||||
readonly property color bgOverlay: '#8009090B'
|
||||
readonly property color bgStateLayer: '#14FAFAFA'
|
||||
readonly property color bgStateLayerPressed: '#1AFAFAFA'
|
||||
readonly property color bgStateLayerAccent: '#14FDBA74'
|
||||
readonly property color bgSurfaceInverse: '#E4E4E7'
|
||||
readonly property color bgSurfaceInverseAlt: '#D4D4D8'
|
||||
@@ -67,6 +66,5 @@ QtObject {
|
||||
|
||||
readonly property color shadowLayer1: '#26000000'
|
||||
readonly property color shadowLayer2: '#4D000000'
|
||||
readonly property color shadowGlow: '#FBB751'
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@ PageType {
|
||||
text: title
|
||||
trailingIconSource: "qrc:/images/controls/chevron-right.svg"
|
||||
icon.source: leftImagePath
|
||||
showIconBadge: modelData.hasBadge === true
|
||||
|
||||
onClicked: clickedHandler()
|
||||
}
|
||||
@@ -150,8 +149,7 @@ PageType {
|
||||
id: news
|
||||
|
||||
property string title: qsTr("News & Notifications")
|
||||
readonly property string leftImagePath: "qrc:/images/controls/news.svg"
|
||||
readonly property bool hasBadge: NewsModel.hasUnread && SettingsController.isNewsNotificationsEnabled()
|
||||
readonly property string leftImagePath: NewsModel.hasUnread && SettingsController.isNewsNotificationsEnabled() ? "qrc:/images/controls/news-unread.svg" : "qrc:/images/controls/news.svg"
|
||||
property bool isVisible: ServersUiController.hasServersFromGatewayApi
|
||||
readonly property var clickedHandler: function() {
|
||||
if (!ServersUiController.hasServersFromGatewayApi) {
|
||||
|
||||
@@ -75,12 +75,6 @@ PageType {
|
||||
return
|
||||
}
|
||||
|
||||
if (isCurrentDevice && LocalProxyController.isEnabled
|
||||
&& LocalProxyController.ownerId === ServersUiController.processedServerId) {
|
||||
PageController.showNotificationMessage(qsTr("Cannot unlink device while local proxy is running"))
|
||||
return
|
||||
}
|
||||
|
||||
var headerText = qsTr("Are you sure you want to unlink this device?")
|
||||
var descriptionText = qsTr("This will unlink the device from your subscription. You can reconnect it anytime by pressing \"Reload API config\" in subscription settings on device.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
|
||||
@@ -236,7 +236,6 @@ PageType {
|
||||
|
||||
visible: root.showRenewLink
|
||||
|
||||
size: ButtonType.Medium
|
||||
variant: ButtonType.Text
|
||||
leftImageSource: "qrc:/images/controls/refresh-cw.svg"
|
||||
text: qsTr("Renew subscription")
|
||||
@@ -268,30 +267,6 @@ PageType {
|
||||
|
||||
spacing: 0
|
||||
|
||||
ListItemType {
|
||||
id: localProxyItem
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
visible: LocalProxyController.isSupported && ServersUiController.processedServerIsPremium
|
||||
|
||||
text: qsTr("Local proxy")
|
||||
subtitle: {
|
||||
if (!LocalProxyController.isEnabled) {
|
||||
return qsTr("Off")
|
||||
}
|
||||
if (LocalProxyController.ownerId !== ServersUiController.processedServerId) {
|
||||
return qsTr("In use by another server")
|
||||
}
|
||||
return qsTr("Running: 127.0.0.1:%1").arg(LocalProxyController.activePort > 0 ? LocalProxyController.activePort : LocalProxyController.port)
|
||||
}
|
||||
trailingIconSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
onClicked: {
|
||||
PageController.goToPage(PageEnum.PageSettingsLocalProxy)
|
||||
}
|
||||
}
|
||||
|
||||
ListItemType {
|
||||
id: vpnKey
|
||||
|
||||
@@ -379,7 +354,6 @@ PageType {
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
size: ButtonType.Medium
|
||||
variant: ButtonType.Text
|
||||
destructive: true
|
||||
text: qsTr("Reload API config")
|
||||
@@ -413,7 +387,6 @@ PageType {
|
||||
|
||||
visible: root.isVisibleForAmneziaFree
|
||||
|
||||
size: ButtonType.Medium
|
||||
variant: ButtonType.Text
|
||||
destructive: true
|
||||
text: qsTr("Unlink this device")
|
||||
@@ -427,9 +400,6 @@ PageType {
|
||||
var yesButtonFunction = function() {
|
||||
if (ServersUiController.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) {
|
||||
PageController.showNotificationMessage(qsTr("Cannot unlink device during active connection"))
|
||||
} else if (LocalProxyController.isEnabled
|
||||
&& LocalProxyController.ownerId === ServersUiController.processedServerId) {
|
||||
PageController.showNotificationMessage(qsTr("Cannot unlink device while local proxy is running"))
|
||||
} else {
|
||||
PageController.showBusyIndicator(true)
|
||||
if (SubscriptionUiController.deactivateDevice(ServersUiController.processedServerId)) {
|
||||
@@ -452,7 +422,6 @@ PageType {
|
||||
Layout.topMargin: 12
|
||||
Layout.bottomMargin: 16 + PageController.safeAreaBottomMargin
|
||||
|
||||
size: ButtonType.Medium
|
||||
variant: ButtonType.Text
|
||||
destructive: true
|
||||
text: qsTr("Remove from application")
|
||||
@@ -465,9 +434,6 @@ PageType {
|
||||
var yesButtonFunction = function() {
|
||||
if (ServersUiController.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) {
|
||||
PageController.showNotificationMessage(qsTr("Cannot remove server during active connection"))
|
||||
} else if (LocalProxyController.isEnabled
|
||||
&& LocalProxyController.ownerId === ServersUiController.processedServerId) {
|
||||
PageController.showNotificationMessage(qsTr("Cannot remove server while local proxy is running"))
|
||||
} else {
|
||||
PageController.showBusyIndicator(true)
|
||||
SubscriptionUiController.removeServer(ServersUiController.processedServerId)
|
||||
|
||||
@@ -1,381 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
readonly property int localProxyPortMin: 1024
|
||||
readonly property int localProxyPortMax: 65535
|
||||
|
||||
readonly property bool isEnabledForThisServer: LocalProxyController.isEnabled
|
||||
&& LocalProxyController.ownerId === ServersUiController.processedServerId
|
||||
readonly property bool isOwnedByOtherServer: LocalProxyController.isEnabled
|
||||
&& LocalProxyController.ownerId !== ""
|
||||
&& LocalProxyController.ownerId !== ServersUiController.processedServerId
|
||||
|
||||
property int pendingStartRequestedPort: -1
|
||||
property bool pendingStartVpnWasActive: false
|
||||
property bool pendingEnableAfterVpnDisconnect: false
|
||||
property string pendingEnableServerId: ""
|
||||
property int pendingEnableRequestedPort: -1
|
||||
|
||||
function clearPendingEnableAfterVpnDisconnect() {
|
||||
root.pendingEnableAfterVpnDisconnect = false
|
||||
root.pendingEnableServerId = ""
|
||||
root.pendingEnableRequestedPort = -1
|
||||
}
|
||||
|
||||
function enableLocalProxyNow(serverId, requestedPort, vpnWasActive) {
|
||||
if (!LocalProxyController.enable(serverId, requestedPort)) {
|
||||
PageController.showNotificationMessage(qsTr("Failed to enable local proxy. Check the port (%1-%2)")
|
||||
.arg(root.localProxyPortMin)
|
||||
.arg(root.localProxyPortMax))
|
||||
return false
|
||||
}
|
||||
|
||||
root.pendingStartRequestedPort = requestedPort
|
||||
root.pendingStartVpnWasActive = vpnWasActive
|
||||
startSuccessToastTimer.restart()
|
||||
return true
|
||||
}
|
||||
|
||||
function portRangeErrorText() {
|
||||
return qsTr("Port must be between %1 and %2").arg(root.localProxyPortMin).arg(root.localProxyPortMax)
|
||||
}
|
||||
|
||||
function portBusyErrorText(port) {
|
||||
return qsTr("Port %1 is already in use on this device. Choose a different one").arg(port)
|
||||
}
|
||||
|
||||
function runningPortText() {
|
||||
const activePort = LocalProxyController.activePort
|
||||
return (activePort > 0 ? activePort : LocalProxyController.port).toString()
|
||||
}
|
||||
|
||||
function computePortErrorText() {
|
||||
const text = portField.textField.text.trim()
|
||||
if (text === "") {
|
||||
return qsTr("Port is required")
|
||||
}
|
||||
const value = parseInt(text)
|
||||
if (isNaN(value) || value < root.localProxyPortMin || value > root.localProxyPortMax) {
|
||||
return root.portRangeErrorText()
|
||||
}
|
||||
const isOwnActivePort = root.isEnabledForThisServer && value === LocalProxyController.activePort
|
||||
if (!isOwnActivePort && LocalProxyController.isPortBusy(value)) {
|
||||
return root.portBusyErrorText(value)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function handleLocalProxyToggle(checked) {
|
||||
if (checked) {
|
||||
if (!ServersUiController.processedServerIsPremium) {
|
||||
PageController.showNotificationMessage(qsTr("Local proxy is available only for Amnezia Premium"))
|
||||
return
|
||||
}
|
||||
const wasVpnActive = ConnectionController.isConnected || ConnectionController.isConnectionInProgress
|
||||
|
||||
let serverId = ServersUiController.processedServerId
|
||||
if (!serverId) {
|
||||
serverId = ServersUiController.defaultServerId
|
||||
}
|
||||
if (!serverId) {
|
||||
PageController.showNotificationMessage(qsTr("Unable to determine the current server"))
|
||||
return
|
||||
}
|
||||
|
||||
if (LocalProxyController.isEnabled
|
||||
&& LocalProxyController.ownerId
|
||||
&& LocalProxyController.ownerId !== serverId) {
|
||||
PageController.showNotificationMessage(qsTr("Local proxy is already enabled for another server"))
|
||||
return
|
||||
}
|
||||
|
||||
let requestedPort = LocalProxyController.port
|
||||
if (portField.textField.text.trim() !== "") {
|
||||
const validationError = root.computePortErrorText()
|
||||
if (validationError !== "") {
|
||||
portField.errorText = validationError
|
||||
return
|
||||
}
|
||||
requestedPort = portField.portValue()
|
||||
LocalProxyController.setPort(requestedPort)
|
||||
} else if (requestedPort < root.localProxyPortMin || requestedPort > root.localProxyPortMax) {
|
||||
portField.errorText = root.portRangeErrorText()
|
||||
return
|
||||
} else if (LocalProxyController.isPortBusy(requestedPort)
|
||||
&& LocalProxyController.isPortUserDefined()) {
|
||||
portField.errorText = root.portBusyErrorText(requestedPort)
|
||||
return
|
||||
}
|
||||
|
||||
portField.errorText = ""
|
||||
|
||||
if (wasVpnActive) {
|
||||
root.pendingEnableAfterVpnDisconnect = true
|
||||
root.pendingEnableServerId = serverId
|
||||
root.pendingEnableRequestedPort = requestedPort
|
||||
ConnectionController.closeConnection()
|
||||
return
|
||||
}
|
||||
|
||||
root.enableLocalProxyNow(serverId, requestedPort, false)
|
||||
} else {
|
||||
startSuccessToastTimer.stop()
|
||||
root.clearPendingEnableAfterVpnDisconnect()
|
||||
root.pendingStartRequestedPort = -1
|
||||
root.pendingStartVpnWasActive = false
|
||||
LocalProxyController.disable()
|
||||
PageController.showNotificationMessage(qsTr("Local proxy stopped"))
|
||||
}
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
id: flickable
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: bottomBar.top
|
||||
|
||||
contentHeight: content.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
spacing: 0
|
||||
|
||||
NavigationBarType {
|
||||
id: navigationBar
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
title: qsTr("Local proxy")
|
||||
description: qsTr("Used by specific apps (e.g., CensorTracker extension) via Amnezia Premium.\nOnly one can be active at a time: VPN or local proxy")
|
||||
|
||||
header.showSwitcher: ServersUiController.processedServerIsPremium
|
||||
header.switcher.checked: root.isEnabledForThisServer
|
||||
header.switcher.enabled: !root.isOwnedByOtherServer
|
||||
|
||||
Connections {
|
||||
target: navigationBar.header
|
||||
|
||||
function onSwitcherToggled(checked) {
|
||||
if (checked !== root.isEnabledForThisServer) {
|
||||
root.handleLocalProxyToggle(checked)
|
||||
}
|
||||
navigationBar.header.switcher.checked = Qt.binding(function() {
|
||||
return root.isEnabledForThisServer
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NotificationType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
|
||||
visible: root.isOwnedByOtherServer
|
||||
|
||||
variant: NotificationType.Warning
|
||||
text: qsTr("Local proxy is already running for \"%1\". Turn it off there to use it with this server")
|
||||
.arg(ServersUiController.serverName(LocalProxyController.ownerId))
|
||||
}
|
||||
|
||||
LinkButtonType {
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
size: LinkButtonType.Medium
|
||||
text: qsTr("Learn more")
|
||||
|
||||
onClicked: {
|
||||
const path = LanguageUiController.currentLanguageName === "Русский"
|
||||
? "ru/documentation/instructions/local-proxy"
|
||||
: "documentation/instructions/local-proxy"
|
||||
Qt.openUrlExternally(LanguageUiController.getCurrentDocsUrl(path))
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldType {
|
||||
id: portField
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
|
||||
headerText: qsTr("Address and port")
|
||||
placeholderText: LocalProxyController.port.toString()
|
||||
trailingIconSource: root.isEnabledForThisServer ? "qrc:/images/controls/copy.svg" : ""
|
||||
|
||||
textField.leftPadding: 16 + portPrefix.implicitWidth
|
||||
textField.inputMethodHints: Qt.ImhDigitsOnly | Qt.ImhNoPredictiveText
|
||||
textField.validator: RegularExpressionValidator {
|
||||
regularExpression: /^[0-9]{0,5}$/
|
||||
}
|
||||
|
||||
onTrailingClicked: {
|
||||
const portText = root.runningPortText()
|
||||
GC.copyToClipBoard("127.0.0.1:" + portText)
|
||||
PageController.showNotificationMessage(qsTr("Copied: 127.0.0.1:%1").arg(portText))
|
||||
}
|
||||
|
||||
function syncPortValue() {
|
||||
if (root.isEnabledForThisServer && LocalProxyController.activePort > 0) {
|
||||
textField.text = LocalProxyController.activePort.toString()
|
||||
} else if (LocalProxyController.isPortUserDefined()) {
|
||||
textField.text = LocalProxyController.port.toString()
|
||||
} else {
|
||||
textField.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
function portValue() {
|
||||
const value = parseInt(textField.text)
|
||||
return isNaN(value) ? -1 : value
|
||||
}
|
||||
|
||||
Component.onCompleted: syncPortValue()
|
||||
|
||||
Text {
|
||||
id: portPrefix
|
||||
|
||||
parent: portField.textField
|
||||
|
||||
x: 16
|
||||
y: portField.textField.topPadding
|
||||
+ (portField.textField.height - portField.textField.topPadding - portField.textField.bottomPadding - height) / 2
|
||||
z: 1
|
||||
|
||||
text: "127.0.0.1:"
|
||||
color: AmneziaStyle.color.textPrimary
|
||||
font: portField.textField.font
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BottomBarType {
|
||||
id: bottomBar
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
ButtonType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("Save")
|
||||
|
||||
onClicked: {
|
||||
const validationError = root.computePortErrorText()
|
||||
portField.errorText = validationError
|
||||
if (validationError !== "") {
|
||||
return
|
||||
}
|
||||
|
||||
const value = portField.portValue()
|
||||
if (!LocalProxyController.setPort(value)) {
|
||||
portField.errorText = root.portRangeErrorText()
|
||||
return
|
||||
}
|
||||
PageController.showNotificationMessage(qsTr("Port saved: %1").arg(value))
|
||||
portField.syncPortValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: startSuccessToastTimer
|
||||
interval: 250
|
||||
repeat: false
|
||||
running: false
|
||||
onTriggered: {
|
||||
if (!LocalProxyController.isEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const activePort = LocalProxyController.activePort
|
||||
const requestedPort = root.pendingStartRequestedPort
|
||||
const shownPort = activePort > 0 ? activePort : requestedPort
|
||||
|
||||
if (activePort > 0 && requestedPort > 0 && activePort !== requestedPort) {
|
||||
PageController.showNotificationMessage(qsTr("Port %1 is in use — selected free port %2")
|
||||
.arg(requestedPort)
|
||||
.arg(activePort))
|
||||
} else if (root.pendingStartVpnWasActive && shownPort > 0) {
|
||||
PageController.showNotificationMessage(qsTr("VPN off. Local proxy running: 127.0.0.1:%1")
|
||||
.arg(shownPort))
|
||||
} else if (shownPort > 0) {
|
||||
PageController.showNotificationMessage(qsTr("Local proxy running: 127.0.0.1:%1")
|
||||
.arg(shownPort))
|
||||
}
|
||||
|
||||
root.pendingStartRequestedPort = -1
|
||||
root.pendingStartVpnWasActive = false
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ConnectionController
|
||||
|
||||
function onConnectionStateChanged() {
|
||||
if (!root.pendingEnableAfterVpnDisconnect) {
|
||||
return
|
||||
}
|
||||
|
||||
if (ConnectionController.isConnected || ConnectionController.isConnectionInProgress) {
|
||||
return
|
||||
}
|
||||
|
||||
const serverId = root.pendingEnableServerId
|
||||
const requestedPort = root.pendingEnableRequestedPort
|
||||
root.clearPendingEnableAfterVpnDisconnect()
|
||||
|
||||
root.enableLocalProxyNow(serverId, requestedPort, true)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: LocalProxyController
|
||||
|
||||
function onStateChanged() {
|
||||
if (!portField.textField.activeFocus) {
|
||||
portField.syncPortValue()
|
||||
}
|
||||
}
|
||||
|
||||
function onStartFailed(message) {
|
||||
startSuccessToastTimer.stop()
|
||||
root.pendingStartRequestedPort = -1
|
||||
root.pendingStartVpnWasActive = false
|
||||
PageController.showNotificationMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ServersUiController
|
||||
|
||||
function onProcessedServerIdChanged() {
|
||||
if (!portField.textField.activeFocus) {
|
||||
portField.syncPortValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,7 +130,7 @@ PageType {
|
||||
visible: GC.isDesktop() && !IsMacOsNeBuild
|
||||
|
||||
text: qsTr("Don't have a server?")
|
||||
subtitle: qsTr("Amnezia Hosting - VPN servers without complicated settings")
|
||||
subtitle: qsTr("Amnezia hosting. VPN servers without complicated settings and headaches")
|
||||
|
||||
icon.source: "qrc:/images/controls/amnezia.svg"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
import PageEnum 1.0
|
||||
import UpdateState 1.0
|
||||
@@ -86,11 +86,8 @@ PageType {
|
||||
Layout.preferredHeight: 98
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: imageBlockMask
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1.0
|
||||
}
|
||||
|
||||
Image {
|
||||
@@ -111,7 +108,6 @@ PageType {
|
||||
anchors.fill: parent
|
||||
radius: 16
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
BackdropBlurType {
|
||||
|
||||
@@ -61,8 +61,6 @@ Window {
|
||||
maximumWidth: 600
|
||||
maximumHeight: 800
|
||||
|
||||
flags: Qt.platform.os === "ios" ? (Qt.Window | Qt.ExpandedClientAreaHint) : Qt.Window
|
||||
|
||||
color: AmneziaStyle.color.bgBase
|
||||
|
||||
onClosing: function(close) {
|
||||
@@ -308,8 +306,6 @@ Window {
|
||||
id: busyIndicator
|
||||
anchors.centerIn: parent
|
||||
z: 1
|
||||
|
||||
blurSource: pageStart
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user