Compare commits

...
Author SHA1 Message Date
Yaroslav Gurov 6d5f10947a feat(conan): get rid of recipes folder 2026-10-27 15:00:02 +01:00
NickVs2015 02838a94d5 fix: add new dep to test (#3301) 2026-10-20 10:22:09 +08:00
NickVs2015 9a1200d6f3 fix: build libagw with go 1.27.1 (#3303) 2026-10-20 10:21:53 +08:00
NickVs2015andvkamn ddde43635e feat: local proxy mode (#3166)
* fix: share double-stop guard via VpnProtocol base class

* feat: add local proxy core module

* feat: local proxy settings api

* feat: wire local proxy into app controllers

* feat: local proxy settings ui

* feat: arbitrate shared xray engine ownership via ipc tokens

* fix: remove connection type

* chore: add qthttpserver to desktop ci qt modules

* fix: local proxy port fallback and active port reporting

* fix: keep text field button aligned when error is shown

* fix: hide local proxy for free servers and guard device unlink

* fix: show local proxy state per server

* fix: anchor text field button to content

* fix: adapt local proxy pages to design

* fix: copy logic and port logic

* refactor: move local proxy files to controllers and utils

* refactor: merge local proxy into LocalProxyController

* refactor: reuse gateway config request and keep local proxy state in memory

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-10-18 21:25:04 +08:00
vkamn ffba1cb2a4 chore: minor ui fixes (#3295)
* fix: sync ds component sizes with updated figma

* fix: hover only on action buttons in list rows, standalone country row
2026-10-17 23:51:22 +08:00
vkamn 30ff3451c7 fix: prepare gateway request (#3291)
* fix: prepare the gateway request once instead of a blocking callback per attempt

* chore: libagw 1.0.8, gateway and failover timeouts 6s, health check 1s
2026-10-16 00:10:29 +08:00
vkamn db951e9928 chore: minor ui fixes (#3290)
* chore: change amnezia hosting description

* fix: ios safe areas and ds component alignment

- extend window into ios safe areas, pass insets to PageController
- use dark interface style and portrait-only orientation on iphone
- drop bottom inset while keyboard is open
- align controls with figma: title-only cards, caption height,
  selectable list items, nav bar spacing, chip hover, switch rows

* fix: news settings icon unread badge

* fix: align ds effects with figma

- three-layer selection glow for switch, radio and connect ring
- connect button disk with glow in all states
- snackbar elevate shadow spread, dropdown panel shadow
- backdrop blur for loading overlay container
- pressed state layer 0.1 for icon buttons, segments, list items, cards

* refactor: remove Qt5Compat.GraphicalEffects
2026-10-15 23:50:51 +08:00
132 changed files with 2139 additions and 3795 deletions

No files matched your search

+8 -8
View File
@@ -88,7 +88,7 @@ jobs:
host: 'linux'
target: 'desktop'
arch: 'linux_gcc_64'
modules: 'qtremoteobjects qt5compat qtshadertools'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
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'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
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'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
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'
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia qthttpserver qtwebsockets'
arch: 'clang_64'
dir: ${{ runner.temp }}
set-env: 'true'
@@ -1105,7 +1105,7 @@ jobs:
host: linux
target: desktop
arch: linux_gcc_64
modules: qtremoteobjects qt5compat qtshadertools
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
dir: ${{ runner.temp }}
set-env: true
aqtversion: '==3.3.0'
@@ -1181,7 +1181,7 @@ jobs:
host: linux
target: desktop
arch: linux_gcc_64
modules: qtremoteobjects qt5compat qtshadertools
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
dir: ${{ runner.temp }}
set-env: true
aqtversion: '==3.3.0'
@@ -1303,7 +1303,7 @@ jobs:
host: windows
target: desktop
arch: win64_msvc2022_64
modules: qtremoteobjects qt5compat qtshadertools
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
dir: ${{ runner.temp }}
set-env: true
@@ -1413,7 +1413,7 @@ jobs:
host: mac
target: desktop
arch: clang_64
modules: qtremoteobjects qt5compat qtshadertools
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
dir: ${{ runner.temp }}
set-env: true
+4
View File
@@ -11,3 +11,7 @@
path = client/3rd/qtgamepad
url = https://github.com/amnezia-vpn/qtgamepad.git
branch = 6.6
[submodule "cmake/cmake-conan"]
path = cmake/cmake-conan
url = https://github.com/amnezia-vpn/cmake-conan
branch = develop2
+3 -3
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PROJECT AmneziaVPN)
@@ -9,10 +9,10 @@ if(NOT AMNEZIAVPN_VERSION)
endif()
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
include(${CMAKE_SOURCE_DIR}/cmake/cmake-conan/conan_bootstrap.cmake)
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
${CMAKE_SOURCE_DIR}/cmake/platform_settings.cmake
${CMAKE_SOURCE_DIR}/cmake/recipes_bootstrap.cmake
${CMAKE_SOURCE_DIR}/cmake/conan_provider.cmake
${CMAKE_SOURCE_DIR}/cmake/cmake-conan/conan_provider.cmake
CACHE STRING "" FORCE)
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
+2 -2
View File
@@ -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)
set(PACKAGES ${PACKAGES} Widgets HttpServer)
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)
set(LIBS ${LIBS} Qt6::Widgets Qt6::HttpServer)
endif()
qt_standard_project_setup()
+5
View File
@@ -129,6 +129,11 @@ 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
+7
View File
@@ -44,6 +44,7 @@ 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
@@ -69,6 +70,9 @@ 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
@@ -130,6 +134,7 @@ 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
@@ -157,6 +162,8 @@ 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,6 +362,33 @@ 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,6 +45,8 @@ 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,6 +179,7 @@ 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()
@@ -212,6 +213,9 @@ 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);
+4
View File
@@ -24,6 +24,7 @@
#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"
@@ -44,6 +45,7 @@
#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"
@@ -175,6 +177,7 @@ private:
ImportController* m_importCoreController;
ExportUiController* m_exportUiController;
SettingsUiController* m_settingsUiController;
LocalProxyUiController* m_localProxyUiController;
LogsUiController* m_logsUiController;
ServersUiController* m_serversUiController;
IpSplitTunnelingUiController* m_ipSplitTunnelingUiController;
@@ -204,6 +207,7 @@ private:
ExportController* m_exportController;
ConnectionController* m_connectionController;
SettingsController* m_settingsController;
LocalProxyController* m_localProxyController;
ContainersModel* m_containersModel;
ContainersModel* m_defaultServerContainersModel;
@@ -1,5 +1,6 @@
#include "coreSignalHandlers.h"
#include <QCoreApplication>
#include <QTimer>
#include <QtConcurrent>
@@ -32,6 +33,7 @@
#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"
@@ -88,6 +90,7 @@ void CoreSignalHandlers::initAllHandlers()
initIosSettingsHandler();
initNotificationHandler();
initUpdateFoundHandler();
initLocalProxyHandler();
}
void CoreSignalHandlers::initErrorMessagesHandler()
@@ -494,3 +497,12 @@ 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,6 +43,7 @@ private:
void initIosSettingsHandler();
void initNotificationHandler();
void initUpdateFoundHandler();
void initLocalProxyHandler();
CoreController* m_coreController;
};
+52 -31
View File
@@ -5,11 +5,13 @@
#include <QFutureWatcher>
#include <QJsonArray>
#include <QJsonDocument>
#include <QMetaObject>
#include <QJsonObject>
#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"
@@ -32,6 +34,9 @@ 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
@@ -40,6 +45,11 @@ 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");
@@ -71,6 +81,7 @@ 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)
{
@@ -93,13 +104,11 @@ 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;
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);
m_client = agw_client_create(QJsonDocument(config).toJson(QJsonDocument::Compact).constData(), nullptr);
if (m_client == 0) {
qCritical() << "GatewayController: failed to create gateway client (missing key or endpoint?)";
return;
@@ -137,6 +146,7 @@ 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); });
}
@@ -190,44 +200,55 @@ amnezia::ErrorCode GatewayController::mapResultCode(const int code, const QByteA
}
}
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)
void GatewayController::prepareRequest()
{
#ifdef Q_OS_IOS
Q_UNUSED(host);
IosController::Instance()->requestInetAccess();
QThread::msleep(10);
#endif
#ifdef AMNEZIA_DESKTOP
if (m_isStrictKillSwitchEnabled) {
if (!m_isStrictKillSwitchEnabled) {
return;
}
QStringList ips;
for (const QString &host : requestHosts()) {
const QString ip = NetworkUtilities::getIPAddress(host);
if (!ip.isEmpty()) {
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;
}
});
ips.append(ip);
}
}
#else
Q_UNUSED(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";
}
});
#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) {
+4 -2
View File
@@ -7,6 +7,7 @@
#include <QObject>
#include <QPair>
#include <QString>
#include <QStringList>
#include "core/utils/errorCodes.h"
@@ -28,13 +29,14 @@ public:
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
private:
static void onBeforeRequest(const char *host, void *userData);
void handleBeforeRequest(const QString &host);
void prepareRequest();
QStringList requestHosts() const;
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;
@@ -0,0 +1,525 @@
#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
@@ -0,0 +1,84 @@
#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
+2 -1
View File
@@ -13,6 +13,7 @@
#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"
@@ -140,7 +141,7 @@ void UpdateController::checkForUpdates()
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(),
m_appSettingsRepository->isDevGatewayEnv(),
7000,
apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled(),
m_appSettingsRepository);
@@ -61,6 +61,10 @@ void OpenVpnProtocol::cleanupResources()
void OpenVpnProtocol::stop()
{
if (guardStop()) {
return;
}
qDebug() << "OpenVpnProtocol::stop()";
const bool wasActive = m_connectionState == Vpn::ConnectionState::Preparing
@@ -179,6 +183,8 @@ void OpenVpnProtocol::updateRouteGateway(QString line)
ErrorCode OpenVpnProtocol::start()
{
resetStopGuard();
cleanupResources();
if (!QFileInfo::exists(configPath())) {
+14
View File
@@ -27,6 +27,20 @@ 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;
+5
View File
@@ -89,6 +89,11 @@ protected:
void startTimeoutTimer();
void stopTimeoutTimer();
bool guardStop();
void resetStopGuard();
bool m_stopped = false;
Vpn::ConnectionState m_connectionState;
QString m_routeGateway;
+4 -1
View File
@@ -50,8 +50,10 @@ WireguardProtocol::~WireguardProtocol()
void WireguardProtocol::stop()
{
if (guardStop()) {
return;
}
stopMzImpl();
return;
}
ErrorCode WireguardProtocol::startMzImpl()
@@ -75,5 +77,6 @@ ErrorCode WireguardProtocol::stopMzImpl()
ErrorCode WireguardProtocol::start()
{
resetStopGuard();
return startMzImpl();
}
+12 -4
View File
@@ -67,6 +67,8 @@ XrayProtocol::~XrayProtocol()
ErrorCode XrayProtocol::start()
{
resetStopGuard();
qDebug() << "XrayProtocol::start()";
// Inject SOCKS5 auth into the inbound before starting xray.
@@ -92,11 +94,12 @@ ErrorCode XrayProtocol::start()
return IpcClient::withInterface(
[&](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStart = iface->xrayStart(xrayConfigStr);
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
auto xrayStart = iface->xrayStartOwned(xrayConfigStr);
if (!xrayStart.waitForFinished() || xrayStart.returnValue() == 0) {
qCritical() << "Failed to start xray";
return ErrorCode::XrayExecutableCrashed;
}
m_xrayToken = xrayStart.returnValue();
return startTun2Socks();
},
[]() { return ErrorCode::AmneziaServiceConnectionFailed; });
@@ -104,9 +107,13 @@ ErrorCode XrayProtocol::start()
void XrayProtocol::stop()
{
if (guardStop()) {
return;
}
qDebug() << "XrayProtocol::stop()";
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> iface) {
auto disableKillSwitch = iface->disableKillSwitch();
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
qWarning() << "Failed to disable killswitch";
@@ -123,9 +130,10 @@ void XrayProtocol::stop()
if (!deleteTun.waitForFinished() || !deleteTun.returnValue())
qWarning() << "Failed to delete tun";
auto xrayStop = iface->xrayStop();
auto xrayStop = iface->xrayStopOwned(m_xrayToken);
if (!xrayStop.waitForFinished() || !xrayStop.returnValue())
qWarning() << "Failed to stop xray";
m_xrayToken = 0;
});
if (m_tun2socksProcess) {
+4
View File
@@ -38,6 +38,10 @@ 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,3 +504,33 @@ 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,6 +105,13 @@ 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,6 +323,15 @@ 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,6 +31,8 @@ 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 -1
View File
@@ -4,7 +4,10 @@
namespace apiDefs
{
constexpr int requestTimeoutMsecs = 12 * 1000; // 12 secs
constexpr int requestTimeoutMsecs = 6 * 1000;
constexpr int proxyRequestTimeoutMsecs = 6 * 1000;
constexpr int proxyStorageTimeoutMsecs = 6 * 1000;
constexpr int proxyHealthTimeoutMsecs = 1000;
} // namespace apiDefs
@@ -0,0 +1,15 @@
#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
@@ -0,0 +1,51 @@
#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;
}
+13
View File
@@ -0,0 +1,13 @@
#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
@@ -0,0 +1,47 @@
#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;
}
@@ -0,0 +1,15 @@
#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
-14
View File
@@ -1,14 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 982 B

+3 -8
View File
@@ -1,8 +1,3 @@
<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>
<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>

Before

Width:  |  Height:  |  Size: 410 B

After

Width:  |  Height:  |  Size: 1.3 KiB

-1
View File
@@ -48,7 +48,6 @@
<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>
+1 -2
View File
@@ -57,7 +57,6 @@
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
@@ -68,7 +67,7 @@
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<string>Dark</string>
<key>com.wireguard.ios.app_group_id</key>
<string>${BUILD_IOS_GROUP_IDENTIFIER}</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
+2 -2
View File
@@ -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 and headaches</source>
<translation>استضافة Amnezia. خوادم VPN بلا إعدادات معقّدة ولا صداع</translation>
<source>Amnezia Hosting - VPN servers without complicated settings</source>
<translation>Amnezia Hosting - خوادم VPN بلا إعدادات معقّدة</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
+2 -2
View File
@@ -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 and headaches</source>
<translation>Hosting de Amnezia. Servidores VPN sin ajustes complicados ni quebraderos de cabeza</translation>
<source>Amnezia Hosting - VPN servers without complicated settings</source>
<translation>Amnezia Hosting - servidores VPN sin ajustes complicados</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
+2 -2
View File
@@ -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 and headaches</source>
<translation>هاستینگ Amnezia. سرورهای VPN بدون تنظیمات پیچیده و دردسر</translation>
<source>Amnezia Hosting - VPN servers without complicated settings</source>
<translation>Amnezia Hosting - سرورهای VPN بدون تنظیمات پیچیده</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
+2 -2
View File
@@ -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 and headaches</source>
<translation>Hébergement Amnezia. Des serveurs VPN sans réglages compliqués ni prise de tête</translation>
<source>Amnezia Hosting - VPN servers without complicated settings</source>
<translation>Amnezia Hosting - des serveurs VPN sans réglages compliqués</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
+2 -2
View File
@@ -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 and headaches</source>
<translation>Amnezia होस्टिंग। जटिल सेटिंग्स और झंझट के बिना VPN सर्वर</translation>
<source>Amnezia Hosting - VPN servers without complicated settings</source>
<translation>Amnezia Hosting - जटिल सेटिंग्स के बिना VPN सर्वर</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
+2 -2
View File
@@ -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 and headaches</source>
<translation>Amnezia 호스팅. 복잡한 설정도 번거로움도 없는 VPN 서버</translation>
<source>Amnezia Hosting - VPN servers without complicated settings</source>
<translation>Amnezia Hosting - 복잡한 설정 없는 VPN 서버</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
+2 -2
View File
@@ -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 and headaches</source>
<translation>Amnezia hosting။ ရှုပ်ထွေးသော ဆက်တင်များနှင့် ခေါင်းကိုက်စရာများ မလိုသော VPN ဆာဗာများ</translation>
<source>Amnezia Hosting - VPN servers without complicated settings</source>
<translation>Amnezia Hosting - ရှုပ်ထွေးသော ဆက်တင်များ မလိုသော VPN ဆာဗာများ</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
+2 -2
View File
@@ -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 and headaches</source>
<translation>Хостинг Amnezia. VPN-серверы без сложных настроек и головной боли</translation>
<source>Amnezia Hosting - VPN servers without complicated settings</source>
<translation>Amnezia Hosting - VPN-серверы без сложных настроек</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
+2 -2
View File
@@ -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 and headaches</source>
<translation>Amnezia hosting เซิร์ฟเวอร์ VPN ที่ไม่ต้องตั้งค่ายากและไม่ต้องกังวล</translation>
<source>Amnezia Hosting - VPN servers without complicated settings</source>
<translation>Amnezia Hosting - เซิร์ฟเวอร์ VPN ที่ไม่ต้องตั้งค่ายุ่งยาก</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
+2 -2
View File
@@ -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 and headaches</source>
<translation>Хостинг Amnezia. VPN-сервери без складних налаштувань і зайвого клопоту</translation>
<source>Amnezia Hosting - VPN servers without complicated settings</source>
<translation>Amnezia Hosting - VPN-сервери без складних налаштувань</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
+2 -2
View File
@@ -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 and headaches</source>
<translation>Amnezia ہوسٹنگ۔ پیچیدہ ترتیبات اور جھنجھٹ کے بغیر VPN سرورز</translation>
<source>Amnezia Hosting - VPN servers without complicated settings</source>
<translation>Amnezia Hosting - پیچیدہ ترتیبات کے بغیر VPN سرورز</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
+2 -2
View File
@@ -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 and headaches</source>
<translation>Amnezia 托管服务。无需复杂设置、省心的 VPN 服务器</translation>
<source>Amnezia Hosting - VPN servers without complicated settings</source>
<translation>Amnezia Hosting - 无需复杂设置的 VPN 服务器</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="147"/>
@@ -0,0 +1,58 @@
#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();
}
@@ -0,0 +1,41 @@
#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
+24 -2
View File
@@ -9,6 +9,7 @@
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(MACOS_NE)
#include <QGuiApplication>
#include <QInputMethod>
#else
#include <QApplication>
#endif
@@ -41,6 +42,10 @@ 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);
@@ -191,7 +196,7 @@ int PageController::getSafeAreaTopMargin()
return result;
}
#endif
return 0;
return m_safeAreaMargins.top();
}
int PageController::getSafeAreaBottomMargin()
@@ -207,7 +212,24 @@ int PageController::getSafeAreaBottomMargin()
return result;
}
#endif
return 0;
#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();
}
}
int PageController::getImeHeight()
@@ -1,6 +1,7 @@
#ifndef PAGECONTROLLER_H
#define PAGECONTROLLER_H
#include <QMargins>
#include <QObject>
#include <QQmlEngine>
@@ -48,6 +49,8 @@ public slots:
int getSafeAreaBottomMargin();
int getImeHeight();
void setSafeAreaMargins(const QMargins &margins);
private slots:
void onShowErrorMessage(amnezia::ErrorCode errorCode);
@@ -98,6 +101,8 @@ private:
mutable int m_cachedNavigationBarHeight = -1;
mutable bool m_cachedEdgeToEdgeEnabled = false;
mutable bool m_edgeToEdgeCached = false;
QMargins m_safeAreaMargins;
int m_imeHeight = 0;
};
+38 -23
View File
@@ -1,6 +1,5 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Shapes
import Style 1.0
@@ -43,27 +42,47 @@ Button {
implicitWidth: 190
implicitHeight: 190
Shape {
Item {
anchors.fill: parent
visible: root.inProgress
opacity: 0.1
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: AmneziaStyle.color.transparent
strokeColor: AmneziaStyle.color.borderEmphasis
strokeWidth: 1
Item {
id: diskSource
PathAngleArc {
centerX: root.width / 2
centerY: root.height / 2
radiusX: d.ringRadius
radiusY: d.ringRadius
startAngle: 0
sweepAngle: 360
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
}
}
}
}
ShadowType {
anchors.fill: diskSource
z: -1
source: diskSource
color: AmneziaStyle.color.shadowGlow
blur: 6
}
}
Item {
@@ -98,16 +117,12 @@ Button {
}
}
MultiEffect {
anchors.fill: ringSource
SelectionGlowType {
anchors.fill: parent
z: -1
source: ringSource
shadowEnabled: true
shadowColor: d.ringColor
shadowBlur: 0.6
shadowHorizontalOffset: 0
shadowVerticalOffset: 0
blurMax: 16
color: d.ringColor
}
RotationAnimator on rotation {
@@ -24,6 +24,7 @@ EmptyStateType {
ButtonType {
visible: root.isSearchResult || root.categoryName !== ""
size: ButtonType.Medium
variant: ButtonType.Text
text: qsTr("Show all")
+105 -9
View File
@@ -1,27 +1,123 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Style 1.0
import "../Controls2"
ListItemType {
ItemDelegate {
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
leadingSlotWidth: 24
icon.source: root.countryFlagCode !== "" ? "qrc:/countriesFlags/images/flagKit/" + root.countryFlagCode + ".svg" : ""
icon.width: 21
icon.height: 15
icon.color: AmneziaStyle.color.transparent
spacing: 12
FavoriteButton {
favorite: root.favorite
hoverEnabled: true
focusPolicy: Qt.TabFocus
opacity: enabled ? 1.0 : 0.4
onClicked: root.favoriteToggled()
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()
}
}
}
+1 -1
View File
@@ -56,7 +56,7 @@ ItemDelegate {
anchors.fill: parent
radius: parent.radius
color: AmneziaStyle.color.bgStateLayer
color: root.down ? AmneziaStyle.color.bgStateLayerPressed : AmneziaStyle.color.bgStateLayer
opacity: root.enabled && (root.down || root.hovered) ? 1 : 0
Behavior on opacity {
@@ -60,6 +60,7 @@ SheetType {
Layout.rightMargin: 16
Layout.leftMargin: 16
size: ButtonType.Medium
variant: ButtonType.Text
text: qsTr("Support")
+12 -4
View File
@@ -1,5 +1,5 @@
import QtQuick
import Qt5Compat.GraphicalEffects
import QtQuick.Effects
import Style 1.0
@@ -17,8 +17,11 @@ Item {
anchors.fill: parent
layer.enabled: true
layer.effect: OpacityMask {
layer.effect: MultiEffect {
maskEnabled: true
maskSource: backdropMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
}
Item {
@@ -43,10 +46,14 @@ Item {
visible: false
}
FastBlur {
MultiEffect {
anchors.fill: parent
source: backdrop
radius: root.blurRadius
autoPaddingEnabled: false
blurEnabled: true
blurMax: 64
blur: root.blurRadius <= 20 ? 0.73 : 0.9
blurMultiplier: root.blurRadius <= 20 ? 0 : 0.5
}
}
}
@@ -57,6 +64,7 @@ Item {
anchors.fill: parent
radius: root.radius
visible: false
layer.enabled: true
}
Rectangle {
+48 -42
View File
@@ -59,7 +59,7 @@ AbstractButton {
anchors.fill: parent
radius: 16
color: !root.muted && root.enabled && (root.hovered || root.down) ? AmneziaStyle.color.bgStateLayer
color: !root.muted && root.enabled && (root.hovered || root.down) ? (root.down ? AmneziaStyle.color.bgStateLayerPressed : AmneziaStyle.color.bgStateLayer)
: AmneziaStyle.color.transparent
Behavior on color {
@@ -95,67 +95,73 @@ AbstractButton {
}
contentItem: RowLayout {
spacing: root.spacing
spacing: 0
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 {
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
Layout.alignment: root.subtitle !== "" || root.footnote !== "" ? Qt.AlignTop : Qt.AlignVCenter
spacing: 4
spacing: root.spacing
H3TextType {
Layout.fillWidth: true
IconImage {
Layout.alignment: Qt.AlignTop
Layout.preferredWidth: root.icon.width
Layout.preferredHeight: root.icon.height
visible: text !== ""
text: root.text
color: root.muted ? AmneziaStyle.color.textTertiary : AmneziaStyle.color.textPrimary
visible: root.icon.source.toString() !== ""
source: root.icon.source
sourceSize: Qt.size(root.icon.width, root.icon.height)
color: root.icon.color
}
SmallTextType {
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
visible: text !== "" && !root.muted
text: root.subtitle
textFormat: Text.RichText
spacing: 4
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
H3TextType {
Layout.fillWidth: true
visible: text !== ""
text: root.text
color: root.muted ? AmneziaStyle.color.textTertiary : AmneziaStyle.color.textPrimary
}
}
CaptionTextType {
Layout.fillWidth: true
SmallTextType {
Layout.fillWidth: true
visible: text !== "" && root.muted
text: root.subtitle
textFormat: Text.RichText
color: AmneziaStyle.color.textQuaternary
}
visible: text !== "" && !root.muted
text: root.subtitle
textFormat: Text.RichText
SmallTextType {
Layout.fillWidth: true
Layout.topMargin: 8
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
}
}
visible: text !== ""
text: root.footnote
color: root.muted ? AmneziaStyle.color.textQuaternary : AmneziaStyle.color.textTertiary
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
}
}
}
IconButtonType {
Layout.alignment: Qt.AlignTop
Layout.leftMargin: -root.spacing
icon.source: "qrc:/images/controls/chevron-right.svg"
icon.color: AmneziaStyle.color.iconDefault
+4 -3
View File
@@ -34,13 +34,14 @@ 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: 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 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 color backgroundColor: {
switch (root.variant) {
+3 -3
View File
@@ -22,14 +22,14 @@ Button {
radius: 16
color: root.checked ? AmneziaStyle.color.bgSelected : AmneziaStyle.color.bgSurfaceAlt
border.width: root.checked ? 1 : 0
border.color: AmneziaStyle.color.accentLight
border.width: root.checked || (root.hovered && !root.down) ? 1 : 0
border.color: root.checked ? AmneziaStyle.color.accentLight : AmneziaStyle.color.borderBold
Rectangle {
anchors.fill: parent
radius: parent.radius
visible: root.down || root.hovered
visible: root.down
color: AmneziaStyle.color.bgStateLayer
}
+4 -1
View File
@@ -46,7 +46,10 @@ Button {
if (root.filled) {
return root.hovered || root.down ? AmneziaStyle.color.bgSurfaceInverseAlt : AmneziaStyle.color.bgSurfaceInverse
}
return root.hovered || root.down ? AmneziaStyle.color.bgStateLayer : AmneziaStyle.color.transparent
if (root.down) {
return AmneziaStyle.color.bgStateLayerPressed
}
return root.hovered ? AmneziaStyle.color.bgStateLayer : AmneziaStyle.color.transparent
}
border.width: !root.filled && root.down ? 1 : 0
border.color: AmneziaStyle.color.borderMedium
+1 -1
View File
@@ -13,7 +13,7 @@ Button {
property string imageSource
implicitWidth: root.imageSource !== "" ? 48 : Math.max(83, implicitContentWidth + leftPadding + rightPadding)
implicitHeight: 64
implicitHeight: 56
horizontalPadding: 12
verticalPadding: 8
@@ -7,35 +7,23 @@ 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: 72
implicitHeight: 56
HoverHandler {
cursorShape: Qt.PointingHandCursor
enabled: root.active
}
Rectangle {
Item {
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,10 +14,11 @@ 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: 4
spacing: 0
SmallTextType {
Layout.fillWidth: true
@@ -68,5 +69,6 @@ ColumnLayout {
text: root.subtitle
color: root.subtitleColor
opacity: root.textOpacity
lineHeight: root.subtitleLineHeight + LanguageUiController.getLineHeightAppend()
}
}
@@ -0,0 +1,25 @@
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 }
}
}
+27 -8
View File
@@ -22,14 +22,19 @@ 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(72, implicitContentHeight + topPadding + bottomPadding)
implicitHeight: Math.max(root.dense ? 72 : 56, implicitContentHeight + topPadding + bottomPadding)
padding: 12
verticalPadding: 4
spacing: 12
hoverEnabled: root.interactive
@@ -48,7 +53,6 @@ ItemDelegate {
background: ListItemBackgroundType {
control: root.interactive ? root : null
selected: root.highlighted
showSeparator: root.showSeparator
separatorColor: root.separatorColor
}
@@ -70,12 +74,22 @@ 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
@@ -91,12 +105,6 @@ 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
}
}
@@ -117,6 +125,10 @@ ItemDelegate {
spacing: 0
visible: root.trailingContentVisible
HoverHandler {
id: trailingHover
}
}
Item {
@@ -125,6 +137,13 @@ ItemDelegate {
visible: root.trailingIconSource !== ""
ListItemStateLayerType {
anchors.centerIn: parent
hovered: root.rowHovered
pressed: root.rowPressed
}
IconImage {
anchors.centerIn: parent
width: 24
@@ -19,9 +19,14 @@ Popup {
color: AmneziaStyle.color.bgOverlay
}
background: Rectangle {
property Item blurSource
background: BackdropBlurType {
sourceItem: root.blurSource
blurRadius: 20
radius: 16
color: AmneziaStyle.color.bgOverlay
tintColor: Qt.alpha(AmneziaStyle.color.bgOverlay, 0.75)
borderColor: AmneziaStyle.color.transparent
}
contentItem: SpinnerType {
@@ -59,7 +59,7 @@ Item {
y: 4
visible: root.toolbarVisible
spacing: 8
spacing: 16
}
}
+27 -5
View File
@@ -57,11 +57,33 @@ Popup {
}
}
background: Rectangle {
radius: 14
color: AmneziaStyle.color.bgSurface
border.width: 1
border.color: AmneziaStyle.color.borderSoft
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
}
}
contentItem: ColumnLayout {
@@ -1,5 +1,4 @@
import QtQuick
import QtQuick.Effects
import Style 1.0
@@ -25,15 +24,12 @@ Item {
}
}
MultiEffect {
anchors.fill: dot
SelectionGlowType {
anchors.fill: parent
source: dot
visible: root.checked
shadowEnabled: true
shadowColor: AmneziaStyle.color.accentLight
shadowBlur: 0.6
shadowHorizontalOffset: 0
shadowVerticalOffset: 0
color: AmneziaStyle.color.accentLight
}
Rectangle {
+18 -7
View File
@@ -18,11 +18,15 @@ 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(72, implicitContentHeight + topPadding + bottomPadding)
implicitHeight: Math.max(56, implicitContentHeight + topPadding + bottomPadding)
padding: 12
verticalPadding: 4
leftPadding: 12 + 48 + spacing
spacing: 12
@@ -52,6 +56,13 @@ RadioDelegate {
width: 48
height: 48
ListItemStateLayerType {
anchors.centerIn: parent
hovered: root.rowHovered
pressed: root.rowPressed
}
RadioIndicatorType {
anchors.centerIn: parent
visible: root.icon.source.toString() === ""
@@ -76,6 +87,8 @@ RadioDelegate {
ListItemLabelType {
Layout.fillWidth: true
Layout.leftMargin: 4
Layout.topMargin: 10
Layout.bottomMargin: 10
title: root.text
subtitle: root.subtitle
@@ -86,12 +99,6 @@ 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
}
}
@@ -112,6 +119,10 @@ RadioDelegate {
spacing: 0
visible: children.length > 0
HoverHandler {
id: trailingHover
}
}
}
}
+2 -1
View File
@@ -11,7 +11,7 @@ TextField {
implicitHeight: 44
leftPadding: 52
rightPadding: clearButton.visible ? clearButton.width + 4 : 16
rightPadding: clearButton.visible ? 52 : 16
topPadding: 10
bottomPadding: 10
verticalAlignment: TextInput.AlignVCenter
@@ -111,6 +111,7 @@ TextField {
id: clearButton
anchors.right: parent.right
anchors.rightMargin: 16 - (width - 24) / 2
anchors.verticalCenter: parent.verticalCenter
implicitWidth: Qt.platform.os === "android" ? 48 : 44
+1 -1
View File
@@ -35,7 +35,7 @@ RadioButton {
anchors.fill: parent
radius: parent.radius
color: AmneziaStyle.color.bgStateLayer
color: root.down ? AmneziaStyle.color.bgStateLayerPressed : AmneziaStyle.color.bgStateLayer
opacity: !root.checked && root.enabled && (root.hovered || root.down) ? 1 : 0
Behavior on opacity {
@@ -12,10 +12,11 @@ RadioDelegate {
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(72, implicitContentHeight + topPadding + bottomPadding)
implicitHeight: Math.max(56, implicitContentHeight + topPadding + bottomPadding)
padding: 12
leftPadding: 16
verticalPadding: 14
hoverEnabled: true
focusPolicy: Qt.StrongFocus
@@ -31,13 +32,13 @@ RadioDelegate {
background: Item {
implicitWidth: 358
implicitHeight: 72
implicitHeight: 56
Rectangle {
anchors.fill: parent
radius: 16
color: root.enabled && !root.checked && (root.hovered || root.down) ? AmneziaStyle.color.bgStateLayer
color: root.enabled && !root.checked && (root.hovered || root.down) ? (root.down ? AmneziaStyle.color.bgStateLayerPressed : AmneziaStyle.color.bgStateLayer)
: AmneziaStyle.color.transparent
border.width: root.checked || root.down ? 1 : 0
border.color: root.checked ? AmneziaStyle.color.accentLight : AmneziaStyle.color.borderBold
@@ -0,0 +1,29 @@
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]
}
}
}
+39
View File
@@ -0,0 +1,39 @@
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]
}
+24 -13
View File
@@ -1,7 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import QtQuick.Effects
import Style 1.0
@@ -81,22 +81,33 @@ Popup {
visible: false
}
DropShadow {
anchors.fill: shadowSource
source: shadowSource
verticalOffset: 4
radius: 8
samples: 17
spread: 0.3
Rectangle {
id: spreadShadowSource
x: -3
y: -3 + 4
width: parent.width + 6
height: parent.height + 6
radius: 15
color: AmneziaStyle.color.shadowLayer1
visible: false
}
DropShadow {
MultiEffect {
anchors.fill: spreadShadowSource
source: spreadShadowSource
blurEnabled: true
blurMax: 48
blur: 0.5
}
ShadowType {
anchors.fill: shadowSource
source: shadowSource
verticalOffset: 1
radius: 3
samples: 7
blur: 3
color: AmneziaStyle.color.shadowLayer2
}
@@ -116,7 +127,7 @@ Popup {
property real offsetX: 0
property real offsetY: 0
implicitHeight: Math.max(content.implicitHeight, actionButton.visible ? 48 : 0)
implicitHeight: Math.max(content.implicitHeight, actionButton.visible ? 40 : 0)
transform: Translate {
x: swipe.offsetX
@@ -188,7 +199,7 @@ Popup {
visible: root.actionText !== "" || root.closable
implicitWidth: actionLabel.implicitWidth + 28
implicitHeight: 48
implicitHeight: 40
hoverEnabled: true
+3 -2
View File
@@ -35,7 +35,8 @@ Item {
BodyTextType {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Layout.alignment: Qt.AlignTop
Layout.topMargin: lineCount === 1 ? Math.max(0, (switcher.implicitHeight - lineHeight) / 2) : 0
text: root.text
color: AmneziaStyle.color.textPrimary
@@ -52,7 +53,7 @@ Item {
SwitchType {
id: switcher
Layout.alignment: Qt.AlignVCenter
Layout.alignment: Qt.AlignTop
Accessible.name: root.text
Accessible.description: root.descriptionText
+3 -8
View File
@@ -1,6 +1,5 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Style 1.0
@@ -57,16 +56,12 @@ Switch {
color: root.checked ? AmneziaStyle.color.bgStateLayerAccent : AmneziaStyle.color.bgStateLayer
}
MultiEffect {
anchors.fill: knob
SelectionGlowType {
anchors.fill: parent
source: knob
visible: root.checked
shadowEnabled: true
shadowColor: AmneziaStyle.color.accentLight
shadowBlur: 0.6
shadowHorizontalOffset: 0
shadowVerticalOffset: 0
color: AmneziaStyle.color.accentLight
}
Rectangle {
+19 -4
View File
@@ -17,6 +17,7 @@ Item {
property bool passwordToggle: false
property bool passwordRevealed: false
property bool clearable: false
property string trailingIconSource
property string buttonText
property string buttonImageSource
@@ -29,6 +30,8 @@ Item {
readonly property bool hasError: root.errorText !== ""
signal trailingClicked()
implicitWidth: content.implicitWidth
implicitHeight: content.implicitHeight
@@ -54,12 +57,12 @@ Item {
id: textField
Layout.fillWidth: true
Layout.preferredHeight: 64
Layout.preferredHeight: root.headerText !== "" ? 64 : 56
leftPadding: 16
rightPadding: actions.visible ? actions.width + 16 : 16
topPadding: root.headerText !== "" ? 28 : 20
bottomPadding: root.headerText !== "" ? 12 : 20
topPadding: root.headerText !== "" ? 28 : 16
bottomPadding: root.headerText !== "" ? 12 : 16
verticalAlignment: TextInput.AlignVCenter
color: AmneziaStyle.color.textPrimary
@@ -146,7 +149,7 @@ Item {
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
visible: clearButton.visible || eyeButton.visible
visible: root.clearable || root.passwordToggle || root.trailingIconSource !== ""
IconButtonType {
id: clearButton
@@ -174,6 +177,18 @@ Item {
onClicked: root.passwordRevealed = !root.passwordRevealed
}
IconButtonType {
id: trailingButton
implicitWidth: 40
implicitHeight: 40
visible: root.trailingIconSource !== ""
icon.source: root.trailingIconSource
onClicked: root.trailingClicked()
}
}
}
@@ -5,13 +5,14 @@ import Style 1.0
Text {
id: root
readonly property real halfLeading: Math.max(0, (root.lineHeight - fontMetrics.height) / 2)
readonly property real halfLeading: (root.lineHeight - fontMetrics.height) / 2
readonly property real overflow: Math.max(0, Math.ceil(fontMetrics.height) - root.lineHeight)
lineHeight: 14 + LanguageUiController.getLineHeightAppend()
lineHeightMode: Text.FixedHeight
topPadding: root.halfLeading
bottomPadding: -root.halfLeading
bottomPadding: -root.halfLeading - root.overflow
color: AmneziaStyle.color.textTertiary
font.pixelSize: 12
@@ -22,6 +22,7 @@ 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'
@@ -66,5 +67,6 @@ QtObject {
readonly property color shadowLayer1: '#26000000'
readonly property color shadowLayer2: '#4D000000'
readonly property color shadowGlow: '#FBB751'
}
}
-1
View File
@@ -1,7 +1,6 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import SortFilterProxyModel 0.2
+3 -1
View File
@@ -67,6 +67,7 @@ PageType {
text: title
trailingIconSource: "qrc:/images/controls/chevron-right.svg"
icon.source: leftImagePath
showIconBadge: modelData.hasBadge === true
onClicked: clickedHandler()
}
@@ -149,7 +150,8 @@ PageType {
id: news
property string title: qsTr("News & Notifications")
readonly property string leftImagePath: NewsModel.hasUnread && SettingsController.isNewsNotificationsEnabled() ? "qrc:/images/controls/news-unread.svg" : "qrc:/images/controls/news.svg"
readonly property string leftImagePath: "qrc:/images/controls/news.svg"
readonly property bool hasBadge: NewsModel.hasUnread && SettingsController.isNewsNotificationsEnabled()
property bool isVisible: ServersUiController.hasServersFromGatewayApi
readonly property var clickedHandler: function() {
if (!ServersUiController.hasServersFromGatewayApi) {
@@ -75,6 +75,12 @@ 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,6 +236,7 @@ PageType {
visible: root.showRenewLink
size: ButtonType.Medium
variant: ButtonType.Text
leftImageSource: "qrc:/images/controls/refresh-cw.svg"
text: qsTr("Renew subscription")
@@ -267,6 +268,30 @@ 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
@@ -354,6 +379,7 @@ PageType {
Layout.alignment: Qt.AlignHCenter
size: ButtonType.Medium
variant: ButtonType.Text
destructive: true
text: qsTr("Reload API config")
@@ -387,6 +413,7 @@ PageType {
visible: root.isVisibleForAmneziaFree
size: ButtonType.Medium
variant: ButtonType.Text
destructive: true
text: qsTr("Unlink this device")
@@ -400,6 +427,9 @@ 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)) {
@@ -422,6 +452,7 @@ PageType {
Layout.topMargin: 12
Layout.bottomMargin: 16 + PageController.safeAreaBottomMargin
size: ButtonType.Medium
variant: ButtonType.Text
destructive: true
text: qsTr("Remove from application")
@@ -434,6 +465,9 @@ 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)
@@ -0,0 +1,381 @@
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 and headaches")
subtitle: qsTr("Amnezia Hosting - VPN servers without complicated settings")
icon.source: "qrc:/images/controls/amnezia.svg"
+6 -2
View File
@@ -1,7 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import QtQuick.Effects
import PageEnum 1.0
import UpdateState 1.0
@@ -86,8 +86,11 @@ PageType {
Layout.preferredHeight: 98
layer.enabled: true
layer.effect: OpacityMask {
layer.effect: MultiEffect {
maskEnabled: true
maskSource: imageBlockMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
}
Image {
@@ -108,6 +111,7 @@ PageType {
anchors.fill: parent
radius: 16
visible: false
layer.enabled: true
}
BackdropBlurType {
+4
View File
@@ -61,6 +61,8 @@ Window {
maximumWidth: 600
maximumHeight: 800
flags: Qt.platform.os === "ios" ? (Qt.Window | Qt.ExpandedClientAreaHint) : Qt.Window
color: AmneziaStyle.color.bgBase
onClosing: function(close) {
@@ -306,6 +308,8 @@ Window {
id: busyIndicator
anchors.centerIn: parent
z: 1
blurSource: pageStart
}
}
+4
View File
@@ -40,6 +40,7 @@
<file>Controls2/ButtonType.qml</file>
<file>Controls2/EmptyStateType.qml</file>
<file>Controls2/ListItemBackgroundType.qml</file>
<file>Controls2/ListItemStateLayerType.qml</file>
<file>Controls2/ListItemLabelType.qml</file>
<file>Controls2/LoadingOverlayType.qml</file>
<file>Controls2/LoaderRingType.qml</file>
@@ -73,6 +74,8 @@
<file>Controls2/ListItemType.qml</file>
<file>Controls2/RadioListType.qml</file>
<file>Controls2/PageType.qml</file>
<file>Controls2/SelectionGlowType.qml</file>
<file>Controls2/ShadowType.qml</file>
<file>Controls2/SnackbarType.qml</file>
<file>Controls2/ProgressBarType.qml</file>
<file>Controls2/ScrollBarType.qml</file>
@@ -142,6 +145,7 @@
<file>Pages2/PageSettingsDns.qml</file>
<file>Pages2/PageSettingsKillSwitch.qml</file>
<file>Pages2/PageSettingsKillSwitchExceptions.qml</file>
<file>Pages2/PageSettingsLocalProxy.qml</file>
<file>Pages2/PageSettingsLogging.qml</file>
<file>Pages2/PageSettingsLogViewer.qml</file>
<file>Pages2/PageSettingsServerData.qml</file>
+1
View File
@@ -41,6 +41,7 @@ namespace PageLoader
PageSettingsApiDevices,
PageSettingsApiSubscriptionKey,
PageSettingsKillSwitchExceptions,
PageSettingsLocalProxy,
PageServiceSftpSettings,
PageServiceTorWebsiteSettings,
+1
Submodule cmake/cmake-conan added at 9eac4bddce.
-726
View File
@@ -1,726 +0,0 @@
# The MIT License (MIT)
#
# Copyright (c) 2024 JFrog
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
set(CONAN_MINIMUM_VERSION 2.0.5)
# Create a new policy scope and set the minimum required cmake version so the
# features behind a policy setting like if(... IN_LIST ...) behaves as expected
# even if the parent project does not specify a minimum cmake version or a minimum
# version less than this module requires (e.g. 3.0) before the first project() call.
# (see: https://cmake.org/cmake/help/latest/variable/CMAKE_PROJECT_TOP_LEVEL_INCLUDES.html)
#
# The policy-affecting calls like cmake_policy(SET...) or `cmake_minimum_required` only
# affects the current policy scope, i.e. between the PUSH and POP in this case.
#
# https://cmake.org/cmake/help/book/mastering-cmake/chapter/Policies.html#the-policy-stack
cmake_policy(PUSH)
cmake_minimum_required(VERSION 3.24)
function(detect_os os os_api_level os_sdk os_subsystem os_version)
# it could be cross compilation
message(STATUS "CMake-Conan: cmake_system_name=${CMAKE_SYSTEM_NAME}")
if(CMAKE_SYSTEM_NAME AND NOT CMAKE_SYSTEM_NAME STREQUAL "Generic")
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
set(${os} Macos PARENT_SCOPE)
elseif(CMAKE_SYSTEM_NAME STREQUAL "QNX")
set(${os} Neutrino PARENT_SCOPE)
elseif(CMAKE_SYSTEM_NAME STREQUAL "CYGWIN")
set(${os} Windows PARENT_SCOPE)
set(${os_subsystem} cygwin PARENT_SCOPE)
elseif(CMAKE_SYSTEM_NAME MATCHES "^MSYS")
set(${os} Windows PARENT_SCOPE)
set(${os_subsystem} msys2 PARENT_SCOPE)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
# https://github.com/emscripten-core/emscripten/blob/4.0.6/cmake/Modules/Platform/Emscripten.cmake#L17C1-L17C34
set(${os} Emscripten PARENT_SCOPE)
else()
set(${os} ${CMAKE_SYSTEM_NAME} PARENT_SCOPE)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Android")
if(DEFINED ANDROID_PLATFORM)
string(REGEX MATCH "[0-9]+" _os_api_level ${ANDROID_PLATFORM})
elseif(DEFINED CMAKE_SYSTEM_VERSION)
set(_os_api_level ${CMAKE_SYSTEM_VERSION})
endif()
message(STATUS "CMake-Conan: android api level=${_os_api_level}")
set(${os_api_level} ${_os_api_level} PARENT_SCOPE)
endif()
if(CMAKE_SYSTEM_NAME MATCHES "Darwin|iOS|tvOS|watchOS")
# CMAKE_OSX_SYSROOT contains the full path to the SDK for MakeFile/Ninja
# generators, but just has the original input string for Xcode.
if(NOT IS_DIRECTORY ${CMAKE_OSX_SYSROOT})
set(_os_sdk ${CMAKE_OSX_SYSROOT})
else()
if(CMAKE_OSX_SYSROOT MATCHES Simulator)
set(apple_platform_suffix simulator)
else()
set(apple_platform_suffix os)
endif()
if(CMAKE_OSX_SYSROOT MATCHES AppleTV)
set(_os_sdk "appletv${apple_platform_suffix}")
elseif(CMAKE_OSX_SYSROOT MATCHES iPhone)
set(_os_sdk "iphone${apple_platform_suffix}")
elseif(CMAKE_OSX_SYSROOT MATCHES Watch)
set(_os_sdk "watch${apple_platform_suffix}")
endif()
endif()
# Macos does not support os.sdk
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
set(_os_sdk "")
endif()
if(DEFINED os_sdk)
message(STATUS "CMake-Conan: cmake_osx_sysroot=${CMAKE_OSX_SYSROOT}")
set(${os_sdk} ${_os_sdk} PARENT_SCOPE)
endif()
if(DEFINED CMAKE_OSX_DEPLOYMENT_TARGET)
message(STATUS "CMake-Conan: cmake_osx_deployment_target=${CMAKE_OSX_DEPLOYMENT_TARGET}")
set(${os_version} ${CMAKE_OSX_DEPLOYMENT_TARGET} PARENT_SCOPE)
endif()
endif()
endif()
endfunction()
function(detect_arch arch)
# CMAKE_OSX_ARCHITECTURES can contain multiple architectures, but Conan only supports one.
# Therefore this code only finds one. If the recipes support multiple architectures, the
# build will work. Otherwise, there will be a linker error for the missing architecture(s).
if(DEFINED CMAKE_OSX_ARCHITECTURES)
string(REPLACE " " ";" apple_arch_list "${CMAKE_OSX_ARCHITECTURES}")
list(LENGTH apple_arch_list apple_arch_count)
if(apple_arch_count GREATER 1)
message(WARNING "CMake-Conan: Multiple architectures detected, this will only work if Conan recipe(s) produce fat binaries.")
endif()
endif()
if(CMAKE_SYSTEM_NAME MATCHES "Darwin|iOS|tvOS|watchOS" AND NOT CMAKE_OSX_ARCHITECTURES STREQUAL "")
set(host_arch ${CMAKE_OSX_ARCHITECTURES})
elseif(MSVC)
set(host_arch ${CMAKE_CXX_COMPILER_ARCHITECTURE_ID})
else()
set(host_arch ${CMAKE_SYSTEM_PROCESSOR})
endif()
if(host_arch MATCHES "aarch64|arm64|ARM64")
list(APPEND _arch armv8)
endif()
if(host_arch MATCHES "armv7|armv7-a|armv7l|ARMV7")
list(APPEND _arch armv7)
endif()
if(host_arch MATCHES armv7s)
list(APPEND _arch armv7s)
endif()
if(host_arch MATCHES "i686|i386|X86")
list(APPEND _arch x86)
endif()
if(host_arch MATCHES "AMD64|amd64|x86_64|x64")
list(APPEND _arch x86_64)
endif()
if(EMSCRIPTEN)
# https://github.com/emscripten-core/emscripten/blob/4.0.6/cmake/Modules/Platform/Emscripten.cmake#L294C1-L294C80
list(APPEND _arch wasm)
endif()
message(STATUS "CMake-Conan: cmake_system_processor=${_arch}")
list(JOIN _arch "|" _arch)
set(${arch} ${_arch} PARENT_SCOPE)
endfunction()
function(detect_cxx_standard compiler cxx_standard)
set(${cxx_standard} ${CMAKE_CXX_STANDARD} PARENT_SCOPE)
if(CMAKE_CXX_EXTENSIONS)
if(compiler STREQUAL "msvc")
set(${cxx_standard} "${CMAKE_CXX_STANDARD}" PARENT_SCOPE)
else()
set(${cxx_standard} "gnu${CMAKE_CXX_STANDARD}" PARENT_SCOPE)
endif()
endif()
endfunction()
macro(detect_gnu_libstdcxx)
# _conan_is_gnu_libstdcxx true if GNU libstdc++
check_cxx_source_compiles("
#include <cstddef>
#if !defined(__GLIBCXX__) && !defined(__GLIBCPP__)
static_assert(false);
#endif
int main(){}" _conan_is_gnu_libstdcxx)
# _conan_gnu_libstdcxx_is_cxx11_abi true if C++11 ABI
check_cxx_source_compiles("
#include <string>
static_assert(sizeof(std::string) != sizeof(void*), \"using libstdc++\");
int main () {}" _conan_gnu_libstdcxx_is_cxx11_abi)
set(_conan_gnu_libstdcxx_suffix "")
if(_conan_gnu_libstdcxx_is_cxx11_abi)
set(_conan_gnu_libstdcxx_suffix "11")
endif()
unset (_conan_gnu_libstdcxx_is_cxx11_abi)
endmacro()
macro(detect_libcxx)
# _conan_is_libcxx true if LLVM libc++
check_cxx_source_compiles("
#include <cstddef>
#if !defined(_LIBCPP_VERSION)
static_assert(false);
#endif
int main(){}" _conan_is_libcxx)
endmacro()
function(detect_lib_cxx lib_cxx)
if(CMAKE_SYSTEM_NAME STREQUAL "Android")
message(STATUS "CMake-Conan: android_stl=${CMAKE_ANDROID_STL_TYPE}")
set(${lib_cxx} ${CMAKE_ANDROID_STL_TYPE} PARENT_SCOPE)
return()
endif()
include(CheckCXXSourceCompiles)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
detect_gnu_libstdcxx()
set(${lib_cxx} "libstdc++${_conan_gnu_libstdcxx_suffix}" PARENT_SCOPE)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "AppleClang")
set(${lib_cxx} "libc++" PARENT_SCOPE)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT CMAKE_SYSTEM_NAME MATCHES "Windows")
# Check for libc++
detect_libcxx()
if(_conan_is_libcxx)
set(${lib_cxx} "libc++" PARENT_SCOPE)
return()
endif()
# Check for libstdc++
detect_gnu_libstdcxx()
if(_conan_is_gnu_libstdcxx)
set(${lib_cxx} "libstdc++${_conan_gnu_libstdcxx_suffix}" PARENT_SCOPE)
return()
endif()
# TODO: it would be an error if we reach this point
elseif(CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
# Do nothing - compiler.runtime and compiler.runtime_type
# should be handled separately: https://github.com/conan-io/cmake-conan/pull/516
return()
else()
# TODO: unable to determine, ask user to provide a full profile file instead
endif()
endfunction()
function(detect_compiler compiler compiler_version compiler_runtime compiler_runtime_type)
if(DEFINED CMAKE_CXX_COMPILER_ID)
set(_compiler ${CMAKE_CXX_COMPILER_ID})
set(_compiler_version ${CMAKE_CXX_COMPILER_VERSION})
else()
if(NOT DEFINED CMAKE_C_COMPILER_ID)
message(FATAL_ERROR "C or C++ compiler not defined")
endif()
set(_compiler ${CMAKE_C_COMPILER_ID})
set(_compiler_version ${CMAKE_C_COMPILER_VERSION})
endif()
message(STATUS "CMake-Conan: CMake compiler=${_compiler}")
message(STATUS "CMake-Conan: CMake compiler version=${_compiler_version}")
if(_compiler MATCHES MSVC)
set(_compiler "msvc")
string(SUBSTRING ${MSVC_VERSION} 0 3 _compiler_version)
# Configure compiler.runtime and compiler.runtime_type settings for MSVC
if(CMAKE_MSVC_RUNTIME_LIBRARY)
set(_msvc_runtime_library ${CMAKE_MSVC_RUNTIME_LIBRARY})
else()
set(_msvc_runtime_library MultiThreaded$<$<CONFIG:Debug>:Debug>DLL) # default value documented by CMake
endif()
set(_KNOWN_MSVC_RUNTIME_VALUES "")
list(APPEND _KNOWN_MSVC_RUNTIME_VALUES MultiThreaded MultiThreadedDLL)
list(APPEND _KNOWN_MSVC_RUNTIME_VALUES MultiThreadedDebug MultiThreadedDebugDLL)
list(APPEND _KNOWN_MSVC_RUNTIME_VALUES MultiThreaded$<$<CONFIG:Debug>:Debug> MultiThreaded$<$<CONFIG:Debug>:Debug>DLL)
# only accept the 6 possible values, otherwise we don't don't know to map this
if(NOT _msvc_runtime_library IN_LIST _KNOWN_MSVC_RUNTIME_VALUES)
message(FATAL_ERROR "CMake-Conan: unable to map MSVC runtime: ${_msvc_runtime_library} to Conan settings")
endif()
# Runtime is "dynamic" in all cases if it ends in DLL
if(_msvc_runtime_library MATCHES ".*DLL$")
set(_compiler_runtime "dynamic")
else()
set(_compiler_runtime "static")
endif()
message(STATUS "CMake-Conan: CMake compiler.runtime=${_compiler_runtime}")
# Only define compiler.runtime_type when explicitly requested
# If a generator expression is used, let Conan handle it conditional on build_type
if(NOT _msvc_runtime_library MATCHES "<CONFIG:Debug>:Debug>")
if(_msvc_runtime_library MATCHES "Debug")
set(_compiler_runtime_type "Debug")
else()
set(_compiler_runtime_type "Release")
endif()
message(STATUS "CMake-Conan: CMake compiler.runtime_type=${_compiler_runtime_type}")
endif()
unset(_KNOWN_MSVC_RUNTIME_VALUES)
elseif(_compiler MATCHES AppleClang)
set(_compiler "apple-clang")
string(REPLACE "." ";" VERSION_LIST ${_compiler_version})
list(GET VERSION_LIST 0 _compiler_version)
elseif(_compiler MATCHES Clang)
set(_compiler "clang")
string(REPLACE "." ";" VERSION_LIST ${_compiler_version})
list(GET VERSION_LIST 0 _compiler_version)
elseif(_compiler MATCHES GNU)
set(_compiler "gcc")
string(REPLACE "." ";" VERSION_LIST ${_compiler_version})
list(GET VERSION_LIST 0 _compiler_version)
endif()
message(STATUS "CMake-Conan: [settings] compiler=${_compiler}")
message(STATUS "CMake-Conan: [settings] compiler.version=${_compiler_version}")
if (_compiler_runtime)
message(STATUS "CMake-Conan: [settings] compiler.runtime=${_compiler_runtime}")
endif()
if (_compiler_runtime_type)
message(STATUS "CMake-Conan: [settings] compiler.runtime_type=${_compiler_runtime_type}")
endif()
set(${compiler} ${_compiler} PARENT_SCOPE)
set(${compiler_version} ${_compiler_version} PARENT_SCOPE)
set(${compiler_runtime} ${_compiler_runtime} PARENT_SCOPE)
set(${compiler_runtime_type} ${_compiler_runtime_type} PARENT_SCOPE)
endfunction()
function(detect_build_type build_type)
get_property(multiconfig_generator GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(NOT multiconfig_generator)
# Only set when we know we are in a single-configuration generator
# Note: we may want to fail early if `CMAKE_BUILD_TYPE` is not defined
set(${build_type} ${CMAKE_BUILD_TYPE} PARENT_SCOPE)
endif()
endfunction()
macro(set_conan_compiler_if_appleclang lang command output_variable)
if(CMAKE_${lang}_COMPILER_ID STREQUAL "AppleClang")
execute_process(COMMAND xcrun --find ${command}
OUTPUT_VARIABLE _xcrun_out OUTPUT_STRIP_TRAILING_WHITESPACE)
cmake_path(GET _xcrun_out PARENT_PATH _xcrun_toolchain_path)
cmake_path(GET CMAKE_${lang}_COMPILER PARENT_PATH _compiler_parent_path)
if ("${_xcrun_toolchain_path}" STREQUAL "${_compiler_parent_path}")
set(${output_variable} "")
endif()
unset(_xcrun_out)
unset(_xcrun_toolchain_path)
unset(_compiler_parent_path)
endif()
endmacro()
macro(append_compiler_executables_configuration)
set(_conan_c_compiler "")
set(_conan_cpp_compiler "")
set(_conan_rc_compiler "")
set(_conan_compilers_list "")
if(CMAKE_C_COMPILER)
set(_conan_c_compiler "\"c\":\"${CMAKE_C_COMPILER}\"")
set_conan_compiler_if_appleclang(C cc _conan_c_compiler)
list(APPEND _conan_compilers_list ${_conan_c_compiler})
else()
message(WARNING "CMake-Conan: The C compiler is not defined. "
"Please define CMAKE_C_COMPILER or enable the C language.")
endif()
if(CMAKE_CXX_COMPILER)
set(_conan_cpp_compiler "\"cpp\":\"${CMAKE_CXX_COMPILER}\"")
set_conan_compiler_if_appleclang(CXX c++ _conan_cpp_compiler)
list(APPEND _conan_compilers_list ${_conan_cpp_compiler})
else()
message(WARNING "CMake-Conan: The C++ compiler is not defined. "
"Please define CMAKE_CXX_COMPILER or enable the C++ language.")
endif()
if(CMAKE_RC_COMPILER)
set(_conan_rc_compiler "\"rc\":\"${CMAKE_RC_COMPILER}\"")
list(APPEND _conan_compilers_list ${_conan_rc_compiler})
# Not necessary to warn if RC not defined
endif()
if(NOT "x${_conan_compilers_list}" STREQUAL "x")
if (NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
string(REPLACE ";" "," _conan_compilers_list "${_conan_compilers_list}")
string(APPEND profile "tools.build:compiler_executables={${_conan_compilers_list}}\n")
endif()
endif()
unset(_conan_c_compiler)
unset(_conan_cpp_compiler)
unset(_conan_rc_compiler)
unset(_conan_compilers_list)
endmacro()
function(detect_host_profile output_file)
detect_os(os os_api_level os_sdk os_subsystem os_version)
detect_arch(arch)
detect_compiler(compiler compiler_version compiler_runtime compiler_runtime_type)
detect_cxx_standard(${compiler} compiler_cppstd)
detect_lib_cxx(compiler_libcxx)
detect_build_type(build_type)
set(profile "")
string(APPEND profile "[settings]\n")
if(arch)
string(APPEND profile arch=${arch} "\n")
endif()
if(os)
string(APPEND profile os=${os} "\n")
endif()
if(os_api_level)
string(APPEND profile os.api_level=${os_api_level} "\n")
endif()
if(os_version)
string(APPEND profile os.version=${os_version} "\n")
endif()
if(os_sdk)
string(APPEND profile os.sdk=${os_sdk} "\n")
endif()
if(os_subsystem)
string(APPEND profile os.subsystem=${os_subsystem} "\n")
endif()
if(compiler)
string(APPEND profile compiler=${compiler} "\n")
endif()
if(compiler_version)
string(APPEND profile compiler.version=${compiler_version} "\n")
endif()
if(compiler_runtime)
string(APPEND profile compiler.runtime=${compiler_runtime} "\n")
endif()
if(compiler_runtime_type)
string(APPEND profile compiler.runtime_type=${compiler_runtime_type} "\n")
endif()
if(compiler_cppstd)
string(APPEND profile compiler.cppstd=${compiler_cppstd} "\n")
endif()
if(compiler_libcxx)
string(APPEND profile compiler.libcxx=${compiler_libcxx} "\n")
endif()
if(build_type)
string(APPEND profile "build_type=${build_type}\n")
endif()
if(NOT DEFINED output_file)
set(file_name "${CMAKE_BINARY_DIR}/profile")
else()
set(file_name ${output_file})
endif()
string(APPEND profile "[conf]\n")
string(APPEND profile "tools.cmake.cmaketoolchain:generator=${CMAKE_GENERATOR}\n")
# propagate compilers via profile
append_compiler_executables_configuration()
if(os STREQUAL "Android")
string(APPEND profile "tools.android:ndk_path=${CMAKE_ANDROID_NDK}\n")
endif()
message(STATUS "CMake-Conan: Creating profile ${file_name}")
file(WRITE ${file_name} ${profile})
message(STATUS "CMake-Conan: Profile: \n${profile}")
endfunction()
function(conan_profile_detect_default)
message(STATUS "CMake-Conan: Checking if a default profile exists")
execute_process(COMMAND ${CONAN_COMMAND} profile path default
RESULT_VARIABLE return_code
OUTPUT_VARIABLE conan_stdout
ERROR_VARIABLE conan_stderr
ECHO_ERROR_VARIABLE # show the text output regardless
ECHO_OUTPUT_VARIABLE
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
if(NOT ${return_code} EQUAL "0")
message(STATUS "CMake-Conan: The default profile doesn't exist, detecting it.")
execute_process(COMMAND ${CONAN_COMMAND} profile detect
RESULT_VARIABLE return_code
OUTPUT_VARIABLE conan_stdout
ERROR_VARIABLE conan_stderr
ECHO_ERROR_VARIABLE # show the text output regardless
ECHO_OUTPUT_VARIABLE
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
endif()
endfunction()
function(conan_install)
set(conan_output_folder ${CMAKE_BINARY_DIR}/conan)
# Invoke "conan install" with the provided arguments
set(conan_args -of=${conan_output_folder})
message(STATUS "CMake-Conan: conan install ${CMAKE_SOURCE_DIR} ${conan_args} ${ARGN}")
# In case there was not a valid cmake executable in the PATH, we inject the
# same we used to invoke the provider to the PATH
if(DEFINED PATH_TO_CMAKE_BIN)
set(old_path $ENV{PATH})
set(ENV{PATH} "$ENV{PATH}:${PATH_TO_CMAKE_BIN}")
endif()
execute_process(COMMAND ${CONAN_COMMAND} install ${CMAKE_SOURCE_DIR} ${conan_args} ${ARGN} --format=json
RESULT_VARIABLE return_code
OUTPUT_VARIABLE conan_stdout
ERROR_VARIABLE conan_stderr
ECHO_ERROR_VARIABLE # show the text output regardless
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
if(DEFINED PATH_TO_CMAKE_BIN)
set(ENV{PATH} "${old_path}")
endif()
if(NOT "${return_code}" STREQUAL "0")
message(FATAL_ERROR "Conan install failed='${return_code}'")
endif()
# the files are generated in a folder that depends on the layout used, if
# one is specified, but we don't know a priori where this is.
# TODO: this can be made more robust if Conan can provide this in the json output
string(JSON conan_generators_folder GET "${conan_stdout}" graph nodes 0 generators_folder)
cmake_path(CONVERT ${conan_generators_folder} TO_CMAKE_PATH_LIST conan_generators_folder)
message(STATUS "CMake-Conan: CONAN_GENERATORS_FOLDER=${conan_generators_folder}")
set_property(GLOBAL PROPERTY CONAN_GENERATORS_FOLDER "${conan_generators_folder}")
# reconfigure on conanfile changes
string(JSON conanfile GET "${conan_stdout}" graph nodes 0 label)
message(STATUS "CMake-Conan: CONANFILE=${CMAKE_SOURCE_DIR}/${conanfile}")
set_property(DIRECTORY ${CMAKE_SOURCE_DIR} APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/${conanfile}")
# success
set_property(GLOBAL PROPERTY CONAN_INSTALL_SUCCESS TRUE)
endfunction()
function(conan_get_version conan_command conan_current_version)
execute_process(
COMMAND ${conan_command} --version
OUTPUT_VARIABLE conan_output
RESULT_VARIABLE conan_result
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if(conan_result)
message(FATAL_ERROR "CMake-Conan: Error when trying to run Conan")
endif()
string(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+" conan_version ${conan_output})
set(${conan_current_version} ${conan_version} PARENT_SCOPE)
endfunction()
function(conan_version_check)
set(options )
set(one_value_args MINIMUM CURRENT)
set(multi_value_args )
cmake_parse_arguments(conan_version_check
"${options}" "${one_value_args}" "${multi_value_args}" ${ARGN})
if(NOT conan_version_check_MINIMUM)
message(FATAL_ERROR "CMake-Conan: Required parameter MINIMUM not set!")
endif()
if(NOT conan_version_check_CURRENT)
message(FATAL_ERROR "CMake-Conan: Required parameter CURRENT not set!")
endif()
if(conan_version_check_CURRENT VERSION_LESS conan_version_check_MINIMUM)
message(FATAL_ERROR "CMake-Conan: Conan version must be ${conan_version_check_MINIMUM} or later")
endif()
endfunction()
macro(construct_profile_argument argument_variable profile_list)
set(${argument_variable} "")
if("${profile_list}" STREQUAL "CONAN_HOST_PROFILE")
set(_arg_flag "--profile:host=")
elseif("${profile_list}" STREQUAL "CONAN_BUILD_PROFILE")
set(_arg_flag "--profile:build=")
endif()
set(_profile_list "${${profile_list}}")
list(TRANSFORM _profile_list REPLACE "auto-cmake" "${CMAKE_BINARY_DIR}/conan_host_profile")
list(TRANSFORM _profile_list PREPEND ${_arg_flag})
set(${argument_variable} ${_profile_list})
unset(_arg_flag)
unset(_profile_list)
endmacro()
macro(conan_provide_dependency method package_name)
set_property(GLOBAL PROPERTY CONAN_PROVIDE_DEPENDENCY_INVOKED TRUE)
get_property(_conan_install_success GLOBAL PROPERTY CONAN_INSTALL_SUCCESS)
if(NOT _conan_install_success)
find_program(CONAN_COMMAND "conan" REQUIRED)
conan_get_version(${CONAN_COMMAND} CONAN_CURRENT_VERSION)
conan_version_check(MINIMUM ${CONAN_MINIMUM_VERSION} CURRENT ${CONAN_CURRENT_VERSION})
message(STATUS "CMake-Conan: first find_package() found. Installing dependencies with Conan")
if("default" IN_LIST CONAN_HOST_PROFILE OR "default" IN_LIST CONAN_BUILD_PROFILE)
conan_profile_detect_default()
endif()
if("auto-cmake" IN_LIST CONAN_HOST_PROFILE)
detect_host_profile(${CMAKE_BINARY_DIR}/conan_host_profile)
endif()
construct_profile_argument(_host_profile_flags CONAN_HOST_PROFILE)
construct_profile_argument(_build_profile_flags CONAN_BUILD_PROFILE)
if(EXISTS "${CMAKE_SOURCE_DIR}/conanfile.py")
file(READ "${CMAKE_SOURCE_DIR}/conanfile.py" outfile)
if(NOT "${outfile}" MATCHES ".*CMakeConfigDeps.*")
message(WARNING "Cmake-conan: CMakeConfigDeps generator was not defined in the conanfile")
endif()
elseif (EXISTS "${CMAKE_SOURCE_DIR}/conanfile.txt")
file(READ "${CMAKE_SOURCE_DIR}/conanfile.txt" outfile)
if(NOT "${outfile}" MATCHES ".*CMakeConfigDeps.*")
message(WARNING "Cmake-conan: CMakeConfigDeps generator was not defined in the conanfile")
endif()
endif()
get_property(_multiconfig_generator GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(DEFINED CONAN_INSTALL_BUILD_CONFIGURATIONS)
# Configurations are specified by the project or user
set(_build_configs "${CONAN_INSTALL_BUILD_CONFIGURATIONS}")
list(LENGTH _build_configs _build_configs_length)
if(NOT _multiconfig_generator AND _build_configs_length GREATER 1)
message(FATAL_ERROR "cmake-conan: when using a single-config CMake generator, "
"please only specify a single configuration in CONAN_INSTALL_BUILD_CONFIGURATIONS")
endif()
unset(_build_configs_length)
else()
# No configuration overrides, provide sensible defaults
if(_multiconfig_generator)
set(_build_configs Release Debug)
else()
set(_build_configs ${CMAKE_BUILD_TYPE})
endif()
endif()
list(JOIN _build_configs ", " _build_configs_msg)
message(STATUS "CMake-Conan: Installing configuration(s): ${_build_configs_msg}")
foreach(_build_config IN LISTS _build_configs)
set(_self_build_config "")
if(NOT _multiconfig_generator AND NOT _build_config STREQUAL "${CMAKE_BUILD_TYPE}")
set(_self_build_config -s &:build_type=${CMAKE_BUILD_TYPE})
endif()
conan_install(${_host_profile_flags} ${_build_profile_flags} -s build_type=${_build_config} ${_self_build_config} ${CONAN_INSTALL_ARGS})
endforeach()
get_property(_conan_generators_folder GLOBAL PROPERTY CONAN_GENERATORS_FOLDER)
if(EXISTS "${_conan_generators_folder}/conan_cmakedeps_paths.cmake")
message(STATUS "CMake-Conan: Loading conan_cmakedeps_paths.cmake file")
include(${_conan_generators_folder}/conan_cmakedeps_paths.cmake)
endif()
unset(_self_build_config)
unset(_multiconfig_generator)
unset(_build_configs)
unset(_build_configs_msg)
unset(_host_profile_flags)
unset(_build_profile_flags)
unset(_conan_install_success)
else()
message(STATUS "CMake-Conan: find_package(${ARGV1}) found, 'conan install' already ran")
unset(_conan_install_success)
endif()
get_property(_conan_generators_folder GLOBAL PROPERTY CONAN_GENERATORS_FOLDER)
# Ensure that we consider Conan-provided packages ahead of any other,
# irrespective of other settings that modify the search order or search paths
# This follows the guidelines from the find_package documentation
# (https://cmake.org/cmake/help/latest/command/find_package.html):
# find_package (<PackageName> PATHS paths... NO_DEFAULT_PATH)
# find_package (<PackageName>)
# Filter out `REQUIRED` from the argument list, as the first call may fail
set(_find_args_${package_name} "${ARGN}")
list(REMOVE_ITEM _find_args_${package_name} "REQUIRED")
if(NOT "MODULE" IN_LIST _find_args_${package_name})
find_package(${package_name} ${_find_args_${package_name}} BYPASS_PROVIDER PATHS "${_conan_generators_folder}" NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH)
unset(_find_args_${package_name})
endif()
# Invoke find_package a second time - if the first call succeeded,
# this will simply reuse the result. If not, fall back to CMake default search
# behaviour, also allowing modules to be searched.
if(NOT ${package_name}_FOUND)
list(FIND CMAKE_MODULE_PATH "${_conan_generators_folder}" _index)
if(_index EQUAL -1)
list(PREPEND CMAKE_MODULE_PATH "${_conan_generators_folder}")
endif()
unset(_index)
find_package(${package_name} ${ARGN} BYPASS_PROVIDER)
list(REMOVE_ITEM CMAKE_MODULE_PATH "${_conan_generators_folder}")
endif()
endmacro()
cmake_language(
SET_DEPENDENCY_PROVIDER conan_provide_dependency
SUPPORTED_METHODS FIND_PACKAGE
)
macro(conan_provide_dependency_check)
set(_conan_provide_dependency_invoked FALSE)
get_property(_conan_provide_dependency_invoked GLOBAL PROPERTY CONAN_PROVIDE_DEPENDENCY_INVOKED)
if(NOT _conan_provide_dependency_invoked)
message(WARNING "Conan is correctly configured as dependency provider, "
"but Conan has not been invoked. Please add at least one "
"call to `find_package()`.")
if(DEFINED CONAN_COMMAND)
# supress warning in case `CONAN_COMMAND` was specified but unused.
set(_conan_command ${CONAN_COMMAND})
unset(_conan_command)
endif()
endif()
unset(_conan_provide_dependency_invoked)
endmacro()
# Add a deferred call at the end of processing the top-level directory
# to check if the dependency provider was invoked at all.
cmake_language(DEFER DIRECTORY "${CMAKE_SOURCE_DIR}" CALL conan_provide_dependency_check)
# Configurable variables for Conan profiles
set(CONAN_HOST_PROFILE "default;auto-cmake" CACHE STRING "Conan host profile")
set(CONAN_BUILD_PROFILE "default" CACHE STRING "Conan build profile")
set(CONAN_INSTALL_ARGS "--build=missing" CACHE STRING "Command line arguments for conan install")
find_program(_cmake_program NAMES cmake NO_PACKAGE_ROOT_PATH NO_CMAKE_PATH NO_CMAKE_ENVIRONMENT_PATH NO_CMAKE_SYSTEM_PATH NO_CMAKE_FIND_ROOT_PATH)
if(NOT _cmake_program)
get_filename_component(PATH_TO_CMAKE_BIN "${CMAKE_COMMAND}" DIRECTORY)
set(PATH_TO_CMAKE_BIN "${PATH_TO_CMAKE_BIN}" CACHE INTERNAL "Path where the CMake executable is")
endif()
cmake_policy(POP)
-18
View File
@@ -1,18 +0,0 @@
find_program(CONAN_COMMAND "conan" REQUIRED
HINTS
"${CMAKE_SOURCE_DIR}/.venv/Scripts"
"${CMAKE_SOURCE_DIR}/.venv/bin"
"/opt/homebrew/bin"
)
file(GLOB_RECURSE LOCAL_RECIPES "${CMAKE_SOURCE_DIR}/recipes/*/conanfile.py")
foreach(RECIPE ${LOCAL_RECIPES})
get_filename_component(RECIPE_DIR ${RECIPE} DIRECTORY)
execute_process(
COMMAND ${CONAN_COMMAND} export ${RECIPE_DIR}
)
endforeach()
execute_process(
COMMAND ${CONAN_COMMAND} remote add amnezia "https://artifactory.amnezia.org/artifactory/api/conan/client-prebuilts" --force
)
+3 -40
View File
@@ -4,46 +4,9 @@ class AmneziaVPN(ConanFile):
settings = "os", "compiler", "build_type", "arch"
generators = "VirtualBuildEnv", "CMakeConfigDeps"
options = {
"macos_ne": [True, False]
}
default_options = {
"macos_ne": False
}
def requirements(self):
os = str(self.settings.os)
self.requires("libagw/1.0.6")
has_ne = os == "iOS" or (os == "Macos" and self.options.macos_ne)
has_service = os == "Windows" or os == "Linux" or (os == "Macos" and not has_ne)
if has_service:
if os == "Windows":
self.requires("awg-windows/3.1.20260814")
self.requires("tap-windows6/9.27.0")
self.requires("win-split-tunnel/1.2.5.0")
self.requires("wintun/0.14.1")
else:
self.requires("awg-go/3.1.20260814")
self.requires("amnezia-xray-bindings/1.4.0")
self.requires("tun2socks/2.6.0")
self.requires("openvpn/2.7.0")
self.requires("v2ray-rules-dat/202603162227")
if has_ne:
self.requires("awg-apple/3.1.4")
self.requires("hev-socks5-tunnel/2.15.0", options={"as_framework": True})
self.requires("openvpnadapter/1.0.0")
if os == "Android":
self.requires("amnezia-libxray/1.0.3")
self.requires("awg-android/3.1.20260814")
self.requires("openvpn-pt-android/1.0.0")
# expicitly use libssh@amnezia to prevent it from being downloaded from conan-center
self.requires("libagw/1.0.8@amnezia")
self.requires("xtun/1.0.0@amnezia")
self.requires("libssh/0.11.3@amnezia")
self.requires("openssl/3.6.2")
self.requires("openssl/[>=1.1]@amnezia")
self.requires("zlib/1.3.2")
+3
View File
@@ -40,6 +40,9 @@ class IpcInterface
SLOT(bool xrayStart(const QString &config));
SLOT(bool xrayStop());
SLOT(qint64 xrayStartOwned(const QString &config));
SLOT(bool xrayStopOwned(qint64 token));
SLOT(qint64 xrayCurrentToken());
SLOT( bool startNetworkCheck(const QString& serverIpv4Gateway, const QString& deviceIpv4Address) );
SLOT( bool stopNetworkCheck() );
+29 -2
View File
@@ -310,7 +310,7 @@ bool IpcServer::xrayStart(const QString& cfg)
qDebug() << "IpcServer::xrayStart";
#endif
return Xray::getInstance().startXray(cfg);
return Xray::getInstance().start(cfg) != 0;
}
bool IpcServer::xrayStop()
@@ -319,5 +319,32 @@ bool IpcServer::xrayStop()
qDebug() << "IpcServer::xrayStop";
#endif
return Xray::getInstance().stopXray();
return Xray::getInstance().stopAny();
}
qint64 IpcServer::xrayStartOwned(const QString& cfg)
{
#ifdef MZ_DEBUG
qDebug() << "IpcServer::xrayStartOwned";
#endif
return Xray::getInstance().start(cfg);
}
bool IpcServer::xrayStopOwned(qint64 token)
{
#ifdef MZ_DEBUG
qDebug() << "IpcServer::xrayStopOwned";
#endif
return Xray::getInstance().stop(token);
}
qint64 IpcServer::xrayCurrentToken()
{
#ifdef MZ_DEBUG
qDebug() << "IpcServer::xrayCurrentToken";
#endif
return Xray::getInstance().currentToken();
}
Loaded 100 of 132 files, more files were not shown because too many files have changed in this diff. Show more