mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-24 02:00:24 +07:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| acf8185ddb | |||
| aaf2c9ddeb | |||
| dbbc7119ec | |||
| c57162c4cc | |||
| 40e39895c9 | |||
| ec3ab2a03c | |||
| ddecfcad26 | |||
| 67bd880cdf | |||
| 477afb9d85 | |||
| f969fcdbb8 | |||
| b0ca16d861 | |||
| 9963359948 |
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||||
|
|
||||||
set(PROJECT AmneziaVPN)
|
set(PROJECT AmneziaVPN)
|
||||||
set(AMNEZIAVPN_VERSION 4.8.14.3)
|
set(AMNEZIAVPN_VERSION 4.8.14.5)
|
||||||
|
|
||||||
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
|
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
|
||||||
DESCRIPTION "AmneziaVPN"
|
DESCRIPTION "AmneziaVPN"
|
||||||
@@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
|
|||||||
set(RELEASE_DATE "${CURRENT_DATE}")
|
set(RELEASE_DATE "${CURRENT_DATE}")
|
||||||
|
|
||||||
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||||
set(APP_ANDROID_VERSION_CODE 2115)
|
set(APP_ANDROID_VERSION_CODE 2118)
|
||||||
|
|
||||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||||
set(MZ_PLATFORM_NAME "linux")
|
set(MZ_PLATFORM_NAME "linux")
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ You may face compiling issues in QT Creator after you've worked in Android Studi
|
|||||||
|
|
||||||
## License
|
## 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
|
## 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
@@ -109,6 +109,16 @@ void AmneziaApplication::init()
|
|||||||
// install filter on main window
|
// install filter on main window
|
||||||
if (auto win = qobject_cast<QQuickWindow*>(obj)) {
|
if (auto win = qobject_cast<QQuickWindow*>(obj)) {
|
||||||
win->installEventFilter(this);
|
win->installEventFilter(this);
|
||||||
|
#ifdef Q_OS_ANDROID
|
||||||
|
QObject::connect(win, &QQuickWindow::sceneGraphError,
|
||||||
|
[](QQuickWindow::SceneGraphError, const QString &msg) {
|
||||||
|
qWarning() << "Scene graph error (suppressed):" << msg;
|
||||||
|
});
|
||||||
|
// Keep graphics context alive across hide/show cycles to avoid
|
||||||
|
// eglSwapBuffers/makeCurrent being called on a context Android has reclaimed.
|
||||||
|
win->setPersistentSceneGraph(true);
|
||||||
|
win->setPersistentGraphics(true);
|
||||||
|
#endif
|
||||||
win->show();
|
win->show();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -313,29 +313,27 @@ class AmneziaActivity : QtActivity() {
|
|||||||
KeyEvent.KEYCODE_BUTTON_Y,
|
KeyEvent.KEYCODE_BUTTON_Y,
|
||||||
KeyEvent.KEYCODE_BUTTON_START,
|
KeyEvent.KEYCODE_BUTTON_START,
|
||||||
KeyEvent.KEYCODE_BUTTON_SELECT -> {
|
KeyEvent.KEYCODE_BUTTON_SELECT -> {
|
||||||
nativeGamepadKeyEvent(0, keyCode, pressed)
|
nativeGamepadKeyEvent(0, keyCode, pressed)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_DPAD_CENTER,
|
KeyEvent.KEYCODE_DPAD_CENTER,
|
||||||
KeyEvent.KEYCODE_DPAD_UP,
|
KeyEvent.KEYCODE_DPAD_UP,
|
||||||
KeyEvent.KEYCODE_DPAD_DOWN,
|
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||||
KeyEvent.KEYCODE_DPAD_LEFT,
|
KeyEvent.KEYCODE_DPAD_LEFT,
|
||||||
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||||
val syntheticKeyCode = if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) KeyEvent.KEYCODE_ENTER else keyCode
|
val syntheticKeyCode = if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) KeyEvent.KEYCODE_ENTER else keyCode
|
||||||
val synthetic = KeyEvent(
|
val synthetic = KeyEvent(
|
||||||
event.downTime, event.eventTime, event.action, syntheticKeyCode,
|
event.downTime, event.eventTime, event.action, syntheticKeyCode,
|
||||||
event.repeatCount, event.metaState, -1, event.scanCode,
|
event.repeatCount, event.metaState, -1, event.scanCode,
|
||||||
event.flags, InputDevice.SOURCE_KEYBOARD
|
event.flags, InputDevice.SOURCE_KEYBOARD
|
||||||
)
|
)
|
||||||
return super.dispatchKeyEvent(synthetic)
|
return super.dispatchKeyEvent(synthetic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.dispatchKeyEvent(event)
|
return super.dispatchKeyEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean)
|
private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean)
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -818,7 +816,7 @@ class AmneziaActivity : QtActivity() {
|
|||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun getFd(fileName: String): Int {
|
fun getFd(fileName: String): Int {
|
||||||
Log.v(TAG, "Get fd for $fileName")
|
Log.v(TAG, "Get fd for $fileName")
|
||||||
return blockingCall {
|
return blockingCall(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
|
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
|
||||||
pfd?.fd ?: -1
|
pfd?.fd ?: -1
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ namespace apiDefs
|
|||||||
AmneziaFreeV3,
|
AmneziaFreeV3,
|
||||||
AmneziaPremiumV1,
|
AmneziaPremiumV1,
|
||||||
AmneziaPremiumV2,
|
AmneziaPremiumV2,
|
||||||
|
AmneziaTrialV2,
|
||||||
SelfHosted,
|
SelfHosted,
|
||||||
ExternalPremium
|
ExternalPremium,
|
||||||
|
ExternalTrial
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ConfigSource {
|
enum ConfigSource {
|
||||||
|
|||||||
@@ -58,18 +58,24 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
|
|||||||
};
|
};
|
||||||
case apiDefs::ConfigSource::AmneziaGateway: {
|
case apiDefs::ConfigSource::AmneziaGateway: {
|
||||||
constexpr QLatin1String servicePremium("amnezia-premium");
|
constexpr QLatin1String servicePremium("amnezia-premium");
|
||||||
|
constexpr QLatin1String serviceTrial("amnezia-trial");
|
||||||
constexpr QLatin1String serviceFree("amnezia-free");
|
constexpr QLatin1String serviceFree("amnezia-free");
|
||||||
constexpr QLatin1String serviceExternalPremium("external-premium");
|
constexpr QLatin1String serviceExternalPremium("external-premium");
|
||||||
|
constexpr QLatin1String serviceExternalTrial("external-trial");
|
||||||
|
|
||||||
auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
|
auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
|
||||||
auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString();
|
auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString();
|
||||||
|
|
||||||
if (serviceType == servicePremium) {
|
if (serviceType == servicePremium) {
|
||||||
return apiDefs::ConfigType::AmneziaPremiumV2;
|
return apiDefs::ConfigType::AmneziaPremiumV2;
|
||||||
|
} else if (serviceType == serviceTrial) {
|
||||||
|
return apiDefs::ConfigType::AmneziaTrialV2;
|
||||||
} else if (serviceType == serviceFree) {
|
} else if (serviceType == serviceFree) {
|
||||||
return apiDefs::ConfigType::AmneziaFreeV3;
|
return apiDefs::ConfigType::AmneziaFreeV3;
|
||||||
} else if (serviceType == serviceExternalPremium) {
|
} else if (serviceType == serviceExternalPremium) {
|
||||||
return apiDefs::ConfigType::ExternalPremium;
|
return apiDefs::ConfigType::ExternalPremium;
|
||||||
|
} else if (serviceType == serviceExternalTrial) {
|
||||||
|
return apiDefs::ConfigType::ExternalTrial;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
@@ -133,7 +139,8 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
|||||||
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
|
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
|
||||||
{
|
{
|
||||||
static const QSet<apiDefs::ConfigType> premiumTypes = { apiDefs::ConfigType::AmneziaPremiumV1, apiDefs::ConfigType::AmneziaPremiumV2,
|
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));
|
return premiumTypes.contains(getConfigType(serverConfigObject));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +184,9 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
|
|||||||
|
|
||||||
QString apiUtils::getPremiumV2VpnKey(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 {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,8 +126,13 @@ extension PacketTunnelProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
vpnReachability.startTracking { [weak self] status in
|
vpnReachability.startTracking { [weak self] status in
|
||||||
guard status == .reachableViaWiFi else { return }
|
switch status {
|
||||||
self?.ovpnAdapter?.reconnect(afterTimeInterval: 5)
|
case .reachableViaWiFi, .reachableViaWWAN:
|
||||||
|
ovpnLog(.info, message: "Reachability changed, reconnecting OpenVPN session")
|
||||||
|
self?.ovpnAdapter?.reconnect(afterTimeInterval: 1)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startHandler = completionHandler
|
startHandler = completionHandler
|
||||||
|
|||||||
@@ -21,6 +21,44 @@ extension Constants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension PacketTunnelProvider {
|
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) {
|
func startXray(completionHandler: @escaping (Error?) -> Void) {
|
||||||
|
|
||||||
// Xray configuration
|
// Xray configuration
|
||||||
@@ -72,6 +110,7 @@ extension PacketTunnelProvider {
|
|||||||
settings.dnsSettings = !dnsArray.isEmpty
|
settings.dnsSettings = !dnsArray.isEmpty
|
||||||
? NEDNSSettings(servers: dnsArray)
|
? NEDNSSettings(servers: dnsArray)
|
||||||
: NEDNSSettings(servers: ["1.1.1.1"])
|
: NEDNSSettings(servers: ["1.1.1.1"])
|
||||||
|
applyXraySplitTunnel(xrayConfig, settings: settings)
|
||||||
|
|
||||||
let xrayConfigData = xrayConfig.config.data(using: .utf8)
|
let xrayConfigData = xrayConfig.config.data(using: .utf8)
|
||||||
|
|
||||||
|
|||||||
@@ -41,10 +41,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
var ovpnAdapter: OpenVPNAdapter?
|
var ovpnAdapter: OpenVPNAdapter?
|
||||||
private lazy var openVPNPacketFlowAdapter = PacketTunnelFlowAdapter(flow: packetFlow)
|
private lazy var openVPNPacketFlowAdapter = PacketTunnelFlowAdapter(flow: packetFlow)
|
||||||
private let pathMonitorQueue = DispatchQueue(label: Constants.processQueueName + ".path-monitor")
|
private let pathMonitorQueue = DispatchQueue(label: Constants.processQueueName + ".path-monitor")
|
||||||
|
private let networkChangeQueue = DispatchQueue(label: Constants.processQueueName + ".network-change")
|
||||||
private let pathMonitor = NWPathMonitor()
|
private let pathMonitor = NWPathMonitor()
|
||||||
private var didReceiveInitialPathUpdate = false
|
private var didReceiveInitialPathUpdate = false
|
||||||
private var currentPath: Network.NWPath?
|
private var currentPath: Network.NWPath?
|
||||||
private var currentPathSignature: String?
|
private var currentPathSignature: String?
|
||||||
|
private var pendingNetworkChangeWorkItem: DispatchWorkItem?
|
||||||
|
private var isApplyingNetworkChange = false
|
||||||
|
|
||||||
var splitTunnelType: Int?
|
var splitTunnelType: Int?
|
||||||
var splitTunnelSites: [String]?
|
var splitTunnelSites: [String]?
|
||||||
@@ -78,14 +81,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
|
|
||||||
guard hasMeaningfulChange, let proto = self.protoType else { return }
|
guard hasMeaningfulChange, let proto = self.protoType else { return }
|
||||||
|
|
||||||
// WireGuard/AWG manages network changes internally; avoid restarting the tunnel here.
|
// OpenVPN and WireGuard/AWG handle network changes internally.
|
||||||
if proto == .wireguard {
|
// Restarting them here can race their own reconnect logic and break tunnel setup.
|
||||||
|
if proto == .wireguard || proto == .openvpn {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
self.scheduleNetworkChangeHandling(for: proto, path: path)
|
||||||
self.handle(networkChange: path) { _ in }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pathMonitor.start(queue: pathMonitorQueue)
|
pathMonitor.start(queue: pathMonitorQueue)
|
||||||
|
|
||||||
@@ -259,9 +261,47 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func handle(networkChange changePath: Network.NWPath, completion: @escaping (Error?) -> Void) {
|
private func handle(networkChange changePath: Network.NWPath, completion: @escaping (Error?) -> Void) {
|
||||||
|
guard protoType == .xray else {
|
||||||
|
updateActiveInterfaceIndex(for: changePath)
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
updateActiveInterfaceIndex(for: changePath)
|
updateActiveInterfaceIndex(for: changePath)
|
||||||
wg_log(.info, message: "Tunnel restarted.")
|
reasserting = true
|
||||||
startTunnel(options: nil, completionHandler: completion)
|
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 "QtAppDelegate.h"
|
||||||
#import "ios_controller.h"
|
#import "ios_controller.h"
|
||||||
|
#import <StoreKit/StoreKit.h>
|
||||||
|
|
||||||
#include <QFile>
|
#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)
|
@implementation QIOSApplicationDelegate (AmneziaVPNDelegate)
|
||||||
#if !MACOS_NE
|
#if !MACOS_NE
|
||||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||||
{
|
{
|
||||||
[application setMinimumBackgroundFetchInterval: UIApplicationBackgroundFetchIntervalMinimum];
|
[application setMinimumBackgroundFetchInterval: UIApplicationBackgroundFetchIntervalMinimum];
|
||||||
|
setupSceneReviewObservers();
|
||||||
// Override point for customization after application launch.
|
// Override point for customization after application launch.
|
||||||
NSLog(@"Application didFinishLaunchingWithOptions");
|
NSLog(@"Application didFinishLaunchingWithOptions");
|
||||||
return YES;
|
return YES;
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ import Foundation
|
|||||||
struct XrayConfig: Decodable {
|
struct XrayConfig: Decodable {
|
||||||
let dns1: String?
|
let dns1: String?
|
||||||
let dns2: String?
|
let dns2: String?
|
||||||
|
let splitTunnelType: Int?
|
||||||
|
let splitTunnelSites: [String]?
|
||||||
let config: String
|
let config: String
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -684,6 +684,15 @@ bool IosController::setupXray()
|
|||||||
QJsonObject finalConfig;
|
QJsonObject finalConfig;
|
||||||
finalConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1].toString());
|
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::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);
|
finalConfig.insert(config_key::config, xrayConfigStr);
|
||||||
|
|
||||||
QJsonDocument finalConfigDoc(finalConfig);
|
QJsonDocument finalConfigDoc(finalConfig);
|
||||||
|
|||||||
@@ -2070,11 +2070,6 @@ Thank you for staying with us!</source>
|
|||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>PageSettingsApiSubscriptionKey</name>
|
<name>PageSettingsApiSubscriptionKey</name>
|
||||||
<message>
|
|
||||||
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="72"/>
|
|
||||||
<source>Subscription key</source>
|
|
||||||
<translation>Ключ для подключения</translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
<message>
|
||||||
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="85"/>
|
<location filename="../ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml" line="85"/>
|
||||||
<source>Copy key</source>
|
<source>Copy key</source>
|
||||||
|
|||||||
@@ -447,7 +447,7 @@ bool ApiConfigsController::importService()
|
|||||||
importSerivceFromAppStore();
|
importSerivceFromAppStore();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else {
|
} else if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaFree) {
|
||||||
importServiceFromGateway();
|
importServiceFromGateway();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
|
|||||||
}
|
}
|
||||||
case IsComponentVisibleRole: {
|
case IsComponentVisibleRole: {
|
||||||
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2
|
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: {
|
case HasExpiredWorkerRole: {
|
||||||
for (int i = 0; i < m_issuedConfigsInfo.size(); i++) {
|
for (int i = 0; i < m_issuedConfigsInfo.size(); i++) {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ namespace
|
|||||||
{
|
{
|
||||||
constexpr char amneziaFree[] = "amnezia-free";
|
constexpr char amneziaFree[] = "amnezia-free";
|
||||||
constexpr char amneziaPremium[] = "amnezia-premium";
|
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: {
|
case CardDescriptionRole: {
|
||||||
auto speed = apiServiceData.serviceInfo.speed;
|
auto speed = apiServiceData.serviceInfo.speed;
|
||||||
if (serviceType == serviceType::amneziaPremium) {
|
if (serviceType == serviceType::amneziaPremium || serviceType == serviceType::amneziaTrial) {
|
||||||
return apiServiceData.serviceInfo.cardDescription.arg(speed);
|
return apiServiceData.serviceInfo.cardDescription.arg(speed);
|
||||||
} else if (serviceType == serviceType::amneziaFree) {
|
} else if (serviceType == serviceType::amneziaFree) {
|
||||||
QString description = apiServiceData.serviceInfo.cardDescription;
|
QString description = apiServiceData.serviceInfo.cardDescription;
|
||||||
@@ -124,8 +125,10 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
|
|||||||
case OrderRole: {
|
case OrderRole: {
|
||||||
if (serviceType == serviceType::amneziaPremium) {
|
if (serviceType == serviceType::amneziaPremium) {
|
||||||
return 0;
|
return 0;
|
||||||
} else if (serviceType == serviceType::amneziaFree) {
|
} else if (serviceType == serviceType::amneziaTrial) {
|
||||||
return 1;
|
return 1;
|
||||||
|
} else if (serviceType == serviceType::amneziaFree) {
|
||||||
|
return 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ Item {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property StackView stackView: StackView.view
|
property StackView stackView: StackView.view
|
||||||
|
property bool enableTimer: true
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (visible) {
|
if (visible && enableTimer) {
|
||||||
timer.start()
|
timer.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,6 +25,6 @@ Item {
|
|||||||
FocusController.setFocusOnDefaultItem()
|
FocusController.setFocusOnDefaultItem()
|
||||||
}
|
}
|
||||||
repeat: false // Stop the timer after one trigger
|
repeat: false // Stop the timer after one trigger
|
||||||
running: true // Start the timer
|
running: enableTimer // Start the timer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,8 +69,7 @@ PageType {
|
|||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
Layout.rightMargin: 16
|
Layout.rightMargin: 16
|
||||||
Layout.topMargin: 16
|
Layout.topMargin: 16
|
||||||
text: (root.processedServer && root.processedServer.name ? root.processedServer.name : "")
|
text: qsTr(root.processedServer.name + "\nsubscription key")
|
||||||
+ "\n" + qsTr("Subscription key")
|
|
||||||
font.pixelSize: 32
|
font.pixelSize: 32
|
||||||
font.bold: true
|
font.bold: true
|
||||||
color: AmneziaStyle.color.paleGray
|
color: AmneziaStyle.color.paleGray
|
||||||
@@ -207,8 +206,7 @@ PageType {
|
|||||||
|
|
||||||
Header2Type {
|
Header2Type {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
headerText: (root.processedServer && root.processedServer.name ? root.processedServer.name : "")
|
headerText: qsTr(root.processedServer.name + " Subscription key")
|
||||||
+ "\n" + qsTr("Subscription key")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TextArea {
|
TextArea {
|
||||||
|
|||||||
@@ -1,226 +1,226 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Dialogs
|
import QtQuick.Dialogs
|
||||||
|
|
||||||
import PageEnum 1.0
|
import PageEnum 1.0
|
||||||
import Style 1.0
|
import Style 1.0
|
||||||
|
|
||||||
import "./"
|
import "./"
|
||||||
import "../Controls2"
|
import "../Controls2"
|
||||||
import "../Controls2/TextTypes"
|
import "../Controls2/TextTypes"
|
||||||
import "../Config"
|
import "../Config"
|
||||||
import "../Components"
|
import "../Components"
|
||||||
|
|
||||||
PageType {
|
PageType {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
BackButtonType {
|
BackButtonType {
|
||||||
id: backButton
|
id: backButton
|
||||||
|
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||||
|
|
||||||
onFocusChanged: {
|
onFocusChanged: {
|
||||||
if (this.activeFocus) {
|
if (this.activeFocus) {
|
||||||
listView.positionViewAtBeginning()
|
listView.positionViewAtBeginning()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ListViewType {
|
ListViewType {
|
||||||
id: listView
|
id: listView
|
||||||
|
|
||||||
anchors.top: backButton.bottom
|
anchors.top: backButton.bottom
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|
||||||
header: ColumnLayout {
|
header: ColumnLayout {
|
||||||
width: listView.width
|
width: listView.width
|
||||||
|
|
||||||
BaseHeaderType {
|
BaseHeaderType {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 8
|
Layout.topMargin: 8
|
||||||
Layout.rightMargin: 16
|
Layout.rightMargin: 16
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
Layout.bottomMargin: 32
|
Layout.bottomMargin: 32
|
||||||
|
|
||||||
headerText: ApiServicesModel.getSelectedServiceData("name")
|
headerText: ApiServicesModel.getSelectedServiceData("name")
|
||||||
descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription")
|
descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
model: inputFields
|
model: inputFields
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
delegate: ColumnLayout {
|
delegate: ColumnLayout {
|
||||||
width: listView.width
|
width: listView.width
|
||||||
|
|
||||||
LabelWithImageType {
|
LabelWithImageType {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.margins: 16
|
Layout.margins: 16
|
||||||
|
|
||||||
imageSource: imagePath
|
imageSource: imagePath
|
||||||
leftText: lText
|
leftText: lText
|
||||||
rightText: rText
|
rightText: rText
|
||||||
|
|
||||||
visible: isVisible
|
visible: isVisible
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
footer: ColumnLayout {
|
footer: ColumnLayout {
|
||||||
width: listView.width
|
width: listView.width
|
||||||
|
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
ParagraphTextType {
|
ParagraphTextType {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.rightMargin: 16
|
Layout.rightMargin: 16
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
|
|
||||||
onLinkActivated: function(link) {
|
onLinkActivated: function(link) {
|
||||||
Qt.openUrlExternally(link)
|
Qt.openUrlExternally(link)
|
||||||
}
|
}
|
||||||
textFormat: Text.RichText
|
textFormat: Text.RichText
|
||||||
text: {
|
text: {
|
||||||
var text = ApiServicesModel.getSelectedServiceData("features")
|
var text = ApiServicesModel.getSelectedServiceData("features")
|
||||||
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
|
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.NoButton
|
acceptedButtons: Qt.NoButton
|
||||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ParagraphTextType {
|
ParagraphTextType {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 16
|
Layout.topMargin: 16
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
Layout.rightMargin: 16
|
Layout.rightMargin: 16
|
||||||
|
|
||||||
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
|
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
|
||||||
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
textFormat: Text.PlainText
|
textFormat: Text.PlainText
|
||||||
color: AmneziaStyle.color.mutedGray
|
color: AmneziaStyle.color.mutedGray
|
||||||
font.pixelSize: 12
|
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.")
|
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 {
|
BasicButtonType {
|
||||||
id: continueButton
|
id: continueButton
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 32
|
Layout.topMargin: 32
|
||||||
Layout.bottomMargin: 16
|
Layout.bottomMargin: 16
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
Layout.rightMargin: 16
|
Layout.rightMargin: 16
|
||||||
|
|
||||||
text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : qsTr("Connect")
|
text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : (ApiServicesModel.getSelectedServiceType() === "amnezia-trial" ? qsTr("Try Trial") : qsTr("Connect"))
|
||||||
|
|
||||||
clickedFunc: function() {
|
clickedFunc: function() {
|
||||||
PageController.showBusyIndicator(true)
|
PageController.showBusyIndicator(true)
|
||||||
var result = ApiConfigsController.importService()
|
var result = ApiConfigsController.importService()
|
||||||
PageController.showBusyIndicator(false)
|
PageController.showBusyIndicator(false)
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
var endpoint = ApiServicesModel.getStoreEndpoint()
|
var endpoint = ApiServicesModel.getStoreEndpoint()
|
||||||
Qt.openUrlExternally(endpoint)
|
Qt.openUrlExternally(endpoint)
|
||||||
PageController.closePage()
|
PageController.closePage()
|
||||||
PageController.closePage()
|
PageController.closePage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ParagraphTextType {
|
ParagraphTextType {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 16
|
Layout.topMargin: 16
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
Layout.rightMargin: 16
|
Layout.rightMargin: 16
|
||||||
Layout.bottomMargin: 32
|
Layout.bottomMargin: 32
|
||||||
|
|
||||||
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
|
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
|
||||||
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
textFormat: Text.RichText
|
textFormat: Text.RichText
|
||||||
color: AmneziaStyle.color.mutedGray
|
color: AmneziaStyle.color.mutedGray
|
||||||
font.pixelSize: 12
|
font.pixelSize: 12
|
||||||
|
|
||||||
text: {
|
text: {
|
||||||
var termsUrl = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
|
var termsUrl = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
|
||||||
var privacyUrl = LanguageModel.getCurrentSiteUrl("policy")
|
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)
|
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) {
|
onLinkActivated: function(link) {
|
||||||
Qt.openUrlExternally(link)
|
Qt.openUrlExternally(link)
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.NoButton
|
acceptedButtons: Qt.NoButton
|
||||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property list<QtObject> inputFields: [
|
property list<QtObject> inputFields: [
|
||||||
region,
|
region,
|
||||||
price,
|
price,
|
||||||
timeLimit,
|
timeLimit,
|
||||||
speed,
|
speed,
|
||||||
features
|
features
|
||||||
]
|
]
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: region
|
id: region
|
||||||
|
|
||||||
readonly property string imagePath: "qrc:/images/controls/map-pin.svg"
|
readonly property string imagePath: "qrc:/images/controls/map-pin.svg"
|
||||||
readonly property string lText: qsTr("For the region")
|
readonly property string lText: qsTr("For the region")
|
||||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("region")
|
readonly property string rText: ApiServicesModel.getSelectedServiceData("region")
|
||||||
property bool isVisible: true
|
property bool isVisible: true
|
||||||
}
|
}
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: price
|
id: price
|
||||||
|
|
||||||
readonly property string imagePath: "qrc:/images/controls/tag.svg"
|
readonly property string imagePath: "qrc:/images/controls/tag.svg"
|
||||||
readonly property string lText: qsTr("Price")
|
readonly property string lText: qsTr("Price")
|
||||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("price")
|
readonly property string rText: ApiServicesModel.getSelectedServiceData("price")
|
||||||
property bool isVisible: true
|
property bool isVisible: true
|
||||||
}
|
}
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: timeLimit
|
id: timeLimit
|
||||||
|
|
||||||
readonly property string imagePath: "qrc:/images/controls/history.svg"
|
readonly property string imagePath: "qrc:/images/controls/history.svg"
|
||||||
readonly property string lText: qsTr("Work period")
|
readonly property string lText: qsTr("Work period")
|
||||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("timeLimit")
|
readonly property string rText: ApiServicesModel.getSelectedServiceData("timeLimit")
|
||||||
property bool isVisible: rText !== ""
|
property bool isVisible: rText !== ""
|
||||||
}
|
}
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: speed
|
id: speed
|
||||||
|
|
||||||
readonly property string imagePath: "qrc:/images/controls/gauge.svg"
|
readonly property string imagePath: "qrc:/images/controls/gauge.svg"
|
||||||
readonly property string lText: qsTr("Speed")
|
readonly property string lText: qsTr("Speed")
|
||||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("speed")
|
readonly property string rText: ApiServicesModel.getSelectedServiceData("speed")
|
||||||
property bool isVisible: true
|
property bool isVisible: true
|
||||||
}
|
}
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: features
|
id: features
|
||||||
|
|
||||||
readonly property string imagePath: "qrc:/images/controls/info.svg"
|
readonly property string imagePath: "qrc:/images/controls/info.svg"
|
||||||
readonly property string lText: qsTr("Features")
|
readonly property string lText: qsTr("Features")
|
||||||
readonly property string rText: ""
|
readonly property string rText: ""
|
||||||
property bool isVisible: true
|
property bool isVisible: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,11 +79,23 @@ PageType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
textField.onTextChanged: {
|
textField.onTextChanged: {
|
||||||
if (headerText == qsTr("Password or SSH private key")) {
|
if (headerText === qsTr("Password or SSH private key")) {
|
||||||
buttonImageSource = textField.text !== "" ? imageSource : ""
|
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 {
|
footer: ColumnLayout {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import "../Components"
|
|||||||
|
|
||||||
PageType {
|
PageType {
|
||||||
id: root
|
id: root
|
||||||
|
enableTimer: (SettingsController.isOnTv()) ? false : true
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: content
|
id: content
|
||||||
@@ -45,4 +46,22 @@ PageType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 250
|
||||||
|
running: SettingsController.isOnTv()
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
startButton.forceActiveFocus()
|
||||||
|
if (startButton.activeFocus) {
|
||||||
|
running = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible && SettingsController.isOnTv()) {
|
||||||
|
startButton.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,14 @@ Window {
|
|||||||
function onStateChanged() {
|
function onStateChanged() {
|
||||||
if (Qt.platform.os === "android") {
|
if (Qt.platform.os === "android") {
|
||||||
if (Qt.application.state === Qt.ApplicationActive) {
|
if (Qt.application.state === Qt.ApplicationActive) {
|
||||||
|
root.visible = true
|
||||||
refreshTimer.restart()
|
refreshTimer.restart()
|
||||||
} else if (Qt.application.state === Qt.ApplicationSuspended ||
|
} else if (Qt.application.state === Qt.ApplicationSuspended) {
|
||||||
Qt.application.state === Qt.ApplicationInactive) {
|
// Hide window to stop the Qt render loop and prevent
|
||||||
console.log("QML: Application going to background, state:", Qt.application.state)
|
// eglSwapBuffers from being called on a lost EGL context.
|
||||||
|
// NOTE: Do NOT hide on ApplicationInactive — that fires on any
|
||||||
|
// focus change (IME, notifications) and would blank the screen.
|
||||||
|
root.visible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user