Compare commits

..

7 Commits

Author SHA1 Message Date
lunardunno
d85e4a413a changing stdErr to stdOut 2024-10-27 18:18:42 +04:00
lunardunno
fbf11c1689 added extended error descriptions 2024-10-27 17:48:13 +04:00
lunardunno
b382257460 adding error codes 2024-10-27 17:19:15 +04:00
lunardunno
8ec4232a96 Renaming one of the errors 2024-10-27 17:17:06 +04:00
lunardunno
43fd9d5d90 Adding error handling
Adding error handling in the server controller for:
Sudo package is not pre-installed for sudo users.
Server user or associated group is not listed in the sudoers file.
Server user password required
2024-10-27 16:33:01 +04:00
lunardunno
ae681ad6d2 simplification 2024-10-27 15:33:08 +04:00
lunardunno
6c22a7372d Checking requirements in script
Checking requirements for sudo users in script
2024-10-27 13:09:41 +04:00
41 changed files with 326 additions and 521 deletions

View File

@@ -217,11 +217,7 @@ 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 | \
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'
sh deploy/build_ios.sh
env:
IOS_TRUST_CERT_BASE64: ${{ secrets.IOS_TRUST_CERT_BASE64 }}
IOS_SIGNING_CERT_BASE64: ${{ secrets.IOS_SIGNING_CERT_BASE64 }}
@@ -260,7 +256,7 @@ jobs:
- name: 'Setup xcode'
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15.4.0'
xcode-version: '14.3.1'
- name: 'Install Qt'
uses: jurplel/install-qt-action@v3

View File

