mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-26 04:46:45 +03:00
Compare commits
20 Commits
checking-s
...
feature/su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4ab57508f | ||
|
|
b196be127b | ||
|
|
9b7303f656 | ||
|
|
4efaf20a1c | ||
|
|
1b57b4f8d5 | ||
|
|
9d96b1cd13 | ||
|
|
1d721ffb9a | ||
|
|
8a5f79fa52 | ||
|
|
2130131a9d | ||
|
|
e0b091b474 | ||
|
|
793abd61ea | ||
|
|
e4e433bff8 | ||
|
|
8547de82ea | ||
|
|
aa871bd1c9 | ||
|
|
23806e1def | ||
|
|
31867993ce | ||
|
|
7b7a922d92 | ||
|
|
09bd958d8d | ||
|
|
576e2226fe | ||
|
|
1533270e4e |
8
.github/workflows/deploy.yml
vendored
8
.github/workflows/deploy.yml
vendored
@@ -217,7 +217,11 @@ jobs:
|
||||
export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/ios/bin"
|
||||
export QT_MACOS_ROOT_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos"
|
||||
export PATH=$PATH:~/go/bin
|
||||
sh deploy/build_ios.sh
|
||||
sh deploy/build_ios.sh | \
|
||||
sed -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/d' | \
|
||||
sed -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/d' | \
|
||||
sed -e '/-DPROD_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DPROD_AGW_PUBLIC_KEY/d' | \
|
||||
sed -e '/-DDEV_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DDEV_AGW_PUBLIC_KEY/d'
|
||||
env:
|
||||
IOS_TRUST_CERT_BASE64: ${{ secrets.IOS_TRUST_CERT_BASE64 }}
|
||||
IOS_SIGNING_CERT_BASE64: ${{ secrets.IOS_SIGNING_CERT_BASE64 }}
|
||||
@@ -256,7 +260,7 @@ jobs:
|
||||
- name: 'Setup xcode'
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '14.3.1'
|
||||
xcode-version: '15.4.0'
|
||||
|
||||
- name: 'Install Qt'
|
||||
uses: jurplel/install-qt-action@v3
|
||||
|
||||
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
|
||||
project(${PROJECT} VERSION 4.8.2.3
|
||||
project(${PROJECT} VERSION 4.8.2.4
|
||||
DESCRIPTION "AmneziaVPN"
|
||||
HOMEPAGE_URL "https://amnezia.org/"
|
||||
)
|
||||
@@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
|
||||
set(RELEASE_DATE "${CURRENT_DATE}")
|
||||
|
||||
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||
set(APP_ANDROID_VERSION_CODE 2069)
|
||||
set(APP_ANDROID_VERSION_CODE 2071)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
|
||||
23
README.md
23
README.md
@@ -4,21 +4,21 @@
|
||||
[](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev)
|
||||
[](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client)
|
||||
|
||||
Amnezia is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server.
|
||||
[Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server.
|
||||
|
||||

|
||||
[](https://amnezia.org)
|
||||
|
||||
<br>
|
||||
### [Website](https://amnezia.org) | [Alt website link](https://storage.googleapis.com/kldscp/amnezia.org) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting)
|
||||
|
||||
<a href="https://amnezia.org/downloads"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/download.png" width="150" style="max-width: 100%;"></a>
|
||||
<a href="https://play.google.com/store/search?q=amnezia+vpn&c=apps"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/play.png" width="150" style="max-width: 100%;"></a>
|
||||
<a href="https://apps.apple.com/us/app/amneziavpn/id1600529900"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/apl.png" width="150" style="max-width: 100%;"></a>
|
||||
> [!TIP]
|
||||
> If the [Amnezia website](https://amnezia.org) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/kldscp/amnezia.org).
|
||||
|
||||
[Alternative download link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org/downloads)
|
||||
<a href="https://amnezia.org/downloads"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/download-website.svg" width="150" style="max-width: 100%; margin-right: 10px"></a>
|
||||
<a href="https://storage.googleapis.com/kldscp/amnezia.org/downloads"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/download-alt.svg" width="150" style="max-width: 100%;"></a>
|
||||
|
||||
[All releases](https://github.com/amnezia-vpn/amnezia-client/releases)
|
||||
|
||||
<br>
|
||||
<br/>
|
||||
|
||||
<a href="https://www.testiny.io"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/testiny.png" height="28px"></a>
|
||||
|
||||
@@ -33,7 +33,8 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep
|
||||
|
||||
## Links
|
||||
|
||||
- [https://amnezia.org](https://amnezia.org) - project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org)
|
||||
- [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org)
|
||||
- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation
|
||||
- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
|
||||
- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English)
|
||||
- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi)
|
||||
@@ -182,8 +183,8 @@ Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn
|
||||
Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p <br>
|
||||
USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4 <br>
|
||||
USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d <br>
|
||||
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
|
||||
|
||||
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3 <br>
|
||||
TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns
|
||||
## Acknowledgments
|
||||
|
||||
This project is tested with BrowserStack.
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- To request network state -->
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package org.amnezia.vpn.protocol.wireguard
|
||||
|
||||
import android.net.VpnService.Builder
|
||||
import java.io.IOException
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.launch
|
||||
import org.amnezia.awg.GoBackend
|
||||
import org.amnezia.vpn.protocol.Protocol
|
||||
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
|
||||
@@ -27,6 +28,8 @@ open class Wireguard : Protocol() {
|
||||
|
||||
private var tunnelHandle: Int = -1
|
||||
protected open val ifName: String = "amn0"
|
||||
private lateinit var scope: CoroutineScope
|
||||
private var statusJob: Job? = null
|
||||
|
||||
override val statistics: Statistics
|
||||
get() {
|
||||
@@ -49,46 +52,17 @@ open class Wireguard : Protocol() {
|
||||
|
||||
override fun internalInit() {
|
||||
if (!isInitialized) loadSharedLibrary(context, "wg-go")
|
||||
if (this::scope.isInitialized) {
|
||||
scope.cancel()
|
||||
}
|
||||
scope = CoroutineScope(Dispatchers.IO)
|
||||
}
|
||||
|
||||
override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) {
|
||||
val wireguardConfig = parseConfig(config)
|
||||
val startTime = System.currentTimeMillis()
|
||||
start(wireguardConfig, vpnBuilder, protect)
|
||||
waitForConnection(startTime)
|
||||
state.value = CONNECTED
|
||||
}
|
||||
|
||||
private suspend fun waitForConnection(startTime: Long) {
|
||||
Log.d(TAG, "Waiting for connection")
|
||||
withContext(Dispatchers.IO) {
|
||||
val time = String.format(Locale.ROOT,"%.3f", startTime / 1000.0)
|
||||
try {
|
||||
delay(1000)
|
||||
var log = getLogcat(time)
|
||||
Log.v(TAG, "First waiting log: $log")
|
||||
// check that there is a connection log,
|
||||
// to avoid infinite connection
|
||||
if (!log.contains("Attaching to interface")) {
|
||||
Log.w(TAG, "Logs do not contain a connection log")
|
||||
return@withContext
|
||||
}
|
||||
while (!log.contains("Received handshake response")) {
|
||||
delay(1000)
|
||||
log = getLogcat(time)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to get logcat: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLogcat(time: String): String =
|
||||
ProcessBuilder("logcat", "--buffer=main", "--format=raw", "*:S AmneziaWG/awg0", "-t", time)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
.inputStream.reader().readText()
|
||||
|
||||
protected open fun parseConfig(config: JSONObject): WireguardConfig {
|
||||
val configData = config.getJSONObject("wireguard_config_data")
|
||||
return WireguardConfig.build {
|
||||
@@ -178,6 +152,43 @@ open class Wireguard : Protocol() {
|
||||
tunnelHandle = -1
|
||||
throw VpnStartException("Protect VPN interface: permission not granted or revoked")
|
||||
}
|
||||
launchStatusJob()
|
||||
}
|
||||
|
||||
private fun launchStatusJob() {
|
||||
Log.d(TAG, "Launch status job")
|
||||
statusJob = scope.launch {
|
||||
while (true) {
|
||||
val lastHandshake = getLastHandshake()
|
||||
Log.v(TAG, "lastHandshake=$lastHandshake")
|
||||
if (lastHandshake == 0L) {
|
||||
delay(1000)
|
||||
continue
|
||||
}
|
||||
if (lastHandshake == -2L || lastHandshake > 0L) state.value = CONNECTED
|
||||
else if (lastHandshake == -1L) state.value = DISCONNECTED
|
||||
statusJob = null
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLastHandshake(): Long {
|
||||
if (tunnelHandle == -1) {
|
||||
Log.e(TAG, "Trying to get config of a non-existent tunnel")
|
||||
return -1
|
||||
}
|
||||
val config = GoBackend.awgGetConfig(tunnelHandle)
|
||||
if (config == null) {
|
||||
Log.e(TAG, "Failed to get tunnel config")
|
||||
return -2
|
||||
}
|
||||
val lastHandshake = config.lines().find { it.startsWith("last_handshake_time_sec=") }?.substring(24)?.toLong()
|
||||
if (lastHandshake == null) {
|
||||
Log.e(TAG, "Failed to get last_handshake_time_sec")
|
||||
return -2
|
||||
}
|
||||
return lastHandshake
|
||||
}
|
||||
|
||||
override fun stopVpn() {
|
||||
@@ -185,6 +196,8 @@ open class Wireguard : Protocol() {
|
||||
Log.w(TAG, "Tunnel already down")
|
||||
return
|
||||
}
|
||||
statusJob?.cancel()
|
||||
statusJob = null
|
||||
val handleToClose = tunnelHandle
|
||||
tunnelHandle = -1
|
||||
GoBackend.awgTurnOff(handleToClose)
|
||||
|
||||
@@ -377,6 +377,13 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody)
|
||||
|
||||
auto errorCode = checkErrors(sslErrors, reply);
|
||||
reply->deleteLater();
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
if (!responseBody.contains("services")) {
|
||||
return ErrorCode::ApiServicesMissingError;
|
||||
}
|
||||
}
|
||||
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +109,7 @@ namespace amnezia
|
||||
ApiConfigSslError = 1104,
|
||||
ApiMissingAgwPublicKey = 1105,
|
||||
ApiConfigDecryptionError = 1106,
|
||||
ApiServicesMissingError = 1107,
|
||||
|
||||
// QFile errors
|
||||
OpenError = 1200,
|
||||
|
||||
@@ -63,7 +63,8 @@ QString errorString(ErrorCode code) {
|
||||
case (ErrorCode::ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break;
|
||||
case (ErrorCode::ApiMissingAgwPublicKey): errorMessage = QObject::tr("Missing AGW public key"); break;
|
||||
case (ErrorCode::ApiConfigDecryptionError): errorMessage = QObject::tr("Failed to decrypt response payload"); break;
|
||||
|
||||
case (ErrorCode::ApiServicesMissingError): errorMessage = QObject::tr("Missing list of available services"); break;
|
||||
|
||||
// QFile errors
|
||||
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
||||
case(ErrorCode::ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break;
|
||||
|
||||
@@ -55,7 +55,7 @@ void ConnectionController::openConnection()
|
||||
&& !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
|
||||
emit updateApiConfigFromGateway();
|
||||
} else if (configVersion && m_serversModel->isApiKeyExpired(serverIndex)) {
|
||||
qDebug() << "attempt to update api config by end_date event";
|
||||
qDebug() << "attempt to update api config by expires_at event";
|
||||
if (configVersion == ApiConfigSources::Telegram) {
|
||||
emit updateApiConfigFromTelegram();
|
||||
} else {
|
||||
|
||||
@@ -848,7 +848,6 @@ bool InstallController::updateServiceFromApi(const int serverIndex, const QStrin
|
||||
|
||||
newServerConfig.insert(configKey::apiConfig, newApiConfig);
|
||||
newServerConfig.insert(configKey::authData, authData);
|
||||
newServerConfig.insert(config_key::crc, serverConfig.value(config_key::crc));
|
||||
m_serversModel->editServer(newServerConfig, serverIndex);
|
||||
|
||||
if (reloadServiceConfig) {
|
||||
|
||||
@@ -27,6 +27,9 @@ namespace
|
||||
constexpr char storeEndpoint[] = "store_endpoint";
|
||||
|
||||
constexpr char isAvailable[] = "is_available";
|
||||
|
||||
constexpr char subscription[] = "subscription";
|
||||
constexpr char endDate[] = "end_date";
|
||||
}
|
||||
|
||||
namespace serviceType
|
||||
@@ -51,23 +54,23 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= static_cast<int>(rowCount()))
|
||||
return QVariant();
|
||||
|
||||
QJsonObject service = m_services.at(index.row()).toObject();
|
||||
QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject();
|
||||
auto serviceType = service.value(configKey::serviceType).toString();
|
||||
auto apiServiceData = m_services.at(index.row());
|
||||
auto serviceType = apiServiceData.type;
|
||||
auto isServiceAvailable = apiServiceData.isServiceAvailable;
|
||||
|
||||
switch (role) {
|
||||
case NameRole: {
|
||||
return serviceInfo.value(configKey::name).toString();
|
||||
return apiServiceData.serviceInfo.name;
|
||||
}
|
||||
case CardDescriptionRole: {
|
||||
auto speed = serviceInfo.value(configKey::speed).toString();
|
||||
auto speed = apiServiceData.serviceInfo.speed;
|
||||
if (serviceType == serviceType::amneziaPremium) {
|
||||
return tr("Classic VPN for comfortable work, downloading large files and watching videos. "
|
||||
"Works for any sites. Speed up to %1 MBit/s")
|
||||
.arg(speed);
|
||||
} else if (serviceType == serviceType::amneziaFree){
|
||||
QString description = tr("VPN to access blocked sites in regions with high levels of Internet censorship. ");
|
||||
if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) {
|
||||
if (isServiceAvailable) {
|
||||
description += tr("<p><a style=\"color: #EB5757;\">Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.</a>");
|
||||
}
|
||||
return description;
|
||||
@@ -83,25 +86,24 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
case IsServiceAvailableRole: {
|
||||
if (serviceType == serviceType::amneziaFree) {
|
||||
if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) {
|
||||
if (isServiceAvailable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case SpeedRole: {
|
||||
auto speed = serviceInfo.value(configKey::speed).toString();
|
||||
return tr("%1 MBit/s").arg(speed);
|
||||
return tr("%1 MBit/s").arg(apiServiceData.serviceInfo.speed);
|
||||
}
|
||||
case WorkPeriodRole: {
|
||||
auto timelimit = serviceInfo.value(configKey::timelimit).toString();
|
||||
if (timelimit == "0") {
|
||||
case TimeLimitRole: {
|
||||
auto timeLimit = apiServiceData.serviceInfo.timeLimit;
|
||||
if (timeLimit == "0") {
|
||||
return "";
|
||||
}
|
||||
return tr("%1 days").arg(timelimit);
|
||||
return tr("%1 days").arg(timeLimit);
|
||||
}
|
||||
case RegionRole: {
|
||||
return serviceInfo.value(configKey::region).toString();
|
||||
return apiServiceData.serviceInfo.region;
|
||||
}
|
||||
case FeaturesRole: {
|
||||
if (serviceType == serviceType::amneziaPremium) {
|
||||
@@ -113,12 +115,15 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
}
|
||||
case PriceRole: {
|
||||
auto price = serviceInfo.value(configKey::price).toString();
|
||||
auto price = apiServiceData.serviceInfo.price;
|
||||
if (price == "free") {
|
||||
return tr("Free");
|
||||
}
|
||||
return tr("%1 $/month").arg(price);
|
||||
}
|
||||
case EndDateRole: {
|
||||
return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy");
|
||||
}
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
@@ -128,15 +133,18 @@ void ApiServicesModel::updateModel(const QJsonObject &data)
|
||||
{
|
||||
beginResetModel();
|
||||
|
||||
m_countryCode = data.value(configKey::userCountryCode).toString();
|
||||
m_services = data.value(configKey::services).toArray();
|
||||
if (m_services.isEmpty()) {
|
||||
QJsonObject service;
|
||||
service.insert(configKey::serviceInfo, data.value(configKey::serviceInfo));
|
||||
service.insert(configKey::serviceType, data.value(configKey::serviceType));
|
||||
m_services.clear();
|
||||
|
||||
m_services.push_back(service);
|
||||
m_countryCode = data.value(configKey::userCountryCode).toString();
|
||||
auto services = data.value(configKey::services).toArray();
|
||||
|
||||
if (services.isEmpty()) {
|
||||
m_services.push_back(getApiServicesData(data));
|
||||
m_selectedServiceIndex = 0;
|
||||
} else {
|
||||
for (const auto &service : services) {
|
||||
m_services.push_back(getApiServicesData(service.toObject()));
|
||||
}
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
@@ -149,32 +157,32 @@ void ApiServicesModel::setServiceIndex(const int index)
|
||||
|
||||
QJsonObject ApiServicesModel::getSelectedServiceInfo()
|
||||
{
|
||||
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
|
||||
return service.value(configKey::serviceInfo).toObject();
|
||||
auto service = m_services.at(m_selectedServiceIndex);
|
||||
return service.serviceInfo.object;
|
||||
}
|
||||
|
||||
QString ApiServicesModel::getSelectedServiceType()
|
||||
{
|
||||
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
|
||||
return service.value(configKey::serviceType).toString();
|
||||
auto service = m_services.at(m_selectedServiceIndex);
|
||||
return service.type;
|
||||
}
|
||||
|
||||
QString ApiServicesModel::getSelectedServiceProtocol()
|
||||
{
|
||||
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
|
||||
return service.value(configKey::serviceProtocol).toString();
|
||||
auto service = m_services.at(m_selectedServiceIndex);
|
||||
return service.protocol;
|
||||
}
|
||||
|
||||
QString ApiServicesModel::getSelectedServiceName()
|
||||
{
|
||||
auto modelIndex = index(m_selectedServiceIndex, 0);
|
||||
return data(modelIndex, ApiServicesModel::Roles::NameRole).toString();
|
||||
auto service = m_services.at(m_selectedServiceIndex);
|
||||
return service.serviceInfo.name;
|
||||
}
|
||||
|
||||
QJsonArray ApiServicesModel::getSelectedServiceCountries()
|
||||
{
|
||||
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
|
||||
return service.value(configKey::availableCountries).toArray();
|
||||
auto service = m_services.at(m_selectedServiceIndex);
|
||||
return service.availableCountries;
|
||||
}
|
||||
|
||||
QString ApiServicesModel::getCountryCode()
|
||||
@@ -184,8 +192,8 @@ QString ApiServicesModel::getCountryCode()
|
||||
|
||||
QString ApiServicesModel::getStoreEndpoint()
|
||||
{
|
||||
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
|
||||
return service.value(configKey::storeEndpoint).toString();
|
||||
auto service = m_services.at(m_selectedServiceIndex);
|
||||
return service.storeEndpoint;
|
||||
}
|
||||
|
||||
QVariant ApiServicesModel::getSelectedServiceData(const QString roleString)
|
||||
@@ -209,10 +217,46 @@ QHash<int, QByteArray> ApiServicesModel::roleNames() const
|
||||
roles[ServiceDescriptionRole] = "serviceDescription";
|
||||
roles[IsServiceAvailableRole] = "isServiceAvailable";
|
||||
roles[SpeedRole] = "speed";
|
||||
roles[WorkPeriodRole] = "workPeriod";
|
||||
roles[TimeLimitRole] = "timeLimit";
|
||||
roles[RegionRole] = "region";
|
||||
roles[FeaturesRole] = "features";
|
||||
roles[PriceRole] = "price";
|
||||
roles[EndDateRole] = "endDate";
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
ApiServicesModel::ApiServicesData ApiServicesModel::getApiServicesData(const QJsonObject &data)
|
||||
{
|
||||
auto serviceInfo = data.value(configKey::serviceInfo).toObject();
|
||||
auto serviceType = data.value(configKey::serviceType).toString();
|
||||
auto serviceProtocol = data.value(configKey::serviceProtocol).toString();
|
||||
auto availableCountries = data.value(configKey::availableCountries).toArray();
|
||||
|
||||
auto subscriptionObject = data.value(configKey::subscription).toObject();
|
||||
|
||||
ApiServicesData serviceData;
|
||||
serviceData.serviceInfo.name = serviceInfo.value(configKey::name).toString();
|
||||
serviceData.serviceInfo.price = serviceInfo.value(configKey::price).toString();
|
||||
serviceData.serviceInfo.region = serviceInfo.value(configKey::region).toString();
|
||||
serviceData.serviceInfo.speed = serviceInfo.value(configKey::speed).toString();
|
||||
serviceData.serviceInfo.timeLimit = serviceInfo.value(configKey::timelimit).toString();
|
||||
|
||||
serviceData.type = serviceType;
|
||||
serviceData.protocol = serviceProtocol;
|
||||
|
||||
serviceData.storeEndpoint = serviceInfo.value(configKey::storeEndpoint).toString();
|
||||
|
||||
if (serviceInfo.value(configKey::isAvailable).isBool()) {
|
||||
serviceData.isServiceAvailable = data.value(configKey::isAvailable).toBool();
|
||||
} else {
|
||||
serviceData.isServiceAvailable = true;
|
||||
}
|
||||
|
||||
serviceData.serviceInfo.object = serviceInfo;
|
||||
serviceData.availableCountries = availableCountries;
|
||||
|
||||
serviceData.subscription.endDate = subscriptionObject.value(configKey::endDate).toString();
|
||||
|
||||
return serviceData;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
class ApiServicesModel : public QAbstractListModel
|
||||
{
|
||||
@@ -15,10 +16,11 @@ public:
|
||||
ServiceDescriptionRole,
|
||||
IsServiceAvailableRole,
|
||||
SpeedRole,
|
||||
WorkPeriodRole,
|
||||
TimeLimitRole,
|
||||
RegionRole,
|
||||
FeaturesRole,
|
||||
PriceRole
|
||||
PriceRole,
|
||||
EndDateRole
|
||||
};
|
||||
|
||||
explicit ApiServicesModel(QObject *parent = nullptr);
|
||||
@@ -48,8 +50,40 @@ protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
struct ServiceInfo
|
||||
{
|
||||
QString name;
|
||||
QString speed;
|
||||
QString timeLimit;
|
||||
QString region;
|
||||
QString price;
|
||||
|
||||
QJsonObject object;
|
||||
};
|
||||
|
||||
struct Subscription
|
||||
{
|
||||
QString endDate;
|
||||
};
|
||||
|
||||
struct ApiServicesData
|
||||
{
|
||||
bool isServiceAvailable;
|
||||
|
||||
QString type;
|
||||
QString protocol;
|
||||
QString storeEndpoint;
|
||||
|
||||
ServiceInfo serviceInfo;
|
||||
Subscription subscription;
|
||||
|
||||
QJsonArray availableCountries;
|
||||
};
|
||||
|
||||
ApiServicesData getApiServicesData(const QJsonObject &data);
|
||||
|
||||
QString m_countryCode;
|
||||
QJsonArray m_services;
|
||||
QVector<ApiServicesData> m_services;
|
||||
|
||||
int m_selectedServiceIndex;
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace
|
||||
constexpr char serviceProtocol[] = "service_protocol";
|
||||
|
||||
constexpr char publicKeyInfo[] = "public_key";
|
||||
constexpr char endDate[] = "end_date";
|
||||
constexpr char expiresAt[] = "expires_at";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ ServersModel::ServersModel(std::shared_ptr<Settings> settings, QObject *parent)
|
||||
emit ServersModel::defaultServerNameChanged();
|
||||
updateDefaultServerContainersModel();
|
||||
});
|
||||
|
||||
connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged);
|
||||
connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged);
|
||||
}
|
||||
|
||||
int ServersModel::rowCount(const QModelIndex &parent) const
|
||||
@@ -79,6 +82,12 @@ bool ServersModel::setData(const QModelIndex &index, const QVariant &value, int
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ServersModel::setData(const int index, const QVariant &value, int role)
|
||||
{
|
||||
QModelIndex modelIndex = this->index(index);
|
||||
return setData(modelIndex, value, role);
|
||||
}
|
||||
|
||||
QVariant ServersModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= static_cast<int>(m_servers.size())) {
|
||||
@@ -679,6 +688,18 @@ QVariant ServersModel::getProcessedServerData(const QString roleString)
|
||||
return {};
|
||||
}
|
||||
|
||||
bool ServersModel::setProcessedServerData(const QString &roleString, const QVariant &value)
|
||||
{
|
||||
const auto roles = roleNames();
|
||||
for (auto it = roles.begin(); it != roles.end(); it++) {
|
||||
if (QString(it.value()) == roleString) {
|
||||
return setData(m_processedServerIndex, value, it.key());
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling()
|
||||
{
|
||||
auto server = m_servers.at(m_defaultServerIndex).toObject();
|
||||
@@ -718,9 +739,9 @@ bool ServersModel::isApiKeyExpired(const int serverIndex)
|
||||
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
|
||||
|
||||
auto publicKeyInfo = apiConfig.value(configKey::publicKeyInfo).toObject();
|
||||
const QString endDate = publicKeyInfo.value(configKey::endDate).toString();
|
||||
if (endDate.isEmpty()) {
|
||||
publicKeyInfo.insert(configKey::endDate, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate));
|
||||
const QString expiresAt = publicKeyInfo.value(configKey::expiresAt).toString();
|
||||
if (expiresAt.isEmpty()) {
|
||||
publicKeyInfo.insert(configKey::expiresAt, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate));
|
||||
apiConfig.insert(configKey::publicKeyInfo, publicKeyInfo);
|
||||
serverConfig.insert(configKey::apiConfig, apiConfig);
|
||||
editServer(serverConfig, serverIndex);
|
||||
@@ -728,8 +749,8 @@ bool ServersModel::isApiKeyExpired(const int serverIndex)
|
||||
return false;
|
||||
}
|
||||
|
||||
auto endDateDateTime = QDateTime::fromString(endDate, Qt::ISODate).toUTC();
|
||||
if (endDateDateTime < QDateTime::currentDateTimeUtc()) {
|
||||
auto expiresAtDateTime = QDateTime::fromString(expiresAt, Qt::ISODate).toUTC();
|
||||
if (expiresAtDateTime < QDateTime::currentDateTimeUtc()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -46,6 +46,7 @@ public:
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
|
||||
bool setData(const int index, const QVariant &value, int role = Qt::EditRole);
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QVariant data(const int index, int role = Qt::DisplayRole) const;
|
||||
|
||||
@@ -115,6 +116,7 @@ public slots:
|
||||
QVariant getDefaultServerData(const QString roleString);
|
||||
|
||||
QVariant getProcessedServerData(const QString roleString);
|
||||
bool setProcessedServerData(const QString &roleString, const QVariant &value);
|
||||
|
||||
bool isDefaultServerDefaultContainerHasSplitTunneling();
|
||||
|
||||
@@ -127,6 +129,9 @@ protected:
|
||||
|
||||
signals:
|
||||
void processedServerIndexChanged(const int index);
|
||||
// emitted when the processed server index or processed server data is changed
|
||||
void processedServerChanged();
|
||||
|
||||
void defaultServerIndexChanged(const int index);
|
||||
void defaultServerNameChanged();
|
||||
void defaultServerDescriptionChanged();
|
||||
|
||||
@@ -84,7 +84,7 @@ DrawerType2 {
|
||||
Layout.topMargin: 16
|
||||
|
||||
text: qsTr("Share")
|
||||
imageSource: "qrc:/images/controls/share-2.svg"
|
||||
leftImageSource: "qrc:/images/controls/share-2.svg"
|
||||
|
||||
KeyNavigation.tab: copyConfigTextButton
|
||||
|
||||
@@ -120,7 +120,7 @@ DrawerType2 {
|
||||
borderWidth: 1
|
||||
|
||||
text: qsTr("Copy")
|
||||
imageSource: "qrc:/images/controls/copy.svg"
|
||||
leftImageSource: "qrc:/images/controls/copy.svg"
|
||||
|
||||
Keys.onReturnPressed: { copyConfigTextButton.clicked() }
|
||||
Keys.onEnterPressed: { copyConfigTextButton.clicked() }
|
||||
@@ -143,7 +143,7 @@ DrawerType2 {
|
||||
borderWidth: 1
|
||||
|
||||
text: qsTr("Copy config string")
|
||||
imageSource: "qrc:/images/controls/copy.svg"
|
||||
leftImageSource: "qrc:/images/controls/copy.svg"
|
||||
|
||||
KeyNavigation.tab: showSettingsButton
|
||||
}
|
||||
|
||||
@@ -22,9 +22,10 @@ Button {
|
||||
property int borderWidth: 0
|
||||
property int borderFocusedWidth: 1
|
||||
|
||||
property string imageSource
|
||||
property string leftImageSource
|
||||
property string rightImageSource
|
||||
property string leftImageColor: textColor
|
||||
property string leftImageColor
|
||||
property bool changeLeftImageSize: true
|
||||
|
||||
property bool squareLeftSide: false
|
||||
|
||||
@@ -127,18 +128,23 @@ Button {
|
||||
anchors.centerIn: parent
|
||||
|
||||
Image {
|
||||
Layout.preferredHeight: 20
|
||||
Layout.preferredWidth: 20
|
||||
|
||||
source: root.imageSource
|
||||
visible: root.imageSource === "" ? false : true
|
||||
id: leftImage
|
||||
source: root.leftImageSource
|
||||
visible: root.leftImageSource === "" ? false : true
|
||||
|
||||
layer {
|
||||
enabled: true
|
||||
enabled: leftImageColor !== "" ? true : false
|
||||
effect: ColorOverlay {
|
||||
color: leftImageColor
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.changeLeftImageSize) {
|
||||
leftImage.Layout.preferredHeight = 20
|
||||
leftImage.Layout.preferredWidth = 20
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ButtonTextType {
|
||||
|
||||
@@ -14,7 +14,7 @@ Popup {
|
||||
visible: false
|
||||
|
||||
Overlay.modal: Rectangle {
|
||||
color: Qt.rgba(14/255, 14/255, 17/255, 0.8)
|
||||
color: AmneziaStyle.color.translucentMidnightBlack
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
|
||||
@@ -19,7 +19,7 @@ RadioButton {
|
||||
|
||||
property string textColor: AmneziaStyle.color.midnightBlack
|
||||
|
||||
property string pressedBorderColor: Qt.rgba(251/255, 178/255, 106/255, 0.3)
|
||||
property string pressedBorderColor: AmneziaStyle.color.softGoldenApricot
|
||||
property string selectedBorderColor: AmneziaStyle.color.goldenApricot
|
||||
property string defaultBodredColor: AmneziaStyle.color.transparent
|
||||
property int borderWidth: 0
|
||||
|
||||
@@ -145,6 +145,7 @@ Button {
|
||||
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
enabled: root.enabled
|
||||
|
||||
onEntered: {
|
||||
backgroundRect.color = root.hoveredColor
|
||||
|
||||
@@ -92,7 +92,7 @@ Item {
|
||||
id: background
|
||||
|
||||
anchors.fill: parent
|
||||
color: root.isCollapsed ? AmneziaStyle.color.transparent : Qt.rgba(14/255, 14/255, 17/255, 0.8)
|
||||
color: root.isCollapsed ? AmneziaStyle.color.transparent : AmneziaStyle.color.translucentMidnightBlack
|
||||
|
||||
Behavior on color {
|
||||
PropertyAnimation { duration: 200 }
|
||||
|
||||
@@ -24,7 +24,7 @@ Popup {
|
||||
|
||||
Overlay.modal: Rectangle {
|
||||
visible: root.closeButtonVisible
|
||||
color: Qt.rgba(14/255, 14/255, 17/255, 0.8)
|
||||
color: AmneziaStyle.color.translucentMidnightBlack
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
|
||||
@@ -183,7 +183,7 @@ Item {
|
||||
|
||||
focusPolicy: Qt.NoFocus
|
||||
text: root.buttonText
|
||||
imageSource: root.buttonImageSource
|
||||
leftImageSource: root.buttonImageSource
|
||||
|
||||
anchors.top: content.top
|
||||
anchors.bottom: content.bottom
|
||||
|
||||
@@ -14,7 +14,7 @@ Popup {
|
||||
visible: false
|
||||
|
||||
Overlay.modal: Rectangle {
|
||||
color: Qt.rgba(14/255, 14/255, 17/255, 0.8)
|
||||
color: AmneziaStyle.color.translucentMidnightBlack
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
|
||||
@@ -22,5 +22,9 @@ QtObject {
|
||||
readonly property color sheerWhite: Qt.rgba(1, 1, 1, 0.12)
|
||||
readonly property color translucentWhite: Qt.rgba(1, 1, 1, 0.08)
|
||||
readonly property color barelyTranslucentWhite: Qt.rgba(1, 1, 1, 0.05)
|
||||
readonly property color translucentMidnightBlack: Qt.rgba(14/255, 14/255, 17/255, 0.8)
|
||||
readonly property color softGoldenApricot: Qt.rgba(251/255, 178/255, 106/255, 0.3)
|
||||
readonly property color mistyGray: Qt.rgba(215/255, 216/255, 219/255, 0.8)
|
||||
readonly property color cloudyGray: Qt.rgba(215/255, 216/255, 219/255, 0.65)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,6 @@ PageType {
|
||||
pressedColor: AmneziaStyle.color.sheerWhite
|
||||
disabledColor: AmneziaStyle.color.mutedGray
|
||||
textColor: AmneziaStyle.color.mutedGray
|
||||
leftImageColor: AmneziaStyle.color.transparent
|
||||
borderWidth: 0
|
||||
|
||||
buttonTextLabel.lineHeight: 20
|
||||
@@ -110,7 +109,7 @@ PageType {
|
||||
|
||||
text: isSplitTunnelingEnabled ? qsTr("Split tunneling enabled") : qsTr("Split tunneling disabled")
|
||||
|
||||
imageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : ""
|
||||
leftImageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : ""
|
||||
rightImageSource: "qrc:/images/controls/chevron-down.svg"
|
||||
|
||||
Keys.onEnterPressed: splitTunnelingButton.clicked()
|
||||
@@ -166,6 +165,7 @@ PageType {
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: 0
|
||||
|
||||
Component.onCompleted: {
|
||||
drawer.collapsedHeight = collapsed.implicitHeight
|
||||
@@ -267,18 +267,39 @@ PageType {
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
Layout.bottomMargin: drawer.isCollapsed ? 44 : ServersModel.isDefaultServerFromApi ? 89 : 44
|
||||
Layout.topMargin: 8
|
||||
Layout.bottomMargin: drawer.isCollapsed ? 44 : ServersModel.isDefaultServerFromApi ? 61 : 16
|
||||
spacing: 0
|
||||
|
||||
Image {
|
||||
Layout.rightMargin: 8
|
||||
visible: source !== ""
|
||||
source: ServersModel.defaultServerImagePathCollapsed
|
||||
}
|
||||
BasicButtonType {
|
||||
enabled: (ServersModel.defaultServerImagePathCollapsed !== "") && drawer.isCollapsed
|
||||
hoverEnabled: enabled
|
||||
|
||||
implicitHeight: 36
|
||||
|
||||
leftPadding: 16
|
||||
rightPadding: 16
|
||||
|
||||
defaultColor: AmneziaStyle.color.transparent
|
||||
hoveredColor: AmneziaStyle.color.translucentWhite
|
||||
pressedColor: AmneziaStyle.color.sheerWhite
|
||||
disabledColor: AmneziaStyle.color.transparent
|
||||
textColor: AmneziaStyle.color.mutedGray
|
||||
|
||||
buttonTextLabel.lineHeight: 16
|
||||
buttonTextLabel.font.pixelSize: 13
|
||||
buttonTextLabel.font.weight: 400
|
||||
|
||||
LabelTextType {
|
||||
id: collapsedServerMenuDescription
|
||||
text: drawer.isCollapsed ? ServersModel.defaultServerDescriptionCollapsed : ServersModel.defaultServerDescriptionExpanded
|
||||
leftImageSource: ServersModel.defaultServerImagePathCollapsed
|
||||
changeLeftImageSize: false
|
||||
|
||||
rightImageSource: hoverEnabled ? "qrc:/images/controls/chevron-down.svg" : ""
|
||||
|
||||
onClicked: {
|
||||
ServersModel.processedIndex = ServersModel.defaultIndex
|
||||
PageController.goToPage(PageEnum.PageSettingsServerInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,8 +337,8 @@ PageType {
|
||||
|
||||
rootButtonImageColor: AmneziaStyle.color.midnightBlack
|
||||
rootButtonBackgroundColor: AmneziaStyle.color.paleGray
|
||||
rootButtonBackgroundHoveredColor: Qt.rgba(215, 216, 219, 0.8)
|
||||
rootButtonBackgroundPressedColor: Qt.rgba(215, 216, 219, 0.65)
|
||||
rootButtonBackgroundHoveredColor: AmneziaStyle.color.mistyGray
|
||||
rootButtonBackgroundPressedColor: AmneziaStyle.color.cloudyGray
|
||||
rootButtonHoveredBorderColor: AmneziaStyle.color.transparent
|
||||
rootButtonDefaultBorderColor: AmneziaStyle.color.transparent
|
||||
rootButtonTextTopMargin: 8
|
||||
|
||||
@@ -54,8 +54,14 @@ PageType {
|
||||
imageSource: "qrc:/images/controls/download.svg"
|
||||
|
||||
checked: index === ApiCountryModel.currentIndex
|
||||
checkable: !ConnectionController.isConnected
|
||||
|
||||
onClicked: {
|
||||
if (ConnectionController.isConnected) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change server location while there is an active connection"))
|
||||
return
|
||||
}
|
||||
|
||||
if (index !== ApiCountryModel.currentIndex) {
|
||||
PageController.showBusyIndicator(true)
|
||||
var prevIndex = ApiCountryModel.currentIndex
|
||||
|
||||
@@ -56,12 +56,15 @@ PageType {
|
||||
}
|
||||
|
||||
LabelWithImageType {
|
||||
property bool showSubscriptionEndDate: ServersModel.getProcessedServerData("isCountrySelectionAvailable")
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
|
||||
imageSource: "qrc:/images/controls/history.svg"
|
||||
leftText: qsTr("Work period")
|
||||
rightText: ApiServicesModel.getSelectedServiceData("workPeriod")
|
||||
leftText: showSubscriptionEndDate ? qsTr("Valid until") : qsTr("Work period")
|
||||
rightText: showSubscriptionEndDate ? ApiServicesModel.getSelectedServiceData("endDate")
|
||||
: ApiServicesModel.getSelectedServiceData("workPeriod")
|
||||
|
||||
visible: rightText !== ""
|
||||
}
|
||||
@@ -132,8 +135,8 @@ PageType {
|
||||
implicitHeight: 32
|
||||
|
||||
defaultColor: "transparent"
|
||||
hoveredColor: Qt.rgba(1, 1, 1, 0.08)
|
||||
pressedColor: Qt.rgba(1, 1, 1, 0.12)
|
||||
hoveredColor: AmneziaStyle.color.translucentWhite
|
||||
pressedColor: AmneziaStyle.color.sheerWhite
|
||||
textColor: AmneziaStyle.color.vibrantRed
|
||||
|
||||
text: qsTr("Reload API config")
|
||||
@@ -172,8 +175,8 @@ PageType {
|
||||
implicitHeight: 32
|
||||
|
||||
defaultColor: "transparent"
|
||||
hoveredColor: Qt.rgba(1, 1, 1, 0.08)
|
||||
pressedColor: Qt.rgba(1, 1, 1, 0.12)
|
||||
hoveredColor: AmneziaStyle.color.translucentWhite
|
||||
pressedColor: AmneziaStyle.color.sheerWhite
|
||||
textColor: AmneziaStyle.color.vibrantRed
|
||||
|
||||
text: qsTr("Remove from application")
|
||||
|
||||
@@ -25,6 +25,8 @@ PageType {
|
||||
property int pageSettingsApiServerInfo: 3
|
||||
property int pageSettingsApiLanguageList: 4
|
||||
|
||||
property var processedServer
|
||||
|
||||
defaultActiveFocusItem: focusItem
|
||||
|
||||
Connections {
|
||||
@@ -35,8 +37,18 @@ PageType {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ServersModel
|
||||
|
||||
function onProcessedServerChanged() {
|
||||
root.processedServer = proxyServersModel.get(0)
|
||||
}
|
||||
}
|
||||
|
||||
SortFilterProxyModel {
|
||||
id: proxyServersModel
|
||||
objectName: "proxyServersModel"
|
||||
|
||||
sourceModel: ServersModel
|
||||
filters: [
|
||||
ValueFilter {
|
||||
@@ -44,147 +56,139 @@ PageType {
|
||||
value: true
|
||||
}
|
||||
]
|
||||
|
||||
Component.onCompleted: {
|
||||
root.processedServer = proxyServersModel.get(0)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: focusItem
|
||||
KeyNavigation.tab: header
|
||||
//KeyNavigation.tab: header
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
spacing: 16
|
||||
spacing: 4
|
||||
|
||||
Repeater {
|
||||
id: header
|
||||
model: proxyServersModel
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
activeFocusOnTab: true
|
||||
onFocusChanged: {
|
||||
header.itemAt(0).focusItem.forceActiveFocus()
|
||||
Layout.topMargin: 20
|
||||
KeyNavigation.tab: headerContent.actionButton
|
||||
|
||||
backButtonFunction: function() {
|
||||
if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo &&
|
||||
root.processedServer.isCountrySelectionAvailable) {
|
||||
nestedStackView.currentIndex = root.pageSettingsApiLanguageList
|
||||
} else {
|
||||
PageController.closePage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HeaderType {
|
||||
id: headerContent
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
actionButtonImage: nestedStackView.currentIndex === root.pageSettingsApiLanguageList ? "qrc:/images/controls/settings.svg"
|
||||
: "qrc:/images/controls/edit-3.svg"
|
||||
|
||||
headerText: root.processedServer.name
|
||||
descriptionText: {
|
||||
if (root.processedServer.isServerFromGatewayApi) {
|
||||
if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) {
|
||||
return qsTr("Subscription is valid until ") + ApiServicesModel.getSelectedServiceData("endDate")
|
||||
} else {
|
||||
return ApiServicesModel.getSelectedServiceData("serviceDescription")
|
||||
}
|
||||
} else if (root.processedServer.isServerFromTelegramApi) {
|
||||
return root.processedServer.serverDescription
|
||||
} else if (root.processedServer.hasWriteAccess) {
|
||||
return root.processedServer.credentialsLogin + " · " + root.processedServer.hostName
|
||||
} else {
|
||||
return root.processedServer.hostName
|
||||
}
|
||||
}
|
||||
|
||||
delegate: ColumnLayout {
|
||||
KeyNavigation.tab: tabBar
|
||||
|
||||
property alias focusItem: backButton
|
||||
actionButtonFunction: function() {
|
||||
if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) {
|
||||
nestedStackView.currentIndex = root.pageSettingsApiServerInfo
|
||||
} else {
|
||||
serverNameEditDrawer.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
id: content
|
||||
DrawerType2 {
|
||||
id: serverNameEditDrawer
|
||||
|
||||
Layout.topMargin: 20
|
||||
parent: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
KeyNavigation.tab: headerContent.actionButton
|
||||
anchors.fill: parent
|
||||
expandedHeight: root.height * 0.35
|
||||
|
||||
backButtonFunction: function() {
|
||||
if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo &&
|
||||
ServersModel.getProcessedServerData("isCountrySelectionAvailable")) {
|
||||
nestedStackView.currentIndex = root.pageSettingsApiLanguageList
|
||||
} else {
|
||||
PageController.closePage()
|
||||
}
|
||||
onClosed: {
|
||||
if (!GC.isMobile()) {
|
||||
headerContent.actionButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
expandedContent: ColumnLayout {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 32
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
|
||||
Connections {
|
||||
target: serverNameEditDrawer
|
||||
enabled: !GC.isMobile()
|
||||
function onOpened() {
|
||||
serverName.textField.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
HeaderType {
|
||||
id: headerContent
|
||||
Item {
|
||||
id: focusItem1
|
||||
KeyNavigation.tab: serverName.textField
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: serverName
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
headerText: qsTr("Server name")
|
||||
textFieldText: root.processedServer.name
|
||||
textField.maximumLength: 30
|
||||
checkEmptyText: true
|
||||
|
||||
actionButtonImage: nestedStackView.currentIndex === root.pageSettingsApiLanguageList ? "qrc:/images/controls/settings.svg" : "qrc:/images/controls/edit-3.svg"
|
||||
|
||||
headerText: name
|
||||
descriptionText: {
|
||||
if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) {
|
||||
return ApiServicesModel.getSelectedServiceData("serviceDescription")
|
||||
} else if (ServersModel.getProcessedServerData("isServerFromTelegramApi")) {
|
||||
return serverDescription
|
||||
} else if (ServersModel.isProcessedServerHasWriteAccess()) {
|
||||
return credentialsLogin + " · " + hostName
|
||||
} else {
|
||||
return hostName
|
||||
}
|
||||
}
|
||||
|
||||
KeyNavigation.tab: tabBar
|
||||
|
||||
actionButtonFunction: function() {
|
||||
if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) {
|
||||
nestedStackView.currentIndex = root.pageSettingsApiServerInfo
|
||||
} else {
|
||||
serverNameEditDrawer.open()
|
||||
}
|
||||
}
|
||||
KeyNavigation.tab: saveButton
|
||||
}
|
||||
|
||||
DrawerType2 {
|
||||
id: serverNameEditDrawer
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
parent: root
|
||||
Layout.fillWidth: true
|
||||
|
||||
anchors.fill: parent
|
||||
expandedHeight: root.height * 0.35
|
||||
text: qsTr("Save")
|
||||
KeyNavigation.tab: focusItem1
|
||||
|
||||
onClosed: {
|
||||
if (!GC.isMobile()) {
|
||||
headerContent.actionButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
expandedContent: ColumnLayout {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 32
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
|
||||
Connections {
|
||||
target: serverNameEditDrawer
|
||||
enabled: !GC.isMobile()
|
||||
function onOpened() {
|
||||
serverName.textField.forceActiveFocus()
|
||||
}
|
||||
clickedFunc: function() {
|
||||
if (serverName.textFieldText === "") {
|
||||
return
|
||||
}
|
||||
|
||||
Item {
|
||||
id: focusItem1
|
||||
KeyNavigation.tab: serverName.textField
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
id: serverName
|
||||
|
||||
Layout.fillWidth: true
|
||||
headerText: qsTr("Server name")
|
||||
textFieldText: name
|
||||
textField.maximumLength: 30
|
||||
checkEmptyText: true
|
||||
|
||||
KeyNavigation.tab: saveButton
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("Save")
|
||||
KeyNavigation.tab: focusItem1
|
||||
|
||||
clickedFunc: function() {
|
||||
if (serverName.textFieldText === "") {
|
||||
return
|
||||
}
|
||||
|
||||
if (serverName.textFieldText !== name) {
|
||||
name = serverName.textFieldText
|
||||
}
|
||||
serverNameEditDrawer.close()
|
||||
}
|
||||
if (serverName.textFieldText !== root.processedServer.name) {
|
||||
ServersModel.setProcessedServerData("name", serverName.textFieldText);
|
||||
}
|
||||
serverNameEditDrawer.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,8 +261,7 @@ PageType {
|
||||
|
||||
StackLayout {
|
||||
id: nestedStackView
|
||||
Layout.preferredWidth: root.width
|
||||
Layout.preferredHeight: root.height - tabBar.implicitHeight - header.implicitHeight
|
||||
Layout.fillWidth: true
|
||||
|
||||
currentIndex: ServersModel.getProcessedServerData("isServerFromGatewayApi") ?
|
||||
(ServersModel.getProcessedServerData("isCountrySelectionAvailable") ?
|
||||
|
||||
@@ -16,83 +16,82 @@ PageType {
|
||||
|
||||
defaultActiveFocusItem: focusItem
|
||||
|
||||
FlickableType {
|
||||
id: fl
|
||||
ColumnLayout {
|
||||
id: header
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
contentHeight: content.height
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
spacing: 0
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
Item {
|
||||
id: focusItem
|
||||
KeyNavigation.tab: backButton
|
||||
}
|
||||
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
id: focusItem
|
||||
KeyNavigation.tab: backButton
|
||||
}
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
Layout.topMargin: 20
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
Layout.topMargin: 20
|
||||
// KeyNavigation.tab: fileButton.rightButton
|
||||
}
|
||||
}
|
||||
|
||||
HeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.bottomMargin: 32
|
||||
HeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
|
||||
headerText: qsTr("VPN by Amnezia")
|
||||
descriptionText: qsTr("Choose a VPN service that suits your needs.")
|
||||
}
|
||||
headerText: qsTr("VPN by Amnezia")
|
||||
descriptionText: qsTr("Choose a VPN service that suits your needs.")
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: containers
|
||||
width: parent.width
|
||||
height: containers.contentItem.height
|
||||
spacing: 16
|
||||
ListView {
|
||||
id: servicesListView
|
||||
anchors.top: header.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: 16
|
||||
spacing: 0
|
||||
|
||||
currentIndex: 1
|
||||
interactive: false
|
||||
model: ApiServicesModel
|
||||
currentIndex: 1
|
||||
clip: true
|
||||
model: ApiServicesModel
|
||||
|
||||
delegate: Item {
|
||||
implicitWidth: containers.width
|
||||
implicitHeight: delegateContent.implicitHeight
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
|
||||
ColumnLayout {
|
||||
id: delegateContent
|
||||
delegate: Item {
|
||||
implicitWidth: servicesListView.width
|
||||
implicitHeight: delegateContent.implicitHeight
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
ColumnLayout {
|
||||
id: delegateContent
|
||||
|
||||
CardWithIconsType {
|
||||
id: card
|
||||
anchors.fill: parent
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
CardWithIconsType {
|
||||
id: card
|
||||
|
||||
headerText: name
|
||||
bodyText: cardDescription
|
||||
footerText: price
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
headerText: name
|
||||
bodyText: cardDescription
|
||||
footerText: price
|
||||
|
||||
onClicked: {
|
||||
if (isServiceAvailable) {
|
||||
ApiServicesModel.setServiceIndex(index)
|
||||
PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo)
|
||||
}
|
||||
}
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
enabled: isServiceAvailable
|
||||
|
||||
onClicked: {
|
||||
if (isServiceAvailable) {
|
||||
ApiServicesModel.setServiceIndex(index)
|
||||
PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ PageType {
|
||||
KeyNavigation.tab: textKey.textField
|
||||
}
|
||||
|
||||
|
||||
HeaderType {
|
||||
property bool isVisible: SettingsController.getInstallationUuid() !== "" || PageController.isStartPageVisible()
|
||||
|
||||
|
||||
@@ -573,7 +573,7 @@ PageType {
|
||||
visible: accessTypeSelector.currentIndex === 0
|
||||
|
||||
text: qsTr("Share")
|
||||
imageSource: "qrc:/images/controls/share-2.svg"
|
||||
leftImageSource: "qrc:/images/controls/share-2.svg"
|
||||
|
||||
Keys.onTabPressed: lastItemTabClicked(focusItem)
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ PageType {
|
||||
Layout.topMargin: 40
|
||||
|
||||
text: qsTr("Share")
|
||||
imageSource: "qrc:/images/controls/share-2.svg"
|
||||
leftImageSource: "qrc:/images/controls/share-2.svg"
|
||||
|
||||
Keys.onTabPressed: lastItemTabClicked(focusItem)
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@ date > $LOG_FILE
|
||||
echo "Script started" >> $LOG_FILE
|
||||
sudo killall -9 $APP_NAME 2>> $LOG_FILE
|
||||
|
||||
if command -v steamos-readonly &> /dev/null; then
|
||||
sudo steamos-readonly disable >> $LOG_FILE
|
||||
echo "steamos-readonly disabled" >> $LOG_FILE
|
||||
fi
|
||||
|
||||
if sudo systemctl is-active --quiet $APP_NAME; then
|
||||
sudo systemctl stop $APP_NAME >> $LOG_FILE
|
||||
sudo systemctl disable $APP_NAME >> $LOG_FILE
|
||||
@@ -42,6 +47,11 @@ sudo chmod 555 /usr/share/applications/$APP_NAME.desktop >> $LOG_FILE
|
||||
|
||||
echo "user desktop creation loop ended" >> $LOG_FILE
|
||||
|
||||
if command -v steamos-readonly &> /dev/null; then
|
||||
sudo steamos-readonly enable >> $LOG_FILE
|
||||
echo "steamos-readonly enabled" >> $LOG_FILE
|
||||
fi
|
||||
|
||||
date >> $LOG_FILE
|
||||
echo "Service status:" >> $LOG_FILE
|
||||
sudo systemctl status $APP_NAME >> $LOG_FILE
|
||||
|
||||
@@ -13,6 +13,11 @@ date >> $LOG_FILE
|
||||
echo "Uninstall Script started" >> $LOG_FILE
|
||||
sudo killall -9 $APP_NAME 2>> $LOG_FILE
|
||||
|
||||
if command -v steamos-readonly &> /dev/null; then
|
||||
sudo steamos-readonly disable >> $LOG_FILE
|
||||
echo "steamos-readonly disabled" >> $LOG_FILE
|
||||
fi
|
||||
|
||||
ls /opt/AmneziaVPN/client/lib/* | while IFS=: read -r dir; do
|
||||
sudo unlink $dir >> $LOG_FILE
|
||||
done
|
||||
@@ -59,6 +64,11 @@ if test -f /usr/share/pixmaps/$APP_NAME.png; then
|
||||
|
||||
fi
|
||||
|
||||
if command -v steamos-readonly &> /dev/null; then
|
||||
sudo steamos-readonly enable >> $LOG_FILE
|
||||
echo "steamos-readonly enabled" >> $LOG_FILE
|
||||
fi
|
||||
|
||||
date >> $LOG_FILE
|
||||
echo "Service after uninstall status:" >> $LOG_FILE
|
||||
sudo systemctl status $APP_NAME >> $LOG_FILE
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
8
metadata/img-readme/download-alt.svg
Normal file
8
metadata/img-readme/download-alt.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 13 KiB |
8
metadata/img-readme/download-website.svg
Normal file
8
metadata/img-readme/download-website.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Reference in New Issue
Block a user