Compare commits

...

23 Commits

Author SHA1 Message Date
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
vkamn f6e7d3ccf1 fix: minor ui fixes (#1917)
* feat: improve storage processing

* fix: minor ui fixes
2025-10-09 23:22:58 +08:00
Mitternacht822 a754a11913 fix: added displaying vpn_key field added in older version of the app (#1873)
* fix(api_key): added displaying vpn_key field added in older version of the app

* revert changes

* fix: implemented generation of api key text for PremiumV2

* fix: deleted unnecessary code

* saving apikey text when generating

* added method for vpn key export, fixed wrong saving file
2025-10-07 23:16:28 +08:00
vkamn 4d25e3b6f6 chore: minor bugfixes (#1915) 2025-10-07 23:15:06 +08:00
MrMirDan 1fac280497 fix: main app info added after clearing logs (#1913) 2025-10-06 21:07:04 +08:00
Yaroslav c886c5e6a7 feat: enhance OpenVPN configuration handling and logging for iOS plat… (#1910)
* feat: enhance OpenVPN configuration handling and logging for iOS platform

* refactor: remove $OPENVPN_TA_KEY_SANITIZED and use $OPENVPN_TA_KEY instead
2025-10-06 21:04:49 +08:00
aiamnezia cd7f78b9ca feat: news and notifications page (#1660)
* Add news and notifications

* Add localization for news and notifications

* Remove news caching

* Add fetching news befor openning news page

* Fix not updating news page

* Delete debug output

* Remove news and notificztions with only self-hosted servers

* Add stack filters to fetching news request

* Add fetching news with changing stack in the client

* small refactoring

* polishing

* Rename newsModel files and fix naming in code

* fix: remove custom signals; fetch news only on stack expansion

* chore: delete unnecessary code

* chore: code style fixes

* fix: fixed memory leak in gateway controller

---------

Co-authored-by: vkamn <vk@amnezia.org>
2025-10-06 12:06:36 +08:00
vkamn a587d3230f fix: again fixed site link for features field (#1908) 2025-10-06 11:38:57 +08:00
MrMirDan 93e7b45136 fix: removed 'clear site list' button icon (#1909) 2025-10-06 11:37:42 +08:00
vkamn e024f71ce1 fix: allow remove expired api configs (#1907) 2025-10-03 14:45:12 +08:00
MrMirDan 50d1be7b4a chore: update for RU translation (#1893) 2025-10-02 20:59:45 +08:00
MrMirDan 3ec6d8973b fix: warning visible only on windows (#1900) 2025-10-02 20:59:23 +08:00
Yaroslav Gurov 3ea47d31a9 fix: restore dns after using xray (#1902) 2025-10-02 20:58:53 +08:00
vkamn 30c8cc4548 feat: add isConnectEvent field to api request (#1896) 2025-09-30 12:10:27 +08:00
vkamn 98586d2dd9 fix: fixed site link (#1897) 2025-09-30 12:07:27 +08:00
120 changed files with 2398 additions and 686 deletions
+2 -2
View File
@@ -469,8 +469,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
ANDROID_BUILD_PLATFORM: android-34 ANDROID_BUILD_PLATFORM: android-36
QT_VERSION: 6.7.3 QT_VERSION: 6.8.3
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools' QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }} PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }} PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
+1
View File
@@ -9,6 +9,7 @@ deploy/build_32/*
deploy/build_64/* deploy/build_64/*
winbuild*.bat winbuild*.bat
.cache/ .cache/
.vscode/
# Qt-es # Qt-es
+2 -2
View File
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN) set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.8.11.0) set(AMNEZIAVPN_VERSION 4.8.11.2)
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION} project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
DESCRIPTION "AmneziaVPN" DESCRIPTION "AmneziaVPN"
@@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}") set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) 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 2097)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux") set(MZ_PLATFORM_NAME "linux")
+2 -1
View File
@@ -45,7 +45,8 @@
android:configChanges="uiMode|screenSize|smallestScreenSize|screenLayout|orientation|density android:configChanges="uiMode|screenSize|smallestScreenSize|screenLayout|orientation|density
|fontScale|layoutDirection|locale|keyboard|keyboardHidden|navigation|mcc|mnc" |fontScale|layoutDirection|locale|keyboard|keyboardHidden|navigation|mcc|mnc"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:windowSoftInputMode="stateUnchanged|adjustResize" android:windowSoftInputMode="adjustResize|stateUnchanged"
android:enableOnBackInvokedCallback="false"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
+3
View File
@@ -6,6 +6,9 @@
<item name="android:colorBackground">@color/black</item> <item name="android:colorBackground">@color/black</item>
<item name="android:windowActionBar">false</item> <item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item> <item name="android:windowNoTitle">true</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:enforceNavigationBarContrast">false</item>
<item name="android:enforceStatusBarContrast">false</item>
</style> </style>
<style name="Translucent" parent="NoActionBar"> <style name="Translucent" parent="NoActionBar">
<item name="android:windowBackground">@android:color/transparent</item> <item name="android:windowBackground">@android:color/transparent</item>
@@ -35,6 +35,11 @@ import android.widget.Toast
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat 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 java.io.IOException
import kotlin.LazyThreadSafetyMode.NONE import kotlin.LazyThreadSafetyMode.NONE
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@@ -170,10 +175,9 @@ class AmneziaActivity : QtActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Log.d(TAG, "Create Amnezia activity") Log.d(TAG, "Create Amnezia activity")
loadLibs() loadLibs()
window.apply {
addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) // Configure window for edge-to-edge display
statusBarColor = getColor(R.color.black) configureWindowForEdgeToEdge()
}
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
val proto = mainScope.async(Dispatchers.IO) { val proto = mainScope.async(Dispatchers.IO) {
VpnStateStore.getVpnState().vpnProto VpnStateStore.getVpnState().vpnProto
@@ -265,6 +269,82 @@ class AmneziaActivity : QtActivity() {
super.onStop() super.onStop()
} }
override fun onResume() {
super.onResume()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
window.decorView.apply {
invalidate()
postDelayed({
sendTouch(1f, 1f)
}, 100)
postDelayed({
sendTouch(2f, 2f)
}, 200)
postDelayed({
requestLayout()
invalidate()
}, 250)
}
}
}
private fun configureWindowForEdgeToEdge() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
window.apply {
addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
addFlags(LayoutParams.FLAG_LAYOUT_NO_LIMITS)
statusBarColor = android.graphics.Color.TRANSPARENT
navigationBarColor = android.graphics.Color.TRANSPARENT
}
WindowInsetsControllerCompat(window, window.decorView).apply {
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)
statusBarColor = getColor(R.color.black)
}
}
}
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() { override fun onDestroy() {
Log.d(TAG, "Destroy Amnezia activity") Log.d(TAG, "Destroy Amnezia activity")
unregisterBroadcastReceiver(notificationStateReceiver) unregisterBroadcastReceiver(notificationStateReceiver)
@@ -666,6 +746,43 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused") @Suppress("unused")
fun isOnTv(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) fun isOnTv(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
@Suppress("unused")
fun isEdgeToEdgeEnabled(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
@Suppress("unused")
fun getStatusBarHeight(): Int {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return 0
val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
val heightPx = if (resourceId > 0) {
resources.getDimensionPixelSize(resourceId)
} else {
0
}
// Convert physical pixels to device-independent pixels for QML
val density = resources.displayMetrics.density
val heightDp = (heightPx / density).toInt()
return heightDp
}
@Suppress("unused")
fun getNavigationBarHeight(): Int {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return 0
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
val heightPx = if (resourceId > 0) {
resources.getDimensionPixelSize(resourceId)
} else {
0
}
// Convert physical pixels to device-independent pixels for QML
val density = resources.displayMetrics.density
val heightDp = (heightPx / density).toInt()
return heightDp
}
@Suppress("unused") @Suppress("unused")
fun startQrCodeReader() { fun startQrCodeReader() {
Log.v(TAG, "Start camera") Log.v(TAG, "Start camera")
@@ -38,15 +38,15 @@ object AppListProvider {
} }
} }
private class App(pi: PackageInfo, pm: PackageManager, ai: ApplicationInfo = pi.applicationInfo) : Comparable<App> { private class App(pi: PackageInfo, pm: PackageManager, ai: ApplicationInfo? = pi.applicationInfo) : Comparable<App> {
val name: String? val name: String?
val packageName: String = pi.packageName val packageName: String = pi.packageName
val icon: Boolean = ai.icon != 0 val icon: Boolean = (ai?.icon ?: 0) != 0
val isLaunchable: Boolean = pm.getLaunchIntentForPackage(packageName) != null val isLaunchable: Boolean = pm.getLaunchIntentForPackage(packageName) != null
init { init {
val name = ai.loadLabel(pm).toString() val name = ai?.loadLabel(pm)?.toString()
this.name = if (name != packageName) name else null this.name = name?.takeIf { it != packageName }
} }
override fun compareTo(other: App): Int { override fun compareTo(other: App): Int {
@@ -28,4 +28,7 @@ object QtAndroidController {
external fun onAuthResult(result: Boolean) external fun onAuthResult(result: Boolean)
external fun decodeQrCode(data: String): Boolean external fun decodeQrCode(data: String): Boolean
external fun onImeInsetsChanged(heightDp: Int)
external fun onSystemBarsInsetsChanged(navBarHeightDp: Int, statusBarHeightDp: Int)
} }
+3 -3
View File
@@ -1,6 +1,6 @@
message("Client android ${CMAKE_ANDROID_ARCH_ABI} build") message("Client android ${CMAKE_ANDROID_ARCH_ABI} build")
set(APP_ANDROID_MIN_SDK 26) set(APP_ANDROID_MIN_SDK 28)
set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING
"The minimum API level supported by the application or library" FORCE) "The minimum API level supported by the application or library" FORCE)
@@ -11,8 +11,8 @@ set_target_properties(${PROJECT} PROPERTIES
QT_ANDROID_VERSION_NAME ${CMAKE_PROJECT_VERSION} QT_ANDROID_VERSION_NAME ${CMAKE_PROJECT_VERSION}
QT_ANDROID_VERSION_CODE ${APP_ANDROID_VERSION_CODE} QT_ANDROID_VERSION_CODE ${APP_ANDROID_VERSION_CODE}
QT_ANDROID_MIN_SDK_VERSION ${APP_ANDROID_MIN_SDK} QT_ANDROID_MIN_SDK_VERSION ${APP_ANDROID_MIN_SDK}
QT_ANDROID_TARGET_SDK_VERSION 34 QT_ANDROID_TARGET_SDK_VERSION 36
QT_ANDROID_SDK_BUILD_TOOLS_REVISION 34.0.0 QT_ANDROID_SDK_BUILD_TOOLS_REVISION 36.0.0
QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android
) )
+19 -1
View File
@@ -83,12 +83,30 @@ QString OpenVpnConfigurator::createConfig(const ServerCredentials &credentials,
return ""; return "";
} }
auto sanitizeStaticKey = [](const QString &key) {
QStringList lines = key.split('\n');
QStringList filtered;
filtered.reserve(lines.size());
for (const QString &line : lines) {
const QString trimmed = line.trimmed();
if (trimmed.startsWith('#')) {
continue;
}
filtered.append(line);
}
QString result = filtered.join('\n');
if (!result.endsWith('\n')) {
result.append('\n');
}
return result;
};
config.replace("$OPENVPN_CA_CERT", connData.caCert); config.replace("$OPENVPN_CA_CERT", connData.caCert);
config.replace("$OPENVPN_CLIENT_CERT", connData.clientCert); config.replace("$OPENVPN_CLIENT_CERT", connData.clientCert);
config.replace("$OPENVPN_PRIV_KEY", connData.privKey); config.replace("$OPENVPN_PRIV_KEY", connData.privKey);
if (config.contains("$OPENVPN_TA_KEY")) { if (config.contains("$OPENVPN_TA_KEY")) {
config.replace("$OPENVPN_TA_KEY", connData.taKey); config.replace("$OPENVPN_TA_KEY", sanitizeStaticKey(connData.taKey));
} else { } else {
config.replace("<tls-auth>", ""); config.replace("<tls-auth>", "");
config.replace("</tls-auth>", ""); config.replace("</tls-auth>", "");
+12
View File
@@ -47,12 +47,14 @@ namespace apiDefs
constexpr QLatin1String serverCountryName("server_country_name"); constexpr QLatin1String serverCountryName("server_country_name");
constexpr QLatin1String osVersion("os_version"); constexpr QLatin1String osVersion("os_version");
constexpr QLatin1String appLanguage("app_language");
constexpr QLatin1String availableCountries("available_countries"); constexpr QLatin1String availableCountries("available_countries");
constexpr QLatin1String activeDeviceCount("active_device_count"); constexpr QLatin1String activeDeviceCount("active_device_count");
constexpr QLatin1String maxDeviceCount("max_device_count"); constexpr QLatin1String maxDeviceCount("max_device_count");
constexpr QLatin1String subscriptionEndDate("subscription_end_date"); constexpr QLatin1String subscriptionEndDate("subscription_end_date");
constexpr QLatin1String issuedConfigs("issued_configs"); constexpr QLatin1String issuedConfigs("issued_configs");
constexpr QLatin1String subscriptionDescription("subscription_description");
constexpr QLatin1String supportInfo("support_info"); constexpr QLatin1String supportInfo("support_info");
constexpr QLatin1String email("email"); constexpr QLatin1String email("email");
@@ -64,6 +66,16 @@ namespace apiDefs
constexpr QLatin1String id("id"); constexpr QLatin1String id("id");
constexpr QLatin1String orderId("order_id"); constexpr QLatin1String orderId("order_id");
constexpr QLatin1String migrationCode("migration_code"); constexpr QLatin1String migrationCode("migration_code");
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 const int requestTimeoutMsecs = 12 * 1000; // 12 secs
+60 -12
View File
@@ -82,7 +82,9 @@ apiDefs::ConfigSource apiUtils::getConfigSource(const QJsonObject &serverConfigO
return static_cast<apiDefs::ConfigSource>(serverConfigObject.value(apiDefs::key::configVersion).toInt()); return static_cast<apiDefs::ConfigSource>(serverConfigObject.value(apiDefs::key::configVersion).toInt());
} }
amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, QNetworkReply *reply) amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
const QByteArray &responseBody)
{ {
const int httpStatusCodeConflict = 409; const int httpStatusCodeConflict = 409;
const int httpStatusCodeNotFound = 404; const int httpStatusCodeNotFound = 404;
@@ -90,21 +92,19 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
if (!sslErrors.empty()) { if (!sslErrors.empty()) {
qDebug().noquote() << sslErrors; qDebug().noquote() << sslErrors;
return amnezia::ErrorCode::ApiConfigSslError; return amnezia::ErrorCode::ApiConfigSslError;
} else if (reply->error() == QNetworkReply::NoError) { } else if (replyError == QNetworkReply::NoError) {
return amnezia::ErrorCode::NoError; return amnezia::ErrorCode::NoError;
} else if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError } else if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) { || replyError == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << reply->error(); qDebug() << replyError;
return amnezia::ErrorCode::ApiConfigTimeoutError; return amnezia::ErrorCode::ApiConfigTimeoutError;
} else if (reply->error() == QNetworkReply::NetworkError::OperationNotImplementedError) { } else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
qDebug() << reply->error(); qDebug() << replyError;
return amnezia::ErrorCode::ApiUpdateRequestError; return amnezia::ErrorCode::ApiUpdateRequestError;
} else { } else {
QString err = reply->errorString(); qDebug() << QString::fromUtf8(responseBody);
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); qDebug() << replyError;
qDebug() << QString::fromUtf8(reply->readAll()); qDebug() << replyErrorString;
qDebug() << reply->error();
qDebug() << err;
qDebug() << httpStatusCode; qDebug() << httpStatusCode;
if (httpStatusCode == httpStatusCodeConflict) { if (httpStatusCode == httpStatusCodeConflict) {
return amnezia::ErrorCode::ApiConfigLimitError; return amnezia::ErrorCode::ApiConfigLimitError;
@@ -162,3 +162,51 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
return QString("vpn://%1").arg(QString(signedData.toBase64(QByteArray::Base64UrlEncoding))); return QString("vpn://%1").arg(QString(signedData.toBase64(QByteArray::Base64UrlEncoding)));
} }
QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
{
if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) {
return {};
}
QString vpnKeyText = "";
auto apiConfig = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
auto authData = serverConfigObject.value(QLatin1String("auth_data")).toObject();
const QString name = serverConfigObject.value(apiDefs::key::name).toString();
const QString description = serverConfigObject.value(apiDefs::key::description).toString();
const double configVersion = serverConfigObject.value(apiDefs::key::configVersion).toDouble();
const QString serviceType = apiConfig.value(apiDefs::key::serviceType).toString();
const QString serviceProtocol = apiConfig.value(QLatin1String("service_protocol")).toString();
const QString userCountryCode = apiConfig.value(QLatin1String("user_country_code")).toString();
const QString apiKey = authData.value(apiDefs::key::apiKey).toString();
QString vpnKeyStr = "{";
vpnKeyStr += "\"" + QString(apiDefs::key::name) + "\": \"" + name + "\", ";
vpnKeyStr += "\"" + QString(apiDefs::key::description) + "\": \"" + description + "\", ";
vpnKeyStr += "\"" + QString(apiDefs::key::configVersion) + "\": " + QString::number(static_cast<int>(configVersion)) + ", ";
vpnKeyStr += "\"" + QString(apiDefs::key::apiConfig) + "\": {";
vpnKeyStr += "\"" + QString(apiDefs::key::serviceType) + "\": \"" + serviceType + "\", ";
vpnKeyStr += "\"service_protocol\": \"" + serviceProtocol + "\", ";
vpnKeyStr += "\"user_country_code\": \"" + userCountryCode + "\"";
vpnKeyStr += "}, ";
vpnKeyStr += "\"auth_data\": {";
vpnKeyStr += "\"" + QString(apiDefs::key::apiKey) + "\": \"" + apiKey + "\"";
vpnKeyStr += "}";
vpnKeyStr += "}";
QByteArray vpnKeyCompressed = escapeUnicode(vpnKeyStr).toUtf8();
vpnKeyCompressed = qCompress(vpnKeyCompressed, 6);
vpnKeyCompressed = vpnKeyCompressed.mid(4);
QByteArray signedData = AMNEZIA_CONFIG_SIGNATURE + vpnKeyCompressed;
vpnKeyText = QString("vpn://%1").arg(QString(signedData.toBase64(QByteArray::Base64UrlEncoding)));
return vpnKeyText;
}
+4 -1
View File
@@ -18,9 +18,12 @@ namespace apiUtils
apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject); apiDefs::ConfigType getConfigType(const QJsonObject &serverConfigObject);
apiDefs::ConfigSource getConfigSource(const QJsonObject &serverConfigObject); apiDefs::ConfigSource getConfigSource(const QJsonObject &serverConfigObject);
amnezia::ErrorCode checkNetworkReplyErrors(const QList<QSslError> &sslErrors, QNetworkReply *reply); amnezia::ErrorCode checkNetworkReplyErrors(const QList<QSslError> &sslErrors, const QString &replyErrorString,
const QNetworkReply::NetworkError &replyError, const int httpStatusCode,
const QByteArray &responseBody);
QString getPremiumV1VpnKey(const QJsonObject &serverConfigObject); QString getPremiumV1VpnKey(const QJsonObject &serverConfigObject);
QString getPremiumV2VpnKey(const QJsonObject &serverConfigObject);
} }
#endif // APIUTILS_H #endif // APIUTILS_H
@@ -99,6 +99,9 @@ void CoreController::initModels()
m_apiDevicesModel.reset(new ApiDevicesModel(m_settings, this)); m_apiDevicesModel.reset(new ApiDevicesModel(m_settings, this));
m_engine->rootContext()->setContextProperty("ApiDevicesModel", m_apiDevicesModel.get()); m_engine->rootContext()->setContextProperty("ApiDevicesModel", m_apiDevicesModel.get());
m_newsModel.reset(new NewsModel(m_settings, this));
m_engine->rootContext()->setContextProperty("NewsModel", m_newsModel.get());
} }
void CoreController::initControllers() void CoreController::initControllers()
@@ -153,6 +156,9 @@ void CoreController::initControllers()
m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this)); m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this));
m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get()); m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get());
m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings, m_serversModel, this));
m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get());
} }
void CoreController::initAndroidController() void CoreController::initAndroidController()
@@ -316,6 +322,11 @@ void CoreController::initContainerModelUpdateHandler()
connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(), &ContainersModel::updateModel); connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(), &ContainersModel::updateModel);
connect(m_serversModel.get(), &ServersModel::defaultServerContainersUpdated, m_defaultServerContainersModel.get(), connect(m_serversModel.get(), &ServersModel::defaultServerContainersUpdated, m_defaultServerContainersModel.get(),
&ContainersModel::updateModel); &ContainersModel::updateModel);
connect(m_serversModel.get(), &ServersModel::gatewayStacksExpanded, this, [this]() {
if (m_serversModel->hasServersFromGatewayApi()) {
m_apiNewsController->fetchNews();
}
});
m_serversModel->resetModel(); m_serversModel->resetModel();
} }
+4
View File
@@ -12,6 +12,7 @@
#include "ui/controllers/api/apiConfigsController.h" #include "ui/controllers/api/apiConfigsController.h"
#include "ui/controllers/api/apiSettingsController.h" #include "ui/controllers/api/apiSettingsController.h"
#include "ui/controllers/api/apiPremV1MigrationController.h" #include "ui/controllers/api/apiPremV1MigrationController.h"
#include "ui/controllers/api/apiNewsController.h"
#include "ui/controllers/appSplitTunnelingController.h" #include "ui/controllers/appSplitTunnelingController.h"
#include "ui/controllers/allowedDnsController.h" #include "ui/controllers/allowedDnsController.h"
#include "ui/controllers/connectionController.h" #include "ui/controllers/connectionController.h"
@@ -47,6 +48,7 @@
#include "ui/models/services/sftpConfigModel.h" #include "ui/models/services/sftpConfigModel.h"
#include "ui/models/services/socks5ProxyConfigModel.h" #include "ui/models/services/socks5ProxyConfigModel.h"
#include "ui/models/sites_model.h" #include "ui/models/sites_model.h"
#include "ui/models/newsModel.h"
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#include "ui/notificationhandler.h" #include "ui/notificationhandler.h"
@@ -118,6 +120,7 @@ private:
QScopedPointer<ApiSettingsController> m_apiSettingsController; QScopedPointer<ApiSettingsController> m_apiSettingsController;
QScopedPointer<ApiConfigsController> m_apiConfigsController; QScopedPointer<ApiConfigsController> m_apiConfigsController;
QScopedPointer<ApiPremV1MigrationController> m_apiPremV1MigrationController; QScopedPointer<ApiPremV1MigrationController> m_apiPremV1MigrationController;
QScopedPointer<ApiNewsController> m_apiNewsController;
QSharedPointer<ContainersModel> m_containersModel; QSharedPointer<ContainersModel> m_containersModel;
QSharedPointer<ContainersModel> m_defaultServerContainersModel; QSharedPointer<ContainersModel> m_defaultServerContainersModel;
@@ -125,6 +128,7 @@ private:
QSharedPointer<LanguageModel> m_languageModel; QSharedPointer<LanguageModel> m_languageModel;
QSharedPointer<ProtocolsModel> m_protocolsModel; QSharedPointer<ProtocolsModel> m_protocolsModel;
QSharedPointer<SitesModel> m_sitesModel; QSharedPointer<SitesModel> m_sitesModel;
QSharedPointer<NewsModel> m_newsModel;
QSharedPointer<AllowedDnsModel> m_allowedDnsModel; QSharedPointer<AllowedDnsModel> m_allowedDnsModel;
QSharedPointer<AppSplitTunnelingModel> m_appSplitTunnelingModel; QSharedPointer<AppSplitTunnelingModel> m_appSplitTunnelingModel;
QSharedPointer<ClientManagementModel> m_clientManagementModel; QSharedPointer<ClientManagementModel> m_clientManagementModel;
+145 -121
View File
@@ -7,6 +7,7 @@
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QNetworkReply> #include <QNetworkReply>
#include <QPromise>
#include <QUrl> #include <QUrl>
#include "QBlockCipher.h" #include "QBlockCipher.h"
@@ -50,87 +51,25 @@ GatewayController::GatewayController(const QString &gatewayEndpoint, const bool
{ {
} }
ErrorCode GatewayController::get(const QString &endpoint, QByteArray &responseBody) GatewayController::EncryptedRequestData GatewayController::prepareRequest(const QString &endpoint, const QJsonObject &apiPayload)
{ {
EncryptedRequestData encRequestData;
encRequestData.errorCode = ErrorCode::NoError;
#ifdef Q_OS_IOS #ifdef Q_OS_IOS
IosController::Instance()->requestInetAccess(); IosController::Instance()->requestInetAccess();
QThread::msleep(10); QThread::msleep(10);
#endif #endif
QNetworkRequest request; encRequestData.request.setTransferTimeout(m_requestTimeoutMsecs);
request.setTransferTimeout(m_requestTimeoutMsecs); encRequestData.request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); encRequestData.request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8());
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));
request.setUrl(QString(endpoint).arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl));
// bypass killSwitch exceptions for API-gateway // bypass killSwitch exceptions for API-gateway
#ifdef AMNEZIA_DESKTOP #ifdef AMNEZIA_DESKTOP
if (m_isStrictKillSwitchEnabled) { 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 });
}
}
#endif
QNetworkReply *reply;
reply = amnApp->networkManager()->get(request);
QEventLoop wait;
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
QList<QSslError> sslErrors;
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec();
responseBody = reply->readAll();
if (sslErrors.isEmpty() && shouldBypassProxy(reply, responseBody, false)) {
auto requestFunction = [&request, &responseBody](const QString &url) {
request.setUrl(url);
return amnApp->networkManager()->get(request);
};
auto replyProcessingFunction = [&responseBody, &reply, &sslErrors, this](QNetworkReply *nestedReply,
const QList<QSslError> &nestedSslErrors) {
responseBody = nestedReply->readAll();
if (!sslErrors.isEmpty() || !shouldBypassProxy(nestedReply, responseBody, false)) {
sslErrors = nestedSslErrors;
reply = nestedReply;
return true;
}
return false;
};
bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction);
}
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, reply);
reply->deleteLater();
return errorCode;
}
ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody)
{
#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));
// bypass killSwitch exceptions for API-gateway
#ifdef AMNEZIA_DESKTOP
if (m_isStrictKillSwitchEnabled) {
QString host = QUrl(request.url()).host();
QString ip = NetworkUtilities::getIPAddress(host); QString ip = NetworkUtilities::getIPAddress(host);
if (!ip.isEmpty()) { if (!ip.isEmpty()) {
IpcClient::Interface()->addKillSwitchAllowedRange(QStringList { ip }); IpcClient::Interface()->addKillSwitchAllowedRange(QStringList { ip });
@@ -139,14 +78,14 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
#endif #endif
QSimpleCrypto::QBlockCipher blockCipher; QSimpleCrypto::QBlockCipher blockCipher;
QByteArray key = blockCipher.generatePrivateSalt(32); encRequestData.key = blockCipher.generatePrivateSalt(32);
QByteArray iv = blockCipher.generatePrivateSalt(32); encRequestData.iv = blockCipher.generatePrivateSalt(32);
QByteArray salt = blockCipher.generatePrivateSalt(8); encRequestData.salt = blockCipher.generatePrivateSalt(8);
QJsonObject keyPayload; QJsonObject keyPayload;
keyPayload[configKey::aesKey] = QString(key.toBase64()); keyPayload[configKey::aesKey] = QString(encRequestData.key.toBase64());
keyPayload[configKey::aesIv] = QString(iv.toBase64()); keyPayload[configKey::aesIv] = QString(encRequestData.iv.toBase64());
keyPayload[configKey::aesSalt] = QString(salt.toBase64()); keyPayload[configKey::aesSalt] = QString(encRequestData.salt.toBase64());
QByteArray encryptedKeyPayload; QByteArray encryptedKeyPayload;
QByteArray encryptedApiPayload; QByteArray encryptedApiPayload;
@@ -161,24 +100,37 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
} catch (...) { } catch (...) {
Utils::logException(); Utils::logException();
qCritical() << "error loading public key from environment variables"; 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); encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
EVP_PKEY_free(publicKey); EVP_PKEY_free(publicKey);
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), key, iv, "", salt); encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv, "", encRequestData.salt);
} catch (...) { // todo change error handling in QSimpleCrypto? } catch (...) {
Utils::logException(); Utils::logException();
qCritical() << "error when encrypting the request body"; qCritical() << "error when encrypting the request body";
return ErrorCode::ApiConfigDecryptionError; encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError;
return encRequestData;
} }
QJsonObject requestBody; QJsonObject requestBody;
requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64()); requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64());
requestBody[configKey::apiPayload] = QString(encryptedApiPayload.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; QEventLoop wait;
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
@@ -188,35 +140,44 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
wait.exec(); wait.exec();
QByteArray encryptedResponseBody = reply->readAll(); QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) { reply->deleteLater();
auto requestFunction = [&request, &encryptedResponseBody, &requestBody](const QString &url) {
request.setUrl(url); if (sslErrors.isEmpty() && shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) {
return amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson()); auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) {
encRequestData.request.setUrl(url);
return amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody);
}; };
auto replyProcessingFunction = [&encryptedResponseBody, &reply, &sslErrors, &key, &iv, &salt, auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &encRequestData,
this](QNetworkReply *nestedReply, const QList<QSslError> &nestedSslErrors) { this](QNetworkReply *reply, const QList<QSslError> &nestedSslErrors) {
encryptedResponseBody = nestedReply->readAll(); encryptedResponseBody = reply->readAll();
reply = nestedReply; replyErrorString = reply->errorString();
if (!sslErrors.isEmpty() || shouldBypassProxy(nestedReply, encryptedResponseBody, true, key, iv, salt)) { replyError = reply->error();
httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (!sslErrors.isEmpty() || shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) {
sslErrors = nestedSslErrors; sslErrors = nestedSslErrors;
return false; return false;
} }
return true; return true;
}; };
bypassProxy(endpoint, reply, requestFunction, replyProcessingFunction); auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString("");
auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString("");
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
} }
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, reply); auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, encryptedResponseBody);
reply->deleteLater();
if (errorCode) { if (errorCode) {
return errorCode; return errorCode;
} }
try { try {
responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt); QSimpleCrypto::QBlockCipher blockCipher;
responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, encRequestData.key, encRequestData.iv, "", encRequestData.salt);
return ErrorCode::NoError; return ErrorCode::NoError;
} catch (...) { // todo change error handling in QSimpleCrypto? } catch (...) { // todo change error handling in QSimpleCrypto?
Utils::logException(); Utils::logException();
@@ -225,7 +186,58 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
} }
} }
QStringList GatewayController::getProxyUrls() 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, [=]() {
QByteArray encryptedResponseBody = reply->readAll();
QString replyErrorString = reply->errorString();
auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
reply->deleteLater();
auto errorCode = apiUtils::checkNetworkReplyErrors(*sslErrors, replyErrorString, replyError, httpStatusCode, encryptedResponseBody);
if (errorCode) {
promise->addResult(qMakePair(errorCode, QByteArray()));
promise->finish();
return;
}
QSimpleCrypto::QBlockCipher blockCipher;
try {
QByteArray responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, 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();
}
});
return promise->future();
}
QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode)
{ {
QNetworkRequest request; QNetworkRequest request;
request.setTransferTimeout(m_requestTimeoutMsecs); request.setTransferTimeout(m_requestTimeoutMsecs);
@@ -235,15 +247,26 @@ QStringList GatewayController::getProxyUrls()
QList<QSslError> sslErrors; QList<QSslError> sslErrors;
QNetworkReply *reply; QNetworkReply *reply;
QStringList proxyStorageUrls; QStringList baseUrls;
if (m_isDevEnvironment) { if (m_isDevEnvironment) {
proxyStorageUrls = QString(DEV_S3_ENDPOINT).split(", "); baseUrls = QString(DEV_S3_ENDPOINT).split(", ");
} else { } else {
proxyStorageUrls = QString(PROD_S3_ENDPOINT).split(", "); baseUrls = QString(PROD_S3_ENDPOINT).split(", ");
} }
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
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");
}
for (const auto &proxyStorageUrl : proxyStorageUrls) { for (const auto &proxyStorageUrl : proxyStorageUrls) {
request.setUrl(proxyStorageUrl); request.setUrl(proxyStorageUrl);
reply = amnApp->networkManager()->get(request); reply = amnApp->networkManager()->get(request);
@@ -288,7 +311,10 @@ QStringList GatewayController::getProxyUrls()
} }
return endpoints; return endpoints;
} else { } else {
apiUtils::checkNetworkReplyErrors(sslErrors, reply); auto replyError = reply->error();
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qDebug() << replyError;
qDebug() << httpStatusCode;
qDebug() << "go to the next storage endpoint"; qDebug() << "go to the next storage endpoint";
reply->deleteLater(); reply->deleteLater();
@@ -297,33 +323,33 @@ QStringList GatewayController::getProxyUrls()
return {}; return {};
} }
bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key, bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &responseBody,
const QByteArray &iv, const QByteArray &salt) bool checkEncryption, const QByteArray &key, const QByteArray &iv, const QByteArray &salt)
{ {
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError || reply->error() == QNetworkReply::NetworkError::TimeoutError) { if (replyError == QNetworkReply::NetworkError::OperationCanceledError || replyError == QNetworkReply::NetworkError::TimeoutError) {
qDebug() << "timeout occurred"; qDebug() << "timeout occurred";
qDebug() << reply->error(); qDebug() << replyError;
return true; return true;
} else if (responseBody.contains("html")) { } else if (responseBody.contains("html")) {
qDebug() << "the response contains an html tag"; qDebug() << "the response contains an html tag";
return true; return true;
} else if (reply->error() == QNetworkReply::NetworkError::ContentNotFoundError) { } else if (replyError == QNetworkReply::NetworkError::ContentNotFoundError) {
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2) if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|| responseBody.contains(errorResponsePattern3)) { || responseBody.contains(errorResponsePattern3)) {
return false; return false;
} else { } else {
qDebug() << reply->error(); qDebug() << replyError;
return true; return true;
} }
} else if (reply->error() == QNetworkReply::NetworkError::OperationNotImplementedError) { } else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
if (responseBody.contains(updateRequestResponsePattern)) { if (responseBody.contains(updateRequestResponsePattern)) {
return false; return false;
} else { } else {
qDebug() << reply->error(); qDebug() << replyError;
return true; return true;
} }
} else if (reply->error() != QNetworkReply::NetworkError::NoError) { } else if (replyError != QNetworkReply::NetworkError::NoError) {
qDebug() << reply->error(); qDebug() << replyError;
return true; return true;
} else if (checkEncryption) { } else if (checkEncryption) {
try { try {
@@ -337,35 +363,33 @@ bool GatewayController::shouldBypassProxy(QNetworkReply *reply, const QByteArray
return false; return false;
} }
void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *reply, void GatewayController::bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
std::function<QNetworkReply *(const QString &url)> requestFunction, std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction) std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction)
{ {
QStringList proxyUrls = getProxyUrls(); QStringList proxyUrls = getProxyUrls(serviceType, userCountryCode);
std::random_device randomDevice; std::random_device randomDevice;
std::mt19937 generator(randomDevice()); std::mt19937 generator(randomDevice());
std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator); std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator);
QByteArray responseBody; QByteArray responseBody;
auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl, QNetworkReply *reply, auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl,
std::function<QNetworkReply *(const QString &url)> requestFunction, std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply * reply, const QList<QSslError> &sslErrors)> replyProcessingFunction) { std::function<bool(QNetworkReply * reply, const QList<QSslError> &sslErrors)> replyProcessingFunction) {
QEventLoop wait; QEventLoop wait;
QList<QSslError> sslErrors; QList<QSslError> sslErrors;
qDebug() << "go to the next proxy endpoint"; qDebug() << "go to the next proxy endpoint";
reply->deleteLater(); // delete the previous reply QNetworkReply *reply = requestFunction(endpoint.arg(proxyUrl));
reply = requestFunction(endpoint.arg(proxyUrl));
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; }); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
wait.exec(); wait.exec();
if (replyProcessingFunction(reply, sslErrors)) { auto result = replyProcessingFunction(reply, sslErrors);
return true; reply->deleteLater();
} return result;
return false;
}; };
if (m_proxyUrl.isEmpty()) { if (m_proxyUrl.isEmpty()) {
@@ -399,13 +423,13 @@ void GatewayController::bypassProxy(const QString &endpoint, QNetworkReply *repl
} }
if (!m_proxyUrl.isEmpty()) { if (!m_proxyUrl.isEmpty()) {
if (bypassFunction(endpoint, m_proxyUrl, reply, requestFunction, replyProcessingFunction)) { if (bypassFunction(endpoint, m_proxyUrl, requestFunction, replyProcessingFunction)) {
return; return;
} }
} }
for (const QString &proxyUrl : proxyUrls) { for (const QString &proxyUrl : proxyUrls) {
if (bypassFunction(endpoint, proxyUrl, reply, requestFunction, replyProcessingFunction)) { if (bypassFunction(endpoint, proxyUrl, requestFunction, replyProcessingFunction)) {
m_proxyUrl = proxyUrl; m_proxyUrl = proxyUrl;
break; break;
} }
+19 -5
View File
@@ -1,8 +1,10 @@
#ifndef GATEWAYCONTROLLER_H #ifndef GATEWAYCONTROLLER_H
#define GATEWAYCONTROLLER_H #define GATEWAYCONTROLLER_H
#include <QFuture>
#include <QNetworkReply> #include <QNetworkReply>
#include <QObject> #include <QObject>
#include <QPair>
#include "core/defs.h" #include "core/defs.h"
@@ -18,14 +20,26 @@ public:
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs, explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr); const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
amnezia::ErrorCode get(const QString &endpoint, QByteArray &responseBody);
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody); amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
private: private:
QStringList getProxyUrls(); struct EncryptedRequestData {
bool shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key = "", QNetworkRequest request;
const QByteArray &iv = "", const QByteArray &salt = ""); QByteArray requestBody;
void bypassProxy(const QString &endpoint, QNetworkReply *reply, std::function<QNetworkReply *(const QString &url)> requestFunction, 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 = "");
void bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode,
std::function<QNetworkReply *(const QString &url)> requestFunction,
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction); std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
int m_requestTimeoutMsecs; int m_requestTimeoutMsecs;
+14
View File
@@ -0,0 +1,14 @@
<svg width="24" height="24" viewBox="0 0 74 74" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4_34)">
<path d="M55.5 12.3333H18.5C15.0942 12.3333 12.3333 15.0943 12.3333 18.5V55.5C12.3333 58.9058 15.0942 61.6667 18.5 61.6667H55.5C58.9057 61.6667 61.6666 58.9058 61.6666 55.5V18.5C61.6666 15.0943 58.9057 12.3333 55.5 12.3333Z" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.5833 24.6667H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.5833 37H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.5833 49.3333H40.0833" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="61.5" cy="12.5" r="15" fill="#FBB36B" stroke="#1C1D21" stroke-width="5"/>
</g>
<defs>
<clipPath id="clip0_4_34">
<rect width="74" height="74" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 982 B

+8
View File
@@ -0,0 +1,8 @@
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#CBCAC8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<!-- Основа газеты -->
<rect x="4" y="4" width="16" height="16" rx="2"/>
<!-- Линии текста -->
<line x1="7" y1="8" x2="17" y2="8"/>
<line x1="7" y1="12" x2="17" y2="12"/>
<line x1="7" y1="16" x2="13" y2="16"/>
</svg>

After

Width:  |  Height:  |  Size: 410 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="17.5" cy="17.5" r="15" fill="#FBB36B" stroke="#1C1D21" stroke-width="5"/>
</svg>

After

Width:  |  Height:  |  Size: 188 B

@@ -99,7 +99,9 @@ bool AndroidController::initialize()
{"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onFileOpened)}, {"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onFileOpened)},
{"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onConfigImported)}, {"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onConfigImported)},
{"onAuthResult", "(Z)V", reinterpret_cast<void *>(onAuthResult)}, {"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; QJniEnvironment env;
@@ -202,6 +204,21 @@ bool AndroidController::isOnTv()
return callActivityMethod<jboolean>("isOnTv", "()Z"); return callActivityMethod<jboolean>("isOnTv", "()Z");
} }
bool AndroidController::isEdgeToEdgeEnabled()
{
return callActivityMethod<jboolean>("isEdgeToEdgeEnabled", "()Z");
}
int AndroidController::getStatusBarHeight()
{
return callActivityMethod<jint>("getStatusBarHeight", "()I");
}
int AndroidController::getNavigationBarHeight()
{
return callActivityMethod<jint>("getNavigationBarHeight", "()I");
}
void AndroidController::startQrReaderActivity() void AndroidController::startQrReaderActivity()
{ {
callActivityMethod("startQrCodeReader", "()V"); callActivityMethod("startQrCodeReader", "()V");
@@ -521,3 +538,23 @@ bool AndroidController::decodeQrCode(JNIEnv *env, jobject thiz, jstring data)
return ImportController::decodeQrCode(AndroidUtils::convertJString(env, 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);
}
@@ -39,6 +39,9 @@ public:
QString getFileName(const QString &uri); QString getFileName(const QString &uri);
bool isCameraPresent(); bool isCameraPresent();
bool isOnTv(); bool isOnTv();
bool isEdgeToEdgeEnabled();
int getStatusBarHeight();
int getNavigationBarHeight();
void startQrReaderActivity(); void startQrReaderActivity();
void setSaveLogs(bool enabled); void setSaveLogs(bool enabled);
void exportLogsFile(const QString &fileName); void exportLogsFile(const QString &fileName);
@@ -70,6 +73,8 @@ signals:
void importConfigFromOutside(QString config); void importConfigFromOutside(QString config);
void initConnectionState(Vpn::ConnectionState state); void initConnectionState(Vpn::ConnectionState state);
void authenticationResult(bool result); void authenticationResult(bool result);
void imeInsetsChanged(int heightDp);
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
private: private:
bool isWaitingStatus = true; bool isWaitingStatus = true;
@@ -98,6 +103,8 @@ private:
static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri); static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri);
static void onAuthResult(JNIEnv *env, jobject thiz, jboolean result); static void onAuthResult(JNIEnv *env, jobject thiz, jboolean result);
static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data); 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> template <typename Ret, typename ...Args>
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args); static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);
+35 -3
View File
@@ -2,7 +2,8 @@ import Foundation
import os.log import os.log
struct Log { struct Log {
static let osLog = Logger() private static let subsystemIdentifier = Bundle.main.bundleIdentifier ?? "org.amnezia.AmneziaVPN"
static let osLog = Logger(subsystem: subsystemIdentifier, category: "App")
private static let IsLoggingEnabledKey = "IsLoggingEnabled" private static let IsLoggingEnabledKey = "IsLoggingEnabled"
static var isLoggingEnabled: Bool { static var isLoggingEnabled: Bool {
@@ -77,9 +78,40 @@ struct Log {
static func log(_ type: OSLogType, title: String = "", message: String, url: URL = neLogURL) { static func log(_ type: OSLogType, title: String = "", message: String, url: URL = neLogURL) {
NSLog("\(title) \(message)") NSLog("\(title) \(message)")
guard isLoggingEnabled else { return } switch type {
case .debug:
if title.isEmpty {
osLog.debug("\(message, privacy: .public)")
} else {
osLog.debug("\(title, privacy: .public) \(message, privacy: .public)")
}
case .info:
if title.isEmpty {
osLog.info("\(message, privacy: .public)")
} else {
osLog.info("\(title, privacy: .public) \(message, privacy: .public)")
}
case .error:
if title.isEmpty {
osLog.error("\(message, privacy: .public)")
} else {
osLog.error("\(title, privacy: .public) \(message, privacy: .public)")
}
case .fault:
if title.isEmpty {
osLog.fault("\(message, privacy: .public)")
} else {
osLog.fault("\(title, privacy: .public) \(message, privacy: .public)")
}
default:
if title.isEmpty {
osLog.log("\(message, privacy: .public)")
} else {
osLog.log("\(title, privacy: .public) \(message, privacy: .public)")
}
}
osLog.log(level: type, "\(title) \(message)") guard isLoggingEnabled else { return }
let date = Date() let date = Date()
let level = Record.Level(from: type) let level = Record.Level(from: type)
+55 -1
View File
@@ -1,22 +1,76 @@
import Foundation import Foundation
import os.log import os.log
private let subsystemIdentifier = Bundle.main.bundleIdentifier ?? "org.amnezia.AmneziaVPN"
private let wireGuardSystemLogger = Logger(subsystem: subsystemIdentifier, category: "WireGuard")
private let openVPNSystemLogger = Logger(subsystem: subsystemIdentifier, category: "OpenVPN")
private let xraySystemLogger = Logger(subsystem: subsystemIdentifier, category: "Xray")
private let networkExtensionLogger = Logger(subsystem: subsystemIdentifier, category: "NetworkExtension")
private func logToSystem(_ logger: Logger, type: OSLogType, prefix: String, title: String, message: String) {
let combinedTitle: String
if title.isEmpty {
combinedTitle = prefix
} else {
combinedTitle = "\(prefix): \(title)"
}
switch type {
case .debug:
if combinedTitle.isEmpty {
logger.debug("\(message, privacy: .public)")
} else {
logger.debug("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
}
case .info:
if combinedTitle.isEmpty {
logger.info("\(message, privacy: .public)")
} else {
logger.info("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
}
case .error:
if combinedTitle.isEmpty {
logger.error("\(message, privacy: .public)")
} else {
logger.error("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
}
case .fault:
if combinedTitle.isEmpty {
logger.fault("\(message, privacy: .public)")
} else {
logger.fault("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
}
default:
if combinedTitle.isEmpty {
logger.log("\(message, privacy: .public)")
} else {
logger.log("\(combinedTitle, privacy: .public) \(message, privacy: .public)")
}
}
}
public func wg_log(_ type: OSLogType, title: String = "", staticMessage: StaticString) { public func wg_log(_ type: OSLogType, title: String = "", staticMessage: StaticString) {
neLog(type, title: "WG: \(title)", message: "\(staticMessage)") let stringMessage = String(describing: staticMessage)
logToSystem(wireGuardSystemLogger, type: type, prefix: "WG", title: title, message: stringMessage)
neLog(type, title: "WG: \(title)", message: stringMessage)
} }
public func wg_log(_ type: OSLogType, title: String = "", message: String) { public func wg_log(_ type: OSLogType, title: String = "", message: String) {
logToSystem(wireGuardSystemLogger, type: type, prefix: "WG", title: title, message: message)
neLog(type, title: "WG: \(title)", message: message) neLog(type, title: "WG: \(title)", message: message)
} }
public func ovpnLog(_ type: OSLogType, title: String = "", message: String) { public func ovpnLog(_ type: OSLogType, title: String = "", message: String) {
logToSystem(openVPNSystemLogger, type: type, prefix: "OVPN", title: title, message: message)
neLog(type, title: "OVPN: \(title)", message: message) neLog(type, title: "OVPN: \(title)", message: message)
} }
public func xrayLog(_ type: OSLogType, title: String = "", message: String) { public func xrayLog(_ type: OSLogType, title: String = "", message: String) {
logToSystem(xraySystemLogger, type: type, prefix: "XRAY", title: title, message: message)
neLog(type, title: "XRAY: \(title)", message: message) neLog(type, title: "XRAY: \(title)", message: message)
} }
public func neLog(_ type: OSLogType, title: String = "", message: String) { public func neLog(_ type: OSLogType, title: String = "", message: String) {
logToSystem(networkExtensionLogger, type: type, prefix: "NE", title: title, message: message)
Log.log(type, title: "NE: \(title)", message: message) Log.log(type, title: "NE: \(title)", message: message)
} }
@@ -1,6 +1,7 @@
import Foundation import Foundation
import NetworkExtension import NetworkExtension
import OpenVPNAdapter import OpenVPNAdapter
import CryptoKit
struct OpenVPNConfig: Decodable { struct OpenVPNConfig: Decodable {
let config: String let config: String
@@ -27,26 +28,83 @@ extension PacketTunnelProvider {
let ovpnConfiguration = Data(openVPNConfig.config.utf8) let ovpnConfiguration = Data(openVPNConfig.config.utf8)
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler) setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
} catch { } catch {
ovpnLog(.error, message: "Can't parse config: \(error.localizedDescription)") ovpnLog(.error, message: "Can't parse OpenVPN config: \(error.localizedDescription)")
if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError {
ovpnLog(.error, message: "Can't parse config: \(underlyingError.localizedDescription)")
}
return return
} }
} }
private func logOpenVPNError(_ error: NSError) {
let fatalFlag = (error.userInfo[OpenVPNAdapterErrorFatalKey] as? Bool) ?? false
var lines: [String] = []
lines.append("domain=\(error.domain) code=\(error.code) fatal=\(fatalFlag)")
if let adapterMessage = error.userInfo[OpenVPNAdapterErrorMessageKey] as? String, !adapterMessage.isEmpty {
lines.append("message=\(adapterMessage)")
}
let userInfoKeys = error.userInfo.keys.map { String(describing: $0) }.sorted()
if !userInfoKeys.isEmpty {
lines.append("userInfoKeys=[\(userInfoKeys.joined(separator: ","))]")
}
if let underlying = error.userInfo[NSUnderlyingErrorKey] as? NSError {
lines.append("underlying=\(underlying.domain)#\(underlying.code) fatal=\((underlying.userInfo[OpenVPNAdapterErrorFatalKey] as? Bool) ?? false)")
if let underlyingMessage = underlying.userInfo[OpenVPNAdapterErrorMessageKey] as? String, !underlyingMessage.isEmpty {
lines.append("underlyingMessage=\(underlyingMessage)")
} else if !underlying.localizedDescription.isEmpty {
lines.append("underlyingLocalized=\(underlying.localizedDescription)")
}
} else if let underlying = error.userInfo[NSUnderlyingErrorKey] {
lines.append("underlyingRaw=\(underlying)")
}
let formatted = lines.joined(separator: "\n ")
ovpnLog(.error, title: "Error", message: formatted)
}
private func setupAndlaunchOpenVPN(withConfig ovpnConfiguration: Data, private func setupAndlaunchOpenVPN(withConfig ovpnConfiguration: Data,
withShadowSocks viaSS: Bool = false, withShadowSocks viaSS: Bool = false,
completionHandler: @escaping (Error?) -> Void) { completionHandler: @escaping (Error?) -> Void) {
ovpnLog(.info, message: "Setup and launch") ovpnLog(.info, message: "Setup and launch")
let str = String(decoding: ovpnConfiguration, as: UTF8.self) var configString = String(decoding: ovpnConfiguration, as: UTF8.self)
let digest = SHA256.hash(data: ovpnConfiguration)
let digestString = digest.map { String(format: "%02x", $0) }.joined()
ovpnLog(.info, title: "ConfigDigest", message: digestString)
let hasTlsAuthOpen = configString.contains("<tls-auth>")
let hasTlsAuthClose = configString.contains("</tls-auth>")
ovpnLog(.info, title: "ConfigFlags", message: "tls-auth open=\(hasTlsAuthOpen) close=\(hasTlsAuthClose)")
let lines = configString.split(separator: "\n")
let head = lines.prefix(10).joined(separator: "\n")
let tail = lines.suffix(10).joined(separator: "\n")
ovpnLog(.debug, title: "ConfigHead", message: head)
ovpnLog(.debug, title: "ConfigTail", message: tail)
if let start = configString.range(of: "<tls-auth>"),
let end = configString.range(of: "</tls-auth>", range: start.upperBound..<configString.endIndex) {
let keyBody = String(configString[start.upperBound..<end.lowerBound])
ovpnLog(.debug, title: "TLSAuthInline", message: keyBody)
let sanitizedLines = keyBody
.split(whereSeparator: { $0.isNewline })
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
.filter { !$0.hasPrefix("#") }
let sanitizedKey = sanitizedLines.joined(separator: "\n")
ovpnLog(.debug, title: "TLSAuthSanitized", message: sanitizedKey)
let sanitizedBlock = "<tls-auth>\n\(sanitizedKey)\n</tls-auth>"
configString.replaceSubrange(start.lowerBound..<end.upperBound, with: sanitizedBlock)
}
let normalizedConfig = configString.replacingOccurrences(of: "\r\n", with: "\n")
let sanitizedData = Data(normalizedConfig.utf8)
let configuration = OpenVPNConfiguration() let configuration = OpenVPNConfiguration()
configuration.fileContent = ovpnConfiguration configuration.fileContent = sanitizedData
if str.contains("cloak") { if configString.contains("cloak") {
configuration.setPTCloak() configuration.setPTCloak()
} }
@@ -57,6 +115,8 @@ extension PacketTunnelProvider {
evaluation = try ovpnAdapter?.apply(configuration: configuration) evaluation = try ovpnAdapter?.apply(configuration: configuration)
} catch { } catch {
let nsError = error as NSError
ovpnLog(.error, title: "ApplyConfig", message: "domain=\(nsError.domain) code=\(nsError.code) info=\(nsError.userInfo)")
completionHandler(error) completionHandler(error)
return return
} }
@@ -208,8 +268,11 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
// Handle errors thrown by the OpenVPN library // Handle errors thrown by the OpenVPN library
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) { func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
let nsError = error as NSError
logOpenVPNError(nsError)
// Handle only fatal errors // Handle only fatal errors
guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool, guard let fatal = nsError.userInfo[OpenVPNAdapterErrorFatalKey] as? Bool,
fatal == true else { return } fatal == true else { return }
if vpnReachability.isTracking { if vpnReachability.isTracking {
+1
View File
@@ -169,6 +169,7 @@ void XrayProtocol::stop()
#if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_MACOS) #if defined(Q_OS_WIN) || defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
IpcClient::Interface()->disableKillSwitch(); IpcClient::Interface()->disableKillSwitch();
IpcClient::Interface()->StartRoutingIpv6(); IpcClient::Interface()->StartRoutingIpv6();
IpcClient::Interface()->restoreResolvers();
#endif #endif
qDebug() << "XrayProtocol::stop()"; qDebug() << "XrayProtocol::stop()";
m_xrayProcess.disconnect(); m_xrayProcess.disconnect();
+7 -1
View File
@@ -35,6 +35,9 @@
<file>images/controls/mail.svg</file> <file>images/controls/mail.svg</file>
<file>images/controls/map-pin.svg</file> <file>images/controls/map-pin.svg</file>
<file>images/controls/more-vertical.svg</file> <file>images/controls/more-vertical.svg</file>
<file>images/controls/news.svg</file>
<file>images/controls/news-unread.svg</file>
<file>images/controls/unread-dot.svg</file>
<file>images/controls/plus.svg</file> <file>images/controls/plus.svg</file>
<file>images/controls/qr-code.svg</file> <file>images/controls/qr-code.svg</file>
<file>images/controls/radio-button-inner-circle-pressed.png</file> <file>images/controls/radio-button-inner-circle-pressed.png</file>
@@ -49,6 +52,7 @@
<file>images/controls/server.svg</file> <file>images/controls/server.svg</file>
<file>images/controls/settings-2.svg</file> <file>images/controls/settings-2.svg</file>
<file>images/controls/settings.svg</file> <file>images/controls/settings.svg</file>
<file>images/controls/settings-news.svg</file>
<file>images/controls/share-2.svg</file> <file>images/controls/share-2.svg</file>
<file>images/controls/split-tunneling.svg</file> <file>images/controls/split-tunneling.svg</file>
<file>images/controls/tag.svg</file> <file>images/controls/tag.svg</file>
@@ -127,7 +131,6 @@
<file>ui/qml/Components/SelectLanguageDrawer.qml</file> <file>ui/qml/Components/SelectLanguageDrawer.qml</file>
<file>ui/qml/Components/ServersListView.qml</file> <file>ui/qml/Components/ServersListView.qml</file>
<file>ui/qml/Components/SettingsContainersListView.qml</file> <file>ui/qml/Components/SettingsContainersListView.qml</file>
<file>ui/qml/Components/TransportProtoSelector.qml</file> <file>ui/qml/Components/TransportProtoSelector.qml</file>
<file>ui/qml/Components/AddSitePanel.qml</file> <file>ui/qml/Components/AddSitePanel.qml</file>
<file>ui/qml/Config/GlobalConfig.qml</file> <file>ui/qml/Config/GlobalConfig.qml</file>
@@ -212,6 +215,8 @@
<file>ui/qml/Pages2/PageSettingsServerServices.qml</file> <file>ui/qml/Pages2/PageSettingsServerServices.qml</file>
<file>ui/qml/Pages2/PageSettingsServersList.qml</file> <file>ui/qml/Pages2/PageSettingsServersList.qml</file>
<file>ui/qml/Pages2/PageSettingsSplitTunneling.qml</file> <file>ui/qml/Pages2/PageSettingsSplitTunneling.qml</file>
<file>ui/qml/Pages2/PageSettingsNewsNotifications.qml</file>
<file>ui/qml/Pages2/PageSettingsNewsDetail.qml</file>
<file>ui/qml/Pages2/PageProtocolAwgClientSettings.qml</file> <file>ui/qml/Pages2/PageProtocolAwgClientSettings.qml</file>
<file>ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml</file> <file>ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml</file>
<file>ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml</file> <file>ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml</file>
@@ -242,6 +247,7 @@
<file>ui/qml/Components/OtpCodeDrawer.qml</file> <file>ui/qml/Components/OtpCodeDrawer.qml</file>
<file>ui/qml/Components/AwgTextField.qml</file> <file>ui/qml/Components/AwgTextField.qml</file>
<file>ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml</file> <file>ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml</file>
<file>ui/qml/Components/SmartScroll.qml</file>
</qresource> </qresource>
<qresource prefix="/countriesFlags"> <qresource prefix="/countriesFlags">
<file>images/flagKit/ZW.svg</file> <file>images/flagKit/ZW.svg</file>
+12 -2
View File
@@ -541,12 +541,12 @@ QString Settings::getGatewayEndpoint()
bool Settings::isDevGatewayEnv() bool Settings::isDevGatewayEnv()
{ {
return m_isDevGatewayEnv; return value("Conf/devGatewayEnv", false).toBool();
} }
void Settings::toggleDevGatewayEnv(bool enabled) void Settings::toggleDevGatewayEnv(bool enabled)
{ {
m_isDevGatewayEnv = enabled; setValue("Conf/devGatewayEnv", enabled);
} }
bool Settings::isHomeAdLabelVisible() bool Settings::isHomeAdLabelVisible()
@@ -578,3 +578,13 @@ void Settings::setAllowedDnsServers(const QStringList &servers)
{ {
setValue("Conf/allowedDnsServers", servers); setValue("Conf/allowedDnsServers", servers);
} }
QStringList Settings::readNewsIds() const
{
return value("News/readIds").toStringList();
}
void Settings::setReadNewsIds(const QStringList &ids)
{
setValue("News/readIds", ids);
}
+3 -1
View File
@@ -236,6 +236,9 @@ public:
QStringList allowedDnsServers() const; QStringList allowedDnsServers() const;
void setAllowedDnsServers(const QStringList &servers); void setAllowedDnsServers(const QStringList &servers);
QStringList readNewsIds() const;
void setReadNewsIds(const QStringList &ids);
signals: signals:
void saveLogsChanged(bool enabled); void saveLogsChanged(bool enabled);
void screenshotsEnabledChanged(bool enabled); void screenshotsEnabledChanged(bool enabled);
@@ -251,7 +254,6 @@ private:
mutable SecureQSettings m_settings; mutable SecureQSettings m_settings;
QString m_gatewayEndpoint; QString m_gatewayEndpoint;
bool m_isDevGatewayEnv = false;
}; };
#endif // SETTINGS_H #endif // SETTINGS_H
+144 -136
View File
@@ -4,9 +4,8 @@
<context> <context>
<name>AdLabel</name> <name>AdLabel</name>
<message> <message>
<location filename="../ui/qml/Components/AdLabel.qml" line="57"/>
<source>Amnezia Premium - for access to all websites and online resources</source> <source>Amnezia Premium - for access to all websites and online resources</source>
<translation>Amnezia Premium - доступ ко всем сайтам и онлайн ресурсам</translation> <translation type="vanished">Amnezia Premium - доступ ко всем сайтам и онлайн ресурсам</translation>
</message> </message>
</context> </context>
<context> <context>
@@ -61,7 +60,7 @@
<name>ApiAccountInfoModel</name> <name>ApiAccountInfoModel</name>
<message> <message>
<location filename="../ui/models/api/apiAccountInfoModel.cpp" line="31"/> <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> <source>Active</source>
<translation>Активна</translation> <translation>Активна</translation>
</message> </message>
@@ -71,35 +70,33 @@
<translation>Не активна</translation> <translation>Не активна</translation>
</message> </message>
<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> <source>%1 out of %2</source>
<translation>%1 из %2</translation> <translation>%1 из %2</translation>
</message> </message>
<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> <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>
<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> <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> </message>
</context> </context>
<context> <context>
<name>ApiConfigsController</name> <name>ApiConfigsController</name>
<message> <message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="413"/> <location filename="../ui/controllers/api/apiConfigsController.cpp" line="448"/>
<source>%1 installed successfully.</source> <source>%1 installed successfully.</source>
<translation>%1 успешно установлен.</translation> <translation>%1 успешно установлен.</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="472"/> <location filename="../ui/controllers/api/apiConfigsController.cpp" line="513"/>
<source>API config reloaded</source> <source>API config reloaded</source>
<translation>Конфигурация API перезагружена</translation> <translation>Конфигурация API перезагружена</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/api/apiConfigsController.cpp" line="476"/> <location filename="../ui/controllers/api/apiConfigsController.cpp" line="517"/>
<source>Successfully changed the country of connection to %1</source> <source>Successfully changed the country of connection to %1</source>
<translation>Страна подключения изменена на %1</translation> <translation>Страна подключения изменена на %1</translation>
</message> </message>
@@ -627,27 +624,32 @@ Thank you for staying with us!</source>
<translation>Продолжить</translation> <translation>Продолжить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="102"/> <location filename="../ui/qml/Pages2/PageHome.qml" line="92"/>
<source>Logging enabled</source> <source>Logging enabled</source>
<translation>Логирование включено</translation> <translation>Логирование включено</translation>
</message> </message>
<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> <source>Split tunneling enabled</source>
<translation>Раздельное туннелирование включено</translation> <translation>Раздельное туннелирование включено</translation>
</message> </message>
<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> <source>Split tunneling disabled</source>
<translation>Раздельное туннелирование выключено</translation> <translation>Раздельное туннелирование выключено</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="381"/> <location filename="../ui/qml/Pages2/PageHome.qml" line="409"/>
<source>VPN protocol</source> <source>VPN protocol</source>
<translation>VPN-протокол</translation> <translation>VPN-протокол</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageHome.qml" line="434"/> <location filename="../ui/qml/Pages2/PageHome.qml" line="462"/>
<source>Servers</source> <source>Servers</source>
<translation>Серверы</translation> <translation>Серверы</translation>
</message> </message>
@@ -1579,32 +1581,37 @@ Thank you for staying with us!</source>
<translation>Настройки</translation> <translation>Настройки</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="101"/> <location filename="../ui/qml/Pages2/PageSettings.qml" line="102"/>
<source>Servers</source> <source>Servers</source>
<translation>Серверы</translation> <translation>Серверы</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="112"/> <location filename="../ui/qml/Pages2/PageSettings.qml" line="113"/>
<source>Connection</source> <source>Connection</source>
<translation>Соединение</translation> <translation>Соединение</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="123"/> <location filename="../ui/qml/Pages2/PageSettings.qml" line="124"/>
<source>Application</source> <source>Application</source>
<translation>Приложение</translation> <translation>Приложение</translation>
</message> </message>
<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> <source>Backup</source>
<translation>Резервное копирование</translation> <translation>Резервное копирование</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="145"/> <location filename="../ui/qml/Pages2/PageSettings.qml" line="163"/>
<source>About AmneziaVPN</source> <source>About AmneziaVPN</source>
<translation>Об AmneziaVPN</translation> <translation>Об AmneziaVPN</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettings.qml" line="156"/> <location filename="../ui/qml/Pages2/PageSettings.qml" line="174"/>
<source>Dev console</source> <source>Dev console</source>
<translation>Dev console</translation> <translation>Dev console</translation>
</message> </message>
@@ -2079,52 +2086,40 @@ Thank you for staying with us!</source>
<context> <context>
<name>PageSettingsApiSubscriptionKey</name> <name>PageSettingsApiSubscriptionKey</name>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="43"/> <location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="85"/>
<source>Amnezia Premium
subscription key</source>
<translation>Amnezia Premium
ключ подключения</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="56"/>
<source>Copy key</source> <source>Copy key</source>
<translation>Скопировать ключ</translation> <translation>Скопировать ключ</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="61"/> <location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="90"/>
<source>Copied</source> <source>Copied</source>
<translation>Скопировано</translation> <translation>Скопировано</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="77"/> <location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="106"/>
<source>Save key as a file</source> <source>Save key as a file</source>
<translation>Сохранить ключ как файл</translation> <translation>Сохранить ключ как файл</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="84"/> <location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="113"/>
<source>Save AmneziaVPN config</source> <source>Save AmneziaVPN config</source>
<translation>Сохранить конфигурацию AmneziaVPN</translation> <translation>Сохранить конфигурацию AmneziaVPN</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="85"/> <location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="114"/>
<source>Config files (*.vpn)</source> <source>Config files (*.vpn)</source>
<translation>Файлы конфигов (*.vpn)</translation> <translation>Файлы конфигов (*.vpn)</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="110"/> <location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="139"/>
<source>Show key text</source> <source>Show key text</source>
<translation>Показать ключ</translation> <translation>Показать ключ</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="147"/> <location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="180"/>
<source>To read the QR code in the Amnezia app, tap + in the main menu &apos;QR code&apos;</source> <source>To read the QR code in the Amnezia app, tap + in the main menu &apos;QR code&apos;</source>
<translation>Для считывания QR-кода в приложении Amnezia выберите + в главном меню &apos;QR-код&apos;</translation> <translation>Для считывания QR-кода в приложении Amnezia выберите + в главном меню &apos;QR-код&apos;</translation>
</message> </message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="176"/>
<source>Amnezia Premium Subscription key</source>
<translation>Ключ подключения Amnezia Premium</translation>
</message>
</context> </context>
<context> <context>
<name>PageSettingsApiSupport</name> <name>PageSettingsApiSupport</name>
@@ -2197,32 +2192,37 @@ subscription key</source>
<translation>Режим</translation> <translation>Режим</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="185"/> <location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="155"/>
<source>Only &quot;Apps from the list should not have access via VPN&quot; mode is available on Windows</source>
<translation>На Windows доступен только режим &quot;Приложения из списка не должны работать через VPN&quot;</translation>
</message>
<message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="199"/>
<source>Remove </source> <source>Remove </source>
<translation>Удалить </translation> <translation>Удалить </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="186"/> <location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="200"/>
<source>Continue</source> <source>Continue</source>
<translation>Продолжить</translation> <translation>Продолжить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="187"/> <location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="201"/>
<source>Cancel</source> <source>Cancel</source>
<translation>Отменить</translation> <translation>Отменить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="228"/> <location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="242"/>
<source>application name</source> <source>application name</source>
<translation>название приложения</translation> <translation>название приложения</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="238"/> <location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="252"/>
<source>Open executable file</source> <source>Open executable file</source>
<translation>Открыть исполняемый файл</translation> <translation>Открыть исполняемый файл</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="239"/> <location filename="../ui/qml/Pages2/PageSettingsAppSplitTunneling.qml" line="253"/>
<source>Executable files (*.*)</source> <source>Executable files (*.*)</source>
<translation>Исполняемые файлы (*.*)</translation> <translation>Исполняемые файлы (*.*)</translation>
</message> </message>
@@ -2770,6 +2770,14 @@ subscription key</source>
<translation>Очистить логи</translation> <translation>Очистить логи</translation>
</message> </message>
</context> </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> <context>
<name>PageSettingsServerData</name> <name>PageSettingsServerData</name>
<message> <message>
@@ -3013,19 +3021,19 @@ subscription key</source>
<translation>Режим</translation> <translation>Режим</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="207"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="209"/>
<source>Remove </source> <source>Remove </source>
<translation>Удалить </translation> <translation>Удалить </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="208"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="210"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="356"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="357"/>
<source>Continue</source> <source>Continue</source>
<translation>Продолжить</translation> <translation>Продолжить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="209"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="211"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="357"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="358"/>
<source>Cancel</source> <source>Cancel</source>
<translation>Отменить</translation> <translation>Отменить</translation>
</message> </message>
@@ -3040,70 +3048,70 @@ subscription key</source>
<translation>Невозможно изменить настройки раздельного туннелирования во время активного соединения</translation> <translation>Невозможно изменить настройки раздельного туннелирования во время активного соединения</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="257"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="259"/>
<source>website or IP</source> <source>website or IP</source>
<translation>веб-сайт или IP</translation> <translation>веб-сайт или IP</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="302"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="304"/>
<source>Additional options</source> <source>Additional options</source>
<translation>Дополнительные настройки</translation> <translation>Дополнительные настройки</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="309"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="311"/>
<source>Import</source> <source>Import</source>
<translation>Импорт</translation> <translation>Импорт</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="322"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="324"/>
<source>Save site list</source> <source>Save site list</source>
<translation>Сохранить список сайтов</translation> <translation>Сохранить список сайтов</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="329"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="331"/>
<source>Save sites</source> <source>Save sites</source>
<translation>Сохранить сайты</translation> <translation>Сохранить сайты</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="330"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="332"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="457"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="458"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="470"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="471"/>
<source>Sites files (*.json)</source> <source>Sites files (*.json)</source>
<translation>Файлы сайтов (*.json)</translation> <translation>Файлы сайтов (*.json)</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="350"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="352"/>
<source>Clear site list</source> <source>Clear site list</source>
<translation>Очистить список сайтов</translation> <translation>Очистить список сайтов</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="354"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="355"/>
<source>Clear site list?</source> <source>Clear site list?</source>
<translation>Очистить список сайтов?</translation> <translation>Очистить список сайтов?</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="355"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="356"/>
<source>All sites will be removed from list.</source> <source>All sites will be removed from list.</source>
<translation>Все сайты будут удалены из списка.</translation> <translation>Все сайты будут удалены из списка.</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="419"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="420"/>
<source>Import a list of sites</source> <source>Import a list of sites</source>
<translation>Импортировать список с сайтами</translation> <translation>Импортировать список с сайтами</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="454"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="455"/>
<source>Replace site list</source> <source>Replace site list</source>
<translation>Заменить список с сайтами</translation> <translation>Заменить список с сайтами</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="456"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="457"/>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="469"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="470"/>
<source>Open sites file</source> <source>Open sites file</source>
<translation>Открыть список с сайтами</translation> <translation>Открыть список с сайтами</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="467"/> <location filename="../ui/qml/Pages2/PageSettingsSplitTunneling.qml" line="468"/>
<source>Add imported sites to existing ones</source> <source>Add imported sites to existing ones</source>
<translation>Добавить импортированные сайты к существующим</translation> <translation>Добавить импортированные сайты к существующим</translation>
</message> </message>
@@ -3394,38 +3402,38 @@ subscription key</source>
<context> <context>
<name>PageSetupWizardInstalling</name> <name>PageSetupWizardInstalling</name>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="59"/> <location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="63"/>
<source>The server has already been added to the application</source> <source>The server has already been added to the application</source>
<translation>Сервер уже был добавлен в приложение</translation> <translation>Сервер уже был добавлен в приложение</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="65"/> <location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="69"/>
<source>Amnezia has detected that your server is currently </source> <source>Amnezia has detected that your server is currently </source>
<translation>Amnezia обнаружила, что ваш сервер в настоящее время </translation> <translation>Amnezia обнаружила, что ваш сервер в настоящее время </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="66"/> <location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="70"/>
<source>busy installing other software. Amnezia installation </source> <source>busy installing other software. Amnezia installation </source>
<translation>занят установкой других протоколов или сервисов. Установка Amnezia </translation> <translation>занят установкой других протоколов или сервисов. Установка Amnezia </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="67"/> <location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="71"/>
<source>will pause until the server finishes installing other software</source> <source>will pause until the server finishes installing other software</source>
<translation>будет приостановлена до тех пор, пока сервер не завершит установку другого ПО</translation> <translation>будет приостановлена до тех пор, пока сервер не завершит установку другого ПО</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="106"/> <location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="110"/>
<source>Installing</source> <source>Installing</source>
<translation>Установка</translation> <translation>Установка</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="151"/> <location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="155"/>
<source>Cancel installation</source> <source>Cancel installation</source>
<translation>Отменить установку</translation> <translation>Отменить установку</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="22"/> <location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="22"/>
<location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="71"/> <location filename="../ui/qml/Pages2/PageSetupWizardInstalling.qml" line="75"/>
<source>Usually it takes no more than 5 minutes</source> <source>Usually it takes no more than 5 minutes</source>
<translation>Обычно это занимает не более 5 минут</translation> <translation>Обычно это занимает не более 5 минут</translation>
</message> </message>
@@ -3528,32 +3536,32 @@ subscription key</source>
<context> <context>
<name>PageSetupWizardViewConfig</name> <name>PageSetupWizardViewConfig</name>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="70"/> <location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="72"/>
<source>New connection</source> <source>New connection</source>
<translation>Новое соединение</translation> <translation>Новое соединение</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="110"/> <location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="112"/>
<source>Collapse content</source> <source>Collapse content</source>
<translation>Свернуть</translation> <translation>Свернуть</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="110"/> <location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="112"/>
<source>Show content</source> <source>Show content</source>
<translation>Показать</translation> <translation>Показать</translation>
</message> </message>
<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> <source>Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider.</source>
<translation>Включить обфускацию WireGuard. Это может быть полезно, если WireGuard блокируется вашим провайдером.</translation> <translation>Включить обфускацию WireGuard. Это может быть полезно, если WireGuard блокируется вашим провайдером.</translation>
</message> </message>
<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> <source>Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data.</source>
<translation>Используйте файлы конфигурации только из тех источников, которым вы доверяете. Файлы из общедоступных источников могли быть созданы с целью перехвата ваших личных данных.</translation> <translation>Используйте файлы конфигурации только из тех источников, которым вы доверяете. Файлы из общедоступных источников могли быть созданы с целью перехвата ваших личных данных.</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="202"/> <location filename="../ui/qml/Pages2/PageSetupWizardViewConfig.qml" line="204"/>
<source>Connect</source> <source>Connect</source>
<translation>Подключиться</translation> <translation>Подключиться</translation>
</message> </message>
@@ -3561,217 +3569,217 @@ subscription key</source>
<context> <context>
<name>PageShare</name> <name>PageShare</name>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="130"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="125"/>
<source>OpenVPN native format</source> <source>OpenVPN native format</source>
<translation>Оригинальный формат OpenVPN</translation> <translation>Оригинальный формат OpenVPN</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="135"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="130"/>
<source>WireGuard native format</source> <source>WireGuard native format</source>
<translation>Оригинальный формат WireGuard</translation> <translation>Оригинальный формат WireGuard</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="260"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="255"/>
<source>Connection</source> <source>Connection</source>
<translation>Соединение</translation> <translation>Соединение</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="328"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="323"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="329"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="324"/>
<source>Server</source> <source>Server</source>
<translation>Сервер</translation> <translation>Сервер</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="39"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="37"/>
<source>Config revoked</source> <source>Config revoked</source>
<translation>Конфигурация отозвана</translation> <translation>Конфигурация отозвана</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="55"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="50"/>
<source>Save AmneziaVPN config</source> <source>Save AmneziaVPN config</source>
<translation>Сохранить конфигурацию AmneziaVPN</translation> <translation>Сохранить конфигурацию AmneziaVPN</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="62"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="57"/>
<source>Save OpenVPN config</source> <source>Save OpenVPN config</source>
<translation>Сохранить конфигурацию OpenVPN</translation> <translation>Сохранить конфигурацию OpenVPN</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="69"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="64"/>
<source>Save WireGuard config</source> <source>Save WireGuard config</source>
<translation>Сохранить конфигурацию WireGuard</translation> <translation>Сохранить конфигурацию WireGuard</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="76"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="71"/>
<source>Save AmneziaWG config</source> <source>Save AmneziaWG config</source>
<translation>Сохранить конфигурацию AmneziaWG</translation> <translation>Сохранить конфигурацию AmneziaWG</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="83"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="78"/>
<source>Save Shadowsocks config</source> <source>Save Shadowsocks config</source>
<translation>Сохранить конфигурацию Shadowsocks</translation> <translation>Сохранить конфигурацию Shadowsocks</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="90"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="85"/>
<source>Save Cloak config</source> <source>Save Cloak config</source>
<translation>Сохранить конфигурацию Cloak</translation> <translation>Сохранить конфигурацию Cloak</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="97"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="92"/>
<source>Save XRay config</source> <source>Save XRay config</source>
<translation>Сохранить конфигурацию XRay</translation> <translation>Сохранить конфигурацию XRay</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="106"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="101"/>
<source>Connection to </source> <source>Connection to </source>
<translation>Подключение к </translation> <translation>Подключение к </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="107"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="102"/>
<source>File with connection settings to </source> <source>File with connection settings to </source>
<translation>Файл с настройками подключения к </translation> <translation>Файл с настройками подключения к </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="125"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="120"/>
<source>For the AmneziaVPN app</source> <source>For the AmneziaVPN app</source>
<translation>Для приложения AmneziaVPN</translation> <translation>Для приложения AmneziaVPN</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="140"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="135"/>
<source>AmneziaWG native format</source> <source>AmneziaWG native format</source>
<translation>Оригинальный формат AmneziaWG</translation> <translation>Оригинальный формат AmneziaWG</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="145"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="140"/>
<source>Shadowsocks native format</source> <source>Shadowsocks native format</source>
<translation>Оригинальный формат Shadowsocks</translation> <translation>Оригинальный формат Shadowsocks</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="150"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="145"/>
<source>Cloak native format</source> <source>Cloak native format</source>
<translation>Оригинальный формат Cloak</translation> <translation>Оригинальный формат Cloak</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="155"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="150"/>
<source>XRay native format</source> <source>XRay native format</source>
<translation>Оригинальный формат XRay</translation> <translation>Оригинальный формат XRay</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="183"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="178"/>
<source>Share VPN Access</source> <source>Share VPN Access</source>
<translation>Поделиться VPN</translation> <translation>Поделиться VPN</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="217"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="212"/>
<source>Share full access to the server and VPN</source> <source>Share full access to the server and VPN</source>
<translation>Поделиться полным доступом к серверу и VPN</translation> <translation>Поделиться полным доступом к серверу и VPN</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="218"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="213"/>
<source>Use for your own devices, or share with those you trust to manage the server.</source> <source>Use for your own devices, or share with those you trust to manage the server.</source>
<translation>Используйте для собственных устройств или передайте управление сервером тем, кому вы доверяете.</translation> <translation>Используйте для собственных устройств или передайте управление сервером тем, кому вы доверяете.</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="275"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="270"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="555"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="550"/>
<source>Users</source> <source>Users</source>
<translation>Пользователи</translation> <translation>Пользователи</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="309"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="304"/>
<source>User name</source> <source>User name</source>
<translation>Имя пользователя</translation> <translation>Имя пользователя</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="571"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="566"/>
<source>Search</source> <source>Search</source>
<translation>Поиск</translation> <translation>Поиск</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="694"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="691"/>
<source>Creation date: %1</source> <source>Creation date: %1</source>
<translation>Дата создания: %1</translation> <translation>Дата создания: %1</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="706"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="703"/>
<source>Latest handshake: %1</source> <source>Latest handshake: %1</source>
<translation>Последнее рукопожатие: %1</translation> <translation>Последнее рукопожатие: %1</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="718"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="715"/>
<source>Data received: %1</source> <source>Data received: %1</source>
<translation>Получено данных: %1</translation> <translation>Получено данных: %1</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="730"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="727"/>
<source>Data sent: %1</source> <source>Data sent: %1</source>
<translation>Отправлено данных: %1</translation> <translation>Отправлено данных: %1</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="740"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="737"/>
<source>Allowed IPs: %1</source> <source>Allowed IPs: %1</source>
<translation>Разрешенные подсети: %1</translation> <translation>Разрешенные подсети: %1</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="755"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="752"/>
<source>Rename</source> <source>Rename</source>
<translation>Переименовать</translation> <translation>Переименовать</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="780"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="777"/>
<source>Client name</source> <source>Client name</source>
<translation>Имя клиента</translation> <translation>Имя клиента</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="791"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="788"/>
<source>Save</source> <source>Save</source>
<translation>Сохранить</translation> <translation>Сохранить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="825"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="824"/>
<source>Revoke</source> <source>Revoke</source>
<translation>Отозвать</translation> <translation>Отозвать</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="828"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="827"/>
<source>Revoke the config for a user - %1?</source> <source>Revoke the config for a user - %1?</source>
<translation>Отозвать конфигурацию для пользователя - %1?</translation> <translation>Отозвать конфигурацию для пользователя - %1?</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="829"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="828"/>
<source>The user will no longer be able to connect to your server.</source> <source>The user will no longer be able to connect to your server.</source>
<translation>Пользователь больше не сможет подключаться к вашему серверу.</translation> <translation>Пользователь больше не сможет подключаться к вашему серверу.</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="830"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="829"/>
<source>Continue</source> <source>Continue</source>
<translation>Продолжить</translation> <translation>Продолжить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="831"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="830"/>
<source>Cancel</source> <source>Cancel</source>
<translation>Отменить</translation> <translation>Отменить</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="298"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="293"/>
<source>Share VPN access without the ability to manage the server</source> <source>Share VPN access without the ability to manage the server</source>
<translation>Поделиться доступом к VPN без возможности управления сервером</translation> <translation>Поделиться доступом к VPN без возможности управления сервером</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="389"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="384"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="390"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="385"/>
<source>Protocol</source> <source>Protocol</source>
<translation>Протокол</translation> <translation>Протокол</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="496"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="491"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="497"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="492"/>
<source>Connection format</source> <source>Connection format</source>
<translation>Формат подключения</translation> <translation>Формат подключения</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShare.qml" line="225"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="220"/>
<location filename="../ui/qml/Pages2/PageShare.qml" line="537"/> <location filename="../ui/qml/Pages2/PageShare.qml" line="532"/>
<source>Share</source> <source>Share</source>
<translation>Поделиться</translation> <translation>Поделиться</translation>
</message> </message>
@@ -3810,7 +3818,7 @@ subscription key</source>
<translation>Скопировано</translation> <translation>Скопировано</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShareConnection.qml" line="319"/> <location filename="../ui/qml/Pages2/PageShareConnection.qml" line="323"/>
<source>To read the QR code in the Amnezia app, select &quot;Add server&quot; &quot;I have data to connect&quot; &quot;QR code, key or settings file&quot;</source> <source>To read the QR code in the Amnezia app, select &quot;Add server&quot; &quot;I have data to connect&quot; &quot;QR code, key or settings file&quot;</source>
<translation>Для считывания QR-кода в приложении Amnezia выберите &quot;Добавить сервер&quot; &quot;У меня есть данные для подключения&quot; &quot;Открыть файл конфигурации, ключ или QR-код&quot;</translation> <translation>Для считывания QR-кода в приложении Amnezia выберите &quot;Добавить сервер&quot; &quot;У меня есть данные для подключения&quot; &quot;Открыть файл конфигурации, ключ или QR-код&quot;</translation>
</message> </message>
@@ -3841,22 +3849,22 @@ subscription key</source>
<translation>Сервер</translation> <translation>Сервер</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="114"/> <location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="115"/>
<source>Accessing </source> <source>Accessing </source>
<translation>Доступ </translation> <translation>Доступ </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="115"/> <location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="116"/>
<source>File with accessing settings to </source> <source>File with accessing settings to </source>
<translation>Файл с настройками доступа к </translation> <translation>Файл с настройками доступа к </translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="146"/> <location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="147"/>
<source>Share</source> <source>Share</source>
<translation>Поделиться</translation> <translation>Поделиться</translation>
</message> </message>
<message> <message>
<location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="154"/> <location filename="../ui/qml/Pages2/PageShareFullAccess.qml" line="155"/>
<source>Access error!</source> <source>Access error!</source>
<translation>Ошибка доступа!</translation> <translation>Ошибка доступа!</translation>
</message> </message>
@@ -4957,12 +4965,12 @@ FileZilla или другие SFTP-клиенты, а также смонтир
<context> <context>
<name>SettingsController</name> <name>SettingsController</name>
<message> <message>
<location filename="../ui/controllers/settingsController.cpp" line="242"/> <location filename="../ui/controllers/settingsController.cpp" line="258"/>
<source>All settings have been reset to default values</source> <source>All settings have been reset to default values</source>
<translation>Все настройки сброшены до значений по умолчанию</translation> <translation>Все настройки сброшены до значений по умолчанию</translation>
</message> </message>
<message> <message>
<location filename="../ui/controllers/settingsController.cpp" line="219"/> <location filename="../ui/controllers/settingsController.cpp" line="235"/>
<source>Backup file is corrupted</source> <source>Backup file is corrupted</source>
<translation>Файл резервной копии поврежден</translation> <translation>Файл резервной копии поврежден</translation>
</message> </message>
@@ -5051,7 +5059,7 @@ FileZilla или другие SFTP-клиенты, а также смонтир
<context> <context>
<name>TextFieldWithHeaderType</name> <name>TextFieldWithHeaderType</name>
<message> <message>
<location filename="../ui/qml/Controls2/TextFieldWithHeaderType.qml" line="117"/> <location filename="../ui/qml/Controls2/TextFieldWithHeaderType.qml" line="133"/>
<source>The field can&apos;t be empty</source> <source>The field can&apos;t be empty</source>
<translation>Поле не может быть пустым</translation> <translation>Поле не может быть пустым</translation>
</message> </message>
@@ -29,7 +29,6 @@ namespace
constexpr char uuid[] = "installation_uuid"; constexpr char uuid[] = "installation_uuid";
constexpr char osVersion[] = "os_version"; constexpr char osVersion[] = "os_version";
constexpr char appVersion[] = "app_version"; constexpr char appVersion[] = "app_version";
constexpr char appLanguage[] = "app_language";
constexpr char userCountryCode[] = "user_country_code"; constexpr char userCountryCode[] = "user_country_code";
constexpr char serverCountryCode[] = "server_country_code"; constexpr char serverCountryCode[] = "server_country_code";
@@ -47,6 +46,8 @@ namespace
constexpr char subscription[] = "subscription"; constexpr char subscription[] = "subscription";
constexpr char endDate[] = "end_date"; constexpr char endDate[] = "end_date";
constexpr char isConnectEvent[] = "is_connect_event";
} }
struct ProtocolData struct ProtocolData
@@ -63,6 +64,7 @@ namespace
{ {
QString osVersion; QString osVersion;
QString appVersion; QString appVersion;
QString appLanguage;
QString installationUuid; QString installationUuid;
@@ -82,6 +84,9 @@ namespace
if (!appVersion.isEmpty()) { if (!appVersion.isEmpty()) {
obj[configKey::appVersion] = appVersion; obj[configKey::appVersion] = appVersion;
} }
if (!appLanguage.isEmpty()) {
obj[apiDefs::key::appLanguage] = appLanguage;
}
if (!installationUuid.isEmpty()) { if (!installationUuid.isEmpty()) {
obj[configKey::uuid] = installationUuid; obj[configKey::uuid] = installationUuid;
} }
@@ -221,6 +226,9 @@ namespace
if (newServerConfig.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway) { if (newServerConfig.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway) {
apiConfig.insert(apiDefs::key::supportedProtocols, apiConfig.insert(apiDefs::key::supportedProtocols,
QJsonDocument::fromJson(apiResponseBody).object().value(apiDefs::key::supportedProtocols).toArray()); 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; serverConfig[configKey::apiConfig] = apiConfig;
@@ -249,6 +257,23 @@ ApiConfigsController::ApiConfigsController(const QSharedPointer<ServersModel> &s
{ {
} }
bool ApiConfigsController::exportVpnKey(const QString &fileName)
{
if (fileName.isEmpty()) {
emit errorOccurred(ErrorCode::PermissionsError);
return false;
}
prepareVpnKeyExport();
if (m_vpnKey.isEmpty()) {
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
return false;
}
SystemController::saveFile(fileName, m_vpnKey);
return true;
}
bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, const QString &fileName) bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, const QString &fileName)
{ {
if (fileName.isEmpty()) { if (fileName.isEmpty()) {
@@ -266,6 +291,7 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode,
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true), m_settings->getInstallationUuid(true),
apiConfigObject.value(configKey::userCountryCode).toString(), apiConfigObject.value(configKey::userCountryCode).toString(),
serverCountryCode, serverCountryCode,
@@ -306,6 +332,7 @@ bool ApiConfigsController::revokeNativeConfig(const QString &serverCountryCode)
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true), m_settings->getInstallationUuid(true),
apiConfigObject.value(configKey::userCountryCode).toString(), apiConfigObject.value(configKey::userCountryCode).toString(),
serverCountryCode, serverCountryCode,
@@ -330,6 +357,13 @@ void ApiConfigsController::prepareVpnKeyExport()
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject(); auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
auto vpnKey = apiConfigObject.value(apiDefs::key::vpnKey).toString(); auto vpnKey = apiConfigObject.value(apiDefs::key::vpnKey).toString();
if (vpnKey.isEmpty()) {
vpnKey = apiUtils::getPremiumV2VpnKey(serverConfigObject);
apiConfigObject.insert(apiDefs::key::vpnKey, vpnKey);
serverConfigObject.insert(configKey::apiConfig, apiConfigObject);
m_serversModel->editServer(serverConfigObject, m_serversModel->getProcessedServerIndex());
}
m_vpnKey = vpnKey; m_vpnKey = vpnKey;
vpnKey.replace("vpn://", ""); vpnKey.replace("vpn://", "");
@@ -349,7 +383,7 @@ bool ApiConfigsController::fillAvailableServices()
{ {
QJsonObject apiPayload; QJsonObject apiPayload;
apiPayload[configKey::osVersion] = QSysInfo::productType(); 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; QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/services"), apiPayload, responseBody); ErrorCode errorCode = executeRequest(QString("%1v1/services"), apiPayload, responseBody);
@@ -373,6 +407,7 @@ bool ApiConfigsController::importServiceFromGateway()
{ {
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true), m_settings->getInstallationUuid(true),
m_apiServicesModel->getCountryCode(), m_apiServicesModel->getCountryCode(),
"", "",
@@ -431,6 +466,7 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true), m_settings->getInstallationUuid(true),
apiConfig.value(configKey::userCountryCode).toString(), apiConfig.value(configKey::userCountryCode).toString(),
newCountryCode, newCountryCode,
@@ -443,6 +479,10 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
QJsonObject apiPayload = gatewayRequestData.toJsonObject(); QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(gatewayRequestData.serviceProtocol, protocolData, apiPayload); appendProtocolDataToApiPayload(gatewayRequestData.serviceProtocol, protocolData, apiPayload);
if (newCountryCode.isEmpty() && newCountryName.isEmpty() && !reloadServiceConfig) {
apiPayload.insert(configKey::isConnectEvent, true);
}
QByteArray responseBody; QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody); ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
@@ -526,7 +566,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
} }
} }
bool ApiConfigsController::deactivateDevice() bool ApiConfigsController::deactivateDevice(const bool isRemoveEvent)
{ {
auto serverIndex = m_serversModel->getProcessedServerIndex(); auto serverIndex = m_serversModel->getProcessedServerIndex();
auto serverConfigObject = m_serversModel->getServerConfig(serverIndex); auto serverConfigObject = m_serversModel->getServerConfig(serverIndex);
@@ -537,12 +577,17 @@ bool ApiConfigsController::deactivateDevice()
} }
if (isSubscriptionExpired(apiConfigObject)) { if (isSubscriptionExpired(apiConfigObject)) {
if (isRemoveEvent) {
return true;
} else {
emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError); emit errorOccurred(ErrorCode::ApiSubscriptionExpiredError);
return false; return false;
} }
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true), m_settings->getInstallationUuid(true),
apiConfigObject.value(configKey::userCountryCode).toString(), apiConfigObject.value(configKey::userCountryCode).toString(),
apiConfigObject.value(configKey::serverCountryCode).toString(), apiConfigObject.value(configKey::serverCountryCode).toString(),
@@ -582,6 +627,7 @@ bool ApiConfigsController::deactivateExternalDevice(const QString &uuid, const Q
GatewayRequestData gatewayRequestData { QSysInfo::productType(), GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION), QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
uuid, uuid,
apiConfigObject.value(configKey::userCountryCode).toString(), apiConfigObject.value(configKey::userCountryCode).toString(),
serverCountryCode, serverCountryCode,
@@ -21,7 +21,7 @@ public:
public slots: public slots:
bool exportNativeConfig(const QString &serverCountryCode, const QString &fileName); bool exportNativeConfig(const QString &serverCountryCode, const QString &fileName);
bool revokeNativeConfig(const QString &serverCountryCode); bool revokeNativeConfig(const QString &serverCountryCode);
// bool exportVpnKey(const QString &fileName); bool exportVpnKey(const QString &fileName);
void prepareVpnKeyExport(); void prepareVpnKeyExport();
void copyVpnKeyToClipboard(); void copyVpnKeyToClipboard();
@@ -30,7 +30,7 @@ public slots:
bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName, bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
bool reloadServiceConfig = false); bool reloadServiceConfig = false);
bool updateServiceFromTelegram(const int serverIndex); bool updateServiceFromTelegram(const int serverIndex);
bool deactivateDevice(); bool deactivateDevice(const bool isRemoveEvent);
bool deactivateExternalDevice(const QString &uuid, const QString &serverCountryCode); bool deactivateExternalDevice(const QString &uuid, const QString &serverCountryCode);
bool isConfigValid(); bool isConfigValid();
@@ -0,0 +1,68 @@
#include "apiNewsController.h"
#include "core/api/apiUtils.h"
#include <QJsonDocument>
#include <QJsonObject>
namespace
{
namespace configKey
{
constexpr char userCountryCode[] = "user_country_code";
constexpr char serviceType[] = "service_type";
}
}
ApiNewsController::ApiNewsController(const QSharedPointer<NewsModel> &newsModel, const std::shared_ptr<Settings> &settings,
const QSharedPointer<ServersModel> &serversModel, QObject *parent)
: QObject(parent), m_newsModel(newsModel), m_settings(settings), m_serversModel(serversModel)
{
}
void ApiNewsController::fetchNews()
{
if (m_serversModel.isNull()) {
qWarning() << "ServersModel is null, skip fetchNews";
return;
}
const auto stacks = m_serversModel->gatewayStacks();
if (stacks.isEmpty()) {
qDebug() << "No Gateway stacks, skip fetchNews";
return;
}
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
QJsonObject payload;
payload.insert("locale", m_settings->getAppLanguage().name().split("_").first());
const QJsonObject stacksJson = stacks.toJson();
if (stacksJson.contains(configKey::userCountryCode)) {
payload.insert(configKey::userCountryCode, stacksJson.value(configKey::userCountryCode));
}
if (stacksJson.contains(configKey::serviceType)) {
payload.insert(configKey::serviceType, stacksJson.value(configKey::serviceType));
}
auto future = gatewayController.postAsync(QString("%1v1/news"), payload);
future.then(this, [this](QPair<ErrorCode, QByteArray> result) {
auto [errorCode, responseBody] = result;
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();
}
}
m_newsModel->updateModel(newsArray);
emit fetchNewsFinished();
});
}
@@ -0,0 +1,34 @@
#ifndef APINEWSCONTROLLER_H
#define APINEWSCONTROLLER_H
#include <QJsonArray>
#include <QObject>
#include <QSharedPointer>
#include <memory>
#include "core/api/apiDefs.h"
#include "core/controllers/gatewayController.h"
#include "settings.h"
#include "ui/models/newsModel.h"
#include "ui/models/servers_model.h"
class ApiNewsController : public QObject
{
Q_OBJECT
public:
explicit ApiNewsController(const QSharedPointer<NewsModel> &newsModel, const std::shared_ptr<Settings> &settings,
const QSharedPointer<ServersModel> &serversModel, QObject *parent = nullptr);
Q_INVOKABLE void fetchNews();
signals:
void errorOccurred(ErrorCode errorCode);
void fetchNewsFinished();
private:
QSharedPointer<NewsModel> m_newsModel;
std::shared_ptr<Settings> m_settings;
QSharedPointer<ServersModel> m_serversModel;
};
#endif // APINEWSCONTROLLER_H
@@ -62,6 +62,7 @@ bool ApiSettingsController::getAccountInfo(bool reload)
apiPayload[configKey::serviceType] = apiConfig.value(configKey::serviceType).toString(); apiPayload[configKey::serviceType] = apiConfig.value(configKey::serviceType).toString();
apiPayload[configKey::authData] = authData; apiPayload[configKey::authData] = authData;
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION); apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
QByteArray responseBody; QByteArray responseBody;
+2
View File
@@ -26,6 +26,8 @@ namespace PageLoader
PageSettingsConnection, PageSettingsConnection,
PageSettingsDns, PageSettingsDns,
PageSettingsApplication, PageSettingsApplication,
PageSettingsNewsNotifications,
PageSettingsNewsDetail,
PageSettingsBackup, PageSettingsBackup,
PageSettingsAbout, PageSettingsAbout,
PageSettingsLogging, PageSettingsLogging,
@@ -1,10 +1,12 @@
#include "settingsController.h" #include "settingsController.h"
#include <QStandardPaths> #include <QStandardPaths>
#include <QOperatingSystemVersion>
#include "logger.h" #include "logger.h"
#include "systemController.h" #include "systemController.h"
#include "ui/qautostart.h" #include "ui/qautostart.h"
#include "amnezia_application.h"
#include "version.h" #include "version.h"
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
#include "platforms/android/android_controller.h" #include "platforms/android/android_controller.h"
@@ -32,7 +34,21 @@ SettingsController::SettingsController(const QSharedPointer<ServersModel> &serve
checkIfNeedDisableLogs(); checkIfNeedDisableLogs();
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
connect(AndroidController::instance(), &AndroidController::notificationStateChanged, this, &SettingsController::onNotificationStateChanged); 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 #endif
m_isDevModeEnabled = m_settings->isDevGatewayEnv();
toggleDevGatewayEnv(m_isDevModeEnabled);
} }
QString getPlatformName() QString getPlatformName()
@@ -139,6 +155,10 @@ void SettingsController::clearLogs()
Logger::clearLogs(false); Logger::clearLogs(false);
Logger::clearServiceLogs(); Logger::clearServiceLogs();
#endif #endif
qInfo().noquote() << QString("Started %1 version %2 %3").arg(APPLICATION_NAME, APP_VERSION, GIT_COMMIT_HASH);
qInfo().noquote() << QString("%1 (%2)").arg(QSysInfo::prettyProductName(), QSysInfo::currentCpuArchitecture());
qInfo().noquote() << QString("SSL backend: %1").arg(QSslSocket::sslLibraryVersionString());
} }
void SettingsController::backupAppConfig(const QString &fileName) void SettingsController::backupAppConfig(const QString &fileName)
@@ -423,6 +443,76 @@ bool SettingsController::isOnTv()
#endif #endif
} }
bool SettingsController::isEdgeToEdgeEnabled()
{
#ifdef Q_OS_ANDROID
if (!m_edgeToEdgeCached) {
m_cachedEdgeToEdgeEnabled = AndroidController::instance()->isEdgeToEdgeEnabled();
m_edgeToEdgeCached = true;
}
return m_cachedEdgeToEdgeEnabled;
#else
return false;
#endif
}
int SettingsController::getStatusBarHeight()
{
#ifdef Q_OS_ANDROID
if (m_cachedStatusBarHeight < 0) {
m_cachedStatusBarHeight = AndroidController::instance()->getStatusBarHeight();
}
return m_cachedStatusBarHeight;
#else
return 0;
#endif
}
int SettingsController::getNavigationBarHeight()
{
#ifdef Q_OS_ANDROID
if (m_cachedNavigationBarHeight < 0) {
m_cachedNavigationBarHeight = AndroidController::instance()->getNavigationBarHeight();
}
return m_cachedNavigationBarHeight;
#else
return 0;
#endif
}
int SettingsController::getSafeAreaTopMargin()
{
#ifdef Q_OS_ANDROID
if (isEdgeToEdgeEnabled()) {
int height = getStatusBarHeight();
int result = height > 0 ? height : 40; // fallback to 40 if system returns 0
return result;
}
#endif
return 0;
}
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;
}
#endif
return 0;
}
int SettingsController::getImeHeight()
{
return m_imeHeight;
}
bool SettingsController::isHomeAdLabelVisible() bool SettingsController::isHomeAdLabelVisible()
{ {
return m_settings->isHomeAdLabelVisible(); return m_settings->isHomeAdLabelVisible();
@@ -33,6 +33,9 @@ public:
Q_PROPERTY(bool isHomeAdLabelVisible READ isHomeAdLabelVisible NOTIFY isHomeAdLabelVisibleChanged) Q_PROPERTY(bool isHomeAdLabelVisible READ isHomeAdLabelVisible NOTIFY isHomeAdLabelVisibleChanged)
Q_PROPERTY(bool startMinimized READ isStartMinimizedEnabled NOTIFY startMinimizedChanged) Q_PROPERTY(bool startMinimized READ isStartMinimizedEnabled NOTIFY startMinimizedChanged)
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: public slots:
void toggleAmneziaDns(bool enable); void toggleAmneziaDns(bool enable);
@@ -96,6 +99,12 @@ public slots:
void toggleDevGatewayEnv(bool enabled); void toggleDevGatewayEnv(bool enabled);
bool isOnTv(); bool isOnTv();
bool isEdgeToEdgeEnabled();
int getStatusBarHeight();
int getNavigationBarHeight();
int getSafeAreaTopMargin();
int getSafeAreaBottomMargin();
int getImeHeight();
bool isHomeAdLabelVisible(); bool isHomeAdLabelVisible();
void disableHomeAdLabel(); void disableHomeAdLabel();
@@ -125,6 +134,10 @@ signals:
void gatewayEndpointChanged(const QString &endpoint); void gatewayEndpointChanged(const QString &endpoint);
void devGatewayEnvChanged(bool enabled); void devGatewayEnvChanged(bool enabled);
void imeHeightChanged(int height);
void safeAreaTopMarginChanged();
void safeAreaBottomMarginChanged();
void isHomeAdLabelVisibleChanged(bool visible); void isHomeAdLabelVisibleChanged(bool visible);
void startMinimizedChanged(); void startMinimizedChanged();
@@ -134,6 +147,12 @@ private:
QSharedPointer<LanguageModel> m_languageModel; QSharedPointer<LanguageModel> m_languageModel;
QSharedPointer<SitesModel> m_sitesModel; QSharedPointer<SitesModel> m_sitesModel;
QSharedPointer<AppSplitTunnelingModel> m_appSplitTunnelingModel; QSharedPointer<AppSplitTunnelingModel> m_appSplitTunnelingModel;
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; std::shared_ptr<Settings> m_settings;
QString m_appVersion; QString m_appVersion;
+5 -11
View File
@@ -31,7 +31,8 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
return tr("Active"); 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: { case EndDateRole: {
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { 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); return tr("%1 out of %2").arg(m_accountInfoData.activeDeviceCount).arg(m_accountInfoData.maxDeviceCount);
} }
case ServiceDescriptionRole: { case ServiceDescriptionRole: {
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2) { return m_accountInfoData.subscriptionDescription;
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 "";
}
} }
case IsComponentVisibleRole: { case IsComponentVisibleRole: {
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2 return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2
@@ -101,6 +93,8 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons
accountInfoData.configType = apiUtils::getConfigType(serverConfig); accountInfoData.configType = apiUtils::getConfigType(serverConfig);
accountInfoData.subscriptionDescription = accountInfoObject.value(apiDefs::key::subscriptionDescription).toString();
for (const auto &protocol : accountInfoObject.value(apiDefs::key::supportedProtocols).toArray()) { for (const auto &protocol : accountInfoObject.value(apiDefs::key::supportedProtocols).toArray()) {
accountInfoData.supportedProtocols.push_back(protocol.toString()); accountInfoData.supportedProtocols.push_back(protocol.toString());
} }
@@ -54,6 +54,8 @@ private:
apiDefs::ConfigType configType; apiDefs::ConfigType configType;
QStringList supportedProtocols; QStringList supportedProtocols;
QString subscriptionDescription;
}; };
AccountInfoData m_accountInfoData; AccountInfoData m_accountInfoData;
+130
View File
@@ -0,0 +1,130 @@
#include "ui/models/newsModel.h"
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QQmlEngine>
#include <QStandardPaths>
#include <algorithm>
NewsModel::NewsModel(const std::shared_ptr<Settings> &settings, QObject *parent) : QAbstractListModel(parent), m_settings(settings)
{
loadReadIds();
}
int NewsModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_items.size();
}
QVariant NewsModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_items.size())
return QVariant();
const NewsItem &item = m_items.at(index.row());
switch (role) {
case IdRole: return item.id;
case TitleRole: return item.title;
case ContentRole: return item.content;
case TimestampRole: return item.timestamp.toString(Qt::ISODate);
case IsReadRole: return item.read;
case IsProcessedRole: return index.row() == m_processedIndex;
default: return QVariant();
}
}
QHash<int, QByteArray> NewsModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[IdRole] = "id";
roles[TitleRole] = "title";
roles[ContentRole] = "content";
roles[TimestampRole] = "timestamp";
roles[IsReadRole] = "read";
roles[IsProcessedRole] = "isProcessed";
return roles;
}
void NewsModel::markAsRead(int index)
{
if (index < 0 || index >= m_items.size())
return;
if (!m_items[index].read) {
m_items[index].read = true;
m_readIds.insert(m_items[index].id);
saveReadIds();
QModelIndex idx = createIndex(index, 0);
emit dataChanged(idx, idx, { IsReadRole });
emit hasUnreadChanged();
}
}
int NewsModel::processedIndex() const
{
return m_processedIndex;
}
void NewsModel::setProcessedIndex(int index)
{
if (index < 0 || index >= m_items.size() || m_processedIndex == index)
return;
m_processedIndex = index;
emit processedIndexChanged(index);
}
void NewsModel::updateModel(const QJsonArray &serverItems)
{
QSet<QString> existingIds;
for (const NewsItem &item : m_items) {
existingIds.insert(item.id);
}
QList<NewsItem> newItems;
for (const QJsonValue &value : serverItems) {
if (!value.isObject())
continue;
const QJsonObject obj = value.toObject();
QString id = obj.value("id").toString();
if (!existingIds.contains(id)) {
NewsItem item;
item.id = id;
item.title = obj.value("title").toString();
item.content = obj.value("content").toString();
item.timestamp = QDateTime::fromString(obj.value("timestamp").toString(), Qt::ISODate);
item.read = m_readIds.contains(id);
newItems.append(item);
existingIds.insert(id);
}
}
beginResetModel();
m_items.append(newItems);
std::sort(m_items.begin(), m_items.end(), [](const NewsItem &a, const NewsItem &b) { return a.timestamp > b.timestamp; });
endResetModel();
emit hasUnreadChanged();
}
bool NewsModel::hasUnread() const
{
for (const NewsItem &item : m_items) {
if (!item.read)
return true;
}
return false;
}
void NewsModel::loadReadIds()
{
QStringList ids = m_settings->readNewsIds();
m_readIds = QSet<QString>(ids.begin(), ids.end());
}
void NewsModel::saveReadIds() const
{
m_settings->setReadNewsIds(QStringList(m_readIds.begin(), m_readIds.end()));
}
+62
View File
@@ -0,0 +1,62 @@
#ifndef NEWSMODEL_H
#define NEWSMODEL_H
#include "settings.h"
#include <QAbstractListModel>
#include <QDateTime>
#include <QJsonArray>
#include <QSet>
#include <QString>
#include <QVector>
#include <memory>
struct NewsItem
{
QString id;
QString title;
QString content;
QDateTime timestamp;
bool read;
};
class NewsModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
IdRole = Qt::UserRole + 1,
TitleRole,
ContentRole,
TimestampRole,
IsReadRole,
IsProcessedRole
};
explicit NewsModel(const std::shared_ptr<Settings> &settings, QObject *parent = nullptr);
Q_INVOKABLE void markAsRead(int index);
Q_PROPERTY(int processedIndex READ processedIndex WRITE setProcessedIndex NOTIFY processedIndexChanged)
Q_PROPERTY(bool hasUnread READ hasUnread NOTIFY hasUnreadChanged)
int processedIndex() const;
void setProcessedIndex(int index);
void updateModel(const QJsonArray &items);
bool hasUnread() const;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
signals:
void processedIndexChanged(int index);
void hasUnreadChanged();
private:
QVector<NewsItem> m_items;
int m_processedIndex = -1;
std::shared_ptr<Settings> m_settings;
QSet<QString> m_readIds;
void loadReadIds();
void saveReadIds() const;
};
#endif // NEWSMODEL_H
+97 -1
View File
@@ -44,6 +44,8 @@ ServersModel::ServersModel(std::shared_ptr<Settings> settings, QObject *parent)
connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged); connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged);
connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged); connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged);
connect(this, &QAbstractItemModel::modelReset, this, &ServersModel::recomputeGatewayStacks);
} }
int ServersModel::rowCount(const QModelIndex &parent) const int ServersModel::rowCount(const QModelIndex &parent) const
@@ -156,6 +158,18 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
QString primaryDns = server.value(config_key::dns1).toString(); QString primaryDns = server.value(config_key::dns1).toString();
return primaryDns == protocols::dns::amneziaDnsIp; 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(); return QVariant();
@@ -375,7 +389,6 @@ QHash<int, QByteArray> ServersModel::roleNames() const
{ {
QHash<int, QByteArray> roles; QHash<int, QByteArray> roles;
roles[NameRole] = "serverName";
roles[NameRole] = "name"; roles[NameRole] = "name";
roles[ServerDescriptionRole] = "serverDescription"; roles[ServerDescriptionRole] = "serverDescription";
roles[CollapsedServerDescriptionRole] = "collapsedServerDescription"; roles[CollapsedServerDescriptionRole] = "collapsedServerDescription";
@@ -402,6 +415,12 @@ QHash<int, QByteArray> ServersModel::roleNames() const
roles[IsCountrySelectionAvailableRole] = "isCountrySelectionAvailable"; roles[IsCountrySelectionAvailableRole] = "isCountrySelectionAvailable";
roles[ApiAvailableCountriesRole] = "apiAvailableCountries"; roles[ApiAvailableCountriesRole] = "apiAvailableCountries";
roles[ApiServerCountryCodeRole] = "apiServerCountryCode"; roles[ApiServerCountryCodeRole] = "apiServerCountryCode";
roles[IsAdVisibleRole] = "isAdVisible";
roles[AdHeaderRole] = "adHeader";
roles[AdDescriptionRole] = "adDescription";
roles[AdEndpointRole] = "adEndpoint";
return roles; return roles;
} }
@@ -756,6 +775,68 @@ bool ServersModel::isServerFromApi(const int serverIndex)
return data(serverIndex, IsServerFromTelegramApiRole).toBool() || data(serverIndex, IsServerFromGatewayApiRole).toBool(); return data(serverIndex, IsServerFromTelegramApiRole).toBool() || data(serverIndex, IsServerFromGatewayApiRole).toBool();
} }
bool ServersModel::hasServersFromGatewayApi()
{
return !m_gatewayStacks.isEmpty();
}
bool ServersModel::GatewayStacks::operator==(const GatewayStacks &other) const
{
return userCountryCodes == other.userCountryCodes && serviceTypes == other.serviceTypes;
}
QJsonObject ServersModel::GatewayStacks::toJson() const
{
QJsonObject obj;
if (!userCountryCodes.isEmpty()) {
obj.insert(configKey::userCountryCode, QJsonArray::fromStringList(userCountryCodes.values()));
}
if (!serviceTypes.isEmpty()) {
obj.insert(configKey::serviceType, QJsonArray::fromStringList(serviceTypes.values()));
}
return obj;
}
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;
}
computed.serviceTypes.insert(serviceType);
}
}
}
m_gatewayStacks = std::move(computed);
if (hasNewTags) {
emit gatewayStacksExpanded();
}
if (wasEmpty != m_gatewayStacks.isEmpty()) {
emit hasServersFromGatewayApiChanged();
}
}
bool ServersModel::isApiKeyExpired(const int serverIndex) bool ServersModel::isApiKeyExpired(const int serverIndex)
{ {
auto serverConfig = m_servers.at(serverIndex).toObject(); auto serverConfig = m_servers.at(serverIndex).toObject();
@@ -822,3 +903,18 @@ bool ServersModel::processedServerIsPremium() const
{ {
return apiUtils::isPremiumServer(getServerConfig(m_processedServerIndex)); 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();
}
+34
View File
@@ -10,6 +10,16 @@ class ServersModel : public QAbstractListModel
{ {
Q_OBJECT Q_OBJECT
public: public:
struct GatewayStacks
{
QSet<QString> userCountryCodes;
QSet<QString> serviceTypes;
bool isEmpty() const { return userCountryCodes.isEmpty() && serviceTypes.isEmpty(); }
bool operator==(const GatewayStacks &other) const;
QJsonObject toJson() const;
};
enum Roles { enum Roles {
NameRole = Qt::UserRole + 1, NameRole = Qt::UserRole + 1,
ServerDescriptionRole, ServerDescriptionRole,
@@ -37,6 +47,10 @@ public:
IsCountrySelectionAvailableRole, IsCountrySelectionAvailableRole,
ApiAvailableCountriesRole, ApiAvailableCountriesRole,
ApiServerCountryCodeRole, ApiServerCountryCodeRole,
IsAdVisibleRole,
AdHeaderRole,
AdDescriptionRole,
AdEndpointRole,
HasAmneziaDns HasAmneziaDns
}; };
@@ -52,6 +66,8 @@ public:
void resetModel(); void resetModel();
GatewayStacks gatewayStacks() const { return m_gatewayStacks; }
Q_PROPERTY(int defaultIndex READ getDefaultServerIndex WRITE setDefaultServerIndex NOTIFY defaultServerIndexChanged) Q_PROPERTY(int defaultIndex READ getDefaultServerIndex WRITE setDefaultServerIndex NOTIFY defaultServerIndexChanged)
Q_PROPERTY(QString defaultServerName READ getDefaultServerName NOTIFY defaultServerNameChanged) Q_PROPERTY(QString defaultServerName READ getDefaultServerName NOTIFY defaultServerNameChanged)
Q_PROPERTY(QString defaultServerDefaultContainerName READ getDefaultServerDefaultContainerName NOTIFY defaultServerDefaultContainerChanged) Q_PROPERTY(QString defaultServerDefaultContainerName READ getDefaultServerDefaultContainerName NOTIFY defaultServerDefaultContainerChanged)
@@ -62,9 +78,15 @@ public:
defaultServerDefaultContainerChanged) defaultServerDefaultContainerChanged)
Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIndexChanged) Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIndexChanged)
Q_PROPERTY(bool hasServersFromGatewayApi READ hasServersFromGatewayApi NOTIFY hasServersFromGatewayApiChanged)
Q_PROPERTY(int processedIndex READ getProcessedServerIndex WRITE setProcessedServerIndex NOTIFY processedServerIndexChanged) Q_PROPERTY(int processedIndex READ getProcessedServerIndex WRITE setProcessedServerIndex NOTIFY processedServerIndexChanged)
Q_PROPERTY(bool processedServerIsPremium READ processedServerIsPremium NOTIFY processedServerChanged) 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; bool processedServerIsPremium() const;
public slots: public slots:
@@ -82,6 +104,8 @@ public slots:
bool isDefaultServerHasWriteAccess(); bool isDefaultServerHasWriteAccess();
bool hasServerWithWriteAccess(); bool hasServerWithWriteAccess();
bool hasServersFromGatewayApi();
const int getServersCount(); const int getServersCount();
void setProcessedServerIndex(const int index); void setProcessedServerIndex(const int index);
@@ -128,6 +152,10 @@ public slots:
bool isApiKeyExpired(const int serverIndex); bool isApiKeyExpired(const int serverIndex);
void removeApiConfig(const int serverIndex); void removeApiConfig(const int serverIndex);
bool isAdVisible();
QString adHeader();
QString adDescription();
protected: protected:
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
@@ -147,6 +175,9 @@ signals:
void updateApiCountryModel(); void updateApiCountryModel();
void updateApiServicesModel(); void updateApiServicesModel();
void hasServersFromGatewayApiChanged();
void gatewayStacksExpanded();
private: private:
ServerCredentials serverCredentials(int index) const; ServerCredentials serverCredentials(int index) const;
@@ -167,6 +198,9 @@ private:
int m_processedServerIndex; int m_processedServerIndex;
bool m_isAmneziaDnsEnabled = m_settings->useAmneziaDns(); bool m_isAmneziaDnsEnabled = m_settings->useAmneziaDns();
GatewayStacks m_gatewayStacks;
void recomputeGatewayStacks();
}; };
#endif // SERVERSMODEL_H #endif // SERVERSMODEL_H
+111 -34
View File
@@ -2,7 +2,6 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Shapes import QtQuick.Shapes
import Qt5Compat.GraphicalEffects
import Style 1.0 import Style 1.0
@@ -13,61 +12,139 @@ import "../Controls2/TextTypes"
Rectangle { Rectangle {
id: root 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.width: 1
border.color: AmneziaStyle.color.goldenApricot border.color: AmneziaStyle.color.onyxBlack
color: AmneziaStyle.color.transparent
radius: 13 radius: 13
visible: false visible: ServersModel.isAdVisible
// visible: GC.isDesktop() && ServersModel.isDefaultServerFromApi
// && ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && SettingsController.isHomeAdLabelVisible
MouseArea { Keys.onTabPressed: {
anchors.fill: parent FocusController.nextKeyTabItem()
cursorShape: Qt.PointingHandCursor
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 { RowLayout {
id: ad id: content
anchors.fill: parent anchors.fill: parent
anchors.margins: 16 anchors.leftMargin: 16
anchors.rightMargin: 12
anchors.topMargin: 12
anchors.bottomMargin: 12
spacing: 20
Image { ColumnLayout {
source: "qrc:/images/controls/amnezia.svg" Layout.fillWidth: true
sourceSize: Qt.size(36, 36) spacing: 4
layer { CaptionTextType {
effect: ColorOverlay { Layout.fillWidth: true
text: ServersModel.adHeader
color: AmneziaStyle.color.paleGray color: AmneziaStyle.color.paleGray
} font.pixelSize: 14
} font.weight: 700
textFormat: Text.RichText
} }
CaptionTextType { CaptionTextType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.rightMargin: 10 text: ServersModel.adDescription
Layout.leftMargin: 10 color: AmneziaStyle.color.mutedGray
wrapMode: Text.WordWrap
text: qsTr("Amnezia Premium - for access to all websites and online resources")
color: AmneziaStyle.color.pearlGray
lineHeight: 18 lineHeight: 18
font.pixelSize: 15 lineHeightMode: Text.FixedHeight
font.pixelSize: 14
visible: text !== ""
}
} }
ImageButtonType { Item {
image: "qrc:/images/controls/close.svg" implicitWidth: 40
imageColor: AmneziaStyle.color.paleGray implicitHeight: 40
Layout.alignment: Qt.AlignVCenter
Rectangle {
id: chevronBackground
anchors.fill: parent
radius: 12
color: AmneziaStyle.color.transparent
border.width: root.activeFocus ? 1 : 0
border.color: AmneziaStyle.color.paleGray
Behavior on color {
PropertyAnimation { duration: 200 }
}
Behavior on border.width {
PropertyAnimation { duration: 200 }
}
}
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() { onClicked: function() {
SettingsController.disableHomeAdLabel() root.forceActiveFocus()
} Qt.openUrlExternally(ServersModel.getDefaultServerData("adEndpoint"))
} }
} }
} }
+1 -1
View File
@@ -32,7 +32,7 @@ DrawerType2 {
spacing: 8 spacing: 8
onImplicitHeightChanged: { onImplicitHeightChanged: {
root.expandedHeight = content.implicitHeight + 32 root.expandedHeight = content.implicitHeight + 32 + SettingsController.safeAreaBottomMargin
} }
Header2TextType { Header2TextType {
+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()
}
}
+16
View File
@@ -49,6 +49,22 @@ Item {
return drawerContent.state === stateName 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 { Connections {
target: PageController target: PageController
+12
View File
@@ -74,6 +74,18 @@ Item {
FocusController.nextKeyRightItem() FocusController.nextKeyRightItem()
} }
Connections {
target: Qt.application
function onStateChanged() {
if (Qt.application.state !== Qt.ApplicationActive) {
if (!menu.isClosed) {
menu.closeTriggered()
}
}
}
}
implicitWidth: rootButtonContent.implicitWidth implicitWidth: rootButtonContent.implicitWidth
implicitHeight: rootButtonContent.implicitHeight implicitHeight: rootButtonContent.implicitHeight
+1 -1
View File
@@ -15,7 +15,7 @@ Popup {
leftMargin: 25 leftMargin: 25
rightMargin: 25 rightMargin: 25
bottomMargin: 70 bottomMargin: 70 + SettingsController.safeAreaBottomMargin
width: parent.width - leftMargin - rightMargin width: parent.width - leftMargin - rightMargin
@@ -19,6 +19,9 @@ Item {
property string buttonText property string buttonText
property string buttonImageSource 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 var clickedFunc
property alias textField: textField property alias textField: textField
@@ -67,7 +70,7 @@ Item {
border.width: 1 border.width: 1
Behavior on border.color { Behavior on border.color {
PropertyAnimation { duration: 200 } PropertyAnimation { duration: 100 }
} }
RowLayout { RowLayout {
@@ -194,6 +197,14 @@ Item {
focusPolicy: Qt.NoFocus focusPolicy: Qt.NoFocus
text: root.buttonText text: root.buttonText
leftImageSource: root.buttonImageSource 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.top: content.top
anchors.bottom: content.bottom anchors.bottom: content.bottom
@@ -201,7 +212,7 @@ Item {
height: content.implicitHeight height: content.implicitHeight
width: content.implicitHeight width: content.implicitHeight
squareLeftSide: true squareLeftSide: false
clickedFunc: function() { clickedFunc: function() {
if (root.clickedFunc && typeof root.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 cloudyGray: Qt.rgba(215/255, 216/255, 219/255, 0.65)
readonly property color pearlGray: '#EAEAEC' readonly property color pearlGray: '#EAEAEC'
readonly property color translucentRichBrown: Qt.rgba(99/255, 51/255, 3/255, 0.26) 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 { BaseHeaderType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 20 Layout.topMargin: 20 + SettingsController.safeAreaTopMargin
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
+1 -1
View File
@@ -22,7 +22,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
} }
ListViewType { ListViewType {
+54 -11
View File
@@ -20,6 +20,21 @@ import "../Components"
PageType { PageType {
id: root 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 { Connections {
objectName: "pageControllerConnections" objectName: "pageControllerConnections"
@@ -68,19 +83,9 @@ PageType {
objectName: "homeColumnLayout" objectName: "homeColumnLayout"
anchors.fill: parent anchors.fill: parent
anchors.topMargin: 12 anchors.topMargin: 12 + SettingsController.safeAreaTopMargin
anchors.bottomMargin: 16 anchors.bottomMargin: 16
AdLabel {
id: adLabel
Layout.fillWidth: true
Layout.preferredHeight: adLabel.contentHeight
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 22
}
BasicButtonType { BasicButtonType {
id: loggingButton id: loggingButton
objectName: "loggingButton" objectName: "loggingButton"
@@ -109,6 +114,34 @@ PageType {
} }
} }
BasicButtonType {
id: devGatewayButton
objectName: "devGatewayButton"
property bool isDevGatewayEnabled: SettingsController.isDevGatewayEnv
Layout.alignment: Qt.AlignHCenter
implicitHeight: 36
defaultColor: AmneziaStyle.color.transparent
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
disabledColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.mutedGray
borderWidth: 0
visible: SettingsController.isDevModeEnabled && isDevGatewayEnabled
text: qsTr("Dev gateway enabled")
Keys.onEnterPressed: this.clicked()
Keys.onReturnPressed: this.clicked()
onClicked: {
PageController.goToPage(PageEnum.PageDevMenu)
}
}
ConnectButton { ConnectButton {
id: connectButton id: connectButton
objectName: "connectButton" objectName: "connectButton"
@@ -161,6 +194,16 @@ PageType {
parent: root 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.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onActiveFocusChanged: { onActiveFocusChanged: {
if(backButton.enabled && backButton.activeFocus) { if(backButton.enabled && backButton.activeFocus) {
@@ -31,6 +31,11 @@ PageType {
} }
} }
SmartScroll {
id: smartScroll
listView: listView
}
ListViewType { ListViewType {
id: listView id: listView
@@ -80,6 +85,13 @@ PageType {
clientMtu = textField.text clientMtu = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(mtuTextField)
}
}
checkEmptyText: true checkEmptyText: true
} }
@@ -97,6 +109,12 @@ PageType {
clientJunkPacketCount = textField.text clientJunkPacketCount = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(junkPacketCountTextField)
}
}
} }
AwgTextField { AwgTextField {
@@ -113,6 +131,12 @@ PageType {
clientJunkPacketMinSize = textField.text clientJunkPacketMinSize = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(junkPacketMinSizeTextField)
}
}
} }
AwgTextField { AwgTextField {
@@ -129,6 +153,12 @@ PageType {
clientJunkPacketMaxSize = textField.text clientJunkPacketMaxSize = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(junkPacketMaxSizeTextField)
}
}
} }
AwgTextField { AwgTextField {
@@ -147,6 +177,12 @@ PageType {
clientSpecialJunk1 = textField.text clientSpecialJunk1 = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk1TextField)
}
}
} }
AwgTextField { AwgTextField {
@@ -165,6 +201,12 @@ PageType {
clientSpecialJunk2 = textField.text clientSpecialJunk2 = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk2TextField)
}
}
} }
AwgTextField { AwgTextField {
@@ -183,6 +225,12 @@ PageType {
clientSpecialJunk3 = textField.text clientSpecialJunk3 = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk3TextField)
}
}
} }
AwgTextField { AwgTextField {
@@ -201,6 +249,12 @@ PageType {
clientSpecialJunk4 = textField.text clientSpecialJunk4 = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk4TextField)
}
}
} }
AwgTextField { AwgTextField {
@@ -219,6 +273,12 @@ PageType {
clientSpecialJunk5 = textField.text clientSpecialJunk5 = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(specialJunk5TextField)
}
}
} }
AwgTextField { AwgTextField {
@@ -237,6 +297,12 @@ PageType {
clientControlledJunk1 = textField.text clientControlledJunk1 = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(controlledJunk1TextField)
}
}
} }
AwgTextField { AwgTextField {
@@ -255,6 +321,12 @@ PageType {
clientControlledJunk2 = textField.text clientControlledJunk2 = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(controlledJunk2TextField)
}
}
} }
AwgTextField { AwgTextField {
@@ -273,6 +345,12 @@ PageType {
clientControlledJunk3 = textField.text clientControlledJunk3 = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(controlledJunk3TextField)
}
}
} }
AwgTextField { AwgTextField {
@@ -290,6 +368,12 @@ PageType {
clientSpecialHandshakeTimeout = textField.text clientSpecialHandshakeTimeout = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(iTimeTextField)
}
}
} }
Header2TextType { Header2TextType {
@@ -25,7 +25,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onActiveFocusChanged: { onActiveFocusChanged: {
if(backButton.enabled && backButton.activeFocus) { if(backButton.enabled && backButton.activeFocus) {
@@ -34,6 +34,11 @@ PageType {
} }
} }
SmartScroll {
id: smartScroll
listView: listView
}
ListViewType { ListViewType {
id: listView id: listView
@@ -81,6 +86,12 @@ PageType {
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(vpnAddressSubnetTextField)
}
}
checkEmptyText: true checkEmptyText: true
} }
@@ -104,6 +115,12 @@ PageType {
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(portTextField)
}
}
checkEmptyText: true checkEmptyText: true
} }
@@ -121,6 +138,12 @@ PageType {
serverJunkPacketCount = textField.text serverJunkPacketCount = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(junkPacketCountTextField)
}
}
} }
AwgTextField { AwgTextField {
@@ -137,6 +160,12 @@ PageType {
serverJunkPacketMinSize = textField.text serverJunkPacketMinSize = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(junkPacketMinSizeTextField)
}
}
} }
AwgTextField { AwgTextField {
@@ -153,6 +182,12 @@ PageType {
serverJunkPacketMaxSize = textField.text serverJunkPacketMaxSize = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(junkPacketMaxSizeTextField)
}
}
} }
AwgTextField { AwgTextField {
@@ -169,6 +204,12 @@ PageType {
serverInitPacketJunkSize = textField.text serverInitPacketJunkSize = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(initPacketJunkSizeTextField)
}
}
} }
AwgTextField { AwgTextField {
@@ -185,6 +226,12 @@ PageType {
serverResponsePacketJunkSize = textField.text serverResponsePacketJunkSize = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(responsePacketJunkSizeTextField)
}
}
} }
// AwgTextField { // AwgTextField {
@@ -233,6 +280,12 @@ PageType {
serverInitPacketMagicHeader = textField.text serverInitPacketMagicHeader = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(initPacketMagicHeaderTextField)
}
}
} }
AwgTextField { AwgTextField {
@@ -249,6 +302,12 @@ PageType {
serverResponsePacketMagicHeader = textField.text serverResponsePacketMagicHeader = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(responsePacketMagicHeaderTextField)
}
}
} }
AwgTextField { AwgTextField {
@@ -265,6 +324,12 @@ PageType {
serverUnderloadPacketMagicHeader = textField.text serverUnderloadPacketMagicHeader = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(underloadPacketMagicHeaderTextField)
}
}
} }
AwgTextField { AwgTextField {
@@ -281,6 +346,12 @@ PageType {
serverTransportPacketMagicHeader = textField.text serverTransportPacketMagicHeader = textField.text
} }
} }
textField.onActiveFocusChanged: {
if (textField.activeFocus) {
smartScroll.scrollToItem(transportPacketMagicHeaderTextField)
}
}
} }
BasicButtonType { BasicButtonType {
@@ -22,7 +22,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onActiveFocusChanged: { onActiveFocusChanged: {
if(backButton.enabled && backButton.activeFocus) { if(backButton.enabled && backButton.activeFocus) {
@@ -23,7 +23,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onActiveFocusChanged: { onActiveFocusChanged: {
if(backButton.enabled && backButton.activeFocus) { if(backButton.enabled && backButton.activeFocus) {
+1 -1
View File
@@ -25,7 +25,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: { onFocusChanged: {
if (this.activeFocus) { if (this.activeFocus) {
@@ -22,7 +22,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: { onFocusChanged: {
if (this.activeFocus) { if (this.activeFocus) {
@@ -22,7 +22,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: { onFocusChanged: {
if (this.activeFocus) { if (this.activeFocus) {
@@ -22,7 +22,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: { onFocusChanged: {
if (this.activeFocus) { if (this.activeFocus) {
@@ -23,7 +23,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: { onFocusChanged: {
if (this.activeFocus) { if (this.activeFocus) {
@@ -22,7 +22,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: { onFocusChanged: {
if (this.activeFocus) { if (this.activeFocus) {
@@ -30,7 +30,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: { onFocusChanged: {
if (this.activeFocus) { if (this.activeFocus) {
@@ -31,7 +31,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: { onFocusChanged: {
if (this.activeFocus) { if (this.activeFocus) {
@@ -31,7 +31,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: { onFocusChanged: {
if (this.activeFocus) { if (this.activeFocus) {
+31 -1
View File
@@ -14,6 +14,19 @@ import "../Config"
PageType { PageType {
id: root id: root
Connections {
target: ApiNewsController
function onFetchNewsFinished() {
PageController.showBusyIndicator(false)
}
function onErrorOccurred(errorCode) {
PageController.showErrorMessage(errorCode)
PageController.closePage()
PageController.showBusyIndicator(false)
}
}
ListViewType { ListViewType {
id: listView id: listView
@@ -25,7 +38,7 @@ PageType {
BaseHeaderType { BaseHeaderType {
id: header id: header
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 24 Layout.topMargin: 24 + SettingsController.safeAreaTopMargin
Layout.bottomMargin: 16 Layout.bottomMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -90,6 +103,7 @@ PageType {
servers, servers,
connection, connection,
application, application,
news,
backup, backup,
about, about,
devConsole devConsole
@@ -128,6 +142,22 @@ PageType {
} }
} }
QtObject {
id: news
property string title: qsTr("News & Notifications")
readonly property string leftImagePath: NewsModel.hasUnread ? "qrc:/images/controls/news-unread.svg" : "qrc:/images/controls/news.svg"
property bool isVisible: ServersModel.hasServersFromGatewayApi
readonly property var clickedHandler: function() {
if (!ServersModel.hasServersFromGatewayApi) {
return;
}
PageController.showBusyIndicator(true)
ApiNewsController.fetchNews()
PageController.goToPage(PageEnum.PageSettingsNewsNotifications)
}
}
QtObject { QtObject {
id: backup id: backup
+1 -1
View File
@@ -20,7 +20,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onActiveFocusChanged: { onActiveFocusChanged: {
if(backButton.enabled && backButton.activeFocus) { if(backButton.enabled && backButton.activeFocus) {
@@ -66,7 +66,7 @@ PageType {
id: backButton id: backButton
objectName: "backButton" objectName: "backButton"
Layout.topMargin: 20 Layout.topMargin: 20 + SettingsController.safeAreaTopMargin
} }
HeaderTypeWithButton { HeaderTypeWithButton {
@@ -23,7 +23,7 @@ PageType {
id: listView id: listView
anchors.fill: parent anchors.fill: parent
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
anchors.bottomMargin: 24 anchors.bottomMargin: 24
model: ApiDevicesModel model: ApiDevicesModel
@@ -79,7 +79,7 @@ PageType {
id: listView id: listView
anchors.fill: parent anchors.fill: parent
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
anchors.bottomMargin: 24 anchors.bottomMargin: 24
model: instructionsModel model: instructionsModel
@@ -28,7 +28,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onActiveFocusChanged: { onActiveFocusChanged: {
if(backButton.enabled && backButton.activeFocus) { if(backButton.enabled && backButton.activeFocus) {
@@ -71,7 +71,7 @@ PageType {
text: countryName text: countryName
descriptionText: isWorkerExpired ? qsTr("The configuration needs to be reissued") : "" descriptionText: isWorkerExpired ? qsTr("The configuration needs to be reissued") : ""
hideDescription: isWorkerExpired ? true : false hideDescription: isWorkerExpired ? false : true
descriptionColor: AmneziaStyle.color.vibrantRed descriptionColor: AmneziaStyle.color.vibrantRed
leftImageSource: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg" leftImageSource: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg"
@@ -93,7 +93,7 @@ PageType {
id: backButton id: backButton
objectName: "backButton" objectName: "backButton"
Layout.topMargin: 20 Layout.topMargin: 20 + SettingsController.safeAreaTopMargin
} }
HeaderTypeWithButton { HeaderTypeWithButton {
@@ -359,7 +359,7 @@ PageType {
PageController.showNotificationMessage(qsTr("Cannot unlink device during active connection")) PageController.showNotificationMessage(qsTr("Cannot unlink device during active connection"))
} else { } else {
PageController.showBusyIndicator(true) PageController.showBusyIndicator(true)
if (ApiConfigsController.deactivateDevice()) { if (ApiConfigsController.deactivateDevice(false)) {
ApiSettingsController.getAccountInfo(true) ApiSettingsController.getAccountInfo(true)
} }
PageController.showBusyIndicator(false) PageController.showBusyIndicator(false)
@@ -396,7 +396,7 @@ PageType {
PageController.showNotificationMessage(qsTr("Cannot remove server during active connection")) PageController.showNotificationMessage(qsTr("Cannot remove server during active connection"))
} else { } else {
PageController.showBusyIndicator(true) PageController.showBusyIndicator(true)
if (ApiConfigsController.deactivateDevice()) { if (ApiConfigsController.deactivateDevice(true)) {
InstallController.removeProcessedServer() InstallController.removeProcessedServer()
} }
PageController.showBusyIndicator(false) PageController.showBusyIndicator(false)
@@ -61,7 +61,7 @@ PageType {
width: root.width width: root.width
BackButtonType { BackButtonType {
Layout.topMargin: 20 Layout.topMargin: 20 + SettingsController.safeAreaTopMargin
} }
Label { Label {
@@ -119,7 +119,7 @@ PageType {
if (fileName !== "") { if (fileName !== "") {
PageController.showBusyIndicator(true) PageController.showBusyIndicator(true)
ExportController.exportConfig(fileName) ApiConfigsController.exportVpnKey(fileName)
PageController.showBusyIndicator(false) PageController.showBusyIndicator(false)
} }
} }
@@ -59,7 +59,7 @@ PageType {
id: listView id: listView
anchors.fill: parent anchors.fill: parent
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
anchors.bottomMargin: 24 anchors.bottomMargin: 24
model: supportModel model: supportModel
@@ -74,7 +74,7 @@ PageType {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
BackButtonType { BackButtonType {
id: backButton id: backButton
@@ -155,7 +155,7 @@ PageType {
textString: qsTr("Only \"Apps from the list should not have access via VPN\" mode is available on Windows") textString: qsTr("Only \"Apps from the list should not have access via VPN\" mode is available on Windows")
iconPath: "qrc:/images/controls/alert-circle.svg" iconPath: "qrc:/images/controls/alert-circle.svg"
enabled: (Qt.platform.os === "windows") && root.pageEnabled visible: (Qt.platform.os === "windows") && root.pageEnabled
} }
} }
@@ -165,9 +165,11 @@ PageType {
ScrollBar.vertical: ScrollBarType { policy: ScrollBar.AlwaysOn } ScrollBar.vertical: ScrollBarType { policy: ScrollBar.AlwaysOn }
anchors.top: header.bottom 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.left: parent.left
anchors.right: parent.right anchors.right: parent.right
clip: true
model: SortFilterProxyModel { model: SortFilterProxyModel {
id: proxyAppSplitTunnelingModel id: proxyAppSplitTunnelingModel
@@ -215,11 +217,14 @@ PageType {
} }
Rectangle { Rectangle {
anchors.fill: addAppButton anchors.left: parent.left
anchors.bottomMargin: -24 anchors.right: parent.right
anchors.bottom: parent.bottom
height: addAppButton.implicitHeight + 48 + SettingsController.safeAreaBottomMargin
color: AmneziaStyle.color.midnightBlack color: AmneziaStyle.color.midnightBlack
opacity: 0.8 opacity: 0.8
}
RowLayout { RowLayout {
id: addAppButton id: addAppButton
@@ -232,7 +237,7 @@ PageType {
anchors.topMargin: 24 anchors.topMargin: 24
anchors.rightMargin: 16 anchors.rightMargin: 16
anchors.leftMargin: 16 anchors.leftMargin: 16
anchors.bottomMargin: 24 anchors.bottomMargin: 24 + SettingsController.safeAreaBottomMargin
TextFieldWithHeaderType { TextFieldWithHeaderType {
id: searchField id: searchField
@@ -262,6 +267,7 @@ PageType {
} }
} }
} }
}
InstalledAppsDrawer { InstalledAppsDrawer {
id: installedAppDrawer id: installedAppDrawer
@@ -20,7 +20,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onActiveFocusChanged: { onActiveFocusChanged: {
if(backButton.enabled && backButton.activeFocus) { if(backButton.enabled && backButton.activeFocus) {
+1 -1
View File
@@ -40,7 +40,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onActiveFocusChanged: { onActiveFocusChanged: {
if(backButton.enabled && backButton.activeFocus) { if(backButton.enabled && backButton.activeFocus) {
@@ -20,7 +20,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onActiveFocusChanged: { onActiveFocusChanged: {
if(backButton.enabled && backButton.activeFocus) { if(backButton.enabled && backButton.activeFocus) {
+1 -1
View File
@@ -20,7 +20,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: { onFocusChanged: {
if (this.activeFocus) { if (this.activeFocus) {
@@ -17,7 +17,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
} }
FlickableType { FlickableType {
@@ -30,7 +30,7 @@ PageType {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
BackButtonType { BackButtonType {
id: backButton id: backButton
+1 -1
View File
@@ -22,7 +22,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: { onFocusChanged: {
if (this.activeFocus) { if (this.activeFocus) {
@@ -0,0 +1,69 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import SortFilterProxyModel 0.2
PageType {
id: root
property var newsItem
SortFilterProxyModel {
id: proxyNews
sourceModel: NewsModel
filters: [ ValueFilter { roleName: "isProcessed"; value: true } ]
Component.onCompleted: root.newsItem = proxyNews.get(0)
}
Connections {
target: NewsModel
function onProcessedIndexChanged() {
root.newsItem = proxyNews.get(0)
}
}
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
}
FlickableType {
id: fl
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
contentHeight: content.height
ColumnLayout {
id: content
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
spacing: 0
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: newsItem.title
}
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
text: newsItem.content
}
}
}
}
@@ -0,0 +1,81 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
PageType {
id: root
ColumnLayout {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
BackButtonType {
id: backButton
}
BaseHeaderType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("News & Notifications")
}
}
ListView {
id: newsList
width: parent.width
anchors.top: header.bottom
anchors.topMargin: 16
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
property bool isFocusable: true
model: NewsModel
clip: true
reuseItems: true
delegate: Item {
implicitWidth: newsList.width
implicitHeight: content.implicitHeight
ColumnLayout {
id: content
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
LabelWithButtonType {
Layout.fillWidth: true
leftImageSource: read ? "" : "qrc:/images/controls/unread-dot.svg"
isSmallLeftImage: !read
text: title
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
NewsModel.markAsRead(index)
NewsModel.processedIndex = index
PageController.goToPage(PageEnum.PageSettingsNewsDetail)
}
}
DividerType {}
}
}
}
}
@@ -62,7 +62,7 @@ PageType {
objectName: "mainLayout" objectName: "mainLayout"
anchors.fill: parent anchors.fill: parent
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
spacing: 4 spacing: 4
@@ -27,7 +27,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: { onFocusChanged: {
if (this.activeFocus) { if (this.activeFocus) {
@@ -25,7 +25,7 @@ PageType {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
BackButtonType { BackButtonType {
id: backButton id: backButton
@@ -88,7 +88,7 @@ PageType {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
BackButtonType { BackButtonType {
id: backButton id: backButton
@@ -168,11 +168,13 @@ PageType {
anchors.top: header.bottom anchors.top: header.bottom
anchors.topMargin: 16 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 width: parent.width
enabled: root.pageEnabled enabled: root.pageEnabled
clip: true
model: SortFilterProxyModel { model: SortFilterProxyModel {
id: proxySitesModel id: proxySitesModel
@@ -231,11 +233,14 @@ PageType {
} }
Rectangle { Rectangle {
anchors.fill: addSiteButton anchors.left: parent.left
anchors.bottomMargin: -24 anchors.right: parent.right
anchors.bottom: parent.bottom
height: addSiteButton.implicitHeight + 48
color: AmneziaStyle.color.midnightBlack color: AmneziaStyle.color.midnightBlack
opacity: 0.8 opacity: 0.8
}
RowLayout { RowLayout {
id: addSiteButton id: addSiteButton
@@ -283,6 +288,7 @@ PageType {
Keys.onEnterPressed: addSiteButtonImage.clicked() Keys.onEnterPressed: addSiteButtonImage.clicked()
} }
} }
}
DrawerType2 { DrawerType2 {
id: moreActionsDrawer id: moreActionsDrawer
@@ -350,7 +356,6 @@ PageType {
Layout.fillWidth: true Layout.fillWidth: true
text: qsTr("Clear site list") text: qsTr("Clear site list")
rightImageSource: "qrc:/images/controls/trash.svg"
clickedFunction: function() { clickedFunction: function() {
var headerText = qsTr("Clear site list?") var headerText = qsTr("Clear site list?")
@@ -21,7 +21,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: { onFocusChanged: {
if (this.activeFocus) { if (this.activeFocus) {
@@ -87,7 +87,7 @@ PageType {
textFormat: Text.RichText textFormat: Text.RichText
text: { text: {
var text = ApiServicesModel.getSelectedServiceData("features") var text = ApiServicesModel.getSelectedServiceData("features")
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")) return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
} }
MouseArea { MouseArea {
@@ -22,7 +22,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
Layout.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onActiveFocusChanged: { onActiveFocusChanged: {
if(backButton.enabled && backButton.activeFocus) { if(backButton.enabled && backButton.activeFocus) {
@@ -41,7 +41,7 @@ PageType {
property bool isVisible: SettingsController.getInstallationUuid() !== "" || PageController.isStartPageVisible() property bool isVisible: SettingsController.getInstallationUuid() !== "" || PageController.isStartPageVisible()
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 24 Layout.topMargin: 24 + SettingsController.safeAreaTopMargin
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -19,7 +19,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: { onFocusChanged: {
if (this.activeFocus) { if (this.activeFocus) {
+1 -1
View File
@@ -40,7 +40,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onActiveFocusChanged: { onActiveFocusChanged: {
if(backButton.enabled && backButton.activeFocus) { if(backButton.enabled && backButton.activeFocus) {
@@ -103,7 +103,7 @@ PageType {
BaseHeaderType { BaseHeaderType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 20 Layout.topMargin: 20 + SettingsController.safeAreaTopMargin
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
@@ -147,6 +147,7 @@ PageType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 24 Layout.topMargin: 24
Layout.bottomMargin: 24 + SettingsController.safeAreaBottomMargin
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
@@ -35,7 +35,7 @@ PageType {
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: { onFocusChanged: {
if (this.activeFocus) { if (this.activeFocus) {
@@ -41,7 +41,7 @@ PageType {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 20 anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onActiveFocusChanged: { onActiveFocusChanged: {
if(backButton.enabled && backButton.activeFocus) { if(backButton.enabled && backButton.activeFocus) {

Some files were not shown because too many files have changed in this diff Show More