@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN)
project(${PROJECT} VERSION 4.8.2.4
project(${PROJECT} VERSION 4.8.2.3
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 2071)
set(APP_ANDROID_VERSION_CODE 2069)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")

View File

@@ -4,21 +4,21 @@
[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev)
[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client)
[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.
Amnezia is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server.
[![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org)
![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)
### [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)
<br>
> [!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).
<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>
<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>
[Alternative download link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org/downloads)
[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,8 +33,7 @@
## Links
- [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://amnezia.org](https://amnezia.org) - project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org)
- [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)
@@ -183,8 +182,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 <br>
TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
## Acknowledgments
This project is tested with BrowserStack.

View File

@@ -20,7 +20,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<!-- To request network state -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" android:maxSdkVersion="30" />
<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" />

View File

@@ -1,12 +1,11 @@
package org.amnezia.vpn.protocol.wireguard
import android.net.VpnService.Builder
import kotlinx.coroutines.CoroutineScope
import java.io.IOException
import java.util.Locale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.amnezia.awg.GoBackend
import org.amnezia.vpn.protocol.Protocol
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
@@ -28,8 +27,6 @@ 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() {
@@ -52,17 +49,46 @@ 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 {
@@ -152,43 +178,6 @@ 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() {
@@ -196,8 +185,6 @@ open class Wireguard : Protocol() {
Log.w(TAG, "Tunnel already down")
return
}
statusJob?.cancel()
statusJob = null
val handleToClose = tunnelHandle
tunnelHandle = -1
GoBackend.awgTurnOff(handleToClose)

View File

@@ -377,13 +377,6 @@ 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;
}

View File

@@ -770,6 +770,12 @@ ErrorCode ServerController::isUserInSudo(const ServerCredentials &credentials, D
if (!stdOut.contains("sudo"))
return ErrorCode::ServerUserNotInSudo;
if (stdOut.contains("command not found"))
return ErrorCode::SudoPackageIsNotPreinstalled;
if (stdOut.contains("sudoers"))
return ErrorCode::ServerUserNotListedInSudoers;
if (stdOut.contains("password is required"))
return ErrorCode::ServerUserPasswordRequired;
return error;
}

View File

@@ -56,6 +56,9 @@ namespace amnezia
ServerCancelInstallation = 204,
ServerUserNotInSudo = 205,
ServerPacketManagerError = 206,
SudoPackageIsNotPreinstalled = 207,
ServerUserNotListedInSudoers = 208,
ServerUserPasswordRequired = 209,
// Ssh connection errors
SshRequestDeniedError = 300,
@@ -109,7 +112,6 @@ namespace amnezia
ApiConfigSslError = 1104,
ApiMissingAgwPublicKey = 1105,
ApiConfigDecryptionError = 1106,
ApiServicesMissingError = 1107,
// QFile errors
OpenError = 1200,

View File

@@ -21,6 +21,9 @@ QString errorString(ErrorCode code) {
case(ErrorCode::ServerCancelInstallation): errorMessage = QObject::tr("Installation canceled by user"); break;
case(ErrorCode::ServerUserNotInSudo): errorMessage = QObject::tr("The user does not have permission to use sudo"); break;
case(ErrorCode::ServerPacketManagerError): errorMessage = QObject::tr("Server error: Packet manager error"); break;
case(ErrorCode::SudoPackageIsNotPreinstalled): errorMessage = QObject::tr("The sudo package is not pre-installed"); break;
case(ErrorCode::ServerUserNotListedInSudoers): errorMessage = QObject::tr("The user is not listed in sudoers"); break;
case(ErrorCode::ServerUserPasswordRequired): errorMessage = QObject::tr("The user's password is required"); break;
// Libssh errors
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
@@ -63,8 +66,7 @@ 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;

View File

@@ -1,2 +1,3 @@
echo $LC_MESSAGES | grep -qE "en_US.UTF-8|C.UTF-8" || export LC_MESSAGES=C.UTF-8;\
CUR_USER=$(whoami);\
groups $CUR_USER
groups $CUR_USER | grep sudo && sudo -nu $CUR_USER sudo -n uname > /dev/null

View File

@@ -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 expires_at event";
qDebug() << "attempt to update api config by end_date event";
if (configVersion == ApiConfigSources::Telegram) {
emit updateApiConfigFromTelegram();
} else {

View File

@@ -848,6 +848,7 @@ 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) {

View File

@@ -27,9 +27,6 @@ namespace
constexpr char storeEndpoint[] = "store_endpoint";
constexpr char isAvailable[] = "is_available";
constexpr char subscription[] = "subscription";
constexpr char endDate[] = "end_date";
}
namespace serviceType
@@ -54,23 +51,23 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
if (!index.isValid() || index.row() < 0 || index.row() >= static_cast<int>(rowCount()))
return QVariant();
auto apiServiceData = m_services.at(index.row());
auto serviceType = apiServiceData.type;
auto isServiceAvailable = apiServiceData.isServiceAvailable;
QJsonObject service = m_services.at(index.row()).toObject();
QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject();
auto serviceType = service.value(configKey::serviceType).toString();
switch (role) {
case NameRole: {
return apiServiceData.serviceInfo.name;
return serviceInfo.value(configKey::name).toString();
}
case CardDescriptionRole: {
auto speed = apiServiceData.serviceInfo.speed;
auto speed = serviceInfo.value(configKey::speed).toString();
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 (isServiceAvailable) {
if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) {
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;
@@ -86,24 +83,25 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
}
case IsServiceAvailableRole: {
if (serviceType == serviceType::amneziaFree) {
if (isServiceAvailable) {
if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) {
return false;
}
}
return true;
}
case SpeedRole: {
return tr("%1 MBit/s").arg(apiServiceData.serviceInfo.speed);
auto speed = serviceInfo.value(configKey::speed).toString();
return tr("%1 MBit/s").arg(speed);
}
case TimeLimitRole: {
auto timeLimit = apiServiceData.serviceInfo.timeLimit;
if (timeLimit == "0") {
case WorkPeriodRole: {
auto timelimit = serviceInfo.value(configKey::timelimit).toString();
if (timelimit == "0") {
return "";
}
return tr("%1 days").arg(timeLimit);
return tr("%1 days").arg(timelimit);
}
case RegionRole: {
return apiServiceData.serviceInfo.region;
return serviceInfo.value(configKey::region).toString();
}
case FeaturesRole: {
if (serviceType == serviceType::amneziaPremium) {
@@ -115,15 +113,12 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
}
}
case PriceRole: {
auto price = apiServiceData.serviceInfo.price;
auto price = serviceInfo.value(configKey::price).toString();
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();
@@ -133,18 +128,15 @@ void ApiServicesModel::updateModel(const QJsonObject &data)
{
beginResetModel();
m_services.clear();
m_countryCode = data.value(configKey::userCountryCode).toString();
auto services = data.value(configKey::services).toArray();
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));
if (services.isEmpty()) {
m_services.push_back(getApiServicesData(data));
m_services.push_back(service);
m_selectedServiceIndex = 0;
} else {
for (const auto &service : services) {
m_services.push_back(getApiServicesData(service.toObject()));
}
}
endResetModel();
@@ -157,32 +149,32 @@ void ApiServicesModel::setServiceIndex(const int index)
QJsonObject ApiServicesModel::getSelectedServiceInfo()
{
auto service = m_services.at(m_selectedServiceIndex);
return service.serviceInfo.object;
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
return service.value(configKey::serviceInfo).toObject();
}
QString ApiServicesModel::getSelectedServiceType()
{
auto service = m_services.at(m_selectedServiceIndex);
return service.type;
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
return service.value(configKey::serviceType).toString();
}
QString ApiServicesModel::getSelectedServiceProtocol()
{
auto service = m_services.at(m_selectedServiceIndex);
return service.protocol;
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
return service.value(configKey::serviceProtocol).toString();
}
QString ApiServicesModel::getSelectedServiceName()
{
auto service = m_services.at(m_selectedServiceIndex);
return service.serviceInfo.name;
auto modelIndex = index(m_selectedServiceIndex, 0);
return data(modelIndex, ApiServicesModel::Roles::NameRole).toString();
}
QJsonArray ApiServicesModel::getSelectedServiceCountries()
{
auto service = m_services.at(m_selectedServiceIndex);
return service.availableCountries;
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
return service.value(configKey::availableCountries).toArray();
}
QString ApiServicesModel::getCountryCode()
@@ -192,8 +184,8 @@ QString ApiServicesModel::getCountryCode()
QString ApiServicesModel::getStoreEndpoint()
{
auto service = m_services.at(m_selectedServiceIndex);
return service.storeEndpoint;
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
return service.value(configKey::storeEndpoint).toString();
}
QVariant ApiServicesModel::getSelectedServiceData(const QString roleString)
@@ -217,46 +209,10 @@ QHash<int, QByteArray> ApiServicesModel::roleNames() const
roles[ServiceDescriptionRole] = "serviceDescription";
roles[IsServiceAvailableRole] = "isServiceAvailable";
roles[SpeedRole] = "speed";
roles[TimeLimitRole] = "timeLimit";
roles[WorkPeriodRole] = "workPeriod";
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;
}

View File

@@ -3,7 +3,6 @@
#include <QAbstractListModel>
#include <QJsonArray>
#include <QJsonObject>
class ApiServicesModel : public QAbstractListModel
{
@@ -16,11 +15,10 @@ public:
ServiceDescriptionRole,
IsServiceAvailableRole,
SpeedRole,
TimeLimitRole,
WorkPeriodRole,
RegionRole,
FeaturesRole,
PriceRole,
EndDateRole
PriceRole
};
explicit ApiServicesModel(QObject *parent = nullptr);
@@ -50,40 +48,8 @@ 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;
QVector<ApiServicesData> m_services;
QJsonArray m_services;
int m_selectedServiceIndex;
};

View File

@@ -22,7 +22,7 @@ namespace
constexpr char serviceProtocol[] = "service_protocol";
constexpr char publicKeyInfo[] = "public_key";
constexpr char expiresAt[] = "expires_at";
constexpr char endDate[] = "end_date";
}
}
@@ -39,9 +39,6 @@ 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
@@ -82,12 +79,6 @@ 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())) {
@@ -688,18 +679,6 @@ 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();
@@ -739,9 +718,9 @@ bool ServersModel::isApiKeyExpired(const int serverIndex)
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
auto publicKeyInfo = apiConfig.value(configKey::publicKeyInfo).toObject();
const QString expiresAt = publicKeyInfo.value(configKey::expiresAt).toString();
if (expiresAt.isEmpty()) {
publicKeyInfo.insert(configKey::expiresAt, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate));
const QString endDate = publicKeyInfo.value(configKey::endDate).toString();
if (endDate.isEmpty()) {
publicKeyInfo.insert(configKey::endDate, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate));
apiConfig.insert(configKey::publicKeyInfo, publicKeyInfo);
serverConfig.insert(configKey::apiConfig, apiConfig);
editServer(serverConfig, serverIndex);
@@ -749,8 +728,8 @@ bool ServersModel::isApiKeyExpired(const int serverIndex)
return false;
}
auto expiresAtDateTime = QDateTime::fromString(expiresAt, Qt::ISODate).toUTC();
if (expiresAtDateTime < QDateTime::currentDateTimeUtc()) {
auto endDateDateTime = QDateTime::fromString(endDate, Qt::ISODate).toUTC();
if (endDateDateTime < QDateTime::currentDateTimeUtc()) {
return true;
}
return false;

View File

@@ -46,7 +46,6 @@ 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;
@@ -116,7 +115,6 @@ public slots:
QVariant getDefaultServerData(const QString roleString);
QVariant getProcessedServerData(const QString roleString);
bool setProcessedServerData(const QString &roleString, const QVariant &value);
bool isDefaultServerDefaultContainerHasSplitTunneling();
@@ -129,9 +127,6 @@ 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();

View File

@@ -84,7 +84,7 @@ DrawerType2 {
Layout.topMargin: 16
text: qsTr("Share")
leftImageSource: "qrc:/images/controls/share-2.svg"
imageSource: "qrc:/images/controls/share-2.svg"
KeyNavigation.tab: copyConfigTextButton
@@ -120,7 +120,7 @@ DrawerType2 {
borderWidth: 1
text: qsTr("Copy")
leftImageSource: "qrc:/images/controls/copy.svg"
imageSource: "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")
leftImageSource: "qrc:/images/controls/copy.svg"
imageSource: "qrc:/images/controls/copy.svg"
KeyNavigation.tab: showSettingsButton
}

View File

@@ -22,10 +22,9 @@ Button {
property int borderWidth: 0
property int borderFocusedWidth: 1
property string leftImageSource
property string imageSource
property string rightImageSource
property string leftImageColor
property bool changeLeftImageSize: true
property string leftImageColor: textColor
property bool squareLeftSide: false
@@ -128,23 +127,18 @@ Button {
anchors.centerIn: parent
Image {
id: leftImage
source: root.leftImageSource
visible: root.leftImageSource === "" ? false : true
Layout.preferredHeight: 20
Layout.preferredWidth: 20
source: root.imageSource
visible: root.imageSource === "" ? false : true
layer {
enabled: leftImageColor !== "" ? true : false
enabled: true
effect: ColorOverlay {
color: leftImageColor
}
}
Component.onCompleted: {
if (root.changeLeftImageSize) {
leftImage.Layout.preferredHeight = 20
leftImage.Layout.preferredWidth = 20
}
}
}
ButtonTextType {

View File

@@ -14,7 +14,7 @@ Popup {
visible: false
Overlay.modal: Rectangle {
color: AmneziaStyle.color.translucentMidnightBlack
color: Qt.rgba(14/255, 14/255, 17/255, 0.8)
}
background: Rectangle {

View File

@@ -19,7 +19,7 @@ RadioButton {
property string textColor: AmneziaStyle.color.midnightBlack
property string pressedBorderColor: AmneziaStyle.color.softGoldenApricot
property string pressedBorderColor: Qt.rgba(251/255, 178/255, 106/255, 0.3)
property string selectedBorderColor: AmneziaStyle.color.goldenApricot
property string defaultBodredColor: AmneziaStyle.color.transparent
property int borderWidth: 0

View File

@@ -145,7 +145,6 @@ Button {
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
enabled: root.enabled
onEntered: {
backgroundRect.color = root.hoveredColor

View File

@@ -92,7 +92,7 @@ Item {
id: background
anchors.fill: parent
color: root.isCollapsed ? AmneziaStyle.color.transparent : AmneziaStyle.color.translucentMidnightBlack
color: root.isCollapsed ? AmneziaStyle.color.transparent : Qt.rgba(14/255, 14/255, 17/255, 0.8)
Behavior on color {
PropertyAnimation { duration: 200 }

View File

@@ -24,7 +24,7 @@ Popup {
Overlay.modal: Rectangle {
visible: root.closeButtonVisible
color: AmneziaStyle.color.translucentMidnightBlack
color: Qt.rgba(14/255, 14/255, 17/255, 0.8)
}
onOpened: {

View File

@@ -183,7 +183,7 @@ Item {
focusPolicy: Qt.NoFocus
text: root.buttonText
leftImageSource: root.buttonImageSource
imageSource: root.buttonImageSource
anchors.top: content.top
anchors.bottom: content.bottom

View File

@@ -14,7 +14,7 @@ Popup {
visible: false
Overlay.modal: Rectangle {
color: AmneziaStyle.color.translucentMidnightBlack
color: Qt.rgba(14/255, 14/255, 17/255, 0.8)
}
background: Rectangle {

View File

@@ -22,9 +22,5 @@ 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)
}
}

View File

@@ -98,6 +98,7 @@ PageType {
pressedColor: AmneziaStyle.color.sheerWhite
disabledColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.mutedGray
leftImageColor: AmneziaStyle.color.transparent
borderWidth: 0
buttonTextLabel.lineHeight: 20
@@ -109,7 +110,7 @@ PageType {
text: isSplitTunnelingEnabled ? qsTr("Split tunneling enabled") : qsTr("Split tunneling disabled")
leftImageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : ""
imageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : ""
rightImageSource: "qrc:/images/controls/chevron-down.svg"
Keys.onEnterPressed: splitTunnelingButton.clicked()
@@ -165,7 +166,6 @@ PageType {
anchors.left: parent.left
anchors.right: parent.right
spacing: 0
Component.onCompleted: {
drawer.collapsedHeight = collapsed.implicitHeight
@@ -267,39 +267,18 @@ PageType {
RowLayout {
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Layout.topMargin: 8
Layout.bottomMargin: drawer.isCollapsed ? 44 : ServersModel.isDefaultServerFromApi ? 61 : 16
Layout.bottomMargin: drawer.isCollapsed ? 44 : ServersModel.isDefaultServerFromApi ? 89 : 44
spacing: 0
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
Image {
Layout.rightMargin: 8
visible: source !== ""
source: ServersModel.defaultServerImagePathCollapsed
}
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)
}
}
}
}
@@ -337,8 +316,8 @@ PageType {
rootButtonImageColor: AmneziaStyle.color.midnightBlack
rootButtonBackgroundColor: AmneziaStyle.color.paleGray
rootButtonBackgroundHoveredColor: AmneziaStyle.color.mistyGray
rootButtonBackgroundPressedColor: AmneziaStyle.color.cloudyGray
rootButtonBackgroundHoveredColor: Qt.rgba(215, 216, 219, 0.8)
rootButtonBackgroundPressedColor: Qt.rgba(215, 216, 219, 0.65)
rootButtonHoveredBorderColor: AmneziaStyle.color.transparent
rootButtonDefaultBorderColor: AmneziaStyle.color.transparent
rootButtonTextTopMargin: 8

View File

@@ -54,14 +54,8 @@ 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

View File

@@ -56,15 +56,12 @@ PageType {
}
LabelWithImageType {
property bool showSubscriptionEndDate: ServersModel.getProcessedServerData("isCountrySelectionAvailable")
Layout.fillWidth: true
Layout.margins: 16
imageSource: "qrc:/images/controls/history.svg"
leftText: showSubscriptionEndDate ? qsTr("Valid until") : qsTr("Work period")
rightText: showSubscriptionEndDate ? ApiServicesModel.getSelectedServiceData("endDate")
: ApiServicesModel.getSelectedServiceData("workPeriod")
leftText: qsTr("Work period")
rightText: ApiServicesModel.getSelectedServiceData("workPeriod")
visible: rightText !== ""
}
@@ -135,8 +132,8 @@ PageType {
implicitHeight: 32
defaultColor: "transparent"
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
hoveredColor: Qt.rgba(1, 1, 1, 0.08)
pressedColor: Qt.rgba(1, 1, 1, 0.12)
textColor: AmneziaStyle.color.vibrantRed
text: qsTr("Reload API config")
@@ -175,8 +172,8 @@ PageType {
implicitHeight: 32
defaultColor: "transparent"
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
hoveredColor: Qt.rgba(1, 1, 1, 0.08)
pressedColor: Qt.rgba(1, 1, 1, 0.12)
textColor: AmneziaStyle.color.vibrantRed
text: qsTr("Remove from application")

View File

@@ -25,8 +25,6 @@ PageType {
property int pageSettingsApiServerInfo: 3
property int pageSettingsApiLanguageList: 4
property var processedServer
defaultActiveFocusItem: focusItem
Connections {
@@ -37,18 +35,8 @@ PageType {
}
}
Connections {
target: ServersModel
function onProcessedServerChanged() {
root.processedServer = proxyServersModel.get(0)
}
}
SortFilterProxyModel {
id: proxyServersModel
objectName: "proxyServersModel"
sourceModel: ServersModel
filters: [
ValueFilter {
@@ -56,139 +44,147 @@ PageType {
value: true
}
]
Component.onCompleted: {
root.processedServer = proxyServersModel.get(0)
}
}
Item {
id: focusItem
//KeyNavigation.tab: header
KeyNavigation.tab: header
}
ColumnLayout {
anchors.fill: parent
spacing: 4
spacing: 16
BackButtonType {
id: backButton
Repeater {
id: header
model: proxyServersModel
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
}
activeFocusOnTab: true
onFocusChanged: {
header.itemAt(0).focusItem.forceActiveFocus()
}
KeyNavigation.tab: tabBar
delegate: ColumnLayout {
actionButtonFunction: function() {
if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) {
nestedStackView.currentIndex = root.pageSettingsApiServerInfo
} else {
serverNameEditDrawer.open()
}
}
}
property alias focusItem: backButton
DrawerType2 {
id: serverNameEditDrawer
id: content
parent: root
Layout.topMargin: 20
anchors.fill: parent
expandedHeight: root.height * 0.35
BackButtonType {
id: backButton
KeyNavigation.tab: headerContent.actionButton
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()
backButtonFunction: function() {
if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo &&
ServersModel.getProcessedServerData("isCountrySelectionAvailable")) {
nestedStackView.currentIndex = root.pageSettingsApiLanguageList
} else {
PageController.closePage()
}
}
}
Item {
id: focusItem1
KeyNavigation.tab: serverName.textField
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: 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()
}
}
}
TextFieldWithHeaderType {
id: serverName
DrawerType2 {
id: serverNameEditDrawer
Layout.fillWidth: true
headerText: qsTr("Server name")
textFieldText: root.processedServer.name
textField.maximumLength: 30
checkEmptyText: true
parent: root
KeyNavigation.tab: saveButton
}
anchors.fill: parent
expandedHeight: root.height * 0.35
BasicButtonType {
id: saveButton
onClosed: {
if (!GC.isMobile()) {
headerContent.actionButton.forceActiveFocus()
}
}
Layout.fillWidth: true
expandedContent: ColumnLayout {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 32
anchors.leftMargin: 16
anchors.rightMargin: 16
text: qsTr("Save")
KeyNavigation.tab: focusItem1
clickedFunc: function() {
if (serverName.textFieldText === "") {
return
Connections {
target: serverNameEditDrawer
enabled: !GC.isMobile()
function onOpened() {
serverName.textField.forceActiveFocus()
}
}
if (serverName.textFieldText !== root.processedServer.name) {
ServersModel.setProcessedServerData("name", serverName.textFieldText);
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()
}
}
serverNameEditDrawer.close()
}
}
}
@@ -261,7 +257,8 @@ PageType {
StackLayout {
id: nestedStackView
Layout.fillWidth: true
Layout.preferredWidth: root.width
Layout.preferredHeight: root.height - tabBar.implicitHeight - header.implicitHeight
currentIndex: ServersModel.getProcessedServerData("isServerFromGatewayApi") ?
(ServersModel.getProcessedServerData("isCountrySelectionAvailable") ?

View File

@@ -16,82 +16,83 @@ PageType {
defaultActiveFocusItem: focusItem
ColumnLayout {
id: header
FlickableType {
id: fl
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
spacing: 0
Item {
id: focusItem
KeyNavigation.tab: backButton
}
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: 16
headerText: qsTr("VPN by Amnezia")
descriptionText: qsTr("Choose a VPN service that suits your needs.")
}
}
ListView {
id: servicesListView
anchors.top: header.bottom
anchors.right: parent.right
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.topMargin: 16
spacing: 0
contentHeight: content.height
currentIndex: 1
clip: true
model: ApiServicesModel
ColumnLayout {
id: content
ScrollBar.vertical: ScrollBar {}
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
delegate: Item {
implicitWidth: servicesListView.width
implicitHeight: delegateContent.implicitHeight
spacing: 0
ColumnLayout {
id: delegateContent
Item {
id: focusItem
KeyNavigation.tab: backButton
}
anchors.fill: parent
BackButtonType {
id: backButton
Layout.topMargin: 20
// KeyNavigation.tab: fileButton.rightButton
}
CardWithIconsType {
id: card
HeaderType {
Layout.fillWidth: true
Layout.topMargin: 8
Layout.rightMargin: 16
Layout.leftMargin: 16
Layout.bottomMargin: 32
Layout.fillWidth: true
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: name
bodyText: cardDescription
footerText: price
ListView {
id: containers
width: parent.width
height: containers.contentItem.height
spacing: 16
rightImageSource: "qrc:/images/controls/chevron-right.svg"
currentIndex: 1
interactive: false
model: ApiServicesModel
enabled: isServiceAvailable
delegate: Item {
implicitWidth: containers.width
implicitHeight: delegateContent.implicitHeight
onClicked: {
if (isServiceAvailable) {
ApiServicesModel.setServiceIndex(index)
PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo)
ColumnLayout {
id: delegateContent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
CardWithIconsType {
id: card
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: name
bodyText: cardDescription
footerText: price
rightImageSource: "qrc:/images/controls/chevron-right.svg"
onClicked: {
if (isServiceAvailable) {
ApiServicesModel.setServiceIndex(index)
PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo)
}
}
}
}
}

View File

@@ -47,6 +47,7 @@ PageType {
KeyNavigation.tab: textKey.textField
}
HeaderType {
property bool isVisible: SettingsController.getInstallationUuid() !== "" || PageController.isStartPageVisible()

View File

@@ -573,7 +573,7 @@ PageType {
visible: accessTypeSelector.currentIndex === 0
text: qsTr("Share")
leftImageSource: "qrc:/images/controls/share-2.svg"
imageSource: "qrc:/images/controls/share-2.svg"
Keys.onTabPressed: lastItemTabClicked(focusItem)

View File

@@ -135,7 +135,7 @@ PageType {
Layout.topMargin: 40
text: qsTr("Share")
leftImageSource: "qrc:/images/controls/share-2.svg"
imageSource: "qrc:/images/controls/share-2.svg"
Keys.onTabPressed: lastItemTabClicked(focusItem)

View File

@@ -19,11 +19,6 @@ 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
@@ -47,11 +42,6 @@ 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

View File

@@ -13,11 +13,6 @@ 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
@@ -64,11 +59,6 @@ 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

BIN
metadata/img-readme/apl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB