mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-21 02:01:03 +07:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| acf8185ddb | |||
| aaf2c9ddeb | |||
| dbbc7119ec | |||
| c57162c4cc | |||
| 40e39895c9 | |||
| ec3ab2a03c | |||
| ddecfcad26 |
@@ -179,7 +179,7 @@ You may face compiling issues in QT Creator after you've worked in Android Studi
|
||||
|
||||
## License
|
||||
|
||||
GPL v3.0
|
||||
This project is licensed under the GNU General Public License v3.0 (see LICENSE) and also includes third-party components distributed under their own terms (see THIRD_PARTY_LICENSES.md).
|
||||
|
||||
## Donate
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
# Third-Party Licenses
|
||||
|
||||
This project is licensed under the GNU General Public License v3.0.
|
||||
This file lists third-party software components used by this repository.
|
||||
Each component is distributed under its own license as linked below.
|
||||
|
||||
---
|
||||
|
||||
## QtKeychain
|
||||
|
||||
- Source: https://github.com/frankosterfeld/qtkeychain
|
||||
- License: BSD License
|
||||
- License Text: https://www.gnu.org/licenses/license-list.html#ModifiedBSD
|
||||
|
||||
---
|
||||
|
||||
## QSimpleCrypto
|
||||
|
||||
- Source: https://github.com/n1flh31mur/QSimpleCrypto
|
||||
- License: Apache License 2.0
|
||||
- License Text: https://github.com/n1flh31mur/QSimpleCrypto/blob/master/LICENSE
|
||||
|
||||
---
|
||||
|
||||
## SortFilterProxyModel
|
||||
|
||||
- Source: https://github.com/oKcerG/SortFilterProxyModel
|
||||
- License: MIT License
|
||||
- License Text: https://github.com/oKcerG/SortFilterProxyModel/blob/master/LICENSE
|
||||
|
||||
---
|
||||
|
||||
## QJsonStruct
|
||||
|
||||
- Source: https://github.com/Qv2ray/QJsonStruct
|
||||
- License: MIT License
|
||||
- License Text: https://github.com/Qv2ray/QJsonStruct/blob/master/LICENSE
|
||||
|
||||
---
|
||||
|
||||
## QR Code Generator (qrcodegen)
|
||||
|
||||
- Source: https://github.com/nayuki/QR-Code-generator
|
||||
- License: MIT License
|
||||
- License Text: https://www.nayuki.io/page/qr-code-generator-library
|
||||
|
||||
---
|
||||
|
||||
## Qt Gamepad
|
||||
|
||||
- Source: https://github.com/qt/qtgamepad
|
||||
- License: GNU General Public License v3.0 (GPL-3.0)
|
||||
- License Text: https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||
|
||||
---
|
||||
|
||||
## AmneziaWG Apple (WireGuard)
|
||||
|
||||
- Source: https://github.com/amnezia-vpn/amneziawg-apple
|
||||
- License: MIT License
|
||||
- License Text: https://github.com/amnezia-vpn/amneziawg-apple/blob/master/COPYING
|
||||
|
||||
---
|
||||
|
||||
## AmneziaWG Android
|
||||
|
||||
- Source: https://github.com/amnezia-vpn/amneziawg-go
|
||||
- License: MIT License
|
||||
- License Text: https://github.com/amnezia-vpn/amneziawg-go/blob/master/LICENSE
|
||||
|
||||
---
|
||||
|
||||
## Xray Core
|
||||
|
||||
- Source: https://github.com/XTLS/Xray-core
|
||||
- License: Mozilla Public License 2.0 (MPL-2.0)
|
||||
- License Text: https://github.com/XTLS/Xray-core/blob/main/LICENSE
|
||||
|
||||
---
|
||||
|
||||
## Cloak
|
||||
|
||||
- Source: https://github.com/cbeuw/Cloak
|
||||
- License: GNU General Public License v3.0 (GPL-3.0)
|
||||
- License Text: https://github.com/cbeuw/Cloak/blob/master/LICENSE
|
||||
|
||||
---
|
||||
|
||||
## Shadowsocks
|
||||
|
||||
- Source: https://github.com/shadowsocks/shadowsocks-libev
|
||||
- License: GPL-3.0-or-later
|
||||
- License Text: http://www.gnu.org/licenses/
|
||||
|
||||
---
|
||||
|
||||
## OpenSSL
|
||||
|
||||
- Source: https://github.com/openssl/openssl
|
||||
- License: Apache License 2.0
|
||||
- License Text: https://www.openssl.org/source/license.html
|
||||
|
||||
---
|
||||
|
||||
## libssh
|
||||
|
||||
- Source: https://www.libssh.org/
|
||||
- License: GNU Lesser General Public License (LGPL)
|
||||
- License Text: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
|
||||
|
||||
---
|
||||
|
||||
## OpenVPNAdapter
|
||||
|
||||
- Source: https://github.com/ss-abramchuk/OpenVPNAdapter
|
||||
- License: GNU Affero General Public License v3.0 (AGPL-3.0)
|
||||
- License Text: https://github.com/ss-abramchuk/OpenVPNAdapter/blob/master/LICENSE
|
||||
|
||||
---
|
||||
|
||||
## Wintun
|
||||
|
||||
- Source: https://www.wintun.net/
|
||||
- License: Prebuilt Binaries License
|
||||
- License Text: https://github.com/WireGuard/wintun/blob/master/prebuilt-binaries-license.txt
|
||||
|
||||
---
|
||||
|
||||
## Mullvad Split Tunnel Driver
|
||||
|
||||
- Source: https://github.com/mullvad/win-split-tunnel
|
||||
- License: GNU General Public License v3.0 (GPL-3.0) and Mozilla Public License Version 2.0
|
||||
- License Text: https://github.com/mullvad/win-split-tunnel/blob/master/LICENSE-GPL.md https://github.com/mullvad/win-split-tunnel/blob/master/LICENSE-MPL.txt
|
||||
|
||||
---
|
||||
|
||||
## tun2socks
|
||||
|
||||
- Source: https://github.com/eycorsican/go-tun2socks
|
||||
- License: MIT License
|
||||
- License Text: https://github.com/eycorsican/go-tun2socks/blob/master/LICENSE
|
||||
|
||||
---
|
||||
|
||||
## TAP-Windows Driver
|
||||
|
||||
- Source: https://github.com/OpenVPN/tap-windows6
|
||||
- License: tap-windows6 license
|
||||
- License Text: https://github.com/OpenVPN/tap-windows6/blob/master/COPYING
|
||||
+1
-1
Submodule client/3rd-prebuilt updated: 568b8d720d...51bb4703a4
@@ -816,7 +816,7 @@ class AmneziaActivity : QtActivity() {
|
||||
@Suppress("unused")
|
||||
fun getFd(fileName: String): Int {
|
||||
Log.v(TAG, "Get fd for $fileName")
|
||||
return blockingCall {
|
||||
return blockingCall(Dispatchers.IO) {
|
||||
try {
|
||||
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
|
||||
pfd?.fd ?: -1
|
||||
|
||||
@@ -10,8 +10,10 @@ namespace apiDefs
|
||||
AmneziaFreeV3,
|
||||
AmneziaPremiumV1,
|
||||
AmneziaPremiumV2,
|
||||
AmneziaTrialV2,
|
||||
SelfHosted,
|
||||
ExternalPremium
|
||||
ExternalPremium,
|
||||
ExternalTrial
|
||||
};
|
||||
|
||||
enum ConfigSource {
|
||||
|
||||
@@ -58,18 +58,24 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
|
||||
};
|
||||
case apiDefs::ConfigSource::AmneziaGateway: {
|
||||
constexpr QLatin1String servicePremium("amnezia-premium");
|
||||
constexpr QLatin1String serviceTrial("amnezia-trial");
|
||||
constexpr QLatin1String serviceFree("amnezia-free");
|
||||
constexpr QLatin1String serviceExternalPremium("external-premium");
|
||||
constexpr QLatin1String serviceExternalTrial("external-trial");
|
||||
|
||||
auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
|
||||
auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString();
|
||||
|
||||
if (serviceType == servicePremium) {
|
||||
return apiDefs::ConfigType::AmneziaPremiumV2;
|
||||
} else if (serviceType == serviceTrial) {
|
||||
return apiDefs::ConfigType::AmneziaTrialV2;
|
||||
} else if (serviceType == serviceFree) {
|
||||
return apiDefs::ConfigType::AmneziaFreeV3;
|
||||
} else if (serviceType == serviceExternalPremium) {
|
||||
return apiDefs::ConfigType::ExternalPremium;
|
||||
} else if (serviceType == serviceExternalTrial) {
|
||||
return apiDefs::ConfigType::ExternalTrial;
|
||||
}
|
||||
}
|
||||
default: {
|
||||
@@ -133,7 +139,8 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
||||
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
|
||||
{
|
||||
static const QSet<apiDefs::ConfigType> premiumTypes = { apiDefs::ConfigType::AmneziaPremiumV1, apiDefs::ConfigType::AmneziaPremiumV2,
|
||||
apiDefs::ConfigType::ExternalPremium };
|
||||
apiDefs::ConfigType::AmneziaTrialV2, apiDefs::ConfigType::ExternalPremium,
|
||||
apiDefs::ConfigType::ExternalTrial };
|
||||
return premiumTypes.contains(getConfigType(serverConfigObject));
|
||||
}
|
||||
|
||||
@@ -177,7 +184,9 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
|
||||
|
||||
QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
|
||||
{
|
||||
if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) {
|
||||
auto configType = apiUtils::getConfigType(serverConfigObject);
|
||||
if (configType != apiDefs::ConfigType::AmneziaPremiumV2 && configType != apiDefs::ConfigType::AmneziaTrialV2
|
||||
&& configType != apiDefs::ConfigType::ExternalPremium && configType != apiDefs::ConfigType::ExternalTrial) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -126,8 +126,13 @@ extension PacketTunnelProvider {
|
||||
}
|
||||
|
||||
vpnReachability.startTracking { [weak self] status in
|
||||
guard status == .reachableViaWiFi else { return }
|
||||
self?.ovpnAdapter?.reconnect(afterTimeInterval: 5)
|
||||
switch status {
|
||||
case .reachableViaWiFi, .reachableViaWWAN:
|
||||
ovpnLog(.info, message: "Reachability changed, reconnecting OpenVPN session")
|
||||
self?.ovpnAdapter?.reconnect(afterTimeInterval: 1)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
startHandler = completionHandler
|
||||
|
||||
@@ -21,6 +21,44 @@ extension Constants {
|
||||
}
|
||||
|
||||
extension PacketTunnelProvider {
|
||||
private func applyXraySplitTunnel(_ xrayConfig: XrayConfig,
|
||||
settings: NEPacketTunnelNetworkSettings) {
|
||||
guard let splitTunnelType = xrayConfig.splitTunnelType else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let splitTunnelSites = xrayConfig.splitTunnelSites else {
|
||||
xrayLog(.error, message: "Split tunnel sites are not set")
|
||||
return
|
||||
}
|
||||
|
||||
if splitTunnelType == 1 {
|
||||
var ipv4IncludedRoutes = [NEIPv4Route]()
|
||||
|
||||
for allowedIPString in splitTunnelSites {
|
||||
if let allowedIP = IPAddressRange(from: allowedIPString) {
|
||||
ipv4IncludedRoutes.append(NEIPv4Route(
|
||||
destinationAddress: "\(allowedIP.address)",
|
||||
subnetMask: "\(allowedIP.subnetMask())"))
|
||||
}
|
||||
}
|
||||
|
||||
settings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||
} else if splitTunnelType == 2 {
|
||||
var ipv4ExcludedRoutes = [NEIPv4Route]()
|
||||
|
||||
for excludedIPString in splitTunnelSites {
|
||||
if let excludedIP = IPAddressRange(from: excludedIPString) {
|
||||
ipv4ExcludedRoutes.append(NEIPv4Route(
|
||||
destinationAddress: "\(excludedIP.address)",
|
||||
subnetMask: "\(excludedIP.subnetMask())"))
|
||||
}
|
||||
}
|
||||
|
||||
settings.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
|
||||
}
|
||||
}
|
||||
|
||||
func startXray(completionHandler: @escaping (Error?) -> Void) {
|
||||
|
||||
// Xray configuration
|
||||
@@ -72,6 +110,7 @@ extension PacketTunnelProvider {
|
||||
settings.dnsSettings = !dnsArray.isEmpty
|
||||
? NEDNSSettings(servers: dnsArray)
|
||||
: NEDNSSettings(servers: ["1.1.1.1"])
|
||||
applyXraySplitTunnel(xrayConfig, settings: settings)
|
||||
|
||||
let xrayConfigData = xrayConfig.config.data(using: .utf8)
|
||||
|
||||
|
||||
@@ -41,10 +41,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
var ovpnAdapter: OpenVPNAdapter?
|
||||
private lazy var openVPNPacketFlowAdapter = PacketTunnelFlowAdapter(flow: packetFlow)
|
||||
private let pathMonitorQueue = DispatchQueue(label: Constants.processQueueName + ".path-monitor")
|
||||
private let networkChangeQueue = DispatchQueue(label: Constants.processQueueName + ".network-change")
|
||||
private let pathMonitor = NWPathMonitor()
|
||||
private var didReceiveInitialPathUpdate = false
|
||||
private var currentPath: Network.NWPath?
|
||||
private var currentPathSignature: String?
|
||||
private var pendingNetworkChangeWorkItem: DispatchWorkItem?
|
||||
private var isApplyingNetworkChange = false
|
||||
|
||||
var splitTunnelType: Int?
|
||||
var splitTunnelSites: [String]?
|
||||
@@ -78,14 +81,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
guard hasMeaningfulChange, let proto = self.protoType else { return }
|
||||
|
||||
// WireGuard/AWG manages network changes internally; avoid restarting the tunnel here.
|
||||
if proto == .wireguard {
|
||||
// OpenVPN and WireGuard/AWG handle network changes internally.
|
||||
// Restarting them here can race their own reconnect logic and break tunnel setup.
|
||||
if proto == .wireguard || proto == .openvpn {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.handle(networkChange: path) { _ in }
|
||||
}
|
||||
self.scheduleNetworkChangeHandling(for: proto, path: path)
|
||||
}
|
||||
pathMonitor.start(queue: pathMonitorQueue)
|
||||
|
||||
@@ -259,9 +261,47 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
}
|
||||
|
||||
private func handle(networkChange changePath: Network.NWPath, completion: @escaping (Error?) -> Void) {
|
||||
guard protoType == .xray else {
|
||||
updateActiveInterfaceIndex(for: changePath)
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
updateActiveInterfaceIndex(for: changePath)
|
||||
wg_log(.info, message: "Tunnel restarted.")
|
||||
startTunnel(options: nil, completionHandler: completion)
|
||||
reasserting = true
|
||||
xrayLog(.info, message: "Applying network change to xray tunnel")
|
||||
stopXray { }
|
||||
startXray { [weak self] error in
|
||||
self?.reasserting = false
|
||||
completion(error)
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduleNetworkChangeHandling(for proto: TunnelProtoType, path: Network.NWPath) {
|
||||
guard proto == .xray else { return }
|
||||
|
||||
pendingNetworkChangeWorkItem?.cancel()
|
||||
|
||||
let workItem = DispatchWorkItem { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
if self.isApplyingNetworkChange {
|
||||
xrayLog(.debug, message: "Skipping network change while restart is already in progress")
|
||||
return
|
||||
}
|
||||
|
||||
self.isApplyingNetworkChange = true
|
||||
DispatchQueue.main.async {
|
||||
self.handle(networkChange: path) { [weak self] _ in
|
||||
self?.networkChangeQueue.async {
|
||||
self?.isApplyingNetworkChange = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pendingNetworkChangeWorkItem = workItem
|
||||
networkChangeQueue.asyncAfter(deadline: .now() + 1.0, execute: workItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,91 @@
|
||||
#import "QtAppDelegate.h"
|
||||
#import "ios_controller.h"
|
||||
#import <StoreKit/StoreKit.h>
|
||||
|
||||
#include <QFile>
|
||||
|
||||
namespace {
|
||||
constexpr NSInteger kReviewRequestOpenInterval = 20;
|
||||
NSString *const kAppOpenCountKey = @"AmneziaVPN.AppOpenCount";
|
||||
BOOL gShouldRequestReviewOnBecomeActive = NO;
|
||||
id gSceneWillEnterForegroundObserver = nil;
|
||||
id gSceneDidActivateObserver = nil;
|
||||
|
||||
void scheduleAppStoreReviewIfNeededOnOpen()
|
||||
{
|
||||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||
const NSInteger openCount = [defaults integerForKey:kAppOpenCountKey] + 1;
|
||||
[defaults setInteger:openCount forKey:kAppOpenCountKey];
|
||||
|
||||
gShouldRequestReviewOnBecomeActive =
|
||||
(openCount > 0 && openCount % kReviewRequestOpenInterval == 0);
|
||||
|
||||
NSLog(@"[Review] scene open count=%ld interval=%ld scheduled=%@",
|
||||
(long)openCount,
|
||||
(long)kReviewRequestOpenInterval,
|
||||
gShouldRequestReviewOnBecomeActive ? @"YES" : @"NO");
|
||||
}
|
||||
|
||||
void requestAppStoreReviewForScene(UIScene *scene)
|
||||
{
|
||||
if (@available(iOS 14.0, *)) {
|
||||
if ([scene isKindOfClass:[UIWindowScene class]] &&
|
||||
scene.activationState == UISceneActivationStateForegroundActive) {
|
||||
[SKStoreReviewController requestReviewInScene:(UIWindowScene *)scene];
|
||||
NSLog(@"[Review] requestReviewInScene invoked");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (@available(iOS 10.3, *)) {
|
||||
[SKStoreReviewController requestReview];
|
||||
NSLog(@"[Review] requestReview fallback invoked");
|
||||
}
|
||||
}
|
||||
|
||||
void setupSceneReviewObservers()
|
||||
{
|
||||
if (gSceneWillEnterForegroundObserver || gSceneDidActivateObserver) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
gSceneWillEnterForegroundObserver =
|
||||
[center addObserverForName:UISceneWillEnterForegroundNotification
|
||||
object:nil
|
||||
queue:[NSOperationQueue mainQueue]
|
||||
usingBlock:^(__unused NSNotification *note) {
|
||||
scheduleAppStoreReviewIfNeededOnOpen();
|
||||
}];
|
||||
|
||||
gSceneDidActivateObserver =
|
||||
[center addObserverForName:UISceneDidActivateNotification
|
||||
object:nil
|
||||
queue:[NSOperationQueue mainQueue]
|
||||
usingBlock:^(NSNotification *note) {
|
||||
if (!gShouldRequestReviewOnBecomeActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
gShouldRequestReviewOnBecomeActive = NO;
|
||||
UIScene *scene = [note.object isKindOfClass:[UIScene class]] ? (UIScene *)note.object : nil;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
requestAppStoreReviewForScene(scene);
|
||||
});
|
||||
}];
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
||||
@implementation QIOSApplicationDelegate (AmneziaVPNDelegate)
|
||||
#if !MACOS_NE
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
[application setMinimumBackgroundFetchInterval: UIApplicationBackgroundFetchIntervalMinimum];
|
||||
setupSceneReviewObservers();
|
||||
// Override point for customization after application launch.
|
||||
NSLog(@"Application didFinishLaunchingWithOptions");
|
||||
return YES;
|
||||
|
||||
@@ -3,5 +3,7 @@ import Foundation
|
||||
struct XrayConfig: Decodable {
|
||||
let dns1: String?
|
||||
let dns2: String?
|
||||
let splitTunnelType: Int?
|
||||
let splitTunnelSites: [String]?
|
||||
let config: String
|
||||
}
|
||||
|
||||
@@ -684,6 +684,15 @@ bool IosController::setupXray()
|
||||
QJsonObject finalConfig;
|
||||
finalConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1].toString());
|
||||
finalConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2].toString());
|
||||
finalConfig.insert(config_key::splitTunnelType, m_rawConfig[config_key::splitTunnelType]);
|
||||
|
||||
QJsonArray splitTunnelSites = m_rawConfig[config_key::splitTunnelSites].toArray();
|
||||
|
||||
for(int index = 0; index < splitTunnelSites.count(); index++) {
|
||||
splitTunnelSites[index] = splitTunnelSites[index].toString().remove(" ");
|
||||
}
|
||||
|
||||
finalConfig.insert(config_key::splitTunnelSites, splitTunnelSites);
|
||||
finalConfig.insert(config_key::config, xrayConfigStr);
|
||||
|
||||
QJsonDocument finalConfigDoc(finalConfig);
|
||||
|
||||
@@ -447,7 +447,7 @@ bool ApiConfigsController::importService()
|
||||
importSerivceFromAppStore();
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
} else if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaFree) {
|
||||
importServiceFromGateway();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,9 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
case IsComponentVisibleRole: {
|
||||
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2
|
||||
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalPremium;
|
||||
|| m_accountInfoData.configType == apiDefs::ConfigType::AmneziaTrialV2
|
||||
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalPremium
|
||||
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalTrial;
|
||||
}
|
||||
case HasExpiredWorkerRole: {
|
||||
for (int i = 0; i < m_issuedConfigsInfo.size(); i++) {
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace
|
||||
{
|
||||
constexpr char amneziaFree[] = "amnezia-free";
|
||||
constexpr char amneziaPremium[] = "amnezia-premium";
|
||||
constexpr char amneziaTrial[] = "amnezia-trial";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +70,7 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
case CardDescriptionRole: {
|
||||
auto speed = apiServiceData.serviceInfo.speed;
|
||||
if (serviceType == serviceType::amneziaPremium) {
|
||||
if (serviceType == serviceType::amneziaPremium || serviceType == serviceType::amneziaTrial) {
|
||||
return apiServiceData.serviceInfo.cardDescription.arg(speed);
|
||||
} else if (serviceType == serviceType::amneziaFree) {
|
||||
QString description = apiServiceData.serviceInfo.cardDescription;
|
||||
@@ -124,8 +125,10 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
|
||||
case OrderRole: {
|
||||
if (serviceType == serviceType::amneziaPremium) {
|
||||
return 0;
|
||||
} else if (serviceType == serviceType::amneziaFree) {
|
||||
} else if (serviceType == serviceType::amneziaTrial) {
|
||||
return 1;
|
||||
} else if (serviceType == serviceType::amneziaFree) {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,226 +1,226 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
|
||||
import PageEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
|
||||
onFocusChanged: {
|
||||
if (this.activeFocus) {
|
||||
listView.positionViewAtBeginning()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
|
||||
header: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.bottomMargin: 32
|
||||
|
||||
headerText: ApiServicesModel.getSelectedServiceData("name")
|
||||
descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription")
|
||||
}
|
||||
}
|
||||
|
||||
model: inputFields
|
||||
spacing: 0
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
LabelWithImageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
|
||||
imageSource: imagePath
|
||||
leftText: lText
|
||||
rightText: rText
|
||||
|
||||
visible: isVisible
|
||||
}
|
||||
}
|
||||
|
||||
footer: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
spacing: 0
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
textFormat: Text.RichText
|
||||
text: {
|
||||
var text = ApiServicesModel.getSelectedServiceData("features")
|
||||
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
textFormat: Text.PlainText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
|
||||
text: qsTr("Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.")
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: continueButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 32
|
||||
Layout.bottomMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : qsTr("Connect")
|
||||
|
||||
clickedFunc: function() {
|
||||
PageController.showBusyIndicator(true)
|
||||
var result = ApiConfigsController.importService()
|
||||
PageController.showBusyIndicator(false)
|
||||
|
||||
if (!result) {
|
||||
var endpoint = ApiServicesModel.getStoreEndpoint()
|
||||
Qt.openUrlExternally(endpoint)
|
||||
PageController.closePage()
|
||||
PageController.closePage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 32
|
||||
|
||||
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
textFormat: Text.RichText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
|
||||
text: {
|
||||
var termsUrl = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
|
||||
var privacyUrl = LanguageModel.getCurrentSiteUrl("policy")
|
||||
return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: #FBB26A;\">Terms of Use</a> and <a href=\"%2\" style=\"color: #FBB26A;\">Privacy Policy</a>").arg(termsUrl).arg(privacyUrl)
|
||||
}
|
||||
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property list<QtObject> inputFields: [
|
||||
region,
|
||||
price,
|
||||
timeLimit,
|
||||
speed,
|
||||
features
|
||||
]
|
||||
|
||||
QtObject {
|
||||
id: region
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/map-pin.svg"
|
||||
readonly property string lText: qsTr("For the region")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("region")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: price
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/tag.svg"
|
||||
readonly property string lText: qsTr("Price")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("price")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: timeLimit
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/history.svg"
|
||||
readonly property string lText: qsTr("Work period")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("timeLimit")
|
||||
property bool isVisible: rText !== ""
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: speed
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/gauge.svg"
|
||||
readonly property string lText: qsTr("Speed")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("speed")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: features
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/info.svg"
|
||||
readonly property string lText: qsTr("Features")
|
||||
readonly property string rText: ""
|
||||
property bool isVisible: true
|
||||
}
|
||||
}
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
|
||||
import PageEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
|
||||
onFocusChanged: {
|
||||
if (this.activeFocus) {
|
||||
listView.positionViewAtBeginning()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
|
||||
header: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.bottomMargin: 32
|
||||
|
||||
headerText: ApiServicesModel.getSelectedServiceData("name")
|
||||
descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription")
|
||||
}
|
||||
}
|
||||
|
||||
model: inputFields
|
||||
spacing: 0
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
LabelWithImageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
|
||||
imageSource: imagePath
|
||||
leftText: lText
|
||||
rightText: rText
|
||||
|
||||
visible: isVisible
|
||||
}
|
||||
}
|
||||
|
||||
footer: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
spacing: 0
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
textFormat: Text.RichText
|
||||
text: {
|
||||
var text = ApiServicesModel.getSelectedServiceData("features")
|
||||
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
textFormat: Text.PlainText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
|
||||
text: qsTr("Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.")
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: continueButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 32
|
||||
Layout.bottomMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : (ApiServicesModel.getSelectedServiceType() === "amnezia-trial" ? qsTr("Try Trial") : qsTr("Connect"))
|
||||
|
||||
clickedFunc: function() {
|
||||
PageController.showBusyIndicator(true)
|
||||
var result = ApiConfigsController.importService()
|
||||
PageController.showBusyIndicator(false)
|
||||
|
||||
if (!result) {
|
||||
var endpoint = ApiServicesModel.getStoreEndpoint()
|
||||
Qt.openUrlExternally(endpoint)
|
||||
PageController.closePage()
|
||||
PageController.closePage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 32
|
||||
|
||||
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
textFormat: Text.RichText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
|
||||
text: {
|
||||
var termsUrl = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
|
||||
var privacyUrl = LanguageModel.getCurrentSiteUrl("policy")
|
||||
return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: #FBB26A;\">Terms of Use</a> and <a href=\"%2\" style=\"color: #FBB26A;\">Privacy Policy</a>").arg(termsUrl).arg(privacyUrl)
|
||||
}
|
||||
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property list<QtObject> inputFields: [
|
||||
region,
|
||||
price,
|
||||
timeLimit,
|
||||
speed,
|
||||
features
|
||||
]
|
||||
|
||||
QtObject {
|
||||
id: region
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/map-pin.svg"
|
||||
readonly property string lText: qsTr("For the region")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("region")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: price
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/tag.svg"
|
||||
readonly property string lText: qsTr("Price")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("price")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: timeLimit
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/history.svg"
|
||||
readonly property string lText: qsTr("Work period")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("timeLimit")
|
||||
property bool isVisible: rText !== ""
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: speed
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/gauge.svg"
|
||||
readonly property string lText: qsTr("Speed")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("speed")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: features
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/info.svg"
|
||||
readonly property string lText: qsTr("Features")
|
||||
readonly property string rText: ""
|
||||
property bool isVisible: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,11 +79,23 @@ PageType {
|
||||
}
|
||||
|
||||
textField.onTextChanged: {
|
||||
if (headerText == qsTr("Password or SSH private key")) {
|
||||
if (headerText === qsTr("Password or SSH private key")) {
|
||||
buttonImageSource = textField.text !== "" ? imageSource : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WarningType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 8
|
||||
|
||||
visible: title === qsTr("Password or SSH private key")
|
||||
backGroundColor: AmneziaStyle.color.translucentWhite
|
||||
iconPath: "qrc:/images/controls/alert-circle.svg"
|
||||
textString: qsTr("SSH key requirements: supported ED25519 or RSA in PEM. Paste the private key including BEGIN/END lines. If your key doesn’t work, generate a compatible one.")
|
||||
}
|
||||
}
|
||||
|
||||
footer: ColumnLayout {
|
||||
|
||||
Reference in New Issue
Block a user