Compare commits

..

14 Commits

Author SHA1 Message Date
vkamn cfe8641fd8 fix: context menu fixes for qt6.9 2025-11-20 10:10:49 +08:00
vkamn 3b3a0aaceb chore: bump version (#1997) 2025-11-18 00:22:58 +08:00
vkamn 01ec79b7d5 fix: news fetch (#1994)
* fix: fixed news nested qml call

* feat: async proxy bypass
2025-11-18 00:21:02 +08:00
vkamn 3d6339e2dd chore: bump version (#1989) 2025-11-14 13:59:47 +08:00
NickVs2015 b4d78d865a fix: fix android crash (#1988) 2025-11-14 13:57:52 +08:00
NickVs2015 b53cdcff08 fix: fix self-hosted TextFields and Keyboard reset issue (#1983)
Co-authored-by: vkamn <vk@amnezia.org>
2025-11-12 15:57:53 +08:00
vkamn 3cc18c5807 chore: bump version (#1982) 2025-11-11 23:03:24 +08:00
NickVs2015 5fdce1e49e fix: fix ui android issues (#1980)
* Fix UI issues

* Fix Screen Swipe
2025-11-11 22:03:27 +08:00
Yaroslav 2ee61a040b fix: iOS appstore publish fix (#1922) 2025-11-04 12:10:30 +08:00
vkamn 741b5cc0f9 fix: qt6 9 support (#1973)
* Fix qt 6.9 support

* add support android sdk 36

* feat: add support SafeMargins from Android

* Fix black screen

---------

Co-authored-by: NickVs2015 <nv@amnezia.org>
2025-11-04 11:43:36 +08:00
MrMirDan aaf0e070dc fix: hide description (#1959) 2025-11-03 10:27:01 +08:00
vkamn e0e126eda8 chore: bump version (#1969) 2025-11-03 10:26:33 +08:00
vkamn 236daf6b3b feat: ad label (#1966)
* refactor: ad label desing refatroing

* feat: add ad label settings processing

* chore: fix ru translations

* chore: minor fixes
2025-11-03 10:26:22 +08:00
vkamn f1481b1b1f feat: add async post in gateway controller (#1963) 2025-10-29 23:24:24 +08:00
53 changed files with 1857 additions and 1083 deletions
+1 -1
View File
@@ -470,7 +470,7 @@ jobs:
env:
ANDROID_BUILD_PLATFORM: android-36
QT_VERSION: 6.9.3
QT_VERSION: 6.8.3
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
+2 -2
View File
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.8.11.0)
set(AMNEZIAVPN_VERSION 4.8.11.4)
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
DESCRIPTION "AmneziaVPN"
@@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 2095)
set(APP_ANDROID_VERSION_CODE 2099)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")
+2 -2
View File
@@ -45,7 +45,7 @@
android:configChanges="uiMode|screenSize|smallestScreenSize|screenLayout|orientation|density
|fontScale|layoutDirection|locale|keyboard|keyboardHidden|navigation|mcc|mnc"
android:launchMode="singleInstance"
android:windowSoftInputMode="stateUnchanged|adjustResize"
android:windowSoftInputMode="adjustResize|stateUnchanged"
android:enableOnBackInvokedCallback="false"
android:exported="true">
@@ -215,4 +215,4 @@
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/qtprovider_paths" />
</provider>
</application>
</manifest>
</manifest>
@@ -35,6 +35,10 @@ import android.widget.Toast
import androidx.annotation.MainThread
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import java.io.IOException
import kotlin.LazyThreadSafetyMode.NONE
@@ -300,6 +304,11 @@ class AmneziaActivity : QtActivity() {
isAppearanceLightStatusBars = false
isAppearanceLightNavigationBars = false
}
// Workaround for Android 14 (API 34+) IME adjustResize bug
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
setupImeInsetsListener()
}
} else {
window.apply {
addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
@@ -308,6 +317,34 @@ class AmneziaActivity : QtActivity() {
}
}
private fun setupImeInsetsListener() {
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { view, windowInsets ->
val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
val imeVisible = windowInsets.isVisible(WindowInsetsCompat.Type.ime())
val imeHeight = if (imeVisible) imeInsets.bottom else 0
val density = resources.displayMetrics.density
val imeHeightDp = (imeHeight / density).toInt()
// Also track system bars (navigation bar, status bar) changes
val systemBarsInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val navBarHeight = systemBarsInsets.bottom
val navBarHeightDp = (navBarHeight / density).toInt()
val statusBarHeight = systemBarsInsets.top
val statusBarHeightDp = (statusBarHeight / density).toInt()
mainScope.launch {
qtInitialized.await()
QtAndroidController.onImeInsetsChanged(imeHeightDp)
QtAndroidController.onSystemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp)
}
// Return windowInsets instead of CONSUMED to allow proper handling
windowInsets
}
}
override fun onDestroy() {
Log.d(TAG, "Destroy Amnezia activity")
unregisterBroadcastReceiver(notificationStateReceiver)
@@ -28,4 +28,7 @@ object QtAndroidController {
external fun onAuthResult(result: Boolean)
external fun decodeQrCode(data: String): Boolean
external fun onImeInsetsChanged(heightDp: Int)
external fun onSystemBarsInsetsChanged(navBarHeightDp: Int, statusBarHeightDp: Int)
}
+4 -15
View File
@@ -136,21 +136,10 @@ set_property(TARGET ${PROJECT} APPEND PROPERTY RESOURCE
add_subdirectory(ios/networkextension)
add_dependencies(${PROJECT} networkextension)
set(OPENVPN_FRAMEWORK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios")
set(OPENVPN_EMBEDDED_FRAMEWORKS
"${OPENVPN_FRAMEWORK_DIR}/OpenVPNAdapter.framework"
"${OPENVPN_FRAMEWORK_DIR}/OpenVPNClient.framework"
"${OPENVPN_FRAMEWORK_DIR}/mbedTLS.framework"
"${OPENVPN_FRAMEWORK_DIR}/LZ4.framework"
set_property(TARGET ${PROJECT} PROPERTY XCODE_EMBED_FRAMEWORKS
"${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework"
)
set_property(TARGET ${PROJECT} PROPERTY XCODE_EMBED_FRAMEWORKS "${OPENVPN_EMBEDDED_FRAMEWORKS}")
set(CMAKE_XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS "$(inherited) ${OPENVPN_FRAMEWORK_DIR}")
set(CMAKE_XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/)
target_link_libraries("networkextension" PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/openvpn/apple/OpenVPNAdapter-ios/OpenVPNAdapter.framework")
foreach(_framework ${OPENVPN_EMBEDDED_FRAMEWORKS})
target_link_libraries(networkextension PRIVATE "${_framework}")
endforeach()
set_property(TARGET networkextension PROPERTY XCODE_EMBED_FRAMEWORKS "${OPENVPN_EMBEDDED_FRAMEWORKS}")
set_property(TARGET networkextension PROPERTY XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY ON)
set_property(TARGET networkextension PROPERTY XCODE_ATTRIBUTE_FRAMEWORK_SEARCH_PATHS "$(inherited) ${OPENVPN_FRAMEWORK_DIR}")
+8
View File
@@ -47,12 +47,14 @@ namespace apiDefs
constexpr QLatin1String serverCountryName("server_country_name");
constexpr QLatin1String osVersion("os_version");
constexpr QLatin1String appLanguage("app_language");
constexpr QLatin1String availableCountries("available_countries");
constexpr QLatin1String activeDeviceCount("active_device_count");
constexpr QLatin1String maxDeviceCount("max_device_count");
constexpr QLatin1String subscriptionEndDate("subscription_end_date");
constexpr QLatin1String issuedConfigs("issued_configs");
constexpr QLatin1String subscriptionDescription("subscription_description");
constexpr QLatin1String supportInfo("support_info");
constexpr QLatin1String email("email");
@@ -68,6 +70,12 @@ namespace apiDefs
constexpr QLatin1String transactionId("transaction_id");
constexpr QLatin1String userCountryCode("user_country_code");
constexpr QLatin1String serviceInfo("service_info");
constexpr QLatin1String isAdVisible("is_ad_visible");
constexpr QLatin1String adHeader("ad_header");
constexpr QLatin1String adDescription("ad_description");
constexpr QLatin1String adEndpoint("ad_endpoint");
}
const int requestTimeoutMsecs = 12 * 1000; // 12 secs
+1 -1
View File
@@ -324,7 +324,7 @@ void CoreController::initContainerModelUpdateHandler()
&ContainersModel::updateModel);
connect(m_serversModel.get(), &ServersModel::gatewayStacksExpanded, this, [this]() {
if (m_serversModel->hasServersFromGatewayApi()) {
m_apiNewsController->fetchNews();
m_apiNewsController->fetchNews(false);
}
});
m_serversModel->resetModel();
+271 -31
View File
@@ -1,12 +1,15 @@
#include "gatewayController.h"
#include <algorithm>
#include <functional>
#include <random>
#include <QCryptographicHash>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QPromise>
#include <QUrl>
#include "QBlockCipher.h"
@@ -50,24 +53,25 @@ GatewayController::GatewayController(const QString &gatewayEndpoint, const bool
{
}
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
GatewayController::EncryptedRequestData GatewayController::prepareRequest(const QString &endpoint, const QJsonObject &apiPayload)
{
EncryptedRequestData encRequestData;
encRequestData.errorCode = ErrorCode::NoError;
#ifdef Q_OS_IOS
IosController::Instance()->requestInetAccess();
QThread::msleep(10);
#endif
QNetworkRequest request;
request.setTransferTimeout(m_requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
encRequestData.request.setTransferTimeout(m_requestTimeoutMsecs);
encRequestData.request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
encRequestData.request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
encRequestData.request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
// bypass killSwitch exceptions for API-gateway
#ifdef AMNEZIA_DESKTOP
if (m_isStrictKillSwitchEnabled) {
QString host = QUrl(request.url()).host();
QString host = QUrl(encRequestData.request.url()).host();
QString ip = NetworkUtilities::getIPAddress(host);
if (!ip.isEmpty()) {
IpcClient::Interface()->addKillSwitchAllowedRange(QStringList { ip });
@@ -76,14 +80,14 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
#endif
QSimpleCrypto::QBlockCipher blockCipher;
QByteArray key = blockCipher.generatePrivateSalt(32);
QByteArray iv = blockCipher.generatePrivateSalt(32);
QByteArray salt = blockCipher.generatePrivateSalt(8);
encRequestData.key = blockCipher.generatePrivateSalt(32);
encRequestData.iv = blockCipher.generatePrivateSalt(32);
encRequestData.salt = blockCipher.generatePrivateSalt(8);
QJsonObject keyPayload;
keyPayload[configKey::aesKey] = QString(key.toBase64());
keyPayload[configKey::aesIv] = QString(iv.toBase64());
keyPayload[configKey::aesSalt] = QString(salt.toBase64());
keyPayload[configKey::aesKey] = QString(encRequestData.key.toBase64());
keyPayload[configKey::aesIv] = QString(encRequestData.iv.toBase64());
keyPayload[configKey::aesSalt] = QString(encRequestData.salt.toBase64());
QByteArray encryptedKeyPayload;
QByteArray encryptedApiPayload;
@@ -98,31 +102,45 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
} catch (...) {
Utils::logException();
qCritical() << "error loading public key from environment variables";
return ErrorCode::ApiMissingAgwPublicKey;
encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey;
return encRequestData;
}
encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
EVP_PKEY_free(publicKey);
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), key, iv, "", salt);
} catch (...) { // todo change error handling in QSimpleCrypto?
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv,
"", encRequestData.salt);
} catch (...) {
Utils::logException();
qCritical() << "error when encrypting the request body";
return ErrorCode::ApiConfigDecryptionError;
encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError;
return encRequestData;
}
QJsonObject requestBody;
requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64());
requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64());
QNetworkReply *reply = amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson());
encRequestData.requestBody = QJsonDocument(requestBody).toJson();
return encRequestData;
}
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
{
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
if (encRequestData.errorCode != ErrorCode::NoError) {
return encRequestData.errorCode;
}
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
QEventLoop wait;
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
QList<QSslError> sslErrors;
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec();
wait.exec(QEventLoop::ExcludeUserInputEvents);
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
@@ -131,19 +149,21 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
reply->deleteLater();
if (sslErrors.isEmpty() && shouldBypassProxy(replyError, encryptedResponseBody, true, key, iv, salt)) {
auto requestFunction = [&request, &encryptedResponseBody, &requestBody](const QString &url) {
request.setUrl(url);
return amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson());
if (sslErrors.isEmpty()
&& shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) {
auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) {
encRequestData.request.setUrl(url);
return amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
};
auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &key, &iv,
&salt, this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) {
auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors,
&encRequestData, this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) {
encryptedResponseBody = reply->readAll();
replyErrorString = reply->errorString();
replyError = reply->error();
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (!sslErrors.isEmpty() || shouldBypassProxy(replyError, encryptedResponseBody, true, key, iv, salt)) {
if (!sslErrors.isEmpty()
|| shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) {
sslErrors = nestedSslErrors;
return false;
}
@@ -161,7 +181,9 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
}
try {
responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
QSimpleCrypto::QBlockCipher blockCipher;
responseBody =
blockCipher.decryptAesBlockCipher(encryptedResponseBody, encRequestData.key, encRequestData.iv, "", encRequestData.salt);
return ErrorCode::NoError;
} catch (...) { // todo change error handling in QSimpleCrypto?
Utils::logException();
@@ -170,6 +192,93 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
}
}
QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
{
auto promise = QSharedPointer<QPromise<QPair<ErrorCode, QByteArray>>>::create();
promise->start();
EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload);
if (encRequestData.errorCode != ErrorCode::NoError) {
promise->addResult(qMakePair(encRequestData.errorCode, QByteArray()));
promise->finish();
return promise->future();
}
QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
connect(reply, &QNetworkReply::finished, reply, [promise, sslErrors, encRequestData, endpoint, apiPayload, reply, this]() mutable {
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
reply->deleteLater();
auto processResponse = [promise, encRequestData](const QByteArray &ecryptedResponseBody, const QList<QSslError> &sslErrors,
QNetworkReply::NetworkError replyError, const QString &replyErrorString,
int httpStatusCode) {
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, ecryptedResponseBody);
if (errorCode) {
promise->addResult(qMakePair(errorCode, QByteArray()));
promise->finish();
return;
}
QSimpleCrypto::QBlockCipher blockCipher;
try {
QByteArray responseBody = blockCipher.decryptAesBlockCipher(ecryptedResponseBody, encRequestData.key, encRequestData.iv, "",
encRequestData.salt);
promise->addResult(qMakePair(ErrorCode::NoError, responseBody));
promise->finish();
} catch (...) {
Utils::logException();
qCritical() << "error when decrypting the request body";
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
promise->finish();
}
};
if (sslErrors->isEmpty()
&& shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) {
auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
QStringList baseUrls;
if (m_isDevEnvironment) {
baseUrls = QString(DEV_S3_ENDPOINT).split(", ");
} else {
baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
}
QStringList proxyStorageUrls;
if (!serviceType.isEmpty()) {
for (const auto &baseUrl : baseUrls) {
QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8();
proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)
+ ".json");
}
}
for (const auto &baseUrl : baseUrls)
proxyStorageUrls.push_back(baseUrl + "endpoints.json");
getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrls) {
bypassProxyAsync(endpoint, proxyUrls, encRequestData, processResponse);
});
});
} else {
processResponse(encryptedResponseBody, *sslErrors, replyError, replyErrorString, httpStatusCode);
}
});
return promise->future();
}
QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode)
{
QNetworkRequest request;
@@ -206,7 +315,7 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec();
wait.exec(QEventLoop::ExcludeUserInputEvents);
if (reply->error() == QNetworkReply::NetworkError::NoError) {
auto encryptedResponseBody = reply->readAll();
@@ -318,7 +427,7 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec();
wait.exec(QEventLoop::ExcludeUserInputEvents);
auto result = replyProcessingFunction(reply, sslErrors);
reply->deleteLater();
@@ -340,7 +449,7 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec();
wait.exec(QEventLoop::ExcludeUserInputEvents);
if (reply->error() == QNetworkReply::NetworkError::NoError) {
reply->deleteLater();
@@ -368,3 +477,134 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
}
}
}
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
std::function<void(const QStringList &)> onComplete)
{
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
onComplete({});
return;
}
QNetworkRequest request;
request.setTransferTimeout(m_requestTimeoutMsecs);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setUrl(proxyStorageUrls[currentProxyStorageIndex]);
QNetworkReply *reply = amnApp->networkManager()->get(request);
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
if (reply->error() == QNetworkReply::NoError) {
QByteArray encrypted = reply->readAll();
reply->deleteLater();
QByteArray responseBody;
try {
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
if (!m_isDevEnvironment) {
QCryptographicHash hash(QCryptographicHash::Sha512);
hash.addData(key);
QByteArray h = hash.result().toHex();
QByteArray decKey = QByteArray::fromHex(h.left(64));
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
QByteArray ba = QByteArray::fromBase64(encrypted);
QSimpleCrypto::QBlockCipher cipher;
responseBody = cipher.decryptAesBlockCipher(ba, decKey, iv);
} else {
responseBody = encrypted;
}
} catch (...) {
Utils::logException();
qCritical() << "error decrypting payload";
QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
return;
}
QJsonArray endpointsArray = QJsonDocument::fromJson(responseBody).array();
QStringList endpoints;
for (const QJsonValue &endpoint : endpointsArray)
endpoints.push_back(endpoint.toString());
QStringList shuffled = endpoints;
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(shuffled.begin(), shuffled.end(), generator);
onComplete(shuffled);
return;
}
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qDebug() << httpStatusCode;
qDebug() << "go to the next storage endpoint";
reply->deleteLater();
QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
});
}
void GatewayController::getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete)
{
if (currentProxyIndex >= proxyUrls.size()) {
onComplete("");
return;
}
QNetworkRequest request;
request.setTransferTimeout(1000);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setUrl(proxyUrls[currentProxyIndex] + "lmbd-health");
QNetworkReply *reply = amnApp->networkManager()->get(request);
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) {
// *(state->sslErrors) = e;
// });
connect(reply, &QNetworkReply::finished, this, [this, proxyUrls, currentProxyIndex, onComplete, reply]() {
reply->deleteLater();
if (reply->error() == QNetworkReply::NoError) {
m_proxyUrl = proxyUrls[currentProxyIndex];
onComplete(m_proxyUrl);
return;
}
qDebug() << "go to the next proxy endpoint";
QMetaObject::invokeMethod(this, [=]() { getProxyUrlAsync(proxyUrls, currentProxyIndex + 1, onComplete); }, Qt::QueuedConnection);
});
}
void GatewayController::bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
std::function<void(const QByteArray &, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete)
{
auto sslErrors = QSharedPointer<QList<QSslError>>::create();
if (proxyUrl.isEmpty()) {
onComplete(QByteArray(), *sslErrors, QNetworkReply::InternalServerError, "empty proxy url", 0);
return;
}
QNetworkRequest request = encRequestData.request;
request.setUrl(endpoint.arg(proxyUrl));
QNetworkReply *reply = amnApp->networkManager()->post(request, encRequestData.requestBody);
connect(reply, &QNetworkReply::sslErrors, this, [sslErrors](const QList<QSslError> &errors) { *sslErrors = errors; });
connect(reply, &QNetworkReply::finished, this, [sslErrors, onComplete, reply]() {
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
reply->deleteLater();
onComplete(encryptedResponseBody, *sslErrors, replyError, replyErrorString, httpStatusCode);
});
}
@@ -1,8 +1,12 @@
#ifndef GATEWAYCONTROLLER_H
#define GATEWAYCONTROLLER_H
#include <QFuture>
#include <QNetworkReply>
#include <QObject>
#include <QPair>
#include <QPromise>
#include <QSharedPointer>
#include "core/defs.h"
@@ -19,8 +23,21 @@ public:
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
private:
struct EncryptedRequestData
{
QNetworkRequest request;
QByteArray requestBody;
QByteArray key;
QByteArray iv;
QByteArray salt;
amnezia::ErrorCode errorCode;
};
EncryptedRequestData prepareRequest(const QString &endpoint, const QJsonObject &apiPayload);
QStringList getProxyUrls(const QString &serviceType, const QString &userCountryCode);
bool shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &responseBody, bool checkEncryption,
const QByteArray &key = "", const QByteArray &iv = "", const QByteArray &salt = "");
@@ -28,6 +45,13 @@ private:
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
std::function<void(const QStringList &)> onComplete);
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
void bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
std::function<void(const QByteArray &, const QList<QSslError> &, QNetworkReply::NetworkError, const QString &, int)> onComplete);
int m_requestTimeoutMsecs;
QString m_gatewayEndpoint;
bool m_isDevEnvironment = false;
@@ -99,7 +99,9 @@ bool AndroidController::initialize()
{"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onFileOpened)},
{"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onConfigImported)},
{"onAuthResult", "(Z)V", reinterpret_cast<void *>(onAuthResult)},
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)}
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)},
{"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)},
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)}
};
QJniEnvironment env;
@@ -536,3 +538,23 @@ bool AndroidController::decodeQrCode(JNIEnv *env, jobject thiz, jstring data)
return ImportController::decodeQrCode(AndroidUtils::convertJString(env, data));
}
// static
void AndroidController::onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
qDebug() << "Android IME insets changed: height =" << heightDp << "dp";
emit AndroidController::instance()->imeInsetsChanged(heightDp);
}
// static
void AndroidController::onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
qDebug() << "Android system bars insets changed: nav bar =" << navBarHeightDp << "dp, status bar =" << statusBarHeightDp << "dp";
emit AndroidController::instance()->systemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp);
}
@@ -73,6 +73,8 @@ signals:
void importConfigFromOutside(QString config);
void initConnectionState(Vpn::ConnectionState state);
void authenticationResult(bool result);
void imeInsetsChanged(int heightDp);
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
private:
bool isWaitingStatus = true;
@@ -101,6 +103,8 @@ private:
static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri);
static void onAuthResult(JNIEnv *env, jobject thiz, jboolean result);
static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);
static void onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp);
static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp);
template <typename Ret, typename ...Args>
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);
+1
View File
@@ -247,6 +247,7 @@
<file>ui/qml/Components/OtpCodeDrawer.qml</file>
<file>ui/qml/Components/AwgTextField.qml</file>
<file>ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml</file>
<file>ui/qml/Components/SmartScroll.qml</file>
</qresource>
<qresource prefix="/countriesFlags">
<file>images/flagKit/ZW.svg</file>
+56 -41
View File
@@ -4,9 +4,8 @@
<context>
<name>AdLabel</name>
<message>
<location filename="../ui/qml/Components/AdLabel.qml" line="57"/>
<source>Amnezia Premium - for access to all websites and online resources</source>
<translation>Amnezia Premium - доступ ко всем сайтам и онлайн ресурсам</translation>
<translation type="vanished">Amnezia Premium - доступ ко всем сайтам и онлайн ресурсам</translation>
</message>
</context>
<context>
@@ -61,7 +60,7 @@
<name>ApiAccountInfoModel</name>
<message>
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="31"/>
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="34"/>
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="35"/>
<source>Active</source>
<translation>Активна</translation>
</message>
@@ -71,35 +70,33 @@
<translation>Не активна</translation>
</message>
<message>
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="47"/>
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="48"/>
<source>%1 out of %2</source>
<translation>%1 из %2</translation>
</message>
<message>
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="51"/>
<source>Classic VPN for seamless work, downloading large files, and watching videos. Access all websites and online resources. Speeds up to 200 Mbps</source>
<translation>Классический VPN для комфортной работы, загрузки больших файлов и просмотра видео. Доступ ко всем сайтам и онлайн-ресурсам. Скорость — до 200 Мбит/с</translation>
<translation type="vanished">Классический VPN для комфортной работы, загрузки больших файлов и просмотра видео. Доступ ко всем сайтам и онлайн-ресурсам. Скорость — до 200 Мбит/с</translation>
</message>
<message>
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="55"/>
<source>Free unlimited access to a basic set of websites such as Facebook, Instagram, Twitter (X), Discord, Telegram and more. YouTube is not included in the free plan.</source>
<translation>Бесплатный неограниченный доступ к базовому набору сайтов и приложений, таким как Facebook, Instagram, Twitter (X), Discord, Telegram и другим. YouTube не включён в бесплатный тариф.</translation>
<translation type="vanished">Бесплатный неограниченный доступ к базовому набору сайтов и приложений, таким как Facebook, Instagram, Twitter (X), Discord, Telegram и другим. YouTube не включён в бесплатный тариф.</translation>
</message>
</context>
<context>
<name>ApiConfigsController</name>
<message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="413"/>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="448"/>
<source>%1 installed successfully.</source>
<translation>%1 успешно установлен.</translation>
</message>
<message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="473"/>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="513"/>
<source>API config reloaded</source>
<translation>Конфигурация API перезагружена</translation>
</message>
<message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="477"/>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="517"/>
<source>Successfully changed the country of connection to %1</source>
<translation>Страна подключения изменена на %1</translation>
</message>
@@ -627,27 +624,32 @@ Thank you for staying with us!</source>
<translation>Продолжить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="102"/>
<location filename="../ui/qml/Pages2/PageHome.qml" line="92"/>
<source>Logging enabled</source>
<translation>Логирование включено</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="144"/>
<location filename="../ui/qml/Pages2/PageHome.qml" line="120"/>
<source>Dev gateway enabled</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="162"/>
<source>Split tunneling enabled</source>
<translation>Раздельное туннелирование включено</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="144"/>
<location filename="../ui/qml/Pages2/PageHome.qml" line="162"/>
<source>Split tunneling disabled</source>
<translation>Раздельное туннелирование выключено</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="381"/>
<location filename="../ui/qml/Pages2/PageHome.qml" line="409"/>
<source>VPN protocol</source>
<translation>VPN-протокол</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="434"/>
<location filename="../ui/qml/Pages2/PageHome.qml" line="462"/>
<source>Servers</source>
<translation>Серверы</translation>
</message>
@@ -1579,32 +1581,37 @@ Thank you for staying with us!</source>
<translation>Настройки</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="101"/>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="102"/>
<source>Servers</source>
<translation>Серверы</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="112"/>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="113"/>
<source>Connection</source>
<translation>Соединение</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="123"/>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="124"/>
<source>Application</source>
<translation>Приложение</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="134"/>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="135"/>
<source>News &amp; Notifications</source>
<translation>Новости и Уведомления</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="152"/>
<source>Backup</source>
<translation>Резервное копирование</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="145"/>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="163"/>
<source>About AmneziaVPN</source>
<translation>Об AmneziaVPN</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="156"/>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="174"/>
<source>Dev console</source>
<translation>Dev console</translation>
</message>
@@ -2763,6 +2770,14 @@ Thank you for staying with us!</source>
<translation>Очистить логи</translation>
</message>
</context>
<context>
<name>PageSettingsNewsNotifications</name>
<message>
<location filename="../ui/qml/Pages2/PageSettingsNewsNotifications.qml" line="33"/>
<source>News &amp; Notifications</source>
<translation>Новости и Уведомления</translation>
</message>
</context>
<context>
<name>PageSettingsServerData</name>
<message>
@@ -3012,13 +3027,13 @@ Thank you for staying with us!</source>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="210"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="358"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="357"/>
<source>Continue</source>
<translation>Продолжить</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="211"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="359"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="358"/>
<source>Cancel</source>
<translation>Отменить</translation>
</message>
@@ -3059,8 +3074,8 @@ Thank you for staying with us!</source>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="332"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="459"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="472"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="458"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="471"/>
<source>Sites files (*.json)</source>
<translation>Файлы сайтов (*.json)</translation>
</message>
@@ -3070,33 +3085,33 @@ Thank you for staying with us!</source>
<translation>Очистить список сайтов</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="356"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="355"/>
<source>Clear site list?</source>
<translation>Очистить список сайтов?</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="357"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="356"/>
<source>All sites will be removed from list.</source>
<translation>Все сайты будут удалены из списка.</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="421"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="420"/>
<source>Import a list of sites</source>
<translation>Импортировать список с сайтами</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="456"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="455"/>
<source>Replace site list</source>
<translation>Заменить список с сайтами</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="458"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="471"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="457"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="470"/>
<source>Open sites file</source>
<translation>Открыть список с сайтами</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="469"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="468"/>
<source>Add imported sites to existing ones</source>
<translation>Добавить импортированные сайты к существующим</translation>
</message>
@@ -3521,32 +3536,32 @@ Thank you for staying with us!</source>
<context>
<name>PageSetupWizardViewConfig</name>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="70"/>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="72"/>
<source>New connection</source>
<translation>Новое соединение</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="110"/>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="112"/>
<source>Collapse content</source>
<translation>Свернуть</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="110"/>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="112"/>
<source>Show content</source>
<translation>Показать</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="127"/>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="129"/>
<source>Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider.</source>
<translation>Включить обфускацию WireGuard. Это может быть полезно, если WireGuard блокируется вашим провайдером.</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="158"/>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="160"/>
<source>Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data.</source>
<translation>Используйте файлы конфигурации только из тех источников, которым вы доверяете. Файлы из общедоступных источников могли быть созданы с целью перехвата ваших личных данных.</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="202"/>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="204"/>
<source>Connect</source>
<translation>Подключиться</translation>
</message>
@@ -4950,12 +4965,12 @@ FileZilla или другие SFTP-клиенты, а также смонтир
<context>
<name>SettingsController</name>
<message>
<location filename="../ui/controllers/settingsController.cpp" line="250"/>
<location filename="../ui/controllers/settingsController.cpp" line="258"/>
<source>All settings have been reset to default values</source>
<translation>Все настройки сброшены до значений по умолчанию</translation>
</message>
<message>
<location filename="../ui/controllers/settingsController.cpp" line="227"/>
<location filename="../ui/controllers/settingsController.cpp" line="235"/>
<source>Backup file is corrupted</source>
<translation>Файл резервной копии поврежден</translation>
</message>
@@ -29,7 +29,6 @@ namespace
constexpr char uuid[] = "installation_uuid";
constexpr char osVersion[] = "os_version";
constexpr char appVersion[] = "app_version";
constexpr char appLanguage[] = "app_language";
constexpr char userCountryCode[] = "user_country_code";
constexpr char serverCountryCode[] = "server_country_code";
@@ -65,6 +64,7 @@ namespace
{
QString osVersion;
QString appVersion;
QString appLanguage;
QString installationUuid;
@@ -84,6 +84,9 @@ namespace
if (!appVersion.isEmpty()) {
obj[configKey::appVersion] = appVersion;
}
if (!appLanguage.isEmpty()) {
obj[apiDefs::key::appLanguage] = appLanguage;
}
if (!installationUuid.isEmpty()) {
obj[configKey::uuid] = installationUuid;
}
@@ -223,6 +226,9 @@ namespace
if (newServerConfig.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway) {
apiConfig.insert(apiDefs::key::supportedProtocols,
QJsonDocument::fromJson(apiResponseBody).object().value(apiDefs::key::supportedProtocols).toArray());
apiConfig.insert(apiDefs::key::serviceInfo,
QJsonDocument::fromJson(apiResponseBody).object().value(apiDefs::key::serviceInfo).toObject());
}
serverConfig[configKey::apiConfig] = apiConfig;
@@ -285,6 +291,7 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode,
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true),
apiConfigObject.value(configKey::userCountryCode).toString(),
serverCountryCode,
@@ -325,6 +332,7 @@ bool ApiConfigsController::revokeNativeConfig(const QString &serverCountryCode)
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true),
apiConfigObject.value(configKey::userCountryCode).toString(),
serverCountryCode,
@@ -375,7 +383,7 @@ bool ApiConfigsController::fillAvailableServices()
{
QJsonObject apiPayload;
apiPayload[configKey::osVersion] = QSysInfo::productType();
apiPayload[configKey::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/services"), apiPayload, responseBody);
@@ -399,6 +407,7 @@ bool ApiConfigsController::importServiceFromGateway()
{
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true),
m_apiServicesModel->getCountryCode(),
"",
@@ -457,6 +466,7 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true),
apiConfig.value(configKey::userCountryCode).toString(),
newCountryCode,
@@ -577,6 +587,7 @@ bool ApiConfigsController::deactivateDevice(const bool isRemoveEvent)
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true),
apiConfigObject.value(configKey::userCountryCode).toString(),
apiConfigObject.value(configKey::serverCountryCode).toString(),
@@ -616,6 +627,7 @@ bool ApiConfigsController::deactivateExternalDevice(const QString &uuid, const Q
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
uuid,
apiConfigObject.value(configKey::userCountryCode).toString(),
serverCountryCode,
+24 -20
View File
@@ -19,7 +19,7 @@ ApiNewsController::ApiNewsController(const QSharedPointer<NewsModel> &newsModel,
{
}
void ApiNewsController::fetchNews()
void ApiNewsController::fetchNews(bool showError)
{
if (m_serversModel.isNull()) {
qWarning() << "ServersModel is null, skip fetchNews";
@@ -30,9 +30,9 @@ void ApiNewsController::fetchNews()
qDebug() << "No Gateway stacks, skip fetchNews";
return;
}
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
QByteArray responseBody;
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
QJsonObject payload;
payload.insert("locale", m_settings->getAppLanguage().name().split("_").first());
@@ -44,22 +44,26 @@ void ApiNewsController::fetchNews()
payload.insert(configKey::serviceType, stacksJson.value(configKey::serviceType));
}
ErrorCode errorCode = gatewayController.post(QString("%1v1/news"), payload, responseBody);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return;
}
QJsonDocument doc = QJsonDocument::fromJson(responseBody);
QJsonArray newsArray;
if (doc.isArray()) {
newsArray = doc.array();
} else if (doc.isObject()) {
QJsonObject obj = doc.object();
if (obj.value("news").isArray()) {
newsArray = obj.value("news").toArray();
auto future = gatewayController->postAsync(QString("%1v1/news"), payload);
future.then(this, [this, showError, gatewayController](QPair<ErrorCode, QByteArray> result) {
auto [errorCode, responseBody] = result;
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode, showError);
return;
}
}
m_newsModel->updateModel(newsArray);
QJsonDocument doc = QJsonDocument::fromJson(responseBody);
QJsonArray newsArray;
if (doc.isArray()) {
newsArray = doc.array();
} else if (doc.isObject()) {
QJsonObject obj = doc.object();
if (obj.value("news").isArray()) {
newsArray = obj.value("news").toArray();
}
}
m_newsModel->updateModel(newsArray);
emit fetchNewsFinished();
});
}
@@ -19,10 +19,11 @@ public:
explicit ApiNewsController(const QSharedPointer<NewsModel> &newsModel, const std::shared_ptr<Settings> &settings,
const QSharedPointer<ServersModel> &serversModel, QObject *parent = nullptr);
Q_INVOKABLE void fetchNews();
Q_INVOKABLE void fetchNews(bool showError);
signals:
void errorOccurred(ErrorCode errorCode);
void errorOccurred(ErrorCode errorCode, bool showError);
void fetchNewsFinished();
private:
QSharedPointer<NewsModel> m_newsModel;
@@ -82,7 +82,7 @@ void ApiPremV1MigrationController::sendMigrationCode(const int subscriptionIndex
{
QEventLoop wait;
QTimer::singleShot(1000, &wait, &QEventLoop::quit);
wait.exec();
wait.exec(QEventLoop::ExcludeUserInputEvents);
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
@@ -46,7 +46,7 @@ bool ApiSettingsController::getAccountInfo(bool reload)
if (reload) {
QEventLoop wait;
QTimer::singleShot(1000, &wait, &QEventLoop::quit);
wait.exec();
wait.exec(QEventLoop::ExcludeUserInputEvents);
}
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), requestTimeoutMsecs,
@@ -62,6 +62,7 @@ bool ApiSettingsController::getAccountInfo(bool reload)
apiPayload[configKey::serviceType] = apiConfig.value(configKey::serviceType).toString();
apiPayload[configKey::authData] = authData;
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
QByteArray responseBody;
+26 -1
View File
@@ -1,6 +1,7 @@
#include "settingsController.h"
#include <QStandardPaths>
#include <QOperatingSystemVersion>
#include "logger.h"
#include "systemController.h"
@@ -33,6 +34,17 @@ SettingsController::SettingsController(const QSharedPointer<ServersModel> &serve
checkIfNeedDisableLogs();
#ifdef Q_OS_ANDROID
connect(AndroidController::instance(), &AndroidController::notificationStateChanged, this, &SettingsController::onNotificationStateChanged);
connect(AndroidController::instance(), &AndroidController::imeInsetsChanged, this, [this](int heightDp) {
m_imeHeight = heightDp;
emit imeHeightChanged(heightDp);
emit safeAreaBottomMarginChanged();
});
connect(AndroidController::instance(), &AndroidController::systemBarsInsetsChanged, this, [this](int navBarHeightDp, int statusBarHeightDp) {
m_cachedNavigationBarHeight = navBarHeightDp;
m_cachedStatusBarHeight = statusBarHeightDp;
emit safeAreaBottomMarginChanged();
emit safeAreaTopMarginChanged();
});
#endif
m_isDevModeEnabled = m_settings->isDevGatewayEnv();
@@ -434,7 +446,11 @@ bool SettingsController::isOnTv()
bool SettingsController::isEdgeToEdgeEnabled()
{
#ifdef Q_OS_ANDROID
return AndroidController::instance()->isEdgeToEdgeEnabled();
if (!m_edgeToEdgeCached) {
m_cachedEdgeToEdgeEnabled = AndroidController::instance()->isEdgeToEdgeEnabled();
m_edgeToEdgeCached = true;
}
return m_cachedEdgeToEdgeEnabled;
#else
return false;
#endif
@@ -480,6 +496,10 @@ int SettingsController::getSafeAreaBottomMargin()
{
#ifdef Q_OS_ANDROID
if (isEdgeToEdgeEnabled()) {
if (m_imeHeight > 0) {
return 0;
}
int height = getNavigationBarHeight();
int result = height > 0 ? height : 56; // fallback to 56 if system returns 0
return result;
@@ -488,6 +508,11 @@ int SettingsController::getSafeAreaBottomMargin()
return 0;
}
int SettingsController::getImeHeight()
{
return m_imeHeight;
}
bool SettingsController::isHomeAdLabelVisible()
{
return m_settings->isHomeAdLabelVisible();
+11 -2
View File
@@ -33,8 +33,9 @@ public:
Q_PROPERTY(bool isHomeAdLabelVisible READ isHomeAdLabelVisible NOTIFY isHomeAdLabelVisibleChanged)
Q_PROPERTY(bool startMinimized READ isStartMinimizedEnabled NOTIFY startMinimizedChanged)
Q_PROPERTY(int safeAreaTopMargin READ getSafeAreaTopMargin CONSTANT)
Q_PROPERTY(int safeAreaBottomMargin READ getSafeAreaBottomMargin CONSTANT)
Q_PROPERTY(int safeAreaTopMargin READ getSafeAreaTopMargin NOTIFY safeAreaTopMarginChanged)
Q_PROPERTY(int safeAreaBottomMargin READ getSafeAreaBottomMargin NOTIFY safeAreaBottomMarginChanged)
Q_PROPERTY(int imeHeight READ getImeHeight NOTIFY imeHeightChanged)
public slots:
void toggleAmneziaDns(bool enable);
@@ -103,6 +104,7 @@ public slots:
int getNavigationBarHeight();
int getSafeAreaTopMargin();
int getSafeAreaBottomMargin();
int getImeHeight();
bool isHomeAdLabelVisible();
void disableHomeAdLabel();
@@ -131,6 +133,10 @@ signals:
void devModeEnabled();
void gatewayEndpointChanged(const QString &endpoint);
void devGatewayEnvChanged(bool enabled);
void imeHeightChanged(int height);
void safeAreaTopMarginChanged();
void safeAreaBottomMarginChanged();
void isHomeAdLabelVisibleChanged(bool visible);
void startMinimizedChanged();
@@ -144,6 +150,9 @@ private:
mutable int m_cachedStatusBarHeight = -1;
mutable int m_cachedNavigationBarHeight = -1;
mutable bool m_cachedEdgeToEdgeEnabled = false;
mutable bool m_edgeToEdgeCached = false;
int m_imeHeight = 0;
std::shared_ptr<Settings> m_settings;
QString m_appVersion;
+5 -11
View File
@@ -31,7 +31,8 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
return tr("Active");
}
return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("<p><a style=\"color: #EB5757;\">Inactive</a>") : tr("Active");
return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("<p><a style=\"color: #EB5757;\">Inactive</a>")
: tr("Active");
}
case EndDateRole: {
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) {
@@ -47,16 +48,7 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
return tr("%1 out of %2").arg(m_accountInfoData.activeDeviceCount).arg(m_accountInfoData.maxDeviceCount);
}
case ServiceDescriptionRole: {
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2) {
return tr("Classic VPN for seamless work, downloading large files, and watching videos. Access all websites and online "
"resources. "
"Speeds up to 200 Mbps");
} else if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) {
return tr("Free unlimited access to a basic set of websites such as Facebook, Instagram, Twitter (X), Discord, Telegram and "
"more. YouTube is not included in the free plan.");
} else {
return "";
}
return m_accountInfoData.subscriptionDescription;
}
case IsComponentVisibleRole: {
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2
@@ -101,6 +93,8 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons
accountInfoData.configType = apiUtils::getConfigType(serverConfig);
accountInfoData.subscriptionDescription = accountInfoObject.value(apiDefs::key::subscriptionDescription).toString();
for (const auto &protocol : accountInfoObject.value(apiDefs::key::supportedProtocols).toArray()) {
accountInfoData.supportedProtocols.push_back(protocol.toString());
}
@@ -54,6 +54,8 @@ private:
apiDefs::ConfigType configType;
QStringList supportedProtocols;
QString subscriptionDescription;
};
AccountInfoData m_accountInfoData;
+39 -6
View File
@@ -158,6 +158,18 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
QString primaryDns = server.value(config_key::dns1).toString();
return primaryDns == protocols::dns::amneziaDnsIp;
}
case IsAdVisibleRole:{
return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::isAdVisible).toBool(false);
}
case AdHeaderRole: {
return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::adHeader).toString();
}
case AdDescriptionRole: {
return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::adDescription).toString();
}
case AdEndpointRole: {
return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::adEndpoint).toString();
}
}
return QVariant();
@@ -403,6 +415,12 @@ QHash<int, QByteArray> ServersModel::roleNames() const
roles[IsCountrySelectionAvailableRole] = "isCountrySelectionAvailable";
roles[ApiAvailableCountriesRole] = "apiAvailableCountries";
roles[ApiServerCountryCodeRole] = "apiServerCountryCode";
roles[IsAdVisibleRole] = "isAdVisible";
roles[AdHeaderRole] = "adHeader";
roles[AdDescriptionRole] = "adDescription";
roles[AdEndpointRole] = "adEndpoint";
return roles;
}
@@ -784,22 +802,22 @@ void ServersModel::recomputeGatewayStacks()
const bool wasEmpty = m_gatewayStacks.isEmpty();
GatewayStacks computed;
bool hasNewTags = false;
for (int i = 0; i < m_servers.count(); ++i) {
if (data(i, IsServerFromGatewayApiRole).toBool()) {
const QJsonObject server = m_servers.at(i).toObject();
const QJsonObject apiConfig = server.value(configKey::apiConfig).toObject();
const QString userCountryCode = apiConfig.value(configKey::userCountryCode).toString();
const QString serviceType = apiConfig.value(configKey::serviceType).toString();
if (!userCountryCode.isEmpty()) {
if (!m_gatewayStacks.userCountryCodes.contains(userCountryCode)) {
hasNewTags = true;
}
computed.userCountryCodes.insert(userCountryCode);
}
if (!serviceType.isEmpty()) {
if (!m_gatewayStacks.serviceTypes.contains(serviceType)) {
hasNewTags = true;
@@ -808,12 +826,12 @@ void ServersModel::recomputeGatewayStacks()
}
}
}
m_gatewayStacks = std::move(computed);
if (hasNewTags) {
emit gatewayStacksExpanded();
}
if (wasEmpty != m_gatewayStacks.isEmpty()) {
emit hasServersFromGatewayApiChanged();
}
@@ -885,3 +903,18 @@ bool ServersModel::processedServerIsPremium() const
{
return apiUtils::isPremiumServer(getServerConfig(m_processedServerIndex));
}
bool ServersModel::isAdVisible()
{
return data(m_defaultServerIndex, IsAdVisibleRole).toBool();
}
QString ServersModel::adHeader()
{
return data(m_defaultServerIndex, AdHeaderRole).toString();
}
QString ServersModel::adDescription()
{
return data(m_defaultServerIndex, AdDescriptionRole).toString();
}
+12
View File
@@ -47,6 +47,10 @@ public:
IsCountrySelectionAvailableRole,
ApiAvailableCountriesRole,
ApiServerCountryCodeRole,
IsAdVisibleRole,
AdHeaderRole,
AdDescriptionRole,
AdEndpointRole,
HasAmneziaDns
};
@@ -79,6 +83,10 @@ public:
Q_PROPERTY(int processedIndex READ getProcessedServerIndex WRITE setProcessedServerIndex NOTIFY processedServerIndexChanged)
Q_PROPERTY(bool processedServerIsPremium READ processedServerIsPremium NOTIFY processedServerChanged)
Q_PROPERTY(bool isAdVisible READ isAdVisible NOTIFY defaultServerIndexChanged)
Q_PROPERTY(QString adHeader READ adHeader NOTIFY defaultServerIndexChanged)
Q_PROPERTY(QString adDescription READ adDescription NOTIFY defaultServerIndexChanged)
bool processedServerIsPremium() const;
public slots:
@@ -144,6 +152,10 @@ public slots:
bool isApiKeyExpired(const int serverIndex);
void removeApiConfig(const int serverIndex);
bool isAdVisible();
QString adHeader();
QString adDescription();
protected:
QHash<int, QByteArray> roleNames() const override;
+113 -36
View File
@@ -2,7 +2,6 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Shapes
import Qt5Compat.GraphicalEffects
import Style 1.0
@@ -13,61 +12,139 @@ import "../Controls2/TextTypes"
Rectangle {
id: root
property real contentHeight: ad.implicitHeight + ad.anchors.topMargin + ad.anchors.bottomMargin
property real contentHeight: content.implicitHeight + content.anchors.topMargin + content.anchors.bottomMargin
property bool isFocusable: true
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: AmneziaStyle.color.translucentSlateGray }
GradientStop { position: 1.0; color: AmneziaStyle.color.translucentOnyxBlack }
}
border.width: 1
border.color: AmneziaStyle.color.goldenApricot
color: AmneziaStyle.color.transparent
border.color: AmneziaStyle.color.onyxBlack
radius: 13
visible: false
// visible: GC.isDesktop() && ServersModel.isDefaultServerFromApi
// && ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && SettingsController.isHomeAdLabelVisible
visible: ServersModel.isAdVisible
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
Keys.onTabPressed: {
FocusController.nextKeyTabItem()
}
onClicked: function() {
Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl("premium"))
}
Keys.onBacktabPressed: {
FocusController.previousKeyTabItem()
}
Keys.onUpPressed: {
FocusController.nextKeyUpItem()
}
Keys.onDownPressed: {
FocusController.nextKeyDownItem()
}
Keys.onLeftPressed: {
FocusController.nextKeyLeftItem()
}
Keys.onRightPressed: {
FocusController.nextKeyRightItem()
}
Keys.onEnterPressed: {
Qt.openUrlExternally(ServersModel.getDefaultServerData("adEndpoint"))
}
Keys.onReturnPressed: {
Qt.openUrlExternally(ServersModel.getDefaultServerData("adEndpoint"))
}
RowLayout {
id: ad
id: content
anchors.fill: parent
anchors.margins: 16
anchors.leftMargin: 16
anchors.rightMargin: 12
anchors.topMargin: 12
anchors.bottomMargin: 12
spacing: 20
Image {
source: "qrc:/images/controls/amnezia.svg"
sourceSize: Qt.size(36, 36)
ColumnLayout {
Layout.fillWidth: true
spacing: 4
layer {
effect: ColorOverlay {
color: AmneziaStyle.color.paleGray
}
CaptionTextType {
Layout.fillWidth: true
text: ServersModel.adHeader
color: AmneziaStyle.color.paleGray
font.pixelSize: 14
font.weight: 700
textFormat: Text.RichText
}
CaptionTextType {
Layout.fillWidth: true
text: ServersModel.adDescription
color: AmneziaStyle.color.mutedGray
wrapMode: Text.WordWrap
lineHeight: 18
lineHeightMode: Text.FixedHeight
font.pixelSize: 14
visible: text !== ""
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.rightMargin: 10
Layout.leftMargin: 10
Item {
implicitWidth: 40
implicitHeight: 40
Layout.alignment: Qt.AlignVCenter
text: qsTr("Amnezia Premium - for access to all websites and online resources")
color: AmneziaStyle.color.pearlGray
Rectangle {
id: chevronBackground
anchors.fill: parent
radius: 12
color: AmneziaStyle.color.transparent
border.width: root.activeFocus ? 1 : 0
border.color: AmneziaStyle.color.paleGray
lineHeight: 18
font.pixelSize: 15
}
Behavior on color {
PropertyAnimation { duration: 200 }
}
ImageButtonType {
image: "qrc:/images/controls/close.svg"
imageColor: AmneziaStyle.color.paleGray
Behavior on border.width {
PropertyAnimation { duration: 200 }
}
}
onClicked: function() {
SettingsController.disableHomeAdLabel()
Image {
anchors.centerIn: parent
source: "qrc:/images/controls/chevron-right.svg"
sourceSize: Qt.size(24, 24)
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: {
chevronBackground.color = AmneziaStyle.color.slateGray
}
onExited: {
chevronBackground.color = AmneziaStyle.color.transparent
}
onPressedChanged: {
chevronBackground.color = pressed ? AmneziaStyle.color.charcoalGray : containsMouse ? AmneziaStyle.color.slateGray : AmneziaStyle.color.transparent
}
onClicked: function() {
root.forceActiveFocus()
Qt.openUrlExternally(ServersModel.getDefaultServerData("adEndpoint"))
}
}
}
+55
View File
@@ -0,0 +1,55 @@
import QtQuick
import QtQuick.Controls
QtObject {
id: root
property var listView: null
property var scrollToItemTarget: null
property Connections imeConnection: Connections {
target: SettingsController
function onImeHeightChanged() {
if (root.scrollToItemTarget && SettingsController.imeHeight > 0) {
scrollTimer.restart()
}
}
}
property Timer scrollTimer: Timer {
interval: 100
repeat: false
onTriggered: {
if (root.scrollToItemTarget && root.listView) {
if (SettingsController.imeHeight > 0) {
var item = root.scrollToItemTarget
var itemY = item.mapToItem(root.listView.contentItem, 0, 0).y
var itemHeight = item.height
var keyboardHeight = SettingsController.imeHeight + SettingsController.safeAreaBottomMargin
var visibleHeight = root.listView.height - keyboardHeight
var desiredTopOffset = visibleHeight * 0.25
var targetContentY = itemY - desiredTopOffset
if (targetContentY < 0) {
targetContentY = 0
}
var maxContentY = root.listView.contentHeight - root.listView.height
if (targetContentY > maxContentY) {
targetContentY = maxContentY
}
root.listView.contentY = targetContentY
root.scrollToItemTarget = null
}
}
}
}
function scrollToItem(item) {
scrollToItemTarget = item
scrollTimer.restart()
}
}
+4 -7
View File
@@ -1,33 +1,30 @@
import QtQuick
import QtQuick.Controls
import Qt.labs.platform
Menu {
property var textObj
popupType: Popup.Native
MenuItem {
text: qsTr("C&ut")
shortcut: StandardKey.Cut
enabled: textObj.selectedText
onTriggered: textObj.cut()
}
MenuItem {
text: qsTr("&Copy")
shortcut: StandardKey.Copy
enabled: textObj.selectedText
onTriggered: textObj.copy()
}
MenuItem {
text: qsTr("&Paste")
shortcut: StandardKey.Paste
// Fix calling paste from clipboard when launching app on android/ios
enabled: (Qt.platform.os === "android" || Qt.platform.os === "ios") ? true : textObj.canPaste
// Fix calling paste from clipboard when launching app on android
enabled: Qt.platform.os === "android" ? true : textObj.canPaste
onTriggered: textObj.paste()
}
MenuItem {
text: qsTr("&SelectAll")
shortcut: StandardKey.SelectAll
enabled: textObj.length > 0
onTriggered: textObj.selectAll()
}
+16
View File
@@ -49,6 +49,22 @@ Item {
return drawerContent.state === stateName
}
Connections {
target: Qt.application
function onStateChanged() {
if (Qt.application.state !== Qt.ApplicationActive) {
if (dragArea.drag.active) {
dragArea.drag.target = null
dragArea.drag.target = drawerContent
}
if (isOpened && !isCollapsedStateActive()) {
root.closeTriggered()
}
}
}
}
Connections {
target: PageController
+12
View File
@@ -74,6 +74,18 @@ Item {
FocusController.nextKeyRightItem()
}
Connections {
target: Qt.application
function onStateChanged() {
if (Qt.application.state !== Qt.ApplicationActive) {
if (!menu.isClosed) {
menu.closeTriggered()
}
}
}
}
implicitWidth: rootButtonContent.implicitWidth
implicitHeight: rootButtonContent.implicitHeight
+2 -14
View File
@@ -93,25 +93,13 @@ Rectangle {
wrapMode: Text.Wrap
MouseArea {
id: textAreaMouse
anchors.fill: parent
acceptedButtons: Qt.RightButton
hoverEnabled: true
onClicked: {
fl.interactive = true
contextMenu.open()
}
ContextMenu.menu: ContextMenuType {
textObj: textArea
}
onFocusChanged: {
root.border.color = getBorderColor(borderNormalColor)
}
ContextMenuType {
id: contextMenu
textObj: textArea
}
}
}
@@ -79,25 +79,13 @@ Rectangle {
wrapMode: Text.Wrap
MouseArea {
id: textAreaMouse
anchors.fill: parent
acceptedButtons: Qt.RightButton
hoverEnabled: true
onClicked: {
fl.interactive = true
contextMenu.open()
}
ContextMenu.menu: ContextMenuType {
textObj: textArea
}
onFocusChanged: {
root.border.color = getBorderColor(borderNormalColor)
}
ContextMenuType {
id: contextMenu
textObj: textArea
}
}
RowLayout {
@@ -19,6 +19,9 @@ Item {
property string buttonText
property string buttonImageSource
property string buttonImageColor: AmneziaStyle.color.midnightBlack
property string buttonBackgroundColor: AmneziaStyle.color.paleGray
property string buttonHoveredColor: AmneziaStyle.color.lightGray
property var clickedFunc
property alias textField: textField
@@ -48,7 +51,7 @@ Item {
Keys.onUpPressed: {
FocusController.nextKeyUpItem()
}
Keys.onDownPressed: {
FocusController.nextKeyDownItem()
}
@@ -67,7 +70,7 @@ Item {
border.width: 1
Behavior on border.color {
PropertyAnimation { duration: 200 }
PropertyAnimation { duration: 100 }
}
RowLayout {
@@ -134,15 +137,7 @@ Item {
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: contextMenu.open()
enabled: true
}
ContextMenuType {
id: contextMenu
ContextMenu.menu: ContextMenuType {
textObj: textField
}
@@ -194,6 +189,14 @@ Item {
focusPolicy: Qt.NoFocus
text: root.buttonText
leftImageSource: root.buttonImageSource
leftImageColor: root.buttonImageColor
defaultColor: root.buttonBackgroundColor
hoveredColor: root.buttonHoveredColor
pressedColor: root.buttonHoveredColor
disabledColor: AmneziaStyle.color.transparent
borderWidth: 0
anchors.top: content.top
anchors.bottom: content.bottom
@@ -201,7 +204,7 @@ Item {
height: content.implicitHeight
width: content.implicitHeight
squareLeftSide: true
squareLeftSide: false
clickedFunc: function() {
if (root.clickedFunc && typeof root.clickedFunc === "function") {
@@ -28,5 +28,7 @@ QtObject {
readonly property color cloudyGray: Qt.rgba(215/255, 216/255, 219/255, 0.65)
readonly property color pearlGray: '#EAEAEC'
readonly property color translucentRichBrown: Qt.rgba(99/255, 51/255, 3/255, 0.26)
readonly property color translucentSlateGray: Qt.rgba(85/255, 86/255, 92/255, 0.13)
readonly property color translucentOnyxBlack: Qt.rgba(28/255, 29/255, 33/255, 0.13)
}
}
+1 -1
View File
@@ -45,7 +45,7 @@ PageType {
BaseHeaderType {
Layout.fillWidth: true
Layout.topMargin: 20
Layout.topMargin: 20 + SettingsController.safeAreaTopMargin
Layout.leftMargin: 16
Layout.rightMargin: 16
+1 -1
View File
@@ -22,7 +22,7 @@ PageType {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
}
ListViewType {
+25 -10
View File
@@ -20,6 +20,21 @@ import "../Components"
PageType {
id: root
Connections {
target: Qt.application
function onStateChanged() {
if (Qt.application.state !== Qt.ApplicationActive) {
if (drawer.isOpened) {
drawer.closeTriggered()
}
if (homeSplitTunnelingDrawer.isOpened) {
homeSplitTunnelingDrawer.closeTriggered()
}
}
}
}
Connections {
objectName: "pageControllerConnections"
@@ -71,16 +86,6 @@ PageType {
anchors.topMargin: 12 + SettingsController.safeAreaTopMargin
anchors.bottomMargin: 16
AdLabel {
id: adLabel
Layout.fillWidth: true
Layout.preferredHeight: adLabel.contentHeight
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 22
}
BasicButtonType {
id: loggingButton
objectName: "loggingButton"
@@ -189,6 +194,16 @@ PageType {
parent: root
}
}
AdLabel {
id: adLabel
Layout.fillWidth: true
Layout.preferredHeight: adLabel.contentHeight
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 22
}
}
}
@@ -22,7 +22,7 @@ PageType {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onActiveFocusChanged: {
if(backButton.enabled && backButton.activeFocus) {
@@ -31,6 +31,11 @@ PageType {
}
}
SmartScroll {
id: smartScroll
listView: listView
}
ListViewType {
id: listView
@@ -80,6 +85,13 @@ PageType {
clientMtu = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(mtuTextField)
}
}
checkEmptyText: true
}
@@ -97,6 +109,12 @@ PageType {
clientJunkPacketCount = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(junkPacketCountTextField)
}
}
}
AwgTextField {
@@ -113,6 +131,12 @@ PageType {
clientJunkPacketMinSize = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(junkPacketMinSizeTextField)
}
}
}
AwgTextField {
@@ -129,6 +153,12 @@ PageType {
clientJunkPacketMaxSize = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(junkPacketMaxSizeTextField)
}
}
}
AwgTextField {
@@ -147,6 +177,12 @@ PageType {
clientSpecialJunk1 = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk1TextField)
}
}
}
AwgTextField {
@@ -165,6 +201,12 @@ PageType {
clientSpecialJunk2 = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk2TextField)
}
}
}
AwgTextField {
@@ -183,6 +225,12 @@ PageType {
clientSpecialJunk3 = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk3TextField)
}
}
}
AwgTextField {
@@ -201,6 +249,12 @@ PageType {
clientSpecialJunk4 = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk4TextField)
}
}
}
AwgTextField {
@@ -219,6 +273,12 @@ PageType {
clientSpecialJunk5 = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk5TextField)
}
}
}
AwgTextField {
@@ -237,6 +297,12 @@ PageType {
clientControlledJunk1 = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(controlledJunk1TextField)
}
}
}
AwgTextField {
@@ -255,6 +321,12 @@ PageType {
clientControlledJunk2 = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(controlledJunk2TextField)
}
}
}
AwgTextField {
@@ -273,6 +345,12 @@ PageType {
clientControlledJunk3 = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(controlledJunk3TextField)
}
}
}
AwgTextField {
@@ -290,6 +368,12 @@ PageType {
clientSpecialHandshakeTimeout = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(iTimeTextField)
}
}
}
Header2TextType {
@@ -34,6 +34,11 @@ PageType {
}
}
SmartScroll {
id: smartScroll
listView: listView
}
ListViewType {
id: listView
@@ -81,6 +86,12 @@ PageType {
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(vpnAddressSubnetTextField)
}
}
checkEmptyText: true
}
@@ -104,6 +115,12 @@ PageType {
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(portTextField)
}
}
checkEmptyText: true
}
@@ -121,6 +138,12 @@ PageType {
serverJunkPacketCount = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(junkPacketCountTextField)
}
}
}
AwgTextField {
@@ -137,6 +160,12 @@ PageType {
serverJunkPacketMinSize = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(junkPacketMinSizeTextField)
}
}
}
AwgTextField {
@@ -153,6 +182,12 @@ PageType {
serverJunkPacketMaxSize = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(junkPacketMaxSizeTextField)
}
}
}
AwgTextField {
@@ -169,6 +204,12 @@ PageType {
serverInitPacketJunkSize = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(initPacketJunkSizeTextField)
}
}
}
AwgTextField {
@@ -185,6 +226,12 @@ PageType {
serverResponsePacketJunkSize = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(responsePacketJunkSizeTextField)
}
}
}
// AwgTextField {
@@ -233,6 +280,12 @@ PageType {
serverInitPacketMagicHeader = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(initPacketMagicHeaderTextField)
}
}
}
AwgTextField {
@@ -249,6 +302,12 @@ PageType {
serverResponsePacketMagicHeader = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(responsePacketMagicHeaderTextField)
}
}
}
AwgTextField {
@@ -265,6 +324,12 @@ PageType {
serverUnderloadPacketMagicHeader = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(underloadPacketMagicHeaderTextField)
}
}
}
AwgTextField {
@@ -281,6 +346,12 @@ PageType {
serverTransportPacketMagicHeader = textField.text
}
}
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(transportPacketMagicHeaderTextField)
}
}
}
BasicButtonType {
+16 -2
View File
@@ -14,6 +14,21 @@ import "../Config"
PageType {
id: root
Connections {
target: ApiNewsController
function onFetchNewsFinished() {
PageController.showBusyIndicator(false)
}
function onErrorOccurred(errorCode, showError) {
if (showError) {
PageController.showErrorMessage(errorCode)
PageController.closePage()
PageController.showBusyIndicator(false)
}
}
}
ListViewType {
id: listView
@@ -140,9 +155,8 @@ PageType {
return;
}
PageController.showBusyIndicator(true)
ApiNewsController.fetchNews();
ApiNewsController.fetchNews(true)
PageController.goToPage(PageEnum.PageSettingsNewsNotifications)
PageController.showBusyIndicator(false)
}
}
@@ -66,7 +66,7 @@ PageType {
id: backButton
objectName: "backButton"
Layout.topMargin: 20
Layout.topMargin: 20 + SettingsController.safeAreaTopMargin
}
HeaderTypeWithButton {
@@ -1,243 +1,243 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import QtCore
import SortFilterProxyModel 0.2
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
property string configExtension: ".conf"
property string configCaption: qsTr("Save AmneziaVPN config")
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onActiveFocusChanged: {
if(backButton.enabled && backButton.activeFocus) {
listView.positionViewAtBeginning()
}
}
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.left: parent.left
model: ApiCountryModel
header: ColumnLayout {
width: listView.width
BaseHeaderType {
id: header
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: qsTr("Configuration Files")
descriptionText: qsTr("For router setup or the AmneziaWG app")
}
}
delegate: ColumnLayout {
width: listView.width
LabelWithButtonType {
Layout.fillWidth: true
Layout.topMargin: 6
text: countryName
descriptionText: isWorkerExpired ? qsTr("The configuration needs to be reissued") : ""
hideDescription: isWorkerExpired ? true : false
descriptionColor: AmneziaStyle.color.vibrantRed
leftImageSource: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg"
rightImageSource: isIssued ? "qrc:/images/controls/more-vertical.svg" : "qrc:/images/controls/download.svg"
clickedFunction: function() {
if (isIssued) {
moreOptionsDrawer.countryName = countryName
moreOptionsDrawer.countryCode = countryCode
moreOptionsDrawer.openTriggered()
} else {
issueConfig(countryCode)
}
}
}
DividerType {}
}
}
DrawerType2 {
id: moreOptionsDrawer
property string countryName
property string countryCode
anchors.fill: parent
expandedHeight: parent.height * 0.4375
expandedStateContent: Item {
implicitHeight: moreOptionsDrawer.expandedHeight
BackButtonType {
id: moreOptionsDrawerBackButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 16
backButtonFunction: function() {
moreOptionsDrawer.closeTriggered()
}
}
ListViewType {
id: drawerListView
anchors.top: moreOptionsDrawerBackButton.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
header: ColumnLayout {
width: drawerListView.width
Header2Type {
Layout.fillWidth: true
Layout.margins: 16
headerText: moreOptionsDrawer.countryName + qsTr(" configuration file")
}
}
model: 1 // fake model to force the ListView to be created without a model
delegate: ColumnLayout {
width: drawerListView.width
LabelWithButtonType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Generate a new configuration file")
descriptionText: qsTr("The previously created one will stop working")
clickedFunction: function() {
showQuestion(true, moreOptionsDrawer.countryCode, moreOptionsDrawer.countryName)
}
}
DividerType {}
}
footer: ColumnLayout {
width: drawerListView.width
LabelWithButtonType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Revoke the current configuration file")
clickedFunction: function() {
showQuestion(false, moreOptionsDrawer.countryCode, moreOptionsDrawer.countryName)
}
}
DividerType {}
}
}
}
}
function issueConfig(countryCode) {
var fileName = ""
if (GC.isMobile()) {
fileName = countryCode + configExtension
} else {
fileName = SystemController.getFileName(configCaption,
qsTr("Config files (*" + configExtension + ")"),
StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/" + countryCode,
true,
configExtension)
}
if (fileName !== "") {
PageController.showBusyIndicator(true)
let result = ApiConfigsController.exportNativeConfig(countryCode, fileName)
if (result) {
ApiSettingsController.getAccountInfo(true)
}
PageController.showBusyIndicator(false)
if (result) {
PageController.showNotificationMessage(qsTr("Config file saved"))
}
}
}
function revokeConfig(countryCode) {
PageController.showBusyIndicator(true)
let result = ApiConfigsController.revokeNativeConfig(countryCode)
if (result) {
ApiSettingsController.getAccountInfo(true)
}
PageController.showBusyIndicator(false)
if (result) {
PageController.showNotificationMessage(qsTr("The config has been revoked"))
}
}
function showQuestion(isConfigIssue, countryCode, countryName) {
var headerText
if (isConfigIssue) {
headerText = qsTr("Generate a new %1 configuration file?").arg(countryName)
} else {
headerText = qsTr("Revoke the current %1 configuration file?").arg(countryName)
}
var descriptionText = qsTr("Your previous configuration file will no longer work, and it will not be possible to connect using it")
var yesButtonText = isConfigIssue ? qsTr("Download") : qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
if (isConfigIssue) {
issueConfig(countryCode)
} else {
revokeConfig(countryCode)
}
moreOptionsDrawer.closeTriggered()
}
var noButtonFunction = function() {}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import QtCore
import SortFilterProxyModel 0.2
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
property string configExtension: ".conf"
property string configCaption: qsTr("Save AmneziaVPN config")
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onActiveFocusChanged: {
if(backButton.enabled && backButton.activeFocus) {
listView.positionViewAtBeginning()
}
}
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.left: parent.left
model: ApiCountryModel
header: ColumnLayout {
width: listView.width
BaseHeaderType {
id: header
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: qsTr("Configuration Files")
descriptionText: qsTr("For router setup or the AmneziaWG app")
}
}
delegate: ColumnLayout {
width: listView.width
LabelWithButtonType {
Layout.fillWidth: true
Layout.topMargin: 6
text: countryName
descriptionText: isWorkerExpired ? qsTr("The configuration needs to be reissued") : ""
hideDescription: isWorkerExpired ? false : true
descriptionColor: AmneziaStyle.color.vibrantRed
leftImageSource: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg"
rightImageSource: isIssued ? "qrc:/images/controls/more-vertical.svg" : "qrc:/images/controls/download.svg"
clickedFunction: function() {
if (isIssued) {
moreOptionsDrawer.countryName = countryName
moreOptionsDrawer.countryCode = countryCode
moreOptionsDrawer.openTriggered()
} else {
issueConfig(countryCode)
}
}
}
DividerType {}
}
}
DrawerType2 {
id: moreOptionsDrawer
property string countryName
property string countryCode
anchors.fill: parent
expandedHeight: parent.height * 0.4375
expandedStateContent: Item {
implicitHeight: moreOptionsDrawer.expandedHeight
BackButtonType {
id: moreOptionsDrawerBackButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 16
backButtonFunction: function() {
moreOptionsDrawer.closeTriggered()
}
}
ListViewType {
id: drawerListView
anchors.top: moreOptionsDrawerBackButton.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
header: ColumnLayout {
width: drawerListView.width
Header2Type {
Layout.fillWidth: true
Layout.margins: 16
headerText: moreOptionsDrawer.countryName + qsTr(" configuration file")
}
}
model: 1 // fake model to force the ListView to be created without a model
delegate: ColumnLayout {
width: drawerListView.width
LabelWithButtonType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Generate a new configuration file")
descriptionText: qsTr("The previously created one will stop working")
clickedFunction: function() {
showQuestion(true, moreOptionsDrawer.countryCode, moreOptionsDrawer.countryName)
}
}
DividerType {}
}
footer: ColumnLayout {
width: drawerListView.width
LabelWithButtonType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Revoke the current configuration file")
clickedFunction: function() {
showQuestion(false, moreOptionsDrawer.countryCode, moreOptionsDrawer.countryName)
}
}
DividerType {}
}
}
}
}
function issueConfig(countryCode) {
var fileName = ""
if (GC.isMobile()) {
fileName = countryCode + configExtension
} else {
fileName = SystemController.getFileName(configCaption,
qsTr("Config files (*" + configExtension + ")"),
StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/" + countryCode,
true,
configExtension)
}
if (fileName !== "") {
PageController.showBusyIndicator(true)
let result = ApiConfigsController.exportNativeConfig(countryCode, fileName)
if (result) {
ApiSettingsController.getAccountInfo(true)
}
PageController.showBusyIndicator(false)
if (result) {
PageController.showNotificationMessage(qsTr("Config file saved"))
}
}
}
function revokeConfig(countryCode) {
PageController.showBusyIndicator(true)
let result = ApiConfigsController.revokeNativeConfig(countryCode)
if (result) {
ApiSettingsController.getAccountInfo(true)
}
PageController.showBusyIndicator(false)
if (result) {
PageController.showNotificationMessage(qsTr("The config has been revoked"))
}
}
function showQuestion(isConfigIssue, countryCode, countryName) {
var headerText
if (isConfigIssue) {
headerText = qsTr("Generate a new %1 configuration file?").arg(countryName)
} else {
headerText = qsTr("Revoke the current %1 configuration file?").arg(countryName)
}
var descriptionText = qsTr("Your previous configuration file will no longer work, and it will not be possible to connect using it")
var yesButtonText = isConfigIssue ? qsTr("Download") : qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
if (isConfigIssue) {
issueConfig(countryCode)
} else {
revokeConfig(countryCode)
}
moreOptionsDrawer.closeTriggered()
}
var noButtonFunction = function() {}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
@@ -61,7 +61,7 @@ PageType {
width: root.width
BackButtonType {
Layout.topMargin: 20
Layout.topMargin: 20 + SettingsController.safeAreaTopMargin
}
Label {
@@ -165,9 +165,11 @@ PageType {
ScrollBar.vertical: ScrollBarType { policy: ScrollBar.AlwaysOn }
anchors.top: header.bottom
anchors.bottom: addAppButton.top
anchors.bottom: parent.bottom
anchors.bottomMargin: addAppButton.implicitHeight + 48 + SettingsController.safeAreaBottomMargin + (searchField.textField.activeFocus ? 0 : SettingsController.imeHeight)
anchors.left: parent.left
anchors.right: parent.right
clip: true
model: SortFilterProxyModel {
id: proxyAppSplitTunnelingModel
@@ -215,50 +217,54 @@ PageType {
}
Rectangle {
anchors.fill: addAppButton
anchors.bottomMargin: -24 - SettingsController.safeAreaBottomMargin
color: AmneziaStyle.color.midnightBlack
opacity: 0.8
}
RowLayout {
id: addAppButton
enabled: root.pageEnabled
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 24
anchors.rightMargin: 16
anchors.leftMargin: 16
anchors.bottomMargin: 24 + SettingsController.safeAreaBottomMargin
anchors.bottom: parent.bottom
height: addAppButton.implicitHeight + 48 + SettingsController.safeAreaBottomMargin
color: AmneziaStyle.color.midnightBlack
opacity: 0.8
RowLayout {
id: addAppButton
TextFieldWithHeaderType {
id: searchField
enabled: root.pageEnabled
Layout.fillWidth: true
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 24
anchors.rightMargin: 16
anchors.leftMargin: 16
anchors.bottomMargin: 24 + SettingsController.safeAreaBottomMargin
textField.placeholderText: qsTr("application name")
buttonImageSource: "qrc:/images/controls/plus.svg"
TextFieldWithHeaderType {
id: searchField
rightButtonClickedOnEnter: true
Layout.fillWidth: true
clickedFunc: function() {
searchField.focus = false
PageController.showBusyIndicator(true)
textField.placeholderText: qsTr("application name")
buttonImageSource: "qrc:/images/controls/plus.svg"
if (Qt.platform.os === "windows") {
var fileName = SystemController.getFileName(qsTr("Open executable file"),
qsTr("Executable files (*.*)"))
if (fileName !== "") {
AppSplitTunnelingController.addApp(fileName)
rightButtonClickedOnEnter: true
clickedFunc: function() {
searchField.focus = false
PageController.showBusyIndicator(true)
if (Qt.platform.os === "windows") {
var fileName = SystemController.getFileName(qsTr("Open executable file"),
qsTr("Executable files (*.*)"))
if (fileName !== "") {
AppSplitTunnelingController.addApp(fileName)
}
} else if (Qt.platform.os === "android"){
installedAppDrawer.openTriggered()
}
} else if (Qt.platform.os === "android"){
installedAppDrawer.openTriggered()
}
PageController.showBusyIndicator(false)
PageController.showBusyIndicator(false)
}
}
}
}
+163 -163
View File
@@ -1,163 +1,163 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Config"
import "../Controls2/TextTypes"
import "../Components"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: {
if (this.activeFocus) {
listView.positionViewAtBeginning()
}
}
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.left: parent.left
property var isServerFromApi: ServersModel.isServerFromApi(ServersModel.defaultIndex)
enabled: !isServerFromApi
Component.onCompleted: {
if (isServerFromApi) {
PageController.showNotificationMessage(qsTr("Default server does not support custom DNS"))
}
}
header: ColumnLayout {
width: listView.width
spacing: 16
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("DNS servers")
}
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("If AmneziaDNS is not used or installed")
}
}
model: 1 // fake model to force the ListView to be created without a model
delegate: ColumnLayout {
width: listView.width
spacing: 16
TextFieldWithHeaderType {
id: primaryDns
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("Primary DNS")
textField.text: SettingsController.primaryDns
textField.validator: RegularExpressionValidator {
regularExpression: InstallController.ipAddressRegExp()
}
}
TextFieldWithHeaderType {
id: secondaryDns
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("Secondary DNS")
textField.text: SettingsController.secondaryDns
textField.validator: RegularExpressionValidator {
regularExpression: InstallController.ipAddressRegExp()
}
}
BasicButtonType {
id: restoreDefaultButton
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
defaultColor: AmneziaStyle.color.transparent
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
disabledColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.paleGray
borderWidth: 1
text: qsTr("Restore default")
clickedFunc: function() {
var headerText = qsTr("Restore default DNS settings?")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
SettingsController.primaryDns = "1.1.1.1"
primaryDns.textField.text = SettingsController.primaryDns
SettingsController.secondaryDns = "1.0.0.1"
secondaryDns.textField.text = SettingsController.secondaryDns
PageController.showNotificationMessage(qsTr("Settings have been reset"))
}
var noButtonFunction = function() {
}
showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
BasicButtonType {
id: saveButton
Layout.fillWidth: true
Layout.margins: 16
text: qsTr("Save")
clickedFunc: function() {
if (primaryDns.textField.text !== SettingsController.primaryDns) {
SettingsController.primaryDns = primaryDns.textField.text
}
if (secondaryDns.textField.text !== SettingsController.secondaryDns) {
SettingsController.secondaryDns = secondaryDns.textField.text
}
PageController.showNotificationMessage(qsTr("Settings saved"))
}
}
}
}
}
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Config"
import "../Controls2/TextTypes"
import "../Components"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: {
if (this.activeFocus) {
listView.positionViewAtBeginning()
}
}
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.left: parent.left
property var isServerFromApi: ServersModel.isServerFromApi(ServersModel.defaultIndex)
enabled: !isServerFromApi
Component.onCompleted: {
if (isServerFromApi) {
PageController.showNotificationMessage(qsTr("Default server does not support custom DNS"))
}
}
header: ColumnLayout {
width: listView.width
spacing: 16
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("DNS servers")
}
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("If AmneziaDNS is not used or installed")
}
}
model: 1 // fake model to force the ListView to be created without a model
delegate: ColumnLayout {
width: listView.width
spacing: 16
TextFieldWithHeaderType {
id: primaryDns
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("Primary DNS")
textField.text: SettingsController.primaryDns
textField.validator: RegularExpressionValidator {
regularExpression: InstallController.ipAddressRegExp()
}
}
TextFieldWithHeaderType {
id: secondaryDns
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("Secondary DNS")
textField.text: SettingsController.secondaryDns
textField.validator: RegularExpressionValidator {
regularExpression: InstallController.ipAddressRegExp()
}
}
BasicButtonType {
id: restoreDefaultButton
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
defaultColor: AmneziaStyle.color.transparent
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
disabledColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.paleGray
borderWidth: 1
text: qsTr("Restore default")
clickedFunc: function() {
var headerText = qsTr("Restore default DNS settings?")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
SettingsController.primaryDns = "1.1.1.1"
primaryDns.textField.text = SettingsController.primaryDns
SettingsController.secondaryDns = "1.0.0.1"
secondaryDns.textField.text = SettingsController.secondaryDns
PageController.showNotificationMessage(qsTr("Settings have been reset"))
}
var noButtonFunction = function() {
}
showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
BasicButtonType {
id: saveButton
Layout.fillWidth: true
Layout.margins: 16
text: qsTr("Save")
clickedFunc: function() {
if (primaryDns.textField.text !== SettingsController.primaryDns) {
SettingsController.primaryDns = primaryDns.textField.text
}
if (secondaryDns.textField.text !== SettingsController.secondaryDns) {
SettingsController.secondaryDns = secondaryDns.textField.text
}
PageController.showNotificationMessage(qsTr("Settings saved"))
}
}
}
}
}
@@ -81,18 +81,18 @@ PageType {
}
}
ColumnLayout {
id: header
ColumnLayout {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
BackButtonType {
id: backButton
}
BackButtonType {
id: backButton
}
HeaderTypeWithSwitcher {
Layout.fillWidth: true
@@ -168,11 +168,13 @@ PageType {
anchors.top: header.bottom
anchors.topMargin: 16
anchors.bottom: addSiteButton.top
anchors.bottom: parent.bottom
anchors.bottomMargin: addSiteButton.implicitHeight + 48 + (searchField.textField.activeFocus ? 0 : SettingsController.imeHeight)
width: parent.width
enabled: root.pageEnabled
clip: true
model: SortFilterProxyModel {
id: proxySitesModel
@@ -231,56 +233,60 @@ PageType {
}
Rectangle {
anchors.fill: addSiteButton
anchors.bottomMargin: -24
color: AmneziaStyle.color.midnightBlack
opacity: 0.8
}
RowLayout {
id: addSiteButton
enabled: root.pageEnabled
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 24
anchors.rightMargin: 16
anchors.leftMargin: 16
anchors.bottomMargin: 24
anchors.bottom: parent.bottom
height: addSiteButton.implicitHeight + 48
color: AmneziaStyle.color.midnightBlack
opacity: 0.8
RowLayout {
id: addSiteButton
TextFieldWithHeaderType {
id: searchField
enabled: root.pageEnabled
Layout.fillWidth: true
rightButtonClickedOnEnter: true
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 24
anchors.rightMargin: 16
anchors.leftMargin: 16
anchors.bottomMargin: 24
textField.placeholderText: qsTr("website or IP")
buttonImageSource: "qrc:/images/controls/plus.svg"
TextFieldWithHeaderType {
id: searchField
clickedFunc: function() {
PageController.showBusyIndicator(true)
SitesController.addSite(textField.text)
textField.text = ""
PageController.showBusyIndicator(false)
}
}
Layout.fillWidth: true
rightButtonClickedOnEnter: true
ImageButtonType {
id: addSiteButtonImage
implicitWidth: 56
implicitHeight: 56
textField.placeholderText: qsTr("website or IP")
buttonImageSource: "qrc:/images/controls/plus.svg"
image: "qrc:/images/controls/more-vertical.svg"
imageColor: AmneziaStyle.color.paleGray
onClicked: function () {
moreActionsDrawer.openTriggered()
clickedFunc: function() {
PageController.showBusyIndicator(true)
SitesController.addSite(textField.text)
textField.text = ""
PageController.showBusyIndicator(false)
}
}
Keys.onReturnPressed: addSiteButtonImage.clicked()
Keys.onEnterPressed: addSiteButtonImage.clicked()
ImageButtonType {
id: addSiteButtonImage
implicitWidth: 56
implicitHeight: 56
image: "qrc:/images/controls/more-vertical.svg"
imageColor: AmneziaStyle.color.paleGray
onClicked: function () {
moreActionsDrawer.openTriggered()
}
Keys.onReturnPressed: addSiteButtonImage.clicked()
Keys.onEnterPressed: addSiteButtonImage.clicked()
}
}
}
@@ -1,179 +1,179 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: {
if (this.activeFocus) {
listView.positionViewAtBeginning()
}
}
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.left: parent.left
header: ColumnLayout {
width: listView.width
BaseHeaderType {
Layout.fillWidth: true
Layout.topMargin: 8
Layout.rightMargin: 16
Layout.leftMargin: 16
Layout.bottomMargin: 32
headerText: ApiServicesModel.getSelectedServiceData("name")
descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription")
}
}
model: inputFields
spacing: 0
delegate: ColumnLayout {
width: listView.width
LabelWithImageType {
Layout.fillWidth: true
Layout.margins: 16
imageSource: imagePath
leftText: lText
rightText: rText
visible: isVisible
}
}
footer: ColumnLayout {
width: listView.width
spacing: 0
ParagraphTextType {
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
}
textFormat: Text.RichText
text: {
var text = ApiServicesModel.getSelectedServiceData("features")
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
BasicButtonType {
id: continueButton
Layout.fillWidth: true
Layout.topMargin: 32
Layout.bottomMargin: 32
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Connect")
clickedFunc: function() {
var endpoint = ApiServicesModel.getStoreEndpoint()
if (endpoint !== undefined && endpoint !== "") {
Qt.openUrlExternally(endpoint)
PageController.closePage()
PageController.closePage()
} else {
PageController.showBusyIndicator(true)
ApiConfigsController.importServiceFromGateway()
PageController.showBusyIndicator(false)
}
}
}
}
}
property list<QtObject> inputFields: [
region,
price,
timeLimit,
speed,
features
]
QtObject {
id: region
readonly property string imagePath: "qrc:/images/controls/map-pin.svg"
readonly property string lText: qsTr("For the region")
readonly property string rText: ApiServicesModel.getSelectedServiceData("region")
property bool isVisible: true
}
QtObject {
id: price
readonly property string imagePath: "qrc:/images/controls/tag.svg"
readonly property string lText: qsTr("Price")
readonly property string rText: ApiServicesModel.getSelectedServiceData("price")
property bool isVisible: true
}
QtObject {
id: timeLimit
readonly property string imagePath: "qrc:/images/controls/history.svg"
readonly property string lText: qsTr("Work period")
readonly property string rText: ApiServicesModel.getSelectedServiceData("timeLimit")
property bool isVisible: rText !== ""
}
QtObject {
id: speed
readonly property string imagePath: "qrc:/images/controls/gauge.svg"
readonly property string lText: qsTr("Speed")
readonly property string rText: ApiServicesModel.getSelectedServiceData("speed")
property bool isVisible: true
}
QtObject {
id: features
readonly property string imagePath: "qrc:/images/controls/info.svg"
readonly property string lText: qsTr("Features")
readonly property string rText: ""
property bool isVisible: true
}
}
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: {
if (this.activeFocus) {
listView.positionViewAtBeginning()
}
}
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.left: parent.left
header: ColumnLayout {
width: listView.width
BaseHeaderType {
Layout.fillWidth: true
Layout.topMargin: 8
Layout.rightMargin: 16
Layout.leftMargin: 16
Layout.bottomMargin: 32
headerText: ApiServicesModel.getSelectedServiceData("name")
descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription")
}
}
model: inputFields
spacing: 0
delegate: ColumnLayout {
width: listView.width
LabelWithImageType {
Layout.fillWidth: true
Layout.margins: 16
imageSource: imagePath
leftText: lText
rightText: rText
visible: isVisible
}
}
footer: ColumnLayout {
width: listView.width
spacing: 0
ParagraphTextType {
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
}
textFormat: Text.RichText
text: {
var text = ApiServicesModel.getSelectedServiceData("features")
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
BasicButtonType {
id: continueButton
Layout.fillWidth: true
Layout.topMargin: 32
Layout.bottomMargin: 32
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Connect")
clickedFunc: function() {
var endpoint = ApiServicesModel.getStoreEndpoint()
if (endpoint !== undefined && endpoint !== "") {
Qt.openUrlExternally(endpoint)
PageController.closePage()
PageController.closePage()
} else {
PageController.showBusyIndicator(true)
ApiConfigsController.importServiceFromGateway()
PageController.showBusyIndicator(false)
}
}
}
}
}
property list<QtObject> inputFields: [
region,
price,
timeLimit,
speed,
features
]
QtObject {
id: region
readonly property string imagePath: "qrc:/images/controls/map-pin.svg"
readonly property string lText: qsTr("For the region")
readonly property string rText: ApiServicesModel.getSelectedServiceData("region")
property bool isVisible: true
}
QtObject {
id: price
readonly property string imagePath: "qrc:/images/controls/tag.svg"
readonly property string lText: qsTr("Price")
readonly property string rText: ApiServicesModel.getSelectedServiceData("price")
property bool isVisible: true
}
QtObject {
id: timeLimit
readonly property string imagePath: "qrc:/images/controls/history.svg"
readonly property string lText: qsTr("Work period")
readonly property string rText: ApiServicesModel.getSelectedServiceData("timeLimit")
property bool isVisible: rText !== ""
}
QtObject {
id: speed
readonly property string imagePath: "qrc:/images/controls/gauge.svg"
readonly property string lText: qsTr("Speed")
readonly property string rText: ApiServicesModel.getSelectedServiceData("speed")
property bool isVisible: true
}
QtObject {
id: features
readonly property string imagePath: "qrc:/images/controls/info.svg"
readonly property string lText: qsTr("Features")
readonly property string rText: ""
property bool isVisible: true
}
}
@@ -103,7 +103,7 @@ PageType {
BaseHeaderType {
Layout.fillWidth: true
Layout.topMargin: 20
Layout.topMargin: 20 + SettingsController.safeAreaTopMargin
Layout.leftMargin: 16
Layout.rightMargin: 16
@@ -147,6 +147,7 @@ PageType {
Layout.fillWidth: true
Layout.topMargin: 24
Layout.bottomMargin: 24 + SettingsController.safeAreaBottomMargin
Layout.leftMargin: 16
Layout.rightMargin: 16
@@ -20,7 +20,7 @@ PageType {
anchors.left: parent.left
anchors.top: parent.top
anchors.topMargin: 20
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
}
ParagraphTextType {
@@ -25,7 +25,7 @@ PageType {
source: "qrc:/images/amneziaBigLogo.png"
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Layout.topMargin: 32
Layout.topMargin: 32 + SettingsController.safeAreaTopMargin
Layout.preferredWidth: 360
Layout.preferredHeight: 287
}
@@ -33,7 +33,7 @@ PageType {
BasicButtonType {
id: startButton
Layout.fillWidth: true
Layout.bottomMargin: 48
Layout.bottomMargin: 48 + SettingsController.safeAreaBottomMargin
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.alignment: Qt.AlignBottom
+168 -168
View File
@@ -1,168 +1,168 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import SortFilterProxyModel 0.2
import PageEnum 1.0
import ContainerProps 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Components"
import "../Config"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: {
if (this.activeFocus) {
listView.positionViewAtBeginning()
}
}
}
ListViewType {
id: listView
property string headerText: ""
property string configContentHeaderText: ""
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.left: parent.left
header: ColumnLayout {
width: listView.width
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 24
headerText: qsTr("Full access to the server and VPN")
}
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 24
Layout.bottomMargin: 24
text: qsTr("We recommend that you use full access to the server only for your own additional devices.\n") +
qsTr("If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. ")
color: AmneziaStyle.color.mutedGray
}
DropDownType {
id: serverSelector
objectName: "serverSelector"
signal severSelectorIndexChanged
property int currentIndex: 0
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
drawerHeight: 0.4375
drawerParent: root
descriptionText: qsTr("Server")
headerText: qsTr("Server")
listView: ListViewWithRadioButtonType {
id: serverSelectorListView
rootWidth: root.width
imageSource: "qrc:/images/controls/check.svg"
model: SortFilterProxyModel {
id: proxyServersModel
sourceModel: ServersModel
filters: [
ValueFilter {
roleName: "hasWriteAccess"
value: true
}
]
}
clickedFunction: function() {
handler()
if (serverSelector.currentIndex !== serverSelectorListView.selectedIndex) {
serverSelector.currentIndex = serverSelectorListView.selectedIndex
serverSelector.severSelectorIndexChanged()
}
listView.headerText = qsTr("Accessing ") + serverSelector.text
listView.configContentHeaderText = qsTr("File with accessing settings to ") + serverSelector.text
serverSelector.closeTriggered()
}
Component.onCompleted: {
serverSelectorListView.currentIndex = ServersModel.isDefaultServerHasWriteAccess() ?
proxyServersModel.mapFromSource(ServersModel.defaultIndex) : 0
serverSelectorListView.triggerCurrentItem()
}
function handler() {
serverSelector.text = selectedText
ServersModel.processedIndex = proxyServersModel.mapToSource(selectedIndex)
}
}
}
}
model: 1 // fake model to force the ListView to be created without a model
spacing: 0
delegate: ColumnLayout {
width: listView.width
BasicButtonType {
id: shareButton
Layout.fillWidth: true
Layout.topMargin: 32
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Share")
leftImageSource: "qrc:/images/controls/share-2.svg"
clickedFunc: function() {
PageController.showBusyIndicator(true)
if (Qt.platform.os === "android" && !SystemController.isAuthenticated()) {
PageController.showBusyIndicator(false)
ExportController.exportErrorOccurred(qsTr("Access error!"))
return
} else {
ExportController.generateFullAccessConfig()
}
PageController.showBusyIndicator(false)
PageController.goToShareConnectionPage(listView.headerText, listView.configContentHeaderText, "", ".vpn", "amnezia_config")
}
}
}
}
}
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import SortFilterProxyModel 0.2
import PageEnum 1.0
import ContainerProps 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Components"
import "../Config"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: {
if (this.activeFocus) {
listView.positionViewAtBeginning()
}
}
}
ListViewType {
id: listView
property string headerText: ""
property string configContentHeaderText: ""
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.left: parent.left
header: ColumnLayout {
width: listView.width
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 24
headerText: qsTr("Full access to the server and VPN")
}
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 24
Layout.bottomMargin: 24
text: qsTr("We recommend that you use full access to the server only for your own additional devices.\n") +
qsTr("If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. ")
color: AmneziaStyle.color.mutedGray
}
DropDownType {
id: serverSelector
objectName: "serverSelector"
signal severSelectorIndexChanged
property int currentIndex: 0
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
drawerHeight: 0.4375
drawerParent: root
descriptionText: qsTr("Server")
headerText: qsTr("Server")
listView: ListViewWithRadioButtonType {
id: serverSelectorListView
rootWidth: root.width
imageSource: "qrc:/images/controls/check.svg"
model: SortFilterProxyModel {
id: proxyServersModel
sourceModel: ServersModel
filters: [
ValueFilter {
roleName: "hasWriteAccess"
value: true
}
]
}
clickedFunction: function() {
handler()
if (serverSelector.currentIndex !== serverSelectorListView.selectedIndex) {
serverSelector.currentIndex = serverSelectorListView.selectedIndex
serverSelector.severSelectorIndexChanged()
}
listView.headerText = qsTr("Accessing ") + serverSelector.text
listView.configContentHeaderText = qsTr("File with accessing settings to ") + serverSelector.text
serverSelector.closeTriggered()
}
Component.onCompleted: {
serverSelectorListView.currentIndex = ServersModel.isDefaultServerHasWriteAccess() ?
proxyServersModel.mapFromSource(ServersModel.defaultIndex) : 0
serverSelectorListView.triggerCurrentItem()
}
function handler() {
serverSelector.text = selectedText
ServersModel.processedIndex = proxyServersModel.mapToSource(selectedIndex)
}
}
}
}
model: 1 // fake model to force the ListView to be created without a model
spacing: 0
delegate: ColumnLayout {
width: listView.width
BasicButtonType {
id: shareButton
Layout.fillWidth: true
Layout.topMargin: 32
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Share")
leftImageSource: "qrc:/images/controls/share-2.svg"
clickedFunc: function() {
PageController.showBusyIndicator(true)
if (Qt.platform.os === "android" && !SystemController.isAuthenticated()) {
PageController.showBusyIndicator(false)
ExportController.exportErrorOccurred(qsTr("Access error!"))
return
} else {
ExportController.generateFullAccessConfig()
}
PageController.showBusyIndicator(false)
PageController.goToShareConnectionPage(listView.headerText, listView.configContentHeaderText, "", ".vpn", "amnezia_config")
}
}
}
}
}
+3
View File
@@ -304,6 +304,9 @@ PageType {
anchors.right: parent.right
anchors.left: parent.left
anchors.bottom: parent.bottom
// Also adjust TabBar position when keyboard appears (Android 14+ workaround)
anchors.bottomMargin: SettingsController.imeHeight
topPadding: 8
bottomPadding: 8 + SettingsController.safeAreaBottomMargin
+1 -1
View File
@@ -183,7 +183,7 @@ Window {
id: privateKeyPassphraseDrawer
anchors.fill: parent
expandedHeight: root.height * 0.35 + SettingsController.safeAreaBottomMargin
expandedHeight: root.height * 0.35 + SettingsController.safeAreaBottomMargin + SettingsController.imeHeight
expandedStateContent: ColumnLayout {
anchors.top: parent.top