mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-22 19:05:39 +03:00
Compare commits
104 Commits
fix/androi
...
button-foc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ced0c27d6 | ||
|
|
0e87550d85 | ||
|
|
dceb0ab832 | ||
|
|
8d07a910bc | ||
|
|
a33590476a | ||
|
|
deaf618520 | ||
|
|
3d8a56d922 | ||
|
|
36af7cf471 | ||
|
|
ebd3449b4a | ||
|
|
99182f4a67 | ||
|
|
da84ba1a4d | ||
|
|
bca68fc185 | ||
|
|
59a7265bac | ||
|
|
9201ca1e03 | ||
|
|
6b6a76d2cc | ||
|
|
840c388ab9 | ||
|
|
5b4ec608c8 | ||
|
|
79ff1b81e0 | ||
|
|
ea67c01da8 | ||
|
|
1137e169ea | ||
|
|
17748cca47 | ||
|
|
080e1d98c6 | ||
|
|
ca633ae882 | ||
|
|
bb7b64fb96 | ||
|
|
bf901631bf | ||
|
|
0c0ce54b1f | ||
|
|
ee762c4cef | ||
|
|
ed9efb5a79 | ||
|
|
73eb85f2f4 | ||
|
|
cd055cff62 | ||
|
|
f8b2cce618 | ||
|
|
e648054c7a | ||
|
|
fe558163cc | ||
|
|
3883b8ff34 | ||
|
|
d286664763 | ||
|
|
b05ad2392b | ||
|
|
6dbdb85aaf | ||
|
|
26b48cfe4f | ||
|
|
2f39136143 | ||
|
|
8d0d3c5ce9 | ||
|
|
256081e4ed | ||
|
|
1dd7b0a221 | ||
|
|
82c0b28906 | ||
|
|
985fe083f0 | ||
|
|
6a0000dc4b | ||
|
|
1dd2f38066 | ||
|
|
004e1e3ca5 | ||
|
|
7c560d709b | ||
|
|
d3743ad62f | ||
|
|
ac234b77e2 | ||
|
|
9886987e68 | ||
|
|
d34cb8898f | ||
|
|
13aadbda64 | ||
|
|
c7c7c8eb01 | ||
|
|
b1e5bba33f | ||
|
|
474e7c6d62 | ||
|
|
794ec921b8 | ||
|
|
b674240362 | ||
|
|
a768c7c451 | ||
|
|
28d2a4ec2c | ||
|
|
9f1210d18f | ||
|
|
3012559627 | ||
|
|
b3ed57aee7 | ||
|
|
89d0a8107d | ||
|
|
6c0b71bd1b | ||
|
|
61abf74b2d | ||
|
|
21fdf02921 | ||
|
|
7a245d80ee | ||
|
|
da85922f23 | ||
|
|
a5356b6319 | ||
|
|
3828891b9b | ||
|
|
15d866ce04 | ||
|
|
560eb3d620 | ||
|
|
ac894254cc | ||
|
|
17e3fbde25 | ||
|
|
ee11a8410c | ||
|
|
ff5c51cfd9 | ||
|
|
b3943ae5e3 | ||
|
|
a32952fde6 | ||
|
|
9c4ee4014d | ||
|
|
dc9069f1f4 | ||
|
|
e402cacc05 | ||
|
|
a98cd248d6 | ||
|
|
00fbfb6a01 | ||
|
|
86c31c3766 | ||
|
|
698cfe910c | ||
|
|
16db23c159 | ||
|
|
b05a5ee1c6 | ||
|
|
8cb298937f | ||
|
|
68fe20ddf6 | ||
|
|
fab167bb34 | ||
|
|
f640d4b5f5 | ||
|
|
074562b141 | ||
|
|
fd030a5fd4 | ||
|
|
82fa6b13c6 | ||
|
|
bf16298c40 | ||
|
|
bcebb0a2b5 | ||
|
|
b27442cf74 | ||
|
|
92fbbd4812 | ||
|
|
321ed810e3 | ||
|
|
17ff530683 | ||
|
|
a416d03614 | ||
|
|
4de9a274dd | ||
|
|
0b8f3c9d9d |
19
.github/workflows/deploy.yml
vendored
19
.github/workflows/deploy.yml
vendored
@@ -14,8 +14,8 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.5.1
|
||||
QIF_VERSION: 4.6
|
||||
QT_VERSION: 6.6.2
|
||||
QIF_VERSION: 4.7
|
||||
|
||||
steps:
|
||||
- name: 'Install Qt'
|
||||
@@ -72,8 +72,8 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.5.1
|
||||
QIF_VERSION: 4.6
|
||||
QT_VERSION: 6.6.2
|
||||
QIF_VERSION: 4.7
|
||||
BUILD_ARCH: 64
|
||||
|
||||
steps:
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
runs-on: macos-13
|
||||
|
||||
env:
|
||||
QT_VERSION: 6.5.2
|
||||
QT_VERSION: 6.6.2
|
||||
CC: cc
|
||||
CXX: c++
|
||||
|
||||
@@ -245,10 +245,15 @@ jobs:
|
||||
modules: 'qtremoteobjects qt5compat qtshadertools'
|
||||
dir: ${{ runner.temp }}
|
||||
setup-python: 'true'
|
||||
tools: 'tools_ifw'
|
||||
set-env: 'true'
|
||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install Qt Installer Framework ${{ env.QIF_VERSION }}'
|
||||
run: |
|
||||
mkdir -pv ${{ runner.temp }}/Qt/Tools/QtInstallerFramework
|
||||
wget https://qt.amzsvc.com/tools/ifw/${{ env.QIF_VERSION }}.zip
|
||||
unzip ${{ env.QIF_VERSION }}.zip -d ${{ runner.temp }}/Qt/Tools/QtInstallerFramework/
|
||||
|
||||
- name: 'Get sources'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -286,7 +291,7 @@ jobs:
|
||||
|
||||
env:
|
||||
ANDROID_BUILD_PLATFORM: android-34
|
||||
QT_VERSION: 6.6.1
|
||||
QT_VERSION: 6.6.2
|
||||
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
|
||||
|
||||
steps:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -131,3 +131,6 @@ client/3rd/ShadowSocks/ss_ios.xcconfig
|
||||
|
||||
# UML generated pics
|
||||
out/
|
||||
|
||||
# CMake files
|
||||
CMakeFiles/
|
||||
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
|
||||
project(${PROJECT} VERSION 4.3.0.0
|
||||
project(${PROJECT} VERSION 4.4.1.2
|
||||
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 44)
|
||||
set(APP_ANDROID_VERSION_CODE 47)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
|
||||
74
README.md
74
README.md
@@ -7,13 +7,15 @@
|
||||
Amnezia is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server.
|
||||
|
||||
## Features
|
||||
- Very easy to use - enter your ip address, ssh login and password, and Amnezia will automatically install VPN docker containers to your server and connect to VPN.
|
||||
- OpenVPN, ShadowSocks, WireGuard, IKEv2 protocols support.
|
||||
|
||||
- Very easy to use - enter your IP address, SSH login, and password, and Amnezia will automatically install VPN docker containers to your server and connect to the VPN.
|
||||
- OpenVPN, ShadowSocks, WireGuard, and IKEv2 protocols support.
|
||||
- Masking VPN with OpenVPN over Cloak plugin
|
||||
- Split tunneling support - add any sites to client to enable VPN only for them (only for desktops)
|
||||
- Split tunneling support - add any sites to the client to enable VPN only for them (only for desktops)
|
||||
- Windows, MacOS, Linux, Android, iOS releases.
|
||||
|
||||
## Links
|
||||
|
||||
[https://amnezia.org](https://amnezia.org) - project website
|
||||
[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)
|
||||
@@ -21,13 +23,13 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep
|
||||
|
||||
## Tech
|
||||
|
||||
AmneziaVPN uses a number of open source projects to work:
|
||||
AmneziaVPN uses several open-source projects to work:
|
||||
|
||||
- [OpenSSL](https://www.openssl.org/)
|
||||
- [OpenVPN](https://openvpn.net/)
|
||||
- [ShadowSocks](https://shadowsocks.org/)
|
||||
- [Qt](https://www.qt.io/)
|
||||
- [LibSsh](https://libssh.org) - forked form Qt Creator
|
||||
- [LibSsh](https://libssh.org) - forked from Qt Creator
|
||||
- and more...
|
||||
|
||||
## Checking out the source code
|
||||
@@ -43,14 +45,15 @@ git submodule update --init --recursive
|
||||
Want to contribute? Welcome!
|
||||
|
||||
### Building sources and deployment
|
||||
Look deploy folder for build scripts.
|
||||
|
||||
### How to build iOS app from source code on MacOS
|
||||
Check deploy folder for build scripts.
|
||||
|
||||
### How to build an iOS app from source code on MacOS
|
||||
|
||||
1. First, make sure you have [XCode](https://developer.apple.com/xcode/) installed, at least version 14 or higher.
|
||||
|
||||
2. We use QT to generate the XCode project. we need QT version 6.6.1. Install QT for macos in [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules:
|
||||
- macOS
|
||||
2. We use QT to generate the XCode project. We need QT version 6.6.1. Install QT for MacOS [here](https://doc.qt.io/qt-6/macos.html) or [QT Online Installer](https://www.qt.io/download-open-source). Required modules:
|
||||
- MacOS
|
||||
- iOS
|
||||
- Qt 5 Compatibility Module
|
||||
- Qt Shader Tools
|
||||
@@ -59,18 +62,18 @@ Look deploy folder for build scripts.
|
||||
- Qt Multimedia
|
||||
- Qt Remote Objects
|
||||
|
||||
3. Install cmake is require. We recommend cmake version 3.25. You can install cmake in [here](https://cmake.org/download/)
|
||||
3. Install CMake if required. We recommend CMake version 3.25. You can install CMake [here](https://cmake.org/download/)
|
||||
|
||||
4. You also need to install go >= v1.16. If you don't have it done already,
|
||||
4. You also need to install go >= v1.16. If you don't have it installed already,
|
||||
download go from the [official website](https://golang.org/dl/) or use Homebrew.
|
||||
Latest version is recommended. Install gomobile
|
||||
The latest version is recommended. Install gomobile
|
||||
```bash
|
||||
export PATH=$PATH:~/go/bin
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
gomobile init
|
||||
```
|
||||
|
||||
5. Build project
|
||||
5. Build the project
|
||||
```bash
|
||||
export QT_BIN_DIR="<PATH-TO-QT-FOLDER>/Qt/<QT-VERSION>/ios/bin"
|
||||
export QT_MACOS_ROOT_DIR="<PATH-TO-QT-FOLDER>/Qt/<QT-VERSION>/macos"
|
||||
@@ -88,62 +91,63 @@ of the bin folder where gomobile was installed. Usually, it's in `GOPATH`.
|
||||
export PATH=$(PATH):/path/to/GOPATH/bin
|
||||
```
|
||||
|
||||
5. Open XCode project. You can then run/test/archive/ship the app.
|
||||
6. Open the XCode project. You can then run /test/archive/ship the app.
|
||||
|
||||
If build fails with the following error
|
||||
If the build fails with the following error
|
||||
```
|
||||
make: ***
|
||||
[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared]
|
||||
Error 1
|
||||
```
|
||||
Add a user defined variable to both AmneziaVPN and WireGuardNetworkExtension targets' build settings with
|
||||
Add a user-defined variable to both AmneziaVPN and WireGuardNetworkExtension targets' build settings with
|
||||
key `PATH` and value `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`.
|
||||
|
||||
if above error still persists on you M1 Mac, then most probably you need to install arch based cmake
|
||||
if the above error persists on your M1 Mac, then most probably you need to install arch based CMake
|
||||
```
|
||||
arch -arm64 brew install cmake
|
||||
```
|
||||
|
||||
Build might fail with "source files not found" error the first time you try it, because modern XCode build system compiles
|
||||
dependencies in parallel, and some dependencies end up being built after the ones that
|
||||
require them. In this case simply restart the build.
|
||||
Build might fail with the "source files not found" error the first time you try it, because the modern XCode build system compiles dependencies in parallel, and some dependencies end up being built after the ones that
|
||||
require them. In this case, simply restart the build.
|
||||
|
||||
## How to build the Android app
|
||||
_tested on Mac OS_
|
||||
|
||||
_Tested on Mac OS_
|
||||
|
||||
The Android app has the following requirements:
|
||||
* JDK 11
|
||||
* Android platform SDK 33
|
||||
* cmake 3.25.0
|
||||
* CMake 3.25.0
|
||||
|
||||
After you have installed QT, QT Creator and Android Studio installed, you need to configure QT Creator correctly. Click in the top menu bar on `QT Creator` -> `Preferences` -> `Devices` and select the tab `Android`.
|
||||
* set path to jdk 11
|
||||
After you have installed QT, QT Creator, and Android Studio, you need to configure QT Creator correctly. Click in the top menu bar on `QT Creator` -> `Preferences` -> `Devices` and select the tab `Android`.
|
||||
* set path to JDK 11
|
||||
* set path to Android SDK ($ANDROID_HOME)
|
||||
|
||||
In case you get errors regarding missing SDK or 'sdkmanager not running', you cannot fix them by correcting the paths and you have some spare GBs on your disk, you can let QT Creator install all requirements by choosing an empty folder for `Android SDK location` and click on `Set Up SDK`. Be aware: This will install a second Android SDK and NDK on your machine!
|
||||
In case you get errors regarding missing SDK or 'SDK manager not running', you cannot fix them by correcting the paths. If you have some spare GBs on your disk, you can let QT Creator install all requirements by choosing an empty folder for `Android SDK location` and clicking on `Set Up SDK`. Be aware: This will install a second Android SDK and NDK on your machine!
|
||||
Double-check that the right CMake version is configured: Click on `QT Creator` -> `Preferences` and click on the side menu on `Kits`. Under the center content view's `Kits` tab, you'll find an entry for `CMake Tool`. If the default selected CMake version is lower than 3.25.0, install on your system CMake >= 3.25.0 and choose `System CMake at <path>` from the drop-down list. If this entry is missing, you either have not installed CMake yet or QT Creator hasn't found the path to it. In that case, click in the preferences window on the side menu item `CMake`, then on the tab `Tools` in the center content view, and finally on the button `Add` to set the path to your installed CMake.
|
||||
Please make sure that you have selected Android Platform SDK 33 for your project: click in the main view's side menu on `Projects`, and on the left, you'll see a section `Build & Run` showing different Android build targets. You can select any of them, Amnezia VPN's project setup is designed in a way that all Android targets will be built. Click on the targets submenu item `Build` and scroll in the center content view to `Build Steps`. Click on `Details` at the end of the headline `Build Android APK` (the `Details` button might be hidden in case the QT Creator Window is not running in full screen!). Here we are: Choose `android-33` as `Android Build Platform SDK`.
|
||||
|
||||
Double check that the right cmake version is configured: Click on `QT Creator` -> `Preferences` and click on the side menu on `Kits`. Under the center content view's `Kits` tab you'll find an entry `CMake Tool`. If the default selected CMake version is lower than 3.25.0, install on your system CMake >= 3.25.0 and choose `System CMake at <path>` from the drop down list. If this entry is missing, you either have not installed CMake yet or QT Creator hasn't found the path to it. In that case click in the preferences window on the side menu item `CMake`, then on the tab `Tools`in the center content view and finally on the Button `Add` to set the path to your installed CMake.
|
||||
|
||||
Please make sure that you have selected Android Platform SDK 33 for your project: click in the main view's side menu on on `Projects`, on the left you'll see a section `Build & Run` showing different Android build targets. You can select any of them, Amnezia VPN's project setup is designed in a way that always all Android targets will be build. Click on the targets submenu item `Build` and scroll in the center content view to `Build Steps`. Click on `Details` at the end of the headline `Build Android APK` (The `Details` button might be hidden in case QT Creator Window is not running in full screen!). Here we are: choose `android-33` as `Android Build platform SDK`.
|
||||
|
||||
That's it you should be ready to compile the project from QT Creator!
|
||||
That's it! You should be ready to compile the project from QT Creator!
|
||||
|
||||
### Development flow
|
||||
After you've hit the build button, QT-Creator copies the whole project to a folder in the repositories parent directory. The folder should look something like `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>`.
|
||||
If you want to develop Amnezia VPNs Android components written in Kotlin, such as components using system APIs, you need to import the generated project in Android Studio with `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>/client/android-build` as the projects root directory. While you should be able to compile the generated project from Android Studio, you cannot work directly in the repository's Android project. So whenever you are confident with your work in the generated project, you'll need to copy and paste the affected files to the corresponding path in the repositories Android project so that you can add and commit your changes!
|
||||
|
||||
You may face compiling issues in QT Creator after you've worked in Android Studio on the generated project. Just do a `./gradlew clean` in the generated project's root directory (`<path>/client/android-build/.`) and you should be good to continue.
|
||||
After you've hit the build button, QT-Creator copies the whole project to a folder in the repository parent directory. The folder should look something like `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>`.
|
||||
If you want to develop Amnezia VPNs Android components written in Kotlin, such as components using system APIs, you need to import the generated project in Android Studio with `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>/client/android-build` as the projects root directory. While you should be able to compile the generated project from Android Studio, you cannot work directly in the repository's Android project. So whenever you are confident with your work in the generated project, you'll need to copy and paste the affected files to the corresponding path in the repository's Android project so that you can add and commit your changes!
|
||||
|
||||
You may face compiling issues in QT Creator after you've worked in Android Studio on the generated project. Just do a `./gradlew clean` in the generated project's root directory (`<path>/client/android-build/.`) and you should be good to go.
|
||||
|
||||
## License
|
||||
GPL v.3
|
||||
|
||||
GPL v3.0
|
||||
|
||||
## Donate
|
||||
|
||||
Bitcoin: bc1qn9rhsffuxwnhcuuu4qzrwp4upkrq94xnh8r26u
|
||||
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
|
||||
payeer.com: P2561305
|
||||
ko-fi.com: [https://ko-fi.com/amnezia_vpn](https://ko-fi.com/amnezia_vpn)
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
## etc
|
||||
This project is tested with BrowserStack.
|
||||
We express our gratitude to [BrowserStack](https://www.browserstack.com) for supporting our project.
|
||||
|
||||
2
client/3rd/OpenVPNAdapter
vendored
2
client/3rd/OpenVPNAdapter
vendored
Submodule client/3rd/OpenVPNAdapter updated: f95f0b2b56...6f71d0743d
2
client/3rd/amneziawg-apple
vendored
2
client/3rd/amneziawg-apple
vendored
Submodule client/3rd/amneziawg-apple updated: f23eee4700...0829e99ea9
@@ -15,6 +15,15 @@ set(PACKAGES
|
||||
Core5Compat Concurrent LinguistTools
|
||||
)
|
||||
|
||||
execute_process(
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||
COMMAND git rev-parse --short HEAD
|
||||
OUTPUT_VARIABLE GIT_COMMIT_HASH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
|
||||
add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}")
|
||||
|
||||
if(IOS)
|
||||
set(PACKAGES ${PACKAGES} Multimedia)
|
||||
endif()
|
||||
@@ -57,6 +66,8 @@ set(AMNEZIAVPN_TS_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_ru.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_zh_CN.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_fa_IR.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_ar.ts
|
||||
${CMAKE_CURRENT_LIST_DIR}/translations/amneziavpn_my_MM.ts
|
||||
)
|
||||
|
||||
file(GLOB_RECURSE AMNEZIAVPN_TS_SOURCES *.qrc *.cpp *.h *.ui)
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#if defined(Q_OS_IOS)
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#include <AmneziaVPN-Swift.h>
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
@@ -95,7 +96,18 @@ void AmneziaApplication::init()
|
||||
qFatal("Android logging initialization failed");
|
||||
}
|
||||
AndroidController::instance()->setSaveLogs(m_settings->isSaveLogs());
|
||||
connect(m_settings.get(), &Settings::saveLogsChanged, AndroidController::instance(), &AndroidController::setSaveLogs);
|
||||
connect(m_settings.get(), &Settings::saveLogsChanged,
|
||||
AndroidController::instance(), &AndroidController::setSaveLogs);
|
||||
|
||||
AndroidController::instance()->setScreenshotsEnabled(m_settings->isScreenshotsEnabled());
|
||||
connect(m_settings.get(), &Settings::screenshotsEnabledChanged,
|
||||
AndroidController::instance(), &AndroidController::setScreenshotsEnabled);
|
||||
|
||||
connect(m_settings.get(), &Settings::serverRemoved,
|
||||
AndroidController::instance(), &AndroidController::resetLastServer);
|
||||
|
||||
connect(m_settings.get(), &Settings::settingsCleared,
|
||||
[](){ AndroidController::instance()->resetLastServer(-1); });
|
||||
|
||||
connect(AndroidController::instance(), &AndroidController::initConnectionState, this,
|
||||
[this](Vpn::ConnectionState state) {
|
||||
@@ -127,6 +139,14 @@ void AmneziaApplication::init()
|
||||
m_pageController->goToPageSettingsBackup();
|
||||
m_settingsController->importBackupFromOutside(filePath);
|
||||
});
|
||||
|
||||
QTimer::singleShot(0, this, [this](){
|
||||
AmneziaVPN::toggleScreenshots(m_settings->isScreenshotsEnabled());
|
||||
});
|
||||
|
||||
connect(m_settings.get(), &Settings::screenshotsEnabledChanged, [](bool enabled) {
|
||||
AmneziaVPN::toggleScreenshots(enabled);
|
||||
});
|
||||
#endif
|
||||
|
||||
m_notificationHandler.reset(NotificationHandler::create(nullptr));
|
||||
@@ -286,13 +306,16 @@ void AmneziaApplication::initModels()
|
||||
m_containersModel.reset(new ContainersModel(this));
|
||||
m_engine->rootContext()->setContextProperty("ContainersModel", m_containersModel.get());
|
||||
|
||||
m_defaultServerContainersModel.reset(new ContainersModel(this));
|
||||
m_engine->rootContext()->setContextProperty("DefaultServerContainersModel", m_defaultServerContainersModel.get());
|
||||
|
||||
m_serversModel.reset(new ServersModel(m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("ServersModel", m_serversModel.get());
|
||||
connect(m_serversModel.get(), &ServersModel::containersUpdated, m_containersModel.get(),
|
||||
&ContainersModel::updateModel);
|
||||
connect(m_serversModel.get(), &ServersModel::defaultContainerChanged, m_containersModel.get(),
|
||||
&ContainersModel::setDefaultContainer);
|
||||
m_containersModel->setDefaultContainer(m_serversModel->getDefaultContainer()); // make better?
|
||||
connect(m_serversModel.get(), &ServersModel::defaultServerContainersUpdated, m_defaultServerContainersModel.get(),
|
||||
&ContainersModel::updateModel);
|
||||
m_serversModel->resetModel();
|
||||
|
||||
m_languageModel.reset(new LanguageModel(m_settings, this));
|
||||
m_engine->rootContext()->setContextProperty("LanguageModel", m_languageModel.get());
|
||||
@@ -336,7 +359,7 @@ void AmneziaApplication::initModels()
|
||||
connect(m_configurator.get(), &VpnConfigurator::newVpnConfigCreated, this,
|
||||
[this](const QString &clientId, const QString &clientName, const DockerContainer container,
|
||||
ServerCredentials credentials) {
|
||||
m_serversModel->reloadContainerConfig();
|
||||
m_serversModel->reloadDefaultServerContainerConfig();
|
||||
m_clientManagementModel->appendClient(clientId, clientName, container, credentials);
|
||||
emit m_configurator->clientModelUpdated();
|
||||
});
|
||||
@@ -388,7 +411,13 @@ void AmneziaApplication::initControllers()
|
||||
m_engine->rootContext()->setContextProperty("ApiController", m_apiController.get());
|
||||
connect(m_apiController.get(), &ApiController::updateStarted, this,
|
||||
[this]() { emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Connecting); });
|
||||
connect(m_apiController.get(), &ApiController::errorOccurred, this,
|
||||
[this]() { emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected); });
|
||||
connect(m_apiController.get(), &ApiController::updateFinished, m_connectionController.get(), &ConnectionController::toggleConnection);
|
||||
connect(m_apiController.get(), &ApiController::errorOccurred, this, [this](const QString &errorMessage) {
|
||||
if (m_connectionController->isConnectionInProgress()) {
|
||||
emit m_pageController->showErrorMessage(errorMessage);
|
||||
}
|
||||
|
||||
emit m_vpnConnection->connectionStateChanged(Vpn::ConnectionState::Disconnected);
|
||||
});
|
||||
connect(m_apiController.get(), &ApiController::updateFinished, m_connectionController.get(),
|
||||
&ConnectionController::toggleConnection);
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ private:
|
||||
QCommandLineParser m_parser;
|
||||
|
||||
QSharedPointer<ContainersModel> m_containersModel;
|
||||
QSharedPointer<ContainersModel> m_defaultServerContainersModel;
|
||||
QSharedPointer<ServersModel> m_serversModel;
|
||||
QSharedPointer<LanguageModel> m_languageModel;
|
||||
QSharedPointer<ProtocolsModel> m_protocolsModel;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<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" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- Enable when VPN-per-app mode will be implemented -->
|
||||
@@ -56,6 +56,10 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.lib_name"
|
||||
android:value="-- %%INSERT_APP_LIB_NAME%% --" />
|
||||
@@ -137,14 +141,29 @@
|
||||
android:name=".AmneziaVpnService"
|
||||
android:process=":amneziaVpnService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:exported="false">
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:exported="false"
|
||||
tools:ignore="ForegroundServicePermission">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="vpn" />
|
||||
<service
|
||||
android:name=".AmneziaTileService"
|
||||
android:process=":amneziaTileService"
|
||||
android:icon="@drawable/ic_amnezia_round"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<provider
|
||||
|
||||
@@ -111,4 +111,5 @@ dependencies {
|
||||
implementation(libs.kotlinx.coroutines)
|
||||
implementation(libs.bundles.androidx.camera)
|
||||
implementation(libs.google.mlkit)
|
||||
implementation(libs.androidx.datastore)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ androidx-activity = "1.8.1"
|
||||
androidx-annotation = "1.7.0"
|
||||
androidx-camera = "1.3.0"
|
||||
androidx-security-crypto = "1.1.0-alpha06"
|
||||
androidx-datastore = "1.1.0-beta01"
|
||||
kotlinx-coroutines = "1.7.3"
|
||||
google-mlkit = "17.2.0"
|
||||
|
||||
@@ -18,6 +19,7 @@ androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.r
|
||||
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidx-camera" }
|
||||
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "androidx-camera" }
|
||||
androidx-security-crypto = { module = "androidx.security:security-crypto-ktx", version.ref = "androidx-security-crypto" }
|
||||
androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" }
|
||||
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
|
||||
google-mlkit = { module = "com.google.mlkit:barcode-scanning", version.ref = "google-mlkit" }
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ package org.amnezia.vpn.protocol
|
||||
|
||||
// keep synchronized with client/platforms/android/android_controller.h ConnectionState
|
||||
enum class ProtocolState {
|
||||
DISCONNECTED,
|
||||
CONNECTED,
|
||||
CONNECTING,
|
||||
DISCONNECTED,
|
||||
DISCONNECTING,
|
||||
RECONNECTING,
|
||||
UNKNOWN
|
||||
|
||||
@@ -28,6 +28,10 @@ fun Bundle.putStatus(status: Status) {
|
||||
putInt(STATE_KEY, status.state.ordinal)
|
||||
}
|
||||
|
||||
fun Bundle.putStatus(state: ProtocolState) {
|
||||
putInt(STATE_KEY, state.ordinal)
|
||||
}
|
||||
|
||||
fun Bundle.getStatus(): Status =
|
||||
Status.build {
|
||||
setState(ProtocolState.entries[getInt(STATE_KEY)])
|
||||
|
||||
5
client/android/res/values-ru/strings.xml
Normal file
5
client/android/res/values-ru/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="connecting">Подключение</string>
|
||||
<string name="disconnecting">Отключение</string>
|
||||
</resources>
|
||||
5
client/android/res/values/strings.xml
Normal file
5
client/android/res/values/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="connecting">Connecting</string>
|
||||
<string name="disconnecting">Disconnecting</string>
|
||||
</resources>
|
||||
@@ -14,6 +14,7 @@ import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.os.Message
|
||||
import android.os.Messenger
|
||||
import android.view.WindowManager.LayoutParams
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.MainThread
|
||||
@@ -26,9 +27,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.amnezia.vpn.protocol.ProtocolState
|
||||
import org.amnezia.vpn.protocol.getStatistics
|
||||
import org.amnezia.vpn.protocol.getStatus
|
||||
import org.amnezia.vpn.qt.QtAndroidController
|
||||
@@ -36,11 +35,11 @@ import org.amnezia.vpn.util.Log
|
||||
import org.qtproject.qt.android.bindings.QtActivity
|
||||
|
||||
private const val TAG = "AmneziaActivity"
|
||||
const val ACTIVITY_MESSENGER_NAME = "Activity"
|
||||
|
||||
private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1
|
||||
private const val CREATE_FILE_ACTION_CODE = 2
|
||||
private const val OPEN_FILE_ACTION_CODE = 3
|
||||
private const val BIND_SERVICE_TIMEOUT = 1000L
|
||||
|
||||
class AmneziaActivity : QtActivity() {
|
||||
|
||||
@@ -58,25 +57,17 @@ class AmneziaActivity : QtActivity() {
|
||||
val event = msg.extractIpcMessage<ServiceEvent>()
|
||||
Log.d(TAG, "Handle event: $event")
|
||||
when (event) {
|
||||
ServiceEvent.CONNECTED -> {
|
||||
QtAndroidController.onVpnConnected()
|
||||
}
|
||||
|
||||
ServiceEvent.DISCONNECTED -> {
|
||||
QtAndroidController.onVpnDisconnected()
|
||||
doUnbindService()
|
||||
}
|
||||
|
||||
ServiceEvent.RECONNECTING -> {
|
||||
QtAndroidController.onVpnReconnecting()
|
||||
ServiceEvent.STATUS_CHANGED -> {
|
||||
msg.data?.getStatus()?.let { (state) ->
|
||||
Log.d(TAG, "Handle protocol state: $state")
|
||||
QtAndroidController.onVpnStateChanged(state.ordinal)
|
||||
}
|
||||
}
|
||||
|
||||
ServiceEvent.STATUS -> {
|
||||
if (isWaitingStatus) {
|
||||
isWaitingStatus = false
|
||||
msg.data?.getStatus()?.let { (state) ->
|
||||
QtAndroidController.onStatus(state.ordinal)
|
||||
}
|
||||
msg.data?.getStatus()?.let { QtAndroidController.onStatus(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +78,7 @@ class AmneziaActivity : QtActivity() {
|
||||
}
|
||||
|
||||
ServiceEvent.ERROR -> {
|
||||
msg.data?.getString(ERROR_MSG)?.let { error ->
|
||||
msg.data?.getString(MSG_ERROR)?.let { error ->
|
||||
Log.e(TAG, "From VpnService: $error")
|
||||
}
|
||||
// todo: add error reporting to Qt
|
||||
@@ -109,14 +100,15 @@ class AmneziaActivity : QtActivity() {
|
||||
// get a messenger from the service to send actions to the service
|
||||
vpnServiceMessenger.set(Messenger(service))
|
||||
// send a messenger to the service to process service events
|
||||
vpnServiceMessenger.send {
|
||||
Action.REGISTER_CLIENT.packToMessage().apply {
|
||||
replyTo = activityMessenger
|
||||
}
|
||||
}
|
||||
vpnServiceMessenger.send(
|
||||
Action.REGISTER_CLIENT.packToMessage {
|
||||
putString(MSG_CLIENT_NAME, ACTIVITY_MESSENGER_NAME)
|
||||
},
|
||||
replyTo = activityMessenger
|
||||
)
|
||||
isServiceConnected = true
|
||||
if (isWaitingStatus) {
|
||||
vpnServiceMessenger.send(Action.REQUEST_STATUS)
|
||||
vpnServiceMessenger.send(Action.REQUEST_STATUS, replyTo = activityMessenger)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +118,7 @@ class AmneziaActivity : QtActivity() {
|
||||
vpnServiceMessenger.reset()
|
||||
isWaitingStatus = true
|
||||
QtAndroidController.onServiceDisconnected()
|
||||
doBindService()
|
||||
}
|
||||
|
||||
override fun onBindingDied(name: ComponentName?) {
|
||||
@@ -148,8 +141,11 @@ class AmneziaActivity : QtActivity() {
|
||||
Log.d(TAG, "Create Amnezia activity: $intent")
|
||||
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
vpnServiceMessenger = IpcMessenger(
|
||||
onDeadObjectException = ::doUnbindService,
|
||||
messengerName = "VpnService"
|
||||
"VpnService",
|
||||
onDeadObjectException = {
|
||||
doUnbindService()
|
||||
doBindService()
|
||||
}
|
||||
)
|
||||
intent?.let(::processIntent)
|
||||
}
|
||||
@@ -244,10 +240,9 @@ class AmneziaActivity : QtActivity() {
|
||||
private fun doBindService() {
|
||||
Log.d(TAG, "Bind service")
|
||||
Intent(this, AmneziaVpnService::class.java).also {
|
||||
bindService(it, serviceConnection, BIND_ABOVE_CLIENT)
|
||||
bindService(it, serviceConnection, BIND_ABOVE_CLIENT and BIND_AUTO_CREATE)
|
||||
}
|
||||
isInBoundState = true
|
||||
handleBindTimeout()
|
||||
}
|
||||
|
||||
@MainThread
|
||||
@@ -256,26 +251,14 @@ class AmneziaActivity : QtActivity() {
|
||||
Log.d(TAG, "Unbind service")
|
||||
isWaitingStatus = true
|
||||
QtAndroidController.onServiceDisconnected()
|
||||
vpnServiceMessenger.reset()
|
||||
isServiceConnected = false
|
||||
vpnServiceMessenger.send(Action.UNREGISTER_CLIENT, activityMessenger)
|
||||
vpnServiceMessenger.reset()
|
||||
isInBoundState = false
|
||||
unbindService(serviceConnection)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBindTimeout() {
|
||||
mainScope.launch {
|
||||
if (isWaitingStatus) {
|
||||
delay(BIND_SERVICE_TIMEOUT)
|
||||
if (isWaitingStatus && !isServiceConnected) {
|
||||
Log.d(TAG, "Bind timeout, reset connection status")
|
||||
isWaitingStatus = false
|
||||
QtAndroidController.onStatus(ProtocolState.DISCONNECTED.ordinal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods of starting and stopping VpnService
|
||||
*/
|
||||
@@ -312,7 +295,7 @@ class AmneziaActivity : QtActivity() {
|
||||
Log.d(TAG, "Connect to VPN")
|
||||
vpnServiceMessenger.send {
|
||||
Action.CONNECT.packToMessage {
|
||||
putString(VPN_CONFIG, vpnConfig)
|
||||
putString(MSG_VPN_CONFIG, vpnConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -320,7 +303,7 @@ class AmneziaActivity : QtActivity() {
|
||||
private fun startVpnService(vpnConfig: String) {
|
||||
Log.d(TAG, "Start VPN service")
|
||||
Intent(this, AmneziaVpnService::class.java).apply {
|
||||
putExtra(VPN_CONFIG, vpnConfig)
|
||||
putExtra(MSG_VPN_CONFIG, vpnConfig)
|
||||
}.also {
|
||||
ContextCompat.startForegroundService(this, it)
|
||||
}
|
||||
@@ -369,6 +352,22 @@ class AmneziaActivity : QtActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun resetLastServer(index: Int) {
|
||||
Log.v(TAG, "Reset server: $index")
|
||||
mainScope.launch {
|
||||
VpnStateStore.store {
|
||||
if (index == -1 || it.serverIndex == index) {
|
||||
VpnState.defaultState
|
||||
} else if (it.serverIndex > index) {
|
||||
it.copy(serverIndex = it.serverIndex - 1)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun saveFile(fileName: String, data: String) {
|
||||
Log.d(TAG, "Save file $fileName")
|
||||
@@ -438,7 +437,7 @@ class AmneziaActivity : QtActivity() {
|
||||
Log.saveLogs = enabled
|
||||
vpnServiceMessenger.send {
|
||||
Action.SET_SAVE_LOGS.packToMessage {
|
||||
putBoolean(SAVE_LOGS, enabled)
|
||||
putBoolean(MSG_SAVE_LOGS, enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -455,4 +454,13 @@ class AmneziaActivity : QtActivity() {
|
||||
Log.v(TAG, "Clear logs")
|
||||
Log.clearLogs()
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun setScreenshotsEnabled(enabled: Boolean) {
|
||||
Log.v(TAG, "Set screenshots enabled: $enabled")
|
||||
mainScope.launch {
|
||||
val flag = if (enabled) 0 else LayoutParams.FLAG_SECURE
|
||||
window.setFlags(flag, LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ class AmneziaApplication : QtApplication(), CameraXConfig.Provider {
|
||||
super.onCreate()
|
||||
Prefs.init(this)
|
||||
Log.init(this)
|
||||
VpnStateStore.init(this)
|
||||
Log.d(TAG, "Create Amnezia application")
|
||||
createNotificationChannel()
|
||||
}
|
||||
|
||||
272
client/android/src/org/amnezia/vpn/AmneziaTileService.kt
Normal file
272
client/android/src/org/amnezia/vpn/AmneziaTileService.kt
Normal file
@@ -0,0 +1,272 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.Messenger
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.amnezia.vpn.protocol.ProtocolState
|
||||
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
|
||||
import org.amnezia.vpn.protocol.ProtocolState.CONNECTING
|
||||
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
|
||||
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTING
|
||||
import org.amnezia.vpn.protocol.ProtocolState.RECONNECTING
|
||||
import org.amnezia.vpn.protocol.ProtocolState.UNKNOWN
|
||||
import org.amnezia.vpn.util.Log
|
||||
|
||||
private const val TAG = "AmneziaTileService"
|
||||
private const val DEFAULT_TILE_LABEL = "AmneziaVPN"
|
||||
|
||||
class AmneziaTileService : TileService() {
|
||||
|
||||
private lateinit var scope: CoroutineScope
|
||||
private var vpnStateListeningJob: Job? = null
|
||||
private lateinit var vpnServiceMessenger: IpcMessenger
|
||||
|
||||
@Volatile
|
||||
private var isServiceConnected = false
|
||||
private var isInBoundState = false
|
||||
@Volatile
|
||||
private var isVpnConfigExists = false
|
||||
|
||||
private val serviceConnection: ServiceConnection by lazy(NONE) {
|
||||
object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
Log.d(TAG, "Service ${name?.flattenToString()} was connected")
|
||||
vpnServiceMessenger.set(Messenger(service))
|
||||
isServiceConnected = true
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
Log.w(TAG, "Service ${name?.flattenToString()} was unexpectedly disconnected")
|
||||
isServiceConnected = false
|
||||
vpnServiceMessenger.reset()
|
||||
updateVpnState(DISCONNECTED)
|
||||
}
|
||||
|
||||
override fun onBindingDied(name: ComponentName?) {
|
||||
Log.w(TAG, "Binding to the ${name?.flattenToString()} unexpectedly died")
|
||||
doUnbindService()
|
||||
doBindService()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "Create Amnezia Tile Service")
|
||||
scope = CoroutineScope(SupervisorJob())
|
||||
vpnServiceMessenger = IpcMessenger(
|
||||
"VpnService",
|
||||
onDeadObjectException = ::doUnbindService
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(TAG, "Destroy Amnezia Tile Service")
|
||||
doUnbindService()
|
||||
scope.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
// Workaround for some bugs
|
||||
override fun onBind(intent: Intent?): IBinder? =
|
||||
try {
|
||||
super.onBind(intent)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Failed to bind AmneziaTileService: $e")
|
||||
null
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
Log.d(TAG, "Start listening")
|
||||
if (AmneziaVpnService.isRunning(applicationContext)) {
|
||||
Log.d(TAG, "Vpn service is running")
|
||||
doBindService()
|
||||
} else {
|
||||
Log.d(TAG, "Vpn service is not running")
|
||||
isServiceConnected = false
|
||||
updateVpnState(DISCONNECTED)
|
||||
}
|
||||
vpnStateListeningJob = launchVpnStateListening()
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
Log.d(TAG, "Stop listening")
|
||||
vpnStateListeningJob?.cancel()
|
||||
vpnStateListeningJob = null
|
||||
doUnbindService()
|
||||
super.onStopListening()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
Log.d(TAG, "onClick")
|
||||
if (isLocked) {
|
||||
unlockAndRun { onClickInternal() }
|
||||
} else {
|
||||
onClickInternal()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onClickInternal() {
|
||||
if (isVpnConfigExists) {
|
||||
Log.d(TAG, "Change VPN state")
|
||||
if (qsTile.state == Tile.STATE_INACTIVE) {
|
||||
Log.d(TAG, "Start VPN")
|
||||
updateVpnState(CONNECTING)
|
||||
startVpn()
|
||||
} else if (qsTile.state == Tile.STATE_ACTIVE) {
|
||||
Log.d(TAG, "Stop vpn")
|
||||
updateVpnState(DISCONNECTING)
|
||||
stopVpn()
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Start Activity")
|
||||
Intent(this, AmneziaActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}.also {
|
||||
startActivityAndCollapseCompat(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doBindService() {
|
||||
Log.d(TAG, "Bind service")
|
||||
Intent(this, AmneziaVpnService::class.java).also {
|
||||
bindService(it, serviceConnection, BIND_ABOVE_CLIENT)
|
||||
}
|
||||
isInBoundState = true
|
||||
}
|
||||
|
||||
private fun doUnbindService() {
|
||||
if (isInBoundState) {
|
||||
Log.d(TAG, "Unbind service")
|
||||
isServiceConnected = false
|
||||
vpnServiceMessenger.reset()
|
||||
isInBoundState = false
|
||||
unbindService(serviceConnection)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startVpn() {
|
||||
if (isServiceConnected) {
|
||||
connectToVpn()
|
||||
} else {
|
||||
if (checkPermission()) {
|
||||
startVpnService()
|
||||
doBindService()
|
||||
} else {
|
||||
updateVpnState(DISCONNECTED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkPermission() =
|
||||
if (VpnService.prepare(applicationContext) != null) {
|
||||
Intent(this, VpnRequestActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}.also {
|
||||
startActivityAndCollapseCompat(it)
|
||||
}
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
private fun startVpnService() =
|
||||
ContextCompat.startForegroundService(
|
||||
applicationContext,
|
||||
Intent(this, AmneziaVpnService::class.java)
|
||||
)
|
||||
|
||||
private fun connectToVpn() = vpnServiceMessenger.send(Action.CONNECT)
|
||||
|
||||
private fun stopVpn() = vpnServiceMessenger.send(Action.DISCONNECT)
|
||||
|
||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||
private fun startActivityAndCollapseCompat(intent: Intent) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startActivityAndCollapse(
|
||||
PendingIntent.getActivity(
|
||||
applicationContext,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
startActivityAndCollapse(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateVpnState(state: ProtocolState) {
|
||||
scope.launch {
|
||||
VpnStateStore.store { it.copy(protocolState = state) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchVpnStateListening() =
|
||||
scope.launch { VpnStateStore.dataFlow().collectLatest(::updateTile) }
|
||||
|
||||
private fun updateTile(vpnState: VpnState) {
|
||||
Log.d(TAG, "Update tile: $vpnState")
|
||||
isVpnConfigExists = vpnState.serverName != null
|
||||
val tile = qsTile ?: return
|
||||
tile.apply {
|
||||
label = vpnState.serverName ?: DEFAULT_TILE_LABEL
|
||||
when (vpnState.protocolState) {
|
||||
CONNECTED -> {
|
||||
state = Tile.STATE_ACTIVE
|
||||
subtitleCompat = null
|
||||
}
|
||||
|
||||
DISCONNECTED, UNKNOWN -> {
|
||||
state = Tile.STATE_INACTIVE
|
||||
subtitleCompat = null
|
||||
}
|
||||
|
||||
CONNECTING, RECONNECTING -> {
|
||||
state = Tile.STATE_UNAVAILABLE
|
||||
subtitleCompat = resources.getString(R.string.connecting)
|
||||
}
|
||||
|
||||
DISCONNECTING -> {
|
||||
state = Tile.STATE_UNAVAILABLE
|
||||
subtitleCompat = resources.getString(R.string.disconnecting)
|
||||
}
|
||||
}
|
||||
updateTile()
|
||||
}
|
||||
// double update to fix weird visual glitches
|
||||
tile.updateTile()
|
||||
}
|
||||
|
||||
private var Tile.subtitleCompat: CharSequence?
|
||||
set(value) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
this.subtitle = value
|
||||
}
|
||||
}
|
||||
get() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
return this.subtitle
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
@@ -16,6 +19,7 @@ import android.os.Process
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -26,6 +30,7 @@ import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -39,14 +44,11 @@ import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
|
||||
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTING
|
||||
import org.amnezia.vpn.protocol.ProtocolState.RECONNECTING
|
||||
import org.amnezia.vpn.protocol.ProtocolState.UNKNOWN
|
||||
import org.amnezia.vpn.protocol.Statistics
|
||||
import org.amnezia.vpn.protocol.Status
|
||||
import org.amnezia.vpn.protocol.VpnException
|
||||
import org.amnezia.vpn.protocol.VpnStartException
|
||||
import org.amnezia.vpn.protocol.awg.Awg
|
||||
import org.amnezia.vpn.protocol.cloak.Cloak
|
||||
import org.amnezia.vpn.protocol.openvpn.OpenVpn
|
||||
import org.amnezia.vpn.protocol.putStatistics
|
||||
import org.amnezia.vpn.protocol.putStatus
|
||||
import org.amnezia.vpn.protocol.wireguard.Wireguard
|
||||
import org.amnezia.vpn.util.Log
|
||||
@@ -57,12 +59,16 @@ import org.json.JSONObject
|
||||
|
||||
private const val TAG = "AmneziaVpnService"
|
||||
|
||||
const val VPN_CONFIG = "VPN_CONFIG"
|
||||
const val ERROR_MSG = "ERROR_MSG"
|
||||
const val SAVE_LOGS = "SAVE_LOGS"
|
||||
const val MSG_VPN_CONFIG = "VPN_CONFIG"
|
||||
const val MSG_ERROR = "ERROR"
|
||||
const val MSG_SAVE_LOGS = "SAVE_LOGS"
|
||||
const val MSG_CLIENT_NAME = "CLIENT_NAME"
|
||||
|
||||
const val AFTER_PERMISSION_CHECK = "AFTER_PERMISSION_CHECK"
|
||||
private const val PREFS_CONFIG_KEY = "LAST_CONF"
|
||||
private const val PREFS_SERVER_NAME = "LAST_SERVER_NAME"
|
||||
private const val PREFS_SERVER_INDEX = "LAST_SERVER_INDEX"
|
||||
private const val PROCESS_NAME = "org.amnezia.vpn:amneziaVpnService"
|
||||
private const val NOTIFICATION_ID = 1337
|
||||
private const val STATISTICS_SENDING_TIMEOUT = 1000L
|
||||
private const val DISCONNECT_TIMEOUT = 5000L
|
||||
@@ -76,6 +82,8 @@ class AmneziaVpnService : VpnService() {
|
||||
private var protocol: Protocol? = null
|
||||
private val protocolCache = mutableMapOf<String, Protocol>()
|
||||
private var protocolState = MutableStateFlow(UNKNOWN)
|
||||
private var serverName: String? = null
|
||||
private var serverIndex: Int = -1
|
||||
|
||||
private val isConnected
|
||||
get() = protocolState.value == CONNECTED
|
||||
@@ -89,8 +97,11 @@ class AmneziaVpnService : VpnService() {
|
||||
private var connectionJob: Job? = null
|
||||
private var disconnectionJob: Job? = null
|
||||
private var statisticsSendingJob: Job? = null
|
||||
private lateinit var clientMessenger: IpcMessenger
|
||||
private lateinit var networkState: NetworkState
|
||||
private val clientMessengers = ConcurrentHashMap<Messenger, IpcMessenger>()
|
||||
|
||||
private val isActivityConnected
|
||||
get() = clientMessengers.any { it.value.name == ACTIVITY_MESSENGER_NAME }
|
||||
|
||||
private val connectionExceptionHandler = CoroutineExceptionHandler { _, e ->
|
||||
protocolState.value = DISCONNECTED
|
||||
@@ -116,13 +127,22 @@ class AmneziaVpnService : VpnService() {
|
||||
Log.d(TAG, "Handle action: $action")
|
||||
when (action) {
|
||||
Action.REGISTER_CLIENT -> {
|
||||
clientMessenger.set(msg.replyTo)
|
||||
val clientName = msg.data.getString(MSG_CLIENT_NAME)
|
||||
val messenger = IpcMessenger(msg.replyTo, clientName)
|
||||
clientMessengers[msg.replyTo] = messenger
|
||||
Log.d(TAG, "Messenger client '$clientName' was registered")
|
||||
if (clientName == ACTIVITY_MESSENGER_NAME && isConnected) launchSendingStatistics()
|
||||
}
|
||||
|
||||
Action.UNREGISTER_CLIENT -> {
|
||||
clientMessengers.remove(msg.replyTo)?.let {
|
||||
Log.d(TAG, "Messenger client '${it.name}' was unregistered")
|
||||
if (it.name == ACTIVITY_MESSENGER_NAME) stopSendingStatistics()
|
||||
}
|
||||
}
|
||||
|
||||
Action.CONNECT -> {
|
||||
val vpnConfig = msg.data.getString(VPN_CONFIG)
|
||||
Prefs.save(PREFS_CONFIG_KEY, vpnConfig)
|
||||
connect(vpnConfig)
|
||||
connect(msg.data.getString(MSG_VPN_CONFIG))
|
||||
}
|
||||
|
||||
Action.DISCONNECT -> {
|
||||
@@ -130,17 +150,17 @@ class AmneziaVpnService : VpnService() {
|
||||
}
|
||||
|
||||
Action.REQUEST_STATUS -> {
|
||||
clientMessenger.send {
|
||||
ServiceEvent.STATUS.packToMessage {
|
||||
putStatus(Status.build {
|
||||
setState(this@AmneziaVpnService.protocolState.value)
|
||||
})
|
||||
clientMessengers[msg.replyTo]?.let { clientMessenger ->
|
||||
clientMessenger.send {
|
||||
ServiceEvent.STATUS.packToMessage {
|
||||
putStatus(this@AmneziaVpnService.protocolState.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Action.SET_SAVE_LOGS -> {
|
||||
Log.saveLogs = msg.data.getBoolean(SAVE_LOGS)
|
||||
Log.saveLogs = msg.data.getBoolean(MSG_SAVE_LOGS)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,7 +176,7 @@ class AmneziaVpnService : VpnService() {
|
||||
*/
|
||||
private val foregroundServiceTypeCompat
|
||||
get() = when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> FOREGROUND_SERVICE_TYPE_MANIFEST
|
||||
else -> 0
|
||||
}
|
||||
@@ -189,7 +209,7 @@ class AmneziaVpnService : VpnService() {
|
||||
Log.d(TAG, "Create Amnezia VPN service")
|
||||
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
connectionScope = CoroutineScope(SupervisorJob() + Dispatchers.IO + connectionExceptionHandler)
|
||||
clientMessenger = IpcMessenger(messengerName = "Client")
|
||||
loadServerData()
|
||||
launchProtocolStateHandler()
|
||||
networkState = NetworkState(this, ::reconnect)
|
||||
}
|
||||
@@ -201,15 +221,13 @@ class AmneziaVpnService : VpnService() {
|
||||
|
||||
if (isAlwaysOnCompat) {
|
||||
Log.d(TAG, "Start service via Always-on")
|
||||
connect(Prefs.load(PREFS_CONFIG_KEY))
|
||||
connect()
|
||||
} else if (intent?.getBooleanExtra(AFTER_PERMISSION_CHECK, false) == true) {
|
||||
Log.d(TAG, "Start service after permission check")
|
||||
connect(Prefs.load(PREFS_CONFIG_KEY))
|
||||
connect()
|
||||
} else {
|
||||
Log.d(TAG, "Start service")
|
||||
val vpnConfig = intent?.getStringExtra(VPN_CONFIG)
|
||||
Prefs.save(PREFS_CONFIG_KEY, vpnConfig)
|
||||
connect(vpnConfig)
|
||||
connect(intent?.getStringExtra(MSG_VPN_CONFIG))
|
||||
}
|
||||
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, foregroundServiceTypeCompat)
|
||||
return START_REDELIVER_INTENT
|
||||
@@ -219,17 +237,16 @@ class AmneziaVpnService : VpnService() {
|
||||
Log.d(TAG, "onBind by $intent")
|
||||
if (intent?.action == SERVICE_INTERFACE) return super.onBind(intent)
|
||||
isServiceBound = true
|
||||
if (isConnected) launchSendingStatistics()
|
||||
return vpnServiceMessenger.binder
|
||||
}
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
Log.d(TAG, "onUnbind by $intent")
|
||||
if (intent?.action != SERVICE_INTERFACE) {
|
||||
isServiceBound = false
|
||||
stopSendingStatistics()
|
||||
clientMessenger.reset()
|
||||
if (isUnknown || isDisconnected) stopService()
|
||||
if (clientMessengers.isEmpty()) {
|
||||
isServiceBound = false
|
||||
if (isUnknown || isDisconnected) stopService()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -238,7 +255,6 @@ class AmneziaVpnService : VpnService() {
|
||||
Log.d(TAG, "onRebind by $intent")
|
||||
if (intent?.action != SERVICE_INTERFACE) {
|
||||
isServiceBound = true
|
||||
if (isConnected) launchSendingStatistics()
|
||||
}
|
||||
super.onRebind(intent)
|
||||
}
|
||||
@@ -278,17 +294,16 @@ class AmneziaVpnService : VpnService() {
|
||||
*/
|
||||
private fun launchProtocolStateHandler() {
|
||||
mainScope.launch {
|
||||
protocolState.collect { protocolState ->
|
||||
// drop first default UNKNOWN state
|
||||
protocolState.drop(1).collect { protocolState ->
|
||||
Log.d(TAG, "Protocol state changed: $protocolState")
|
||||
when (protocolState) {
|
||||
CONNECTED -> {
|
||||
clientMessenger.send(ServiceEvent.CONNECTED)
|
||||
networkState.bindNetworkListener()
|
||||
if (isServiceBound) launchSendingStatistics()
|
||||
if (isActivityConnected) launchSendingStatistics()
|
||||
}
|
||||
|
||||
DISCONNECTED -> {
|
||||
clientMessenger.send(ServiceEvent.DISCONNECTED)
|
||||
networkState.unbindNetworkListener()
|
||||
stopSendingStatistics()
|
||||
if (!isServiceBound) stopService()
|
||||
@@ -300,12 +315,19 @@ class AmneziaVpnService : VpnService() {
|
||||
}
|
||||
|
||||
RECONNECTING -> {
|
||||
clientMessenger.send(ServiceEvent.RECONNECTING)
|
||||
stopSendingStatistics()
|
||||
}
|
||||
|
||||
CONNECTING, UNKNOWN -> {}
|
||||
}
|
||||
|
||||
clientMessengers.send {
|
||||
ServiceEvent.STATUS_CHANGED.packToMessage {
|
||||
putStatus(protocolState)
|
||||
}
|
||||
}
|
||||
|
||||
VpnStateStore.store { VpnState(protocolState, serverName, serverIndex) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -332,7 +354,17 @@ class AmneziaVpnService : VpnService() {
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun connect(vpnConfig: String?) {
|
||||
private fun connect(vpnConfig: String? = null) {
|
||||
if (vpnConfig == null) {
|
||||
connectToVpn(Prefs.load(PREFS_CONFIG_KEY))
|
||||
} else {
|
||||
Prefs.save(PREFS_CONFIG_KEY, vpnConfig)
|
||||
connectToVpn(vpnConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun connectToVpn(vpnConfig: String) {
|
||||
if (isConnected || protocolState.value == CONNECTING) return
|
||||
|
||||
Log.d(TAG, "Start VPN connection")
|
||||
@@ -340,6 +372,7 @@ class AmneziaVpnService : VpnService() {
|
||||
protocolState.value = CONNECTING
|
||||
|
||||
val config = parseConfigToJson(vpnConfig)
|
||||
saveServerData(config)
|
||||
if (config == null) {
|
||||
onError("Invalid VPN config")
|
||||
protocolState.value = DISCONNECTED
|
||||
@@ -417,24 +450,38 @@ class AmneziaVpnService : VpnService() {
|
||||
private fun onError(msg: String) {
|
||||
Log.e(TAG, msg)
|
||||
mainScope.launch {
|
||||
clientMessenger.send {
|
||||
clientMessengers.send {
|
||||
ServiceEvent.ERROR.packToMessage {
|
||||
putString(ERROR_MSG, msg)
|
||||
putString(MSG_ERROR, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseConfigToJson(vpnConfig: String?): JSONObject? =
|
||||
try {
|
||||
vpnConfig?.let {
|
||||
JSONObject(it)
|
||||
}
|
||||
} catch (e: JSONException) {
|
||||
onError("Invalid VPN config json format: ${e.message}")
|
||||
private fun parseConfigToJson(vpnConfig: String): JSONObject? =
|
||||
if (vpnConfig.isBlank()) {
|
||||
null
|
||||
} else {
|
||||
try {
|
||||
JSONObject(vpnConfig)
|
||||
} catch (e: JSONException) {
|
||||
onError("Invalid VPN config json format: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveServerData(config: JSONObject?) {
|
||||
serverName = config?.opt("description") as String?
|
||||
serverIndex = config?.opt("serverIndex") as Int? ?: -1
|
||||
Prefs.save(PREFS_SERVER_NAME, serverName)
|
||||
Prefs.save(PREFS_SERVER_INDEX, serverIndex)
|
||||
}
|
||||
|
||||
private fun loadServerData() {
|
||||
serverName = Prefs.load<String>(PREFS_SERVER_NAME).ifBlank { null }
|
||||
if (serverName != null) serverIndex = Prefs.load(PREFS_SERVER_INDEX)
|
||||
}
|
||||
|
||||
private fun checkPermission(): Boolean =
|
||||
if (prepare(applicationContext) != null) {
|
||||
Intent(this, VpnRequestActivity::class.java).apply {
|
||||
@@ -446,4 +493,12 @@ class AmneziaVpnService : VpnService() {
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun isRunning(context: Context): Boolean =
|
||||
(context.getSystemService(ACTIVITY_SERVICE) as ActivityManager)
|
||||
.runningAppProcesses.any {
|
||||
it.processName == PROCESS_NAME && it.importance <= IMPORTANCE_FOREGROUND_SERVICE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,7 @@ sealed interface IpcMessage {
|
||||
}
|
||||
|
||||
enum class ServiceEvent : IpcMessage {
|
||||
CONNECTED,
|
||||
DISCONNECTED,
|
||||
RECONNECTING,
|
||||
STATUS_CHANGED,
|
||||
STATUS,
|
||||
STATISTICS_UPDATE,
|
||||
ERROR
|
||||
@@ -30,6 +28,7 @@ enum class ServiceEvent : IpcMessage {
|
||||
|
||||
enum class Action : IpcMessage {
|
||||
REGISTER_CLIENT,
|
||||
UNREGISTER_CLIENT,
|
||||
CONNECT,
|
||||
DISCONNECT,
|
||||
REQUEST_STATUS,
|
||||
|
||||
@@ -9,11 +9,21 @@ import org.amnezia.vpn.util.Log
|
||||
private const val TAG = "IpcMessenger"
|
||||
|
||||
class IpcMessenger(
|
||||
messengerName: String? = null,
|
||||
private val onDeadObjectException: () -> Unit = {},
|
||||
private val onRemoteException: () -> Unit = {},
|
||||
private val messengerName: String = "Unknown"
|
||||
private val onRemoteException: () -> Unit = {}
|
||||
) {
|
||||
private var messenger: Messenger? = null
|
||||
val name = messengerName ?: "Unknown"
|
||||
|
||||
constructor(
|
||||
messenger: Messenger,
|
||||
messengerName: String? = null,
|
||||
onDeadObjectException: () -> Unit = {},
|
||||
onRemoteException: () -> Unit = {}
|
||||
) : this(messengerName, onDeadObjectException, onRemoteException) {
|
||||
this.messenger = messenger
|
||||
}
|
||||
|
||||
fun set(messenger: Messenger) {
|
||||
this.messenger = messenger
|
||||
@@ -25,19 +35,29 @@ class IpcMessenger(
|
||||
|
||||
fun send(msg: () -> Message) = messenger?.sendMsg(msg())
|
||||
|
||||
fun send(msg: Message, replyTo: Messenger) = messenger?.sendMsg(msg.apply { this.replyTo = replyTo })
|
||||
|
||||
fun <T> send(msg: T)
|
||||
where T : Enum<T>, T : IpcMessage = messenger?.sendMsg(msg.packToMessage())
|
||||
|
||||
fun <T> send(msg: T, replyTo: Messenger)
|
||||
where T : Enum<T>, T : IpcMessage = messenger?.sendMsg(msg.packToMessage().apply { this.replyTo = replyTo })
|
||||
|
||||
private fun Messenger.sendMsg(msg: Message) {
|
||||
try {
|
||||
send(msg)
|
||||
} catch (e: DeadObjectException) {
|
||||
Log.w(TAG, "$messengerName messenger is dead")
|
||||
Log.w(TAG, "$name messenger is dead")
|
||||
messenger = null
|
||||
onDeadObjectException()
|
||||
} catch (e: RemoteException) {
|
||||
Log.w(TAG, "Sending a message to the $messengerName messenger failed: ${e.message}")
|
||||
Log.w(TAG, "Sending a message to the $name messenger failed: ${e.message}")
|
||||
onRemoteException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Map<Messenger, IpcMessenger>.send(msg: () -> Message) = this.values.forEach { it.send(msg) }
|
||||
|
||||
fun <T> Map<Messenger, IpcMessenger>.send(msg: T)
|
||||
where T : Enum<T>, T : IpcMessage = this.values.forEach { it.send(msg) }
|
||||
|
||||
75
client/android/src/org/amnezia/vpn/VpnState.kt
Normal file
75
client/android/src/org/amnezia/vpn/VpnState.kt
Normal file
@@ -0,0 +1,75 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.app.Application
|
||||
import androidx.datastore.core.MultiProcessDataStoreFactory
|
||||
import androidx.datastore.core.Serializer
|
||||
import androidx.datastore.dataStoreFile
|
||||
import java.io.InputStream
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.io.Serializable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.amnezia.vpn.protocol.ProtocolState
|
||||
import org.amnezia.vpn.protocol.ProtocolState.DISCONNECTED
|
||||
import org.amnezia.vpn.util.Log
|
||||
|
||||
private const val TAG = "VpnState"
|
||||
private const val STORE_FILE_NAME = "vpnState"
|
||||
|
||||
data class VpnState(
|
||||
val protocolState: ProtocolState,
|
||||
val serverName: String? = null,
|
||||
val serverIndex: Int = -1
|
||||
) : Serializable {
|
||||
companion object {
|
||||
private const val serialVersionUID: Long = -1760654961004181606
|
||||
val defaultState: VpnState = VpnState(DISCONNECTED)
|
||||
}
|
||||
}
|
||||
|
||||
object VpnStateStore {
|
||||
private lateinit var app: Application
|
||||
|
||||
private val dataStore = MultiProcessDataStoreFactory.create(
|
||||
serializer = VpnStateSerializer(),
|
||||
produceFile = { app.dataStoreFile(STORE_FILE_NAME) }
|
||||
)
|
||||
|
||||
fun init(app: Application) {
|
||||
Log.v(TAG, "Init VpnStateStore")
|
||||
this.app = app
|
||||
}
|
||||
|
||||
fun dataFlow(): Flow<VpnState> = dataStore.data
|
||||
|
||||
suspend fun store(f: (vpnState: VpnState) -> VpnState) {
|
||||
try {
|
||||
dataStore.updateData(f)
|
||||
} catch (e : Exception) {
|
||||
Log.e(TAG, "Failed to store VpnState: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class VpnStateSerializer : Serializer<VpnState> {
|
||||
override val defaultValue: VpnState = VpnState.defaultState
|
||||
|
||||
override suspend fun readFrom(input: InputStream): VpnState {
|
||||
return withContext(Dispatchers.IO) {
|
||||
ObjectInputStream(input).use {
|
||||
it.readObject() as VpnState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun writeTo(t: VpnState, output: OutputStream) {
|
||||
withContext(Dispatchers.IO) {
|
||||
ObjectOutputStream(output).use {
|
||||
it.writeObject(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,23 @@
|
||||
package org.amnezia.vpn.qt
|
||||
|
||||
import org.amnezia.vpn.protocol.ProtocolState
|
||||
import org.amnezia.vpn.protocol.Status
|
||||
|
||||
/**
|
||||
* JNI functions of the AndroidController class from android_controller.cpp,
|
||||
* called by events in the Android part of the client
|
||||
*/
|
||||
object QtAndroidController {
|
||||
|
||||
fun onStatus(status: Status) = onStatus(status.state)
|
||||
fun onStatus(protocolState: ProtocolState) = onStatus(protocolState.ordinal)
|
||||
|
||||
external fun onStatus(stateCode: Int)
|
||||
external fun onServiceDisconnected()
|
||||
external fun onServiceError()
|
||||
|
||||
external fun onVpnPermissionRejected()
|
||||
external fun onVpnConnected()
|
||||
external fun onVpnDisconnected()
|
||||
external fun onVpnReconnecting()
|
||||
external fun onVpnStateChanged(stateCode: Int)
|
||||
external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long)
|
||||
|
||||
external fun onFileOpened(uri: String)
|
||||
|
||||
@@ -107,6 +107,7 @@ target_sources(${PROJECT} PRIVATE
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/LogController.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/Log.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
|
||||
)
|
||||
|
||||
target_sources(${PROJECT} PRIVATE
|
||||
|
||||
@@ -76,7 +76,7 @@ OpenVpnConfigurator::ConnectionData OpenVpnConfigurator::prepareOpenVpnConfig(co
|
||||
|
||||
if (connData.caCert.isEmpty() || connData.clientCert.isEmpty() || connData.taKey.isEmpty()) {
|
||||
if (errorCode)
|
||||
*errorCode = ErrorCode::SshSftpFailureError;
|
||||
*errorCode = ErrorCode::SshScpFailureError;
|
||||
}
|
||||
|
||||
return connData;
|
||||
|
||||
@@ -159,7 +159,7 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardCon
|
||||
.arg(connData.clientPubKey, connData.pskKey, connData.clientIP);
|
||||
|
||||
e = serverController.uploadTextFileToContainer(container, credentials, configPart, m_serverConfigPath,
|
||||
libssh::SftpOverwriteMode::SftpAppendToExisting);
|
||||
libssh::ScpOverwriteMode::ScpAppendToExisting);
|
||||
|
||||
if (e) {
|
||||
if (errorCode)
|
||||
|
||||
@@ -118,7 +118,7 @@ ServerController::runContainerScript(const ServerCredentials &credentials, Docke
|
||||
|
||||
ErrorCode ServerController::uploadTextFileToContainer(DockerContainer container, const ServerCredentials &credentials,
|
||||
const QString &file, const QString &path,
|
||||
libssh::SftpOverwriteMode overwriteMode)
|
||||
libssh::ScpOverwriteMode overwriteMode)
|
||||
{
|
||||
ErrorCode e = ErrorCode::NoError;
|
||||
QString tmpFileName = QString("/tmp/%1.tmp").arg(Utils::getRandomString(16));
|
||||
@@ -139,7 +139,7 @@ ErrorCode ServerController::uploadTextFileToContainer(DockerContainer container,
|
||||
if (e)
|
||||
return e;
|
||||
|
||||
if (overwriteMode == libssh::SftpOverwriteMode::SftpOverwriteExisting) {
|
||||
if (overwriteMode == libssh::ScpOverwriteMode::ScpOverwriteExisting) {
|
||||
e = runScript(credentials,
|
||||
replaceVars(QString("sudo docker cp %1 $CONTAINER_NAME:/%2").arg(tmpFileName).arg(path),
|
||||
genVarsForScript(credentials, container)),
|
||||
@@ -147,7 +147,7 @@ ErrorCode ServerController::uploadTextFileToContainer(DockerContainer container,
|
||||
|
||||
if (e)
|
||||
return e;
|
||||
} else if (overwriteMode == libssh::SftpOverwriteMode::SftpAppendToExisting) {
|
||||
} else if (overwriteMode == libssh::ScpOverwriteMode::ScpAppendToExisting) {
|
||||
e = runScript(credentials,
|
||||
replaceVars(QString("sudo docker cp %1 $CONTAINER_NAME:/%2").arg(tmpFileName).arg(tmpFileName),
|
||||
genVarsForScript(credentials, container)),
|
||||
@@ -199,7 +199,7 @@ QByteArray ServerController::getTextFileFromContainer(DockerContainer container,
|
||||
}
|
||||
|
||||
ErrorCode ServerController::uploadFileToHost(const ServerCredentials &credentials, const QByteArray &data,
|
||||
const QString &remotePath, libssh::SftpOverwriteMode overwriteMode)
|
||||
const QString &remotePath, libssh::ScpOverwriteMode overwriteMode)
|
||||
{
|
||||
auto error = m_sshClient.connectToHost(credentials);
|
||||
if (error != ErrorCode::NoError) {
|
||||
@@ -211,13 +211,7 @@ ErrorCode ServerController::uploadFileToHost(const ServerCredentials &credential
|
||||
localFile.write(data);
|
||||
localFile.close();
|
||||
|
||||
#ifdef Q_OS_WINDOWS
|
||||
error = m_sshClient.sftpFileCopy(overwriteMode, localFile.fileName().toLocal8Bit().toStdString(), remotePath.toStdString(),
|
||||
"non_desc");
|
||||
#else
|
||||
error = m_sshClient.sftpFileCopy(overwriteMode, localFile.fileName().toStdString(), remotePath.toStdString(),
|
||||
"non_desc");
|
||||
#endif
|
||||
error = m_sshClient.scpFileCopy(overwriteMode, localFile.fileName(), remotePath, "non_desc");
|
||||
|
||||
if (error != ErrorCode::NoError) {
|
||||
return error;
|
||||
|
||||
@@ -38,7 +38,7 @@ public:
|
||||
|
||||
ErrorCode uploadTextFileToContainer(
|
||||
DockerContainer container, const ServerCredentials &credentials, const QString &file, const QString &path,
|
||||
libssh::SftpOverwriteMode overwriteMode = libssh::SftpOverwriteMode::SftpOverwriteExisting);
|
||||
libssh::ScpOverwriteMode overwriteMode = libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
QByteArray getTextFileFromContainer(DockerContainer container, const ServerCredentials &credentials,
|
||||
const QString &path, ErrorCode *errorCode = nullptr);
|
||||
|
||||
@@ -80,7 +80,7 @@ private:
|
||||
ErrorCode isServerDpkgBusy(const ServerCredentials &credentials, DockerContainer container);
|
||||
|
||||
ErrorCode uploadFileToHost(const ServerCredentials &credentials, const QByteArray &data, const QString &remotePath,
|
||||
libssh::SftpOverwriteMode overwriteMode = libssh::SftpOverwriteMode::SftpOverwriteExisting);
|
||||
libssh::ScpOverwriteMode overwriteMode = libssh::ScpOverwriteMode::ScpOverwriteExisting);
|
||||
|
||||
ErrorCode setupServerFirewall(const ServerCredentials &credentials);
|
||||
|
||||
|
||||
@@ -46,25 +46,12 @@ namespace amnezia
|
||||
SshPrivateKeyFormatError = 304,
|
||||
SshTimeoutError = 305,
|
||||
|
||||
// Ssh sftp errors
|
||||
SshSftpEofError = 400,
|
||||
SshSftpNoSuchFileError = 401,
|
||||
SshSftpPermissionDeniedError = 402,
|
||||
SshSftpFailureError = 403,
|
||||
SshSftpBadMessageError = 404,
|
||||
SshSftpNoConnectionError = 405,
|
||||
SshSftpConnectionLostError = 406,
|
||||
SshSftpOpUnsupportedError = 407,
|
||||
SshSftpInvalidHandleError = 408,
|
||||
SshSftpNoSuchPathError = 409,
|
||||
SshSftpFileAlreadyExistsError = 410,
|
||||
SshSftpWriteProtectError = 411,
|
||||
SshSftpNoMediaError = 412,
|
||||
// Ssh scp errors
|
||||
SshScpFailureError = 400,
|
||||
|
||||
// Local errors
|
||||
OpenVpnConfigMissing = 500,
|
||||
OpenVpnManagementServerError = 501,
|
||||
ConfigMissing = 502,
|
||||
|
||||
// Distro errors
|
||||
OpenVpnExecutableMissing = 600,
|
||||
@@ -92,7 +79,15 @@ namespace amnezia
|
||||
|
||||
// Api errors
|
||||
ApiConfigDownloadError = 1100,
|
||||
ApiConfigAlreadyAdded = 1101
|
||||
ApiConfigAlreadyAdded = 1101,
|
||||
|
||||
// QFile errors
|
||||
OpenError = 1200,
|
||||
ReadError = 1201,
|
||||
PermissionsError = 1202,
|
||||
UnspecifiedError = 1203,
|
||||
FatalError = 1204,
|
||||
AbortError = 1205
|
||||
};
|
||||
|
||||
} // namespace amnezia
|
||||
|
||||
@@ -28,20 +28,8 @@ QString errorString(ErrorCode code) {
|
||||
case(SshPrivateKeyFormatError): errorMessage = QObject::tr("The selected private key format is not supported, use openssh ED25519 key types or PEM key types"); break;
|
||||
case(SshTimeoutError): errorMessage = QObject::tr("Timeout connecting to server"); break;
|
||||
|
||||
// Libssh sftp errors
|
||||
case(SshSftpEofError): errorMessage = QObject::tr("Sftp error: End-of-file encountered"); break;
|
||||
case(SshSftpNoSuchFileError): errorMessage = QObject::tr("Sftp error: File does not exist"); break;
|
||||
case(SshSftpPermissionDeniedError): errorMessage = QObject::tr("Sftp error: Permission denied"); break;
|
||||
case(SshSftpFailureError): errorMessage = QObject::tr("Sftp error: Generic failure"); break;
|
||||
case(SshSftpBadMessageError): errorMessage = QObject::tr("Sftp error: Garbage received from server"); break;
|
||||
case(SshSftpNoConnectionError): errorMessage = QObject::tr("Sftp error: No connection has been set up"); break;
|
||||
case(SshSftpConnectionLostError): errorMessage = QObject::tr("Sftp error: There was a connection, but we lost it"); break;
|
||||
case(SshSftpOpUnsupportedError): errorMessage = QObject::tr("Sftp error: Operation not supported by libssh yet"); break;
|
||||
case(SshSftpInvalidHandleError): errorMessage = QObject::tr("Sftp error: Invalid file handle"); break;
|
||||
case(SshSftpNoSuchPathError): errorMessage = QObject::tr("Sftp error: No such file or directory path exists"); break;
|
||||
case(SshSftpFileAlreadyExistsError): errorMessage = QObject::tr("Sftp error: An attempt to create an already existing file or directory has been made"); break;
|
||||
case(SshSftpWriteProtectError): errorMessage = QObject::tr("Sftp error: Write-protected filesystem"); break;
|
||||
case(SshSftpNoMediaError): errorMessage = QObject::tr("Sftp error: No media was in remote drive"); break;
|
||||
// Ssh scp errors
|
||||
case(SshScpFailureError): errorMessage = QObject::tr("Scp error: Generic failure"); break;
|
||||
|
||||
// Local errors
|
||||
case (OpenVpnConfigMissing): errorMessage = QObject::tr("OpenVPN config missing"); break;
|
||||
@@ -68,6 +56,14 @@ QString errorString(ErrorCode code) {
|
||||
case (ApiConfigDownloadError): errorMessage = QObject::tr("Error when retrieving configuration from API"); break;
|
||||
case (ApiConfigAlreadyAdded): errorMessage = QObject::tr("This config has already been added to the application"); break;
|
||||
|
||||
// QFile errors
|
||||
case(OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
||||
case(ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break;
|
||||
case(PermissionsError): errorMessage = QObject::tr("QFile error: The file could not be accessed"); break;
|
||||
case(UnspecifiedError): errorMessage = QObject::tr("QFile error: An unspecified error occurred"); break;
|
||||
case(FatalError): errorMessage = QObject::tr("QFile error: A fatal error occurred"); break;
|
||||
case(AbortError): errorMessage = QObject::tr("QFile error: The operation was aborted"); break;
|
||||
|
||||
case(InternalError):
|
||||
default:
|
||||
errorMessage = QObject::tr("Internal error"); break;
|
||||
|
||||
@@ -10,16 +10,10 @@ const uint32_t S_IRWXU = 0644;
|
||||
#endif
|
||||
|
||||
namespace libssh {
|
||||
const QString libsshTimeoutError = "Timeout connecting to";
|
||||
constexpr auto libsshTimeoutError{"Timeout connecting to"};
|
||||
|
||||
std::function<QString()> Client::m_passphraseCallback;
|
||||
|
||||
Client::Client(QObject *parent) : QObject(parent)
|
||||
{ }
|
||||
|
||||
Client::~Client()
|
||||
{ }
|
||||
|
||||
int Client::callback(const char *prompt, char *buf, size_t len, int echo, int verify, void *userdata)
|
||||
{
|
||||
auto passphrase = m_passphraseCallback();
|
||||
@@ -171,13 +165,13 @@ namespace libssh {
|
||||
return ErrorCode::NoError;
|
||||
};
|
||||
|
||||
auto error = readOutput(false);
|
||||
if (error != ErrorCode::NoError) {
|
||||
return error;
|
||||
auto errorCode = readOutput(false);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
error = readOutput(true);
|
||||
if (error != ErrorCode::NoError) {
|
||||
return error;
|
||||
errorCode = readOutput(true);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
} else {
|
||||
return closeChannel();
|
||||
@@ -222,102 +216,79 @@ namespace libssh {
|
||||
return fromLibsshErrorCode();
|
||||
}
|
||||
|
||||
ErrorCode Client::sftpFileCopy(const SftpOverwriteMode overwriteMode, const std::string& localPath, const std::string& remotePath, const std::string& fileDesc)
|
||||
ErrorCode Client::scpFileCopy(const ScpOverwriteMode overwriteMode, const QString& localPath, const QString& remotePath, const QString &fileDesc)
|
||||
{
|
||||
m_sftpSession = sftp_new(m_session);
|
||||
m_scpSession = ssh_scp_new(m_session, SSH_SCP_WRITE, remotePath.toStdString().c_str());
|
||||
|
||||
if (m_sftpSession == nullptr) {
|
||||
return closeSftpSession();
|
||||
if (m_scpSession == nullptr) {
|
||||
return fromLibsshErrorCode();
|
||||
}
|
||||
|
||||
int result = sftp_init(m_sftpSession);
|
||||
|
||||
if (result != SSH_OK) {
|
||||
return closeSftpSession();
|
||||
if (ssh_scp_init(m_scpSession) != SSH_OK) {
|
||||
auto errorCode = fromLibsshErrorCode();
|
||||
closeScpSession();
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
QFutureWatcher<ErrorCode> watcher;
|
||||
connect(&watcher, &QFutureWatcher<ErrorCode>::finished, this, &Client::sftpFileCopyFinished);
|
||||
|
||||
connect(&watcher, &QFutureWatcher<ErrorCode>::finished, this, &Client::scpFileCopyFinished);
|
||||
QFuture<ErrorCode> future = QtConcurrent::run([this, overwriteMode, &localPath, &remotePath, &fileDesc]() {
|
||||
int accessType = O_WRONLY | O_CREAT | overwriteMode;
|
||||
sftp_file file;
|
||||
const size_t bufferSize = 16384;
|
||||
char buffer[bufferSize];
|
||||
const int accessType = O_WRONLY | O_CREAT | overwriteMode;
|
||||
const int localFileSize = QFileInfo(localPath).size();
|
||||
|
||||
file = sftp_open(m_sftpSession, remotePath.c_str(), accessType, S_IRWXU);
|
||||
|
||||
if (file == nullptr) {
|
||||
return closeSftpSession();
|
||||
int result = ssh_scp_push_file(m_scpSession, remotePath.toStdString().c_str(), localFileSize, accessType);
|
||||
if (result != SSH_OK) {
|
||||
return fromLibsshErrorCode();
|
||||
}
|
||||
|
||||
int localFileSize = std::filesystem::file_size(localPath);
|
||||
int chunksCount = localFileSize / (bufferSize);
|
||||
QFile fin(localPath);
|
||||
|
||||
std::ifstream fin(localPath, std::ios::binary | std::ios::in);
|
||||
if (fin.open(QIODevice::ReadOnly)) {
|
||||
constexpr size_t bufferSize = 16384;
|
||||
int transferred = 0;
|
||||
int currentChunkSize = bufferSize;
|
||||
|
||||
if (fin.is_open()) {
|
||||
for (int currentChunkId = 0; currentChunkId < chunksCount; currentChunkId++) {
|
||||
fin.read(buffer, bufferSize);
|
||||
while (transferred < localFileSize) {
|
||||
|
||||
int bytesWritten = sftp_write(file, buffer, bufferSize);
|
||||
|
||||
std::string chunk(buffer, bufferSize);
|
||||
|
||||
if (bytesWritten != bufferSize) {
|
||||
fin.close();
|
||||
sftp_close(file);
|
||||
return closeSftpSession();
|
||||
// Last Chunk
|
||||
if ((localFileSize - transferred) < bufferSize) {
|
||||
currentChunkSize = localFileSize % bufferSize;
|
||||
}
|
||||
}
|
||||
|
||||
int lastChunkSize = localFileSize % (bufferSize);
|
||||
|
||||
if (lastChunkSize != 0) {
|
||||
fin.read(buffer, lastChunkSize);
|
||||
|
||||
std::string chunk(buffer, lastChunkSize);
|
||||
|
||||
int bytesWritten = sftp_write(file, buffer, lastChunkSize);
|
||||
|
||||
if (bytesWritten != lastChunkSize) {
|
||||
fin.close();
|
||||
sftp_close(file);
|
||||
return closeSftpSession();
|
||||
QByteArray chunk = fin.read(currentChunkSize);
|
||||
if (chunk.size() != currentChunkSize) {
|
||||
return fromFileErrorCode(fin.error());
|
||||
}
|
||||
|
||||
result = ssh_scp_write(m_scpSession, chunk.data(), chunk.size());
|
||||
if (result != SSH_OK) {
|
||||
return fromLibsshErrorCode();
|
||||
}
|
||||
|
||||
transferred += currentChunkSize;
|
||||
}
|
||||
} else {
|
||||
sftp_close(file);
|
||||
return closeSftpSession();
|
||||
return fromFileErrorCode(fin.error());
|
||||
}
|
||||
|
||||
fin.close();
|
||||
|
||||
int result = sftp_close(file);
|
||||
if (result != SSH_OK) {
|
||||
return closeSftpSession();
|
||||
}
|
||||
|
||||
return closeSftpSession();
|
||||
return ErrorCode::NoError;
|
||||
});
|
||||
watcher.setFuture(future);
|
||||
|
||||
QEventLoop wait;
|
||||
QObject::connect(this, &Client::sftpFileCopyFinished, &wait, &QEventLoop::quit);
|
||||
QObject::connect(this, &Client::scpFileCopyFinished, &wait, &QEventLoop::quit);
|
||||
wait.exec();
|
||||
|
||||
closeScpSession();
|
||||
return watcher.result();
|
||||
}
|
||||
|
||||
ErrorCode Client::closeSftpSession()
|
||||
void Client::closeScpSession()
|
||||
{
|
||||
auto errorCode = fromLibsshSftpErrorCode(sftp_get_error(m_sftpSession));
|
||||
if (m_sftpSession != nullptr) {
|
||||
sftp_free(m_sftpSession);
|
||||
m_sftpSession = nullptr;
|
||||
if (m_scpSession != nullptr) {
|
||||
ssh_scp_free(m_scpSession);
|
||||
m_scpSession = nullptr;
|
||||
}
|
||||
qCritical() << ssh_get_error(m_session);
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
ErrorCode Client::fromLibsshErrorCode()
|
||||
@@ -339,24 +310,17 @@ namespace libssh {
|
||||
default: return ErrorCode::SshInternalError;
|
||||
}
|
||||
}
|
||||
ErrorCode Client::fromLibsshSftpErrorCode(int errorCode)
|
||||
|
||||
ErrorCode Client::fromFileErrorCode(QFileDevice::FileError fileError)
|
||||
{
|
||||
switch (errorCode) {
|
||||
case(SSH_FX_OK): return ErrorCode::NoError;
|
||||
case(SSH_FX_EOF): return ErrorCode::SshSftpEofError;
|
||||
case(SSH_FX_NO_SUCH_FILE): return ErrorCode::SshSftpNoSuchFileError;
|
||||
case(SSH_FX_PERMISSION_DENIED): return ErrorCode::SshSftpPermissionDeniedError;
|
||||
case(SSH_FX_FAILURE): return ErrorCode::SshSftpFailureError;
|
||||
case(SSH_FX_BAD_MESSAGE): return ErrorCode::SshSftpBadMessageError;
|
||||
case(SSH_FX_NO_CONNECTION): return ErrorCode::SshSftpNoConnectionError;
|
||||
case(SSH_FX_CONNECTION_LOST): return ErrorCode::SshSftpConnectionLostError;
|
||||
case(SSH_FX_OP_UNSUPPORTED): return ErrorCode::SshSftpOpUnsupportedError;
|
||||
case(SSH_FX_INVALID_HANDLE): return ErrorCode::SshSftpInvalidHandleError;
|
||||
case(SSH_FX_NO_SUCH_PATH): return ErrorCode::SshSftpNoSuchPathError;
|
||||
case(SSH_FX_FILE_ALREADY_EXISTS): return ErrorCode::SshSftpFileAlreadyExistsError;
|
||||
case(SSH_FX_WRITE_PROTECT): return ErrorCode::SshSftpWriteProtectError;
|
||||
case(SSH_FX_NO_MEDIA): return ErrorCode::SshSftpNoMediaError;
|
||||
default: return ErrorCode::SshSftpFailureError;
|
||||
switch (fileError) {
|
||||
case QFileDevice::NoError: return ErrorCode::NoError;
|
||||
case QFileDevice::ReadError: return ErrorCode::ReadError;
|
||||
case QFileDevice::OpenError: return ErrorCode::OpenError;
|
||||
case QFileDevice::PermissionsError: return ErrorCode::PermissionsError;
|
||||
case QFileDevice::FatalError: return ErrorCode::FatalError;
|
||||
case QFileDevice::AbortError: return ErrorCode::AbortError;
|
||||
default: return ErrorCode::UnspecifiedError;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,29 +2,29 @@
|
||||
#define SSHCLIENT_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QFile>
|
||||
|
||||
#include <fcntl.h>
|
||||
|
||||
#include <libssh/libssh.h>
|
||||
#include <libssh/sftp.h>
|
||||
|
||||
#include "defs.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace libssh {
|
||||
enum SftpOverwriteMode {
|
||||
enum ScpOverwriteMode {
|
||||
/*! Overwrite any existing files */
|
||||
SftpOverwriteExisting = O_TRUNC,
|
||||
ScpOverwriteExisting = O_TRUNC,
|
||||
/*! Append new content if the file already exists */
|
||||
SftpAppendToExisting = O_APPEND
|
||||
ScpAppendToExisting = O_APPEND
|
||||
};
|
||||
class Client : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
Client(QObject *parent = nullptr);
|
||||
~Client();
|
||||
Client() = default;
|
||||
~Client() = default;
|
||||
|
||||
ErrorCode connectToHost(const ServerCredentials &credentials);
|
||||
void disconnectFromHost();
|
||||
@@ -32,26 +32,26 @@ namespace libssh {
|
||||
const std::function<ErrorCode (const QString &, Client &)> &cbReadStdOut,
|
||||
const std::function<ErrorCode (const QString &, Client &)> &cbReadStdErr);
|
||||
ErrorCode writeResponse(const QString &data);
|
||||
ErrorCode sftpFileCopy(const SftpOverwriteMode overwriteMode,
|
||||
const std::string& localPath,
|
||||
const std::string& remotePath,
|
||||
const std::string& fileDesc);
|
||||
ErrorCode scpFileCopy(const ScpOverwriteMode overwriteMode,
|
||||
const QString &localPath,
|
||||
const QString &remotePath,
|
||||
const QString &fileDesc);
|
||||
ErrorCode getDecryptedPrivateKey(const ServerCredentials &credentials, QString &decryptedPrivateKey, const std::function<QString()> &passphraseCallback);
|
||||
private:
|
||||
ErrorCode closeChannel();
|
||||
ErrorCode closeSftpSession();
|
||||
void closeScpSession();
|
||||
ErrorCode fromLibsshErrorCode();
|
||||
ErrorCode fromLibsshSftpErrorCode(int errorCode);
|
||||
ErrorCode fromFileErrorCode(QFileDevice::FileError fileError);
|
||||
static int callback(const char *prompt, char *buf, size_t len, int echo, int verify, void *userdata);
|
||||
|
||||
ssh_session m_session = nullptr;
|
||||
ssh_channel m_channel = nullptr;
|
||||
sftp_session m_sftpSession = nullptr;
|
||||
ssh_scp m_scpSession = nullptr;
|
||||
|
||||
static std::function<QString()> m_passphraseCallback;
|
||||
signals:
|
||||
void writeToChannelFinished();
|
||||
void sftpFileCopyFinished();
|
||||
void scpFileCopyFinished();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
6
client/images/controls/split-tunneling.svg
Normal file
6
client/images/controls/split-tunneling.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" width="18" height="18" rx="5" fill="white"/>
|
||||
<path d="M8.49219 13.5L8.49219 9.44141L14.0191 4.99484" stroke="#0E0E11" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.47363 5.49805L6.98828 8.0127" stroke="#0E0E11" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14.4727 9.5L14.4727 4.5033L9.50195 4.5033" stroke="#0E0E11" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 511 B |
@@ -85,6 +85,7 @@ target_sources(networkextension PRIVATE
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/PacketTunnelProvider+OpenVPNAdapterDelegate.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/WGConfig.swift
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/iosglue.mm
|
||||
)
|
||||
|
||||
|
||||
@@ -48,10 +48,6 @@ int main(int argc, char *argv[])
|
||||
AllowSetForegroundWindow(0);
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_IOS)
|
||||
QtAppDelegateInitialize();
|
||||
#endif
|
||||
|
||||
app.registerTypes();
|
||||
|
||||
app.setApplicationName(APPLICATION_NAME);
|
||||
@@ -65,7 +61,7 @@ int main(int argc, char *argv[])
|
||||
if (doExec) {
|
||||
app.init();
|
||||
|
||||
qInfo().noquote() << QString("Started %1 version %2").arg(APPLICATION_NAME, APP_VERSION);
|
||||
qInfo().noquote() << QString("Started %1 version %2 %3").arg(APPLICATION_NAME, APP_VERSION, GIT_COMMIT_HASH);
|
||||
qInfo().noquote() << QString("%1 (%2)").arg(QSysInfo::prettyProductName(), QSysInfo::currentCpuArchitecture());
|
||||
|
||||
return app.exec();
|
||||
|
||||
@@ -124,7 +124,10 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||
// json.insert("hopindex", QJsonValue((double)hop.m_hopindex));
|
||||
json.insert("privateKey", wgConfig.value(amnezia::config_key::client_priv_key));
|
||||
json.insert("deviceIpv4Address", wgConfig.value(amnezia::config_key::client_ip));
|
||||
// todo review wg ipv6
|
||||
#ifdef Q_OS_MACOS
|
||||
json.insert("deviceIpv6Address", "dead::1");
|
||||
#endif
|
||||
json.insert("serverPublicKey", wgConfig.value(amnezia::config_key::server_pub_key));
|
||||
json.insert("serverPskKey", wgConfig.value(amnezia::config_key::psk_key));
|
||||
json.insert("serverIpv4AddrIn", wgConfig.value(amnezia::config_key::hostName));
|
||||
|
||||
@@ -56,26 +56,10 @@ AndroidController::AndroidController() : QObject()
|
||||
Qt::QueuedConnection);
|
||||
|
||||
connect(
|
||||
this, &AndroidController::vpnConnected, this,
|
||||
[this]() {
|
||||
qDebug() << "Android event: VPN connected";
|
||||
emit connectionStateChanged(Vpn::ConnectionState::Connected);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
connect(
|
||||
this, &AndroidController::vpnDisconnected, this,
|
||||
[this]() {
|
||||
qDebug() << "Android event: VPN disconnected";
|
||||
emit connectionStateChanged(Vpn::ConnectionState::Disconnected);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
connect(
|
||||
this, &AndroidController::vpnReconnecting, this,
|
||||
[this]() {
|
||||
qDebug() << "Android event: VPN reconnecting";
|
||||
emit connectionStateChanged(Vpn::ConnectionState::Reconnecting);
|
||||
this, &AndroidController::vpnStateChanged, this,
|
||||
[this](AndroidController::ConnectionState state) {
|
||||
qDebug() << "Android event: VPN state changed:" << textConnectionState(state);
|
||||
emit connectionStateChanged(convertState(state));
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
@@ -106,9 +90,7 @@ bool AndroidController::initialize()
|
||||
{"onServiceDisconnected", "()V", reinterpret_cast<void *>(onServiceDisconnected)},
|
||||
{"onServiceError", "()V", reinterpret_cast<void *>(onServiceError)},
|
||||
{"onVpnPermissionRejected", "()V", reinterpret_cast<void *>(onVpnPermissionRejected)},
|
||||
{"onVpnConnected", "()V", reinterpret_cast<void *>(onVpnConnected)},
|
||||
{"onVpnDisconnected", "()V", reinterpret_cast<void *>(onVpnDisconnected)},
|
||||
{"onVpnReconnecting", "()V", reinterpret_cast<void *>(onVpnReconnecting)},
|
||||
{"onVpnStateChanged", "(I)V", reinterpret_cast<void *>(onVpnStateChanged)},
|
||||
{"onStatisticsUpdate", "(JJ)V", reinterpret_cast<void *>(onStatisticsUpdate)},
|
||||
{"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onFileOpened)},
|
||||
{"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onConfigImported)},
|
||||
@@ -158,6 +140,11 @@ void AndroidController::stop()
|
||||
callActivityMethod("stop", "()V");
|
||||
}
|
||||
|
||||
void AndroidController::resetLastServer(int serverIndex)
|
||||
{
|
||||
callActivityMethod("resetLastServer", "(I)V", serverIndex);
|
||||
}
|
||||
|
||||
void AndroidController::saveFile(const QString &fileName, const QString &data)
|
||||
{
|
||||
callActivityMethod("saveFile", "(Ljava/lang/String;Ljava/lang/String;)V",
|
||||
@@ -217,6 +204,11 @@ void AndroidController::clearLogs()
|
||||
callActivityMethod("clearLogs", "()V");
|
||||
}
|
||||
|
||||
void AndroidController::setScreenshotsEnabled(bool enabled)
|
||||
{
|
||||
callActivityMethod("setScreenshotsEnabled", "(Z)V", enabled);
|
||||
}
|
||||
|
||||
// Moving log processing to the Android side
|
||||
jclass AndroidController::log;
|
||||
jmethodID AndroidController::logDebug;
|
||||
@@ -370,30 +362,14 @@ void AndroidController::onVpnPermissionRejected(JNIEnv *env, jobject thiz)
|
||||
}
|
||||
|
||||
// static
|
||||
void AndroidController::onVpnConnected(JNIEnv *env, jobject thiz)
|
||||
void AndroidController::onVpnStateChanged(JNIEnv *env, jobject thiz, jint stateCode)
|
||||
{
|
||||
Q_UNUSED(env);
|
||||
Q_UNUSED(thiz);
|
||||
|
||||
emit AndroidController::instance()->vpnConnected();
|
||||
}
|
||||
auto state = ConnectionState(stateCode);
|
||||
|
||||
// static
|
||||
void AndroidController::onVpnDisconnected(JNIEnv *env, jobject thiz)
|
||||
{
|
||||
Q_UNUSED(env);
|
||||
Q_UNUSED(thiz);
|
||||
|
||||
emit AndroidController::instance()->vpnDisconnected();
|
||||
}
|
||||
|
||||
// static
|
||||
void AndroidController::onVpnReconnecting(JNIEnv *env, jobject thiz)
|
||||
{
|
||||
Q_UNUSED(env);
|
||||
Q_UNUSED(thiz);
|
||||
|
||||
emit AndroidController::instance()->vpnReconnecting();
|
||||
emit AndroidController::instance()->vpnStateChanged(state);
|
||||
}
|
||||
|
||||
// static
|
||||
|
||||
@@ -20,9 +20,9 @@ public:
|
||||
// keep synchronized with org.amnezia.vpn.protocol.ProtocolState
|
||||
enum class ConnectionState
|
||||
{
|
||||
DISCONNECTED,
|
||||
CONNECTED,
|
||||
CONNECTING,
|
||||
DISCONNECTED,
|
||||
DISCONNECTING,
|
||||
RECONNECTING,
|
||||
UNKNOWN
|
||||
@@ -30,6 +30,7 @@ public:
|
||||
|
||||
ErrorCode start(const QJsonObject &vpnConfig);
|
||||
void stop();
|
||||
void resetLastServer(int serverIndex);
|
||||
void setNotificationText(const QString &title, const QString &message, int timerSec);
|
||||
void saveFile(const QString &fileName, const QString &data);
|
||||
QString openFile(const QString &filter);
|
||||
@@ -38,6 +39,7 @@ public:
|
||||
void setSaveLogs(bool enabled);
|
||||
void exportLogsFile(const QString &fileName);
|
||||
void clearLogs();
|
||||
void setScreenshotsEnabled(bool enabled);
|
||||
|
||||
static bool initLogging();
|
||||
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
|
||||
@@ -48,9 +50,7 @@ signals:
|
||||
void serviceDisconnected();
|
||||
void serviceError();
|
||||
void vpnPermissionRejected();
|
||||
void vpnConnected();
|
||||
void vpnDisconnected();
|
||||
void vpnReconnecting();
|
||||
void vpnStateChanged(ConnectionState state);
|
||||
void statisticsUpdated(quint64 rxBytes, quint64 txBytes);
|
||||
void fileOpened(QString uri);
|
||||
void configImported(QString config);
|
||||
@@ -77,9 +77,7 @@ private:
|
||||
static void onServiceDisconnected(JNIEnv *env, jobject thiz);
|
||||
static void onServiceError(JNIEnv *env, jobject thiz);
|
||||
static void onVpnPermissionRejected(JNIEnv *env, jobject thiz);
|
||||
static void onVpnConnected(JNIEnv *env, jobject thiz);
|
||||
static void onVpnDisconnected(JNIEnv *env, jobject thiz);
|
||||
static void onVpnReconnecting(JNIEnv *env, jobject thiz);
|
||||
static void onVpnStateChanged(JNIEnv *env, jobject thiz, jint stateCode);
|
||||
static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes);
|
||||
static void onConfigImported(JNIEnv *env, jobject thiz, jstring data);
|
||||
static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import NetworkExtension
|
||||
|
||||
public func swiftUpdateLogData(_ qtString: std.string) -> std.string {
|
||||
let qtLog = Log(String(describing: qtString))
|
||||
@@ -24,3 +25,26 @@ public func swiftDeleteLog() {
|
||||
public func toggleLogging(_ isEnabled: Bool) {
|
||||
Log.isLoggingEnabled = isEnabled
|
||||
}
|
||||
|
||||
public func clearSettings() {
|
||||
NETunnelProviderManager.loadAllFromPreferences { managers, error in
|
||||
if let error {
|
||||
NSLog("clearSettings removeFromPreferences error: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
managers?.forEach { manager in
|
||||
manager.removeFromPreferences { error in
|
||||
if let error {
|
||||
NSLog("NE removeFromPreferences error: \(error.localizedDescription)")
|
||||
} else {
|
||||
manager.loadFromPreferences { error in
|
||||
if let error {
|
||||
NSLog("NE loadFromPreferences after remove error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,40 +18,32 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate {
|
||||
// send empty string to NEDNSSettings.matchDomains
|
||||
networkSettings?.dnsSettings?.matchDomains = [""]
|
||||
|
||||
if splitTunnelType == "1" {
|
||||
if splitTunnelType == 1 {
|
||||
var ipv4IncludedRoutes = [NEIPv4Route]()
|
||||
let STSdata = Data(splitTunnelSites!.utf8)
|
||||
do {
|
||||
guard let STSArray = try JSONSerialization.jsonObject(with: STSdata) as? [String] else { return }
|
||||
for allowedIPString in STSArray {
|
||||
if let allowedIP = IPAddressRange(from: allowedIPString) {
|
||||
ipv4IncludedRoutes.append(NEIPv4Route(
|
||||
destinationAddress: "\(allowedIP.address)",
|
||||
subnetMask: "\(allowedIP.subnetMask())"))
|
||||
}
|
||||
|
||||
for allowedIPString in splitTunnelSites {
|
||||
if let allowedIP = IPAddressRange(from: allowedIPString) {
|
||||
ipv4IncludedRoutes.append(NEIPv4Route(
|
||||
destinationAddress: "\(allowedIP.address)",
|
||||
subnetMask: "\(allowedIP.subnetMask())"))
|
||||
}
|
||||
} catch {
|
||||
wg_log(.error, message: "Parse JSONSerialization Error")
|
||||
}
|
||||
|
||||
networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
|
||||
} else {
|
||||
if splitTunnelType == "2" {
|
||||
if splitTunnelType == 2 {
|
||||
var ipv4ExcludedRoutes = [NEIPv4Route]()
|
||||
var ipv4IncludedRoutes = [NEIPv4Route]()
|
||||
var ipv6IncludedRoutes = [NEIPv6Route]()
|
||||
let STSdata = Data(splitTunnelSites!.utf8)
|
||||
do {
|
||||
guard let STSArray = try JSONSerialization.jsonObject(with: STSdata) as? [String] else { return }
|
||||
for excludeIPString in STSArray {
|
||||
if let excludeIP = IPAddressRange(from: excludeIPString) {
|
||||
ipv4ExcludedRoutes.append(NEIPv4Route(
|
||||
destinationAddress: "\(excludeIP.address)",
|
||||
subnetMask: "\(excludeIP.subnetMask())"))
|
||||
}
|
||||
|
||||
for excludeIPString in splitTunnelSites {
|
||||
if let excludeIP = IPAddressRange(from: excludeIPString) {
|
||||
ipv4ExcludedRoutes.append(NEIPv4Route(
|
||||
destinationAddress: "\(excludeIP.address)",
|
||||
subnetMask: "\(excludeIP.subnetMask())"))
|
||||
}
|
||||
} catch {
|
||||
wg_log(.error, message: "Parse JSONSerialization Error")
|
||||
}
|
||||
|
||||
if let allIPv4 = IPAddressRange(from: "0.0.0.0/0") {
|
||||
ipv4IncludedRoutes.append(NEIPv4Route(
|
||||
destinationAddress: "\(allIPv4.address)",
|
||||
|
||||
@@ -50,8 +50,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
private let dispatchQueue = DispatchQueue(label: "PacketTunnel", qos: .utility)
|
||||
|
||||
private var openVPNConfig: Data?
|
||||
var splitTunnelType: String?
|
||||
var splitTunnelSites: String?
|
||||
var splitTunnelType: Int!
|
||||
var splitTunnelSites: [String]!
|
||||
|
||||
let vpnReachability = OpenVPNReachability()
|
||||
|
||||
@@ -59,10 +59,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
var stopHandler: (() -> Void)?
|
||||
var protoType: TunnelProtoType = .none
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
|
||||
let tmpStr = String(data: messageData, encoding: .utf8)!
|
||||
wg_log(.error, message: tmpStr)
|
||||
@@ -71,7 +67,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
return
|
||||
}
|
||||
|
||||
guard let completionHandler = completionHandler else {
|
||||
guard let completionHandler else {
|
||||
log(.error, message: "Missing message completion handler")
|
||||
return
|
||||
}
|
||||
@@ -85,22 +81,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
if action == Constants.kActionStatus {
|
||||
handleStatusAppMessage(messageData, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
if action == Constants.kActionStart {
|
||||
splitTunnelType = message[Constants.kMessageKeySplitTunnelType] as? String
|
||||
splitTunnelSites = message[Constants.kMessageKeySplitTunnelSites] as? String
|
||||
}
|
||||
|
||||
let callbackWrapper: (NSNumber?) -> Void = { errorCode in
|
||||
// let tunnelId = self.tunnelConfig?.id ?? ""
|
||||
let response: [String: Any] = [
|
||||
Constants.kMessageKeyAction: action,
|
||||
Constants.kMessageKeyErrorCode: errorCode ?? NSNull(),
|
||||
Constants.kMessageKeyTunnelId: 0
|
||||
]
|
||||
|
||||
completionHandler(try? JSONSerialization.data(withJSONObject: response, options: []))
|
||||
}
|
||||
}
|
||||
|
||||
override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) {
|
||||
@@ -173,108 +153,118 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
completionHandler: @escaping (Error?) -> Void) {
|
||||
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
|
||||
let providerConfiguration = protocolConfiguration.providerConfiguration,
|
||||
let wgConfig: Data = providerConfiguration[Constants.wireGuardConfigKey] as? Data else {
|
||||
let wgConfigData: Data = providerConfiguration[Constants.wireGuardConfigKey] as? Data else {
|
||||
wg_log(.error, message: "Can't start WireGuard config missing")
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let wgConfigStr = String(data: wgConfig, encoding: .utf8)!
|
||||
do {
|
||||
let wgConfig = try JSONDecoder().decode(WGConfig.self, from: wgConfigData)
|
||||
let wgConfigStr = wgConfig.str
|
||||
log(.info, message: "wgConfig: \(wgConfig.redux.replacingOccurrences(of: "\n", with: " "))")
|
||||
|
||||
guard let tunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: wgConfigStr) else {
|
||||
wg_log(.error, message: "Can't parse WireGuard config")
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: wgConfigStr)
|
||||
|
||||
if tunnelConfiguration.peers.first!.allowedIPs
|
||||
.map({ $0.stringRepresentation })
|
||||
.joined(separator: ", ") == "0.0.0.0/0, ::/0" {
|
||||
if splitTunnelType == "1" {
|
||||
for index in tunnelConfiguration.peers.indices {
|
||||
tunnelConfiguration.peers[index].allowedIPs.removeAll()
|
||||
var allowedIPs = [IPAddressRange]()
|
||||
let STSdata = Data(splitTunnelSites!.utf8)
|
||||
do {
|
||||
guard let STSArray = try JSONSerialization.jsonObject(with: STSdata) as? [String] else { return }
|
||||
for allowedIPString in STSArray {
|
||||
if tunnelConfiguration.peers.first!.allowedIPs
|
||||
.map({ $0.stringRepresentation })
|
||||
.joined(separator: ", ") == "0.0.0.0/0, ::/0" {
|
||||
if wgConfig.splitTunnelType == 1 {
|
||||
for index in tunnelConfiguration.peers.indices {
|
||||
tunnelConfiguration.peers[index].allowedIPs.removeAll()
|
||||
var allowedIPs = [IPAddressRange]()
|
||||
|
||||
for allowedIPString in wgConfig.splitTunnelSites {
|
||||
if let allowedIP = IPAddressRange(from: allowedIPString) {
|
||||
allowedIPs.append(allowedIP)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
wg_log(.error, message: "Parse JSONSerialization Error")
|
||||
|
||||
tunnelConfiguration.peers[index].allowedIPs = allowedIPs
|
||||
}
|
||||
tunnelConfiguration.peers[index].allowedIPs = allowedIPs
|
||||
}
|
||||
} else if splitTunnelType == "2" {
|
||||
for index in tunnelConfiguration.peers.indices {
|
||||
var excludeIPs = [IPAddressRange]()
|
||||
let STSdata = Data(splitTunnelSites!.utf8)
|
||||
do {
|
||||
guard let STSArray = try JSONSerialization.jsonObject(with: STSdata) as? [String] else { return }
|
||||
for excludeIPString in STSArray {
|
||||
} else if wgConfig.splitTunnelType == 2 {
|
||||
for index in tunnelConfiguration.peers.indices {
|
||||
var excludeIPs = [IPAddressRange]()
|
||||
|
||||
for excludeIPString in wgConfig.splitTunnelSites {
|
||||
if let excludeIP = IPAddressRange(from: excludeIPString) {
|
||||
excludeIPs.append(excludeIP)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
wg_log(.error, message: "Parse JSONSerialization Error")
|
||||
|
||||
tunnelConfiguration.peers[index].excludeIPs = excludeIPs
|
||||
}
|
||||
tunnelConfiguration.peers[index].excludeIPs = excludeIPs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wg_log(.info, message: "Starting wireguard tunnel from the " +
|
||||
(activationAttemptId == nil ? "OS directly, rather than the app" : "app"))
|
||||
wg_log(.info, message: "Starting wireguard tunnel from the " +
|
||||
(activationAttemptId == nil ? "OS directly, rather than the app" : "app"))
|
||||
|
||||
// Start the tunnel
|
||||
wgAdapter.start(tunnelConfiguration: tunnelConfiguration) { adapterError in
|
||||
guard let adapterError else {
|
||||
let interfaceName = self.wgAdapter.interfaceName ?? "unknown"
|
||||
wg_log(.info, message: "Tunnel interface is \(interfaceName)")
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
|
||||
switch adapterError {
|
||||
case .cannotLocateTunnelFileDescriptor:
|
||||
wg_log(.error, staticMessage: "Starting tunnel failed: could not determine file descriptor")
|
||||
errorNotifier.notify(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
|
||||
completionHandler(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
|
||||
case .dnsResolution(let dnsErrors):
|
||||
let hostnamesWithDnsResolutionFailure = dnsErrors.map { $0.address }
|
||||
.joined(separator: ", ")
|
||||
wg_log(.error, message:
|
||||
"DNS resolution failed for the following hostnames: \(hostnamesWithDnsResolutionFailure)")
|
||||
errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure)
|
||||
completionHandler(PacketTunnelProviderError.dnsResolutionFailure)
|
||||
case .setNetworkSettings(let error):
|
||||
wg_log(.error, message:
|
||||
"Starting tunnel failed with setTunnelNetworkSettings returning \(error.localizedDescription)")
|
||||
errorNotifier.notify(PacketTunnelProviderError.couldNotSetNetworkSettings)
|
||||
completionHandler(PacketTunnelProviderError.couldNotSetNetworkSettings)
|
||||
case .startWireGuardBackend(let errorCode):
|
||||
wg_log(.error, message: "Starting tunnel failed with wgTurnOn returning \(errorCode)")
|
||||
errorNotifier.notify(PacketTunnelProviderError.couldNotStartBackend)
|
||||
completionHandler(PacketTunnelProviderError.couldNotStartBackend)
|
||||
case .invalidState:
|
||||
fatalError()
|
||||
// Start the tunnel
|
||||
wgAdapter.start(tunnelConfiguration: tunnelConfiguration) { adapterError in
|
||||
guard let adapterError else {
|
||||
let interfaceName = self.wgAdapter.interfaceName ?? "unknown"
|
||||
wg_log(.info, message: "Tunnel interface is \(interfaceName)")
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
|
||||
switch adapterError {
|
||||
case .cannotLocateTunnelFileDescriptor:
|
||||
wg_log(.error, staticMessage: "Starting tunnel failed: could not determine file descriptor")
|
||||
errorNotifier.notify(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
|
||||
completionHandler(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
|
||||
case .dnsResolution(let dnsErrors):
|
||||
let hostnamesWithDnsResolutionFailure = dnsErrors.map { $0.address }
|
||||
.joined(separator: ", ")
|
||||
wg_log(.error, message:
|
||||
"DNS resolution failed for the following hostnames: \(hostnamesWithDnsResolutionFailure)")
|
||||
errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure)
|
||||
completionHandler(PacketTunnelProviderError.dnsResolutionFailure)
|
||||
case .setNetworkSettings(let error):
|
||||
wg_log(.error, message:
|
||||
"Starting tunnel failed with setTunnelNetworkSettings returning \(error.localizedDescription)")
|
||||
errorNotifier.notify(PacketTunnelProviderError.couldNotSetNetworkSettings)
|
||||
completionHandler(PacketTunnelProviderError.couldNotSetNetworkSettings)
|
||||
case .startWireGuardBackend(let errorCode):
|
||||
wg_log(.error, message: "Starting tunnel failed with wgTurnOn returning \(errorCode)")
|
||||
errorNotifier.notify(PacketTunnelProviderError.couldNotStartBackend)
|
||||
completionHandler(PacketTunnelProviderError.couldNotStartBackend)
|
||||
case .invalidState:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
log(.error, message: "Can't parse WG config: \(error.localizedDescription)")
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func startOpenVPN(completionHandler: @escaping (Error?) -> Void) {
|
||||
guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol,
|
||||
let providerConfiguration = protocolConfiguration.providerConfiguration,
|
||||
let ovpnConfiguration: Data = providerConfiguration[Constants.ovpnConfigKey] as? Data else {
|
||||
|
||||
let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else {
|
||||
wg_log(.error, message: "Can't start startOpenVPN()")
|
||||
return
|
||||
}
|
||||
|
||||
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
|
||||
do {
|
||||
log(.info, message: "providerConfiguration: \(String(decoding: openVPNConfigData, as: UTF8.self).replacingOccurrences(of: "\n", with: " "))")
|
||||
|
||||
let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData)
|
||||
log(.info, message: "openVPNConfig: \(openVPNConfig.str.replacingOccurrences(of: "\n", with: " "))")
|
||||
let ovpnConfiguration = Data(openVPNConfig.config.utf8)
|
||||
setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler)
|
||||
} catch {
|
||||
log(.error, message: "Can't parse OpenVPN config: \(error.localizedDescription)")
|
||||
|
||||
if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError {
|
||||
log(.error, message: "Can't parse OpenVPN config: \(underlyingError.localizedDescription)")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func stopWireguard(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface QtAppDelegate : UIResponder <UIApplicationDelegate>
|
||||
@interface QIOSApplicationDelegate
|
||||
@end
|
||||
|
||||
@interface QIOSApplicationDelegate (AmneziaVPNDelegate)
|
||||
@end
|
||||
|
||||
@@ -3,41 +3,17 @@
|
||||
|
||||
#include <QFile>
|
||||
|
||||
@implementation QtAppDelegate {
|
||||
UIView *_screen;
|
||||
}
|
||||
|
||||
+(QtAppDelegate *)sharedQtAppDelegate {
|
||||
static dispatch_once_t pred;
|
||||
static QtAppDelegate *shared = nil;
|
||||
dispatch_once(&pred, ^{
|
||||
shared = [[super alloc] init];
|
||||
});
|
||||
return shared;
|
||||
}
|
||||
|
||||
@implementation QIOSApplicationDelegate (AmneziaVPNDelegate)
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
[application setMinimumBackgroundFetchInterval: UIApplicationBackgroundFetchIntervalMinimum];
|
||||
// Override point for customization after application launch.
|
||||
NSLog(@"Did this launch option happen");
|
||||
NSLog(@"Application didFinishLaunchingWithOptions");
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)applicationWillResignActive:(UIApplication *)application
|
||||
{
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
|
||||
_screen = [UIScreen.mainScreen snapshotViewAfterScreenUpdates: false];
|
||||
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle: UIBlurEffectStyleDark];
|
||||
UIVisualEffectView *blurBackground = [[UIVisualEffectView alloc] initWithEffect: blurEffect];
|
||||
[_screen addSubview: blurBackground];
|
||||
blurBackground.frame = _screen.frame;
|
||||
UIWindow *_window = UIApplication.sharedApplication.keyWindow;
|
||||
[_window addSubview: _screen];
|
||||
}
|
||||
|
||||
- (void)applicationDidEnterBackground:(UIApplication *)application
|
||||
{
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
@@ -51,17 +27,6 @@
|
||||
NSLog(@"In the foreground");
|
||||
}
|
||||
|
||||
- (void)applicationDidBecomeActive:(UIApplication *)application
|
||||
{
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
[_screen removeFromSuperview];
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(UIApplication *)application
|
||||
{
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
-(void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
|
||||
// We will add content here soon.
|
||||
NSLog(@"In the completionHandler");
|
||||
@@ -70,31 +35,27 @@
|
||||
- (BOOL)application:(UIApplication *)app
|
||||
openURL:(NSURL *)url
|
||||
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
|
||||
|
||||
NSLog(@"Application openURL: %@", url);
|
||||
if (url.fileURL) {
|
||||
QString filePath(url.path.UTF8String);
|
||||
if (filePath.isEmpty()) return NO;
|
||||
|
||||
if (filePath.contains("backup")) {
|
||||
IosController::Instance()->importBackupFromOutside(filePath);
|
||||
} else {
|
||||
QFile file(filePath);
|
||||
bool isOpenFile = file.open(QIODevice::ReadOnly);
|
||||
QByteArray data = file.readAll();
|
||||
|
||||
IosController::Instance()->importConfigFromOutside(QString(data));
|
||||
}
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
|
||||
NSLog(@"Application openURL: %@", url);
|
||||
|
||||
if (filePath.contains("backup")) {
|
||||
IosController::Instance()->importBackupFromOutside(filePath);
|
||||
} else {
|
||||
QFile file(filePath);
|
||||
bool isOpenFile = file.open(QIODevice::ReadOnly);
|
||||
QByteArray data = file.readAll();
|
||||
|
||||
IosController::Instance()->importConfigFromOutside(QString(data));
|
||||
}
|
||||
});
|
||||
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
void QtAppDelegateInitialize()
|
||||
{
|
||||
[[UIApplication sharedApplication] setDelegate: [QtAppDelegate sharedQtAppDelegate]];
|
||||
NSLog(@"Created a new AppDelegate");
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
87
client/platforms/ios/ScreenProtection.swift
Normal file
87
client/platforms/ios/ScreenProtection.swift
Normal file
@@ -0,0 +1,87 @@
|
||||
import UIKit
|
||||
|
||||
public func toggleScreenshots(_ isEnabled: Bool) {
|
||||
let window = UIApplication.shared.keyWindows.first!
|
||||
|
||||
if isEnabled {
|
||||
ScreenProtection.shared.disable(for: window.rootViewController!.view)
|
||||
} else {
|
||||
ScreenProtection.shared.enable(for: window.rootViewController!.view)
|
||||
}
|
||||
}
|
||||
|
||||
extension UIApplication {
|
||||
var keyWindows: [UIWindow] {
|
||||
connectedScenes
|
||||
.compactMap {
|
||||
if #available(iOS 15.0, *) {
|
||||
($0 as? UIWindowScene)?.keyWindow
|
||||
} else {
|
||||
($0 as? UIWindowScene)?.windows.first { $0.isKeyWindow }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ScreenProtection {
|
||||
public static let shared = ScreenProtection()
|
||||
|
||||
var pairs = [ProtectionPair]()
|
||||
|
||||
private var blurView: UIVisualEffectView?
|
||||
private var recordingObservation: NSKeyValueObservation?
|
||||
|
||||
public func enable(for view: UIView) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
view.subviews.forEach {
|
||||
self.pairs.append(ProtectionPair(from: $0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func disable(for view: UIView) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
self.pairs.forEach {
|
||||
$0.removeProtection()
|
||||
}
|
||||
|
||||
self.pairs.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProtectionPair {
|
||||
let textField: UITextField
|
||||
let layer: CALayer
|
||||
|
||||
init(from view: UIView) {
|
||||
let secureTextField = UITextField()
|
||||
secureTextField.backgroundColor = .clear
|
||||
secureTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
secureTextField.isSecureTextEntry = true
|
||||
|
||||
view.insertSubview(secureTextField, at: 0)
|
||||
secureTextField.isUserInteractionEnabled = false
|
||||
|
||||
view.layer.superlayer?.addSublayer(secureTextField.layer)
|
||||
secureTextField.layer.sublayers?.last?.addSublayer(view.layer)
|
||||
|
||||
secureTextField.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
|
||||
secureTextField.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
|
||||
secureTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
|
||||
secureTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
|
||||
|
||||
self.init(textField: secureTextField, layer: view.layer)
|
||||
}
|
||||
|
||||
init(textField: UITextField, layer: CALayer) {
|
||||
self.textField = textField
|
||||
self.layer = layer
|
||||
}
|
||||
|
||||
func removeProtection() {
|
||||
textField.superview?.superview?.layer.addSublayer(layer)
|
||||
textField.layer.removeFromSuperlayer()
|
||||
textField.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
97
client/platforms/ios/WGConfig.swift
Normal file
97
client/platforms/ios/WGConfig.swift
Normal file
@@ -0,0 +1,97 @@
|
||||
import Foundation
|
||||
|
||||
struct WGConfig: Decodable {
|
||||
let initPacketMagicHeader, responsePacketMagicHeader: String?
|
||||
let underloadPacketMagicHeader, transportPacketMagicHeader: String?
|
||||
let junkPacketCount, junkPacketMinSize, junkPacketMaxSize: String?
|
||||
let initPacketJunkSize, responsePacketJunkSize: String?
|
||||
let dns1: String
|
||||
let dns2: String
|
||||
let hostName: String
|
||||
let port: Int
|
||||
let clientIP: String
|
||||
let clientPrivateKey: String
|
||||
let serverPublicKey: String
|
||||
let presharedKey: String
|
||||
var allowedIPs: [String]
|
||||
var persistentKeepAlive: String
|
||||
let splitTunnelType: Int
|
||||
let splitTunnelSites: [String]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case initPacketMagicHeader = "H1", responsePacketMagicHeader = "H2"
|
||||
case underloadPacketMagicHeader = "H3", transportPacketMagicHeader = "H4"
|
||||
case junkPacketCount = "Jc", junkPacketMinSize = "Jmin", junkPacketMaxSize = "Jmax"
|
||||
case initPacketJunkSize = "S1", responsePacketJunkSize = "S2"
|
||||
case dns1
|
||||
case dns2
|
||||
case hostName
|
||||
case port
|
||||
case clientIP = "client_ip"
|
||||
case clientPrivateKey = "client_priv_key"
|
||||
case serverPublicKey = "server_pub_key"
|
||||
case presharedKey = "psk_key"
|
||||
case allowedIPs = "allowed_ips"
|
||||
case persistentKeepAlive = "persistent_keep_alive"
|
||||
case splitTunnelType
|
||||
case splitTunnelSites
|
||||
}
|
||||
|
||||
var settings: String {
|
||||
junkPacketCount == nil ? "" :
|
||||
"""
|
||||
Jc = \(junkPacketCount!)
|
||||
Jmin = \(junkPacketMinSize!)
|
||||
Jmax = \(junkPacketMaxSize!)
|
||||
S1 = \(initPacketJunkSize!)
|
||||
S2 = \(responsePacketJunkSize!)
|
||||
H1 = \(initPacketMagicHeader!)
|
||||
H2 = \(responsePacketMagicHeader!)
|
||||
H3 = \(underloadPacketMagicHeader!)
|
||||
H4 = \(transportPacketMagicHeader!)
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
var str: String {
|
||||
"""
|
||||
[Interface]
|
||||
Address = \(clientIP)
|
||||
DNS = \(dns1), \(dns2)
|
||||
PrivateKey = \(clientPrivateKey)
|
||||
\(settings)
|
||||
[Peer]
|
||||
PublicKey = \(serverPublicKey)
|
||||
PresharedKey = \(presharedKey)
|
||||
AllowedIPs = \(allowedIPs.joined(separator: ", "))
|
||||
Endpoint = \(hostName):\(port)
|
||||
PersistentKeepalive = \(persistentKeepAlive)
|
||||
"""
|
||||
}
|
||||
|
||||
var redux: String {
|
||||
"""
|
||||
[Interface]
|
||||
Address = \(clientIP)
|
||||
DNS = \(dns1), \(dns2)
|
||||
PrivateKey = ***
|
||||
\(settings)
|
||||
[Peer]
|
||||
PublicKey = ***
|
||||
PresharedKey = ***
|
||||
AllowedIPs = \(allowedIPs.joined(separator: ", "))
|
||||
Endpoint = \(hostName):\(port)
|
||||
PersistentKeepalive = \(persistentKeepAlive)
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenVPNConfig: Decodable {
|
||||
let config: String
|
||||
let splitTunnelType: Int
|
||||
let splitTunnelSites: [String]
|
||||
|
||||
var str: String {
|
||||
"splitTunnelType: \(splitTunnelType) splitTunnelSites: \(splitTunnelSites) config: \(config)"
|
||||
}
|
||||
}
|
||||
@@ -235,7 +235,6 @@ void IosController::checkStatus()
|
||||
m_rxBytes = rxBytes;
|
||||
m_txBytes = txBytes;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
void IosController::vpnStatusDidChange(void *pNotification)
|
||||
@@ -244,13 +243,13 @@ void IosController::vpnStatusDidChange(void *pNotification)
|
||||
|
||||
if (session /* && session == TunnelManager.session */ ) {
|
||||
qDebug() << "IosController::vpnStatusDidChange" << iosStatusToState(session.status) << session;
|
||||
|
||||
|
||||
if (session.status == NEVPNStatusDisconnected) {
|
||||
if (@available(iOS 16.0, *)) {
|
||||
[session fetchLastDisconnectErrorWithCompletionHandler:^(NSError * _Nullable error) {
|
||||
if (error != nil) {
|
||||
qDebug() << "Disconnect error" << error.domain << error.code << error.localizedDescription;
|
||||
|
||||
|
||||
if ([error.domain isEqualToString:NEVPNConnectionErrorDomain]) {
|
||||
switch (error.code) {
|
||||
case NEVPNConnectionErrorOverslept:
|
||||
@@ -315,11 +314,11 @@ void IosController::vpnStatusDidChange(void *pNotification)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
NSError *underlyingError = error.userInfo[@"NSUnderlyingError"];
|
||||
if (underlyingError != nil) {
|
||||
qDebug() << "Disconnect underlying error" << underlyingError.domain << underlyingError.code << underlyingError.localizedDescription;
|
||||
|
||||
|
||||
if ([underlyingError.domain isEqualToString:@"NEAgentErrorDomain"]) {
|
||||
switch (underlyingError.code) {
|
||||
case 1:
|
||||
@@ -342,7 +341,7 @@ void IosController::vpnStatusDidChange(void *pNotification)
|
||||
qDebug() << "Disconnect error is unavailable on iOS < 16.0";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
emit connectionStateChanged(iosStatusToState(session.status));
|
||||
}
|
||||
}
|
||||
@@ -357,7 +356,22 @@ bool IosController::setupOpenVPN()
|
||||
QJsonObject ovpn = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::OpenVpn)].toObject();
|
||||
QString ovpnConfig = ovpn[config_key::config].toString();
|
||||
|
||||
return startOpenVPN(ovpnConfig);
|
||||
QJsonObject openVPNConfig {};
|
||||
openVPNConfig.insert(config_key::config, ovpnConfig);
|
||||
openVPNConfig.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(" ");
|
||||
}
|
||||
|
||||
openVPNConfig.insert(config_key::splitTunnelSites, splitTunnelSites);
|
||||
|
||||
QJsonDocument openVPNConfigDoc(openVPNConfig);
|
||||
QString openVPNConfigStr(openVPNConfigDoc.toJson(QJsonDocument::Compact));
|
||||
|
||||
return startOpenVPN(openVPNConfigStr);
|
||||
}
|
||||
|
||||
bool IosController::setupCloak()
|
||||
@@ -394,25 +408,123 @@ bool IosController::setupCloak()
|
||||
ovpnConfig.append(cloakBase64);
|
||||
ovpnConfig.append("\n</cloak>\n");
|
||||
|
||||
return startOpenVPN(ovpnConfig);
|
||||
QJsonObject openVPNConfig {};
|
||||
openVPNConfig.insert(config_key::config, ovpnConfig);
|
||||
openVPNConfig.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(" ");
|
||||
}
|
||||
|
||||
openVPNConfig.insert(config_key::splitTunnelSites, splitTunnelSites);
|
||||
|
||||
QJsonDocument openVPNConfigDoc(openVPNConfig);
|
||||
QString openVPNConfigStr(openVPNConfigDoc.toJson(QJsonDocument::Compact));
|
||||
|
||||
return startOpenVPN(openVPNConfigStr);
|
||||
}
|
||||
|
||||
bool IosController::setupWireGuard()
|
||||
{
|
||||
QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::WireGuard)].toObject();
|
||||
|
||||
QString wgConfig = config[config_key::config].toString();
|
||||
|
||||
return startWireGuard(wgConfig);
|
||||
QJsonObject wgConfig {};
|
||||
wgConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1]);
|
||||
wgConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2]);
|
||||
wgConfig.insert(config_key::hostName, config[config_key::hostName]);
|
||||
wgConfig.insert(config_key::port, config[config_key::port]);
|
||||
wgConfig.insert(config_key::client_ip, config[config_key::client_ip]);
|
||||
wgConfig.insert(config_key::client_priv_key, config[config_key::client_priv_key]);
|
||||
wgConfig.insert(config_key::server_pub_key, config[config_key::server_pub_key]);
|
||||
wgConfig.insert(config_key::psk_key, config[config_key::psk_key]);
|
||||
wgConfig.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(" ");
|
||||
}
|
||||
|
||||
wgConfig.insert(config_key::splitTunnelSites, splitTunnelSites);
|
||||
|
||||
if (config.contains(config_key::allowed_ips)) {
|
||||
QJsonArray allowed_ips;
|
||||
QStringList allowed_ips_list = config[config_key::allowed_ips].toString().split(", ");
|
||||
|
||||
for(int index = 0; index < allowed_ips_list.length(); index++) {
|
||||
allowed_ips.append(allowed_ips_list[index]);
|
||||
}
|
||||
|
||||
wgConfig.insert(config_key::allowed_ips, allowed_ips);
|
||||
} else {
|
||||
QJsonArray allowed_ips { "0.0.0.0/0", "::/0" };
|
||||
wgConfig.insert(config_key::allowed_ips, allowed_ips);
|
||||
}
|
||||
|
||||
wgConfig.insert("persistent_keep_alive", "25");
|
||||
|
||||
QJsonDocument wgConfigDoc(wgConfig);
|
||||
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
|
||||
|
||||
return startWireGuard(wgConfigDocStr);
|
||||
}
|
||||
|
||||
bool IosController::setupAwg()
|
||||
{
|
||||
QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::Awg)].toObject();
|
||||
|
||||
QString wgConfig = config[config_key::config].toString();
|
||||
|
||||
return startWireGuard(wgConfig);
|
||||
QJsonObject wgConfig {};
|
||||
wgConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1]);
|
||||
wgConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2]);
|
||||
wgConfig.insert(config_key::hostName, config[config_key::hostName]);
|
||||
wgConfig.insert(config_key::port, config[config_key::port]);
|
||||
wgConfig.insert(config_key::client_ip, config[config_key::client_ip]);
|
||||
wgConfig.insert(config_key::client_priv_key, config[config_key::client_priv_key]);
|
||||
wgConfig.insert(config_key::server_pub_key, config[config_key::server_pub_key]);
|
||||
wgConfig.insert(config_key::psk_key, config[config_key::psk_key]);
|
||||
wgConfig.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(" ");
|
||||
}
|
||||
|
||||
wgConfig.insert(config_key::splitTunnelSites, splitTunnelSites);
|
||||
|
||||
if (config.contains(config_key::allowed_ips)) {
|
||||
QJsonArray allowed_ips;
|
||||
QStringList allowed_ips_list = config[config_key::allowed_ips].toString().split(", ");
|
||||
|
||||
for(int index = 0; index < allowed_ips_list.length(); index++) {
|
||||
allowed_ips.append(allowed_ips_list[index]);
|
||||
}
|
||||
|
||||
wgConfig.insert(config_key::allowed_ips, allowed_ips);
|
||||
} else {
|
||||
QJsonArray allowed_ips { "0.0.0.0/0", "::/0" };
|
||||
wgConfig.insert(config_key::allowed_ips, allowed_ips);
|
||||
}
|
||||
|
||||
wgConfig.insert("persistent_keep_alive", "25");
|
||||
wgConfig.insert(config_key::initPacketMagicHeader, config[config_key::initPacketMagicHeader]);
|
||||
wgConfig.insert(config_key::responsePacketMagicHeader, config[config_key::responsePacketMagicHeader]);
|
||||
wgConfig.insert(config_key::underloadPacketMagicHeader, config[config_key::underloadPacketMagicHeader]);
|
||||
wgConfig.insert(config_key::transportPacketMagicHeader, config[config_key::transportPacketMagicHeader]);
|
||||
|
||||
wgConfig.insert(config_key::initPacketJunkSize, config[config_key::initPacketJunkSize]);
|
||||
wgConfig.insert(config_key::responsePacketJunkSize, config[config_key::responsePacketJunkSize]);
|
||||
|
||||
wgConfig.insert(config_key::junkPacketCount, config[config_key::junkPacketCount]);
|
||||
wgConfig.insert(config_key::junkPacketMinSize, config[config_key::junkPacketMinSize]);
|
||||
wgConfig.insert(config_key::junkPacketMaxSize, config[config_key::junkPacketMaxSize]);
|
||||
|
||||
QJsonDocument wgConfigDoc(wgConfig);
|
||||
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
|
||||
|
||||
return startWireGuard(wgConfigDocStr);
|
||||
}
|
||||
|
||||
bool IosController::startOpenVPN(const QString &config)
|
||||
@@ -446,23 +558,17 @@ bool IosController::startWireGuard(const QString &config)
|
||||
void IosController::startTunnel()
|
||||
{
|
||||
NSString *protocolName = @"Unknown";
|
||||
|
||||
|
||||
NETunnelProviderProtocol *tunnelProtocol = (NETunnelProviderProtocol *)m_currentTunnel.protocolConfiguration;
|
||||
if (tunnelProtocol.providerConfiguration[@"wireguard"] != nil) {
|
||||
protocolName = @"WireGuard";
|
||||
} else if (tunnelProtocol.providerConfiguration[@"ovpn"] != nil) {
|
||||
protocolName = @"OpenVPN";
|
||||
}
|
||||
|
||||
|
||||
m_rxBytes = 0;
|
||||
m_txBytes = 0;
|
||||
|
||||
int STT = m_rawConfig["splitTunnelType"].toInt();
|
||||
QJsonArray splitTunnelSites = m_rawConfig["splitTunnelSites"].toArray();
|
||||
QJsonDocument doc;
|
||||
doc.setArray(splitTunnelSites);
|
||||
QString STS(doc.toJson());
|
||||
|
||||
|
||||
[m_currentTunnel setEnabled:YES];
|
||||
|
||||
[m_currentTunnel saveToPreferencesWithCompletionHandler:^(NSError *saveError) {
|
||||
@@ -483,23 +589,6 @@ void IosController::startTunnel()
|
||||
NSError *startError = nil;
|
||||
qDebug() << iosStatusToState(m_currentTunnel.connection.status);
|
||||
|
||||
|
||||
NSString *actionKey = [NSString stringWithUTF8String:MessageKey::action];
|
||||
NSString *actionValue = [NSString stringWithUTF8String:Action::start];
|
||||
NSString *tunnelIdKey = [NSString stringWithUTF8String:MessageKey::tunnelId];
|
||||
NSString *tunnelIdValue = !m_tunnelId.isEmpty() ? m_tunnelId.toNSString() : @"";
|
||||
NSString *SplitTunnelTypeKey = [NSString stringWithUTF8String:MessageKey::SplitTunnelType];
|
||||
NSString *SplitTunnelTypeValue = [NSString stringWithFormat:@"%d",STT];
|
||||
NSString *SplitTunnelSitesKey = [NSString stringWithUTF8String:MessageKey::SplitTunnelSites];
|
||||
NSString *SplitTunnelSitesValue = STS.toNSString();
|
||||
|
||||
|
||||
NSDictionary* message = @{actionKey: actionValue, tunnelIdKey: tunnelIdValue,
|
||||
SplitTunnelTypeKey: SplitTunnelTypeValue, SplitTunnelSitesKey: SplitTunnelSitesValue};
|
||||
|
||||
sendVpnExtensionMessage(message);
|
||||
|
||||
|
||||
BOOL started = [m_currentTunnel.connection startVPNTunnelWithOptions:nil andReturnError:&startError];
|
||||
|
||||
if (!started || startError) {
|
||||
@@ -514,7 +603,6 @@ void IosController::startTunnel()
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
bool IosController::isOurManager(NETunnelProviderManager* manager) {
|
||||
NETunnelProviderProtocol* tunnelProto = (NETunnelProviderProtocol*)manager.protocolConfiguration;
|
||||
|
||||
@@ -576,7 +664,7 @@ void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function
|
||||
NETunnelProviderSession *session = (NETunnelProviderSession *)m_currentTunnel.connection;
|
||||
|
||||
NSError *sendError = nil;
|
||||
|
||||
|
||||
if ([session respondsToSelector:@selector(sendProviderMessage:returnError:responseHandler:)]) {
|
||||
[session sendProviderMessage:data returnError:&sendError responseHandler:completionHandler];
|
||||
} else {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
|
||||
- (void) vpnConfigurationDidChange:(NSNotification *)notification {
|
||||
cppController->vpnStatusDidChange(notification);
|
||||
// cppController->vpnStatusDidChange(notification);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace amnezia
|
||||
constexpr char dns1[] = "dns1";
|
||||
constexpr char dns2[] = "dns2";
|
||||
|
||||
constexpr char serverIndex[] = "serverIndex";
|
||||
constexpr char description[] = "description";
|
||||
constexpr char name[] = "name";
|
||||
constexpr char cert[] = "cert";
|
||||
|
||||
@@ -160,7 +160,6 @@
|
||||
<file>ui/qml/Components/SettingsContainersListView.qml</file>
|
||||
<file>ui/qml/Controls2/TextTypes/ListItemTitleType.qml</file>
|
||||
<file>ui/qml/Controls2/DividerType.qml</file>
|
||||
<file>ui/qml/Controls2/DrawerType.qml</file>
|
||||
<file>ui/qml/Controls2/StackViewType.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettings.qml</file>
|
||||
<file>images/controls/amnezia.svg</file>
|
||||
@@ -225,5 +224,8 @@
|
||||
<file>ui/qml/Pages2/PageShareFullAccess.qml</file>
|
||||
<file>images/controls/close.svg</file>
|
||||
<file>images/controls/search.svg</file>
|
||||
<file>ui/qml/Components/HomeSplitTunnelingDrawer.qml</file>
|
||||
<file>images/controls/split-tunneling.svg</file>
|
||||
<file>ui/qml/Controls2/DrawerType2.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -1 +1 @@
|
||||
sudo docker build --no-cache --pull -t $CONTAINER_NAME $DOCKERFILE_FOLDER --build-arg SERVER_ARCH=$(uname -m)
|
||||
sudo docker build --no-cache --pull -t $CONTAINER_NAME $DOCKERFILE_FOLDER
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
FROM alpine:3.15
|
||||
LABEL maintainer="AmneziaVPN"
|
||||
|
||||
ARG SS_RELEASE="v1.13.1"
|
||||
ARG CLOAK_RELEASE="v2.5.5"
|
||||
ARG SERVER_ARCH
|
||||
ARG SS_RELEASE="v1.18.1"
|
||||
ARG CLOAK_RELEASE="v2.8.0"
|
||||
|
||||
#Install required packages
|
||||
RUN apk add --no-cache curl openvpn easy-rsa bash netcat-openbsd dumb-init rng-tools
|
||||
@@ -16,20 +15,19 @@ RUN mkdir -p /opt/amnezia
|
||||
RUN echo -e "#!/bin/bash\ntail -f /dev/null" > /opt/amnezia/start.sh
|
||||
RUN chmod a+x /opt/amnezia/start.sh
|
||||
|
||||
RUN if [ $SERVER_ARCH="x86_64" ]; then CK_ARCH="amd64"; \
|
||||
elif [ $SERVER_ARCH="i686" ]; then CK_ARCH="386"; \
|
||||
elif [ $SERVER_ARCH="aarch64" ]; then CK_ARCH="arm64"; \
|
||||
elif [ $SERVER_ARCH="arm" ]; then CK_ARCH="arm"; \
|
||||
else exit -1; fi && \
|
||||
curl -L https://github.com/cbeuw/Cloak/releases/download/${CLOAK_RELEASE}/ck-server-linux-${CK_ARCH}-${CLOAK_RELEASE} > /usr/bin/ck-server
|
||||
RUN chmod a+x /usr/bin/ck-server
|
||||
RUN SERVER_ARCH=$(uname -m) && \
|
||||
if [ $SERVER_ARCH="x86_64" ]; then CK_ARCH="amd64"; \
|
||||
elif [ $SERVER_ARCH="i686" ]; then CK_ARCH="386"; \
|
||||
elif [ $SERVER_ARCH="aarch64" ]; then CK_ARCH="arm64"; \
|
||||
elif [ $SERVER_ARCH="arm" ]; then CK_ARCH="arm"; \
|
||||
else exit -1; fi && \
|
||||
curl -L https://github.com/cbeuw/Cloak/releases/download/${CLOAK_RELEASE}/ck-server-linux-${CK_ARCH}-${CLOAK_RELEASE} > /usr/bin/ck-server && \
|
||||
chmod a+x /usr/bin/ck-server && \
|
||||
curl -L https://github.com/shadowsocks/shadowsocks-rust/releases/download/${SS_RELEASE}/shadowsocks-${SS_RELEASE}.${SERVER_ARCH}-unknown-linux-musl.tar.xz > /usr/bin/ss.tar.xz && \
|
||||
tar -Jxvf /usr/bin/ss.tar.xz -C /usr/bin/ && \
|
||||
chmod a+x /usr/bin/ssserver
|
||||
|
||||
RUN curl -L https://github.com/shadowsocks/shadowsocks-rust/releases/download/${SS_RELEASE}/shadowsocks-${SS_RELEASE}.${SERVER_ARCH}-unknown-linux-musl.tar.xz > /usr/bin/ss.tar.xz
|
||||
|
||||
RUN tar -Jxvf /usr/bin/ss.tar.xz -C /usr/bin/
|
||||
RUN chmod a+x /usr/bin/ssserver
|
||||
|
||||
# Tune network
|
||||
# Tune network
|
||||
RUN echo -e " \n\
|
||||
fs.file-max = 51200 \n\
|
||||
\n\
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
FROM alpine:3.15
|
||||
LABEL maintainer="AmneziaVPN"
|
||||
|
||||
ARG SS_RELEASE="v1.13.1"
|
||||
ARG SERVER_ARCH
|
||||
ARG SS_RELEASE="v1.18.1"
|
||||
|
||||
#Install required packages
|
||||
RUN apk add --no-cache curl openvpn easy-rsa bash netcat-openbsd dumb-init rng-tools xz
|
||||
@@ -15,7 +14,16 @@ RUN mkdir -p /opt/amnezia
|
||||
RUN echo -e "#!/bin/bash\ntail -f /dev/null" > /opt/amnezia/start.sh
|
||||
RUN chmod a+x /opt/amnezia/start.sh
|
||||
|
||||
RUN curl -L https://github.com/shadowsocks/shadowsocks-rust/releases/download/${SS_RELEASE}/shadowsocks-${SS_RELEASE}.${SERVER_ARCH}-unknown-linux-musl.tar.xz > /usr/bin/ss.tar.xz;\
|
||||
RUN SERVER_ARCH=$(uname -m); \
|
||||
SUFFIX=""; \
|
||||
if [ ! -z "$(echo ${SERVER_ARCH} | grep -i arm)" ]; then \
|
||||
if [ ! -z "$(cat /proc/cpuinfo | grep -i vfp)" ]; then \
|
||||
SUFFIX="eabihf"; \
|
||||
else \
|
||||
SUFFIX="eabi"; \
|
||||
fi; \
|
||||
fi; \
|
||||
curl -L https://github.com/shadowsocks/shadowsocks-rust/releases/download/${SS_RELEASE}/shadowsocks-${SS_RELEASE}.${SERVER_ARCH}-unknown-linux-musl${SUFFIX}.tar.xz > /usr/bin/ss.tar.xz;\
|
||||
tar -Jxvf /usr/bin/ss.tar.xz -C /usr/bin/;\
|
||||
chmod a+x /usr/bin/ssserver;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker stop
|
||||
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker rm -fv
|
||||
sudo docker images -a | grep amnezia | awk '{print $3}' | xargs sudo docker rmi
|
||||
sudo docker network ls | grep amnezia-dns-net | awk '{print $1}' | xargs sudo docker network rm
|
||||
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker stop;\
|
||||
sudo docker ps -a | grep amnezia | awk '{print $1}' | xargs sudo docker rm -fv;\
|
||||
sudo docker images -a | grep amnezia | awk '{print $3}' | xargs sudo docker rmi;\
|
||||
sudo docker network ls | grep amnezia-dns-net | awk '{print $1}' | xargs sudo docker network rm;\
|
||||
sudo rm -frd /opt/amnezia
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
sudo docker stop $CONTAINER_NAME
|
||||
sudo docker rm -fv $CONTAINER_NAME
|
||||
sudo docker stop $CONTAINER_NAME;\
|
||||
sudo docker rm -fv $CONTAINER_NAME;\
|
||||
sudo docker rmi $CONTAINER_NAME
|
||||
|
||||
@@ -68,6 +68,7 @@ void Settings::removeServer(int index)
|
||||
|
||||
servers.removeAt(index);
|
||||
setServersArray(servers);
|
||||
emit serverRemoved(index);
|
||||
}
|
||||
|
||||
bool Settings::editServer(int index, const QJsonObject &server)
|
||||
@@ -338,6 +339,7 @@ QString Settings::secondaryDns() const
|
||||
void Settings::clearSettings()
|
||||
{
|
||||
m_settings.clearSettings();
|
||||
emit settingsCleared();
|
||||
}
|
||||
|
||||
ServerCredentials Settings::defaultServerCredentials() const
|
||||
|
||||
@@ -185,12 +185,16 @@ public:
|
||||
void setScreenshotsEnabled(bool enabled)
|
||||
{
|
||||
setValue("Conf/screenshotsEnabled", enabled);
|
||||
emit screenshotsEnabledChanged(enabled);
|
||||
}
|
||||
|
||||
void clearSettings();
|
||||
|
||||
signals:
|
||||
void saveLogsChanged(bool enabled);
|
||||
void screenshotsEnabledChanged(bool enabled);
|
||||
void serverRemoved(int serverIndex);
|
||||
void settingsCleared();
|
||||
|
||||
private:
|
||||
QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const;
|
||||
|
||||
3297
client/translations/amneziavpn_ar.ts
Normal file
3297
client/translations/amneziavpn_ar.ts
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3192
client/translations/amneziavpn_my_MM.ts
Normal file
3192
client/translations/amneziavpn_my_MM.ts
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -70,14 +70,17 @@ QJsonObject ApiController::fillApiPayload(const QString &protocol, const ApiCont
|
||||
void ApiController::updateServerConfigFromApi()
|
||||
{
|
||||
QtConcurrent::run([this]() {
|
||||
if (m_isConfigUpdateStarted) {
|
||||
emit updateFinished(false);
|
||||
return;
|
||||
}
|
||||
|
||||
auto serverConfig = m_serversModel->getDefaultServerConfig();
|
||||
auto containerConfig = serverConfig.value(config_key::containers).toArray();
|
||||
|
||||
bool isConfigUpdateStarted = false;
|
||||
|
||||
if (serverConfig.value(config_key::configVersion).toInt() && containerConfig.isEmpty()) {
|
||||
emit updateStarted();
|
||||
isConfigUpdateStarted = true;
|
||||
m_isConfigUpdateStarted = true;
|
||||
|
||||
QNetworkAccessManager manager;
|
||||
|
||||
@@ -87,7 +90,7 @@ void ApiController::updateServerConfigFromApi()
|
||||
request.setRawHeader("Authorization",
|
||||
"Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8());
|
||||
QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString();
|
||||
request.setUrl(endpoint.replace("https", "http")); // todo remove
|
||||
request.setUrl(endpoint);
|
||||
|
||||
QString protocol = serverConfig.value(configKey::protocol).toString();
|
||||
|
||||
@@ -110,6 +113,12 @@ void ApiController::updateServerConfigFromApi()
|
||||
QByteArray ba = QByteArray::fromBase64(data.toUtf8(),
|
||||
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||
|
||||
if (ba.isEmpty()) {
|
||||
emit errorOccurred(errorString(ApiConfigDownloadError));
|
||||
m_isConfigUpdateStarted = false;
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray ba_uncompressed = qUncompress(ba);
|
||||
if (!ba_uncompressed.isEmpty()) {
|
||||
ba = ba_uncompressed;
|
||||
@@ -127,17 +136,21 @@ void ApiController::updateServerConfigFromApi()
|
||||
|
||||
auto defaultContainer = apiConfig.value(config_key::defaultContainer).toString();
|
||||
serverConfig.insert(config_key::defaultContainer, defaultContainer);
|
||||
m_serversModel->editServer(serverConfig);
|
||||
emit m_serversModel->defaultContainerChanged(ContainerProps::containerFromString(defaultContainer));
|
||||
m_serversModel->editServer(serverConfig, m_serversModel->getDefaultServerIndex());
|
||||
} else {
|
||||
QString err = reply->errorString();
|
||||
qDebug() << QString::fromUtf8(reply->readAll());
|
||||
qDebug() << reply->error();
|
||||
qDebug() << err;
|
||||
qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
||||
emit errorOccurred(errorString(ApiConfigDownloadError));
|
||||
m_isConfigUpdateStarted = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
emit updateFinished(isConfigUpdateStarted);
|
||||
emit updateFinished(m_isConfigUpdateStarted);
|
||||
m_isConfigUpdateStarted = false;
|
||||
return;
|
||||
});
|
||||
}
|
||||
@@ -153,5 +166,5 @@ void ApiController::clearApiConfig()
|
||||
|
||||
serverConfig.insert(config_key::defaultContainer, ContainerProps::containerToString(DockerContainer::None));
|
||||
|
||||
m_serversModel->editServer(serverConfig);
|
||||
m_serversModel->editServer(serverConfig, m_serversModel->getDefaultServerIndex());
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ private:
|
||||
|
||||
QSharedPointer<ServersModel> m_serversModel;
|
||||
QSharedPointer<ContainersModel> m_containersModel;
|
||||
|
||||
bool m_isConfigUpdateStarted = false;
|
||||
};
|
||||
|
||||
#endif // APICONTROLLER_H
|
||||
|
||||
@@ -25,15 +25,16 @@ ConnectionController::ConnectionController(const QSharedPointer<ServersModel> &s
|
||||
|
||||
void ConnectionController::openConnection()
|
||||
{
|
||||
if (!m_containersModel->isAnyContainerInstalled()) {
|
||||
int serverIndex = m_serversModel->getDefaultServerIndex();
|
||||
|
||||
if (!m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
|
||||
emit noInstalledContainers();
|
||||
return;
|
||||
}
|
||||
|
||||
int serverIndex = m_serversModel->getDefaultServerIndex();
|
||||
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
|
||||
|
||||
DockerContainer container = m_containersModel->getDefaultContainer();
|
||||
DockerContainer container = qvariant_cast<DockerContainer>(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole));
|
||||
const QJsonObject &containerConfig = m_containersModel->getContainerConfig(container);
|
||||
|
||||
if (container == DockerContainer::None) {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <QImage>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include "configurators/awg_configurator.h"
|
||||
#include "configurators/cloak_configurator.h"
|
||||
#include "configurators/openvpn_configurator.h"
|
||||
#include "configurators/shadowsocks_configurator.h"
|
||||
@@ -45,7 +46,7 @@ void ExportController::generateFullAccessConfig()
|
||||
{
|
||||
clearPreviousConfig();
|
||||
|
||||
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
|
||||
int serverIndex = m_serversModel->getProcessedServerIndex();
|
||||
QJsonObject config = m_settings->server(serverIndex);
|
||||
|
||||
QJsonArray containers = config.value(config_key::containers).toArray();
|
||||
@@ -99,7 +100,7 @@ void ExportController::generateConnectionConfig(const QString &clientName)
|
||||
{
|
||||
clearPreviousConfig();
|
||||
|
||||
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
|
||||
int serverIndex = m_serversModel->getProcessedServerIndex();
|
||||
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
|
||||
|
||||
DockerContainer container = static_cast<DockerContainer>(m_containersModel->getCurrentlyProcessedContainerIndex());
|
||||
@@ -155,7 +156,7 @@ void ExportController::generateOpenVpnConfig(const QString &clientName)
|
||||
{
|
||||
clearPreviousConfig();
|
||||
|
||||
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
|
||||
int serverIndex = m_serversModel->getProcessedServerIndex();
|
||||
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
|
||||
|
||||
DockerContainer container = static_cast<DockerContainer>(m_containersModel->getCurrentlyProcessedContainerIndex());
|
||||
@@ -193,7 +194,7 @@ void ExportController::generateWireGuardConfig(const QString &clientName)
|
||||
{
|
||||
clearPreviousConfig();
|
||||
|
||||
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
|
||||
int serverIndex = m_serversModel->getProcessedServerIndex();
|
||||
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
|
||||
|
||||
DockerContainer container = static_cast<DockerContainer>(m_containersModel->getCurrentlyProcessedContainerIndex());
|
||||
@@ -228,11 +229,50 @@ void ExportController::generateWireGuardConfig(const QString &clientName)
|
||||
emit exportConfigChanged();
|
||||
}
|
||||
|
||||
void ExportController::generateAwgConfig(const QString &clientName)
|
||||
{
|
||||
clearPreviousConfig();
|
||||
|
||||
int serverIndex = m_serversModel->getProcessedServerIndex();
|
||||
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
|
||||
|
||||
DockerContainer container = static_cast<DockerContainer>(m_containersModel->getCurrentlyProcessedContainerIndex());
|
||||
QJsonObject containerConfig = m_containersModel->getContainerConfig(container);
|
||||
containerConfig.insert(config_key::container, ContainerProps::containerToString(container));
|
||||
|
||||
QString clientId;
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
QString config = m_configurator->awgConfigurator->genAwgConfig(credentials, container, containerConfig,
|
||||
clientId, &errorCode);
|
||||
if (errorCode) {
|
||||
emit exportErrorOccurred(errorString(errorCode));
|
||||
return;
|
||||
}
|
||||
config = m_configurator->processConfigWithExportSettings(serverIndex, container, Proto::Awg, config);
|
||||
|
||||
auto configJson = QJsonDocument::fromJson(config.toUtf8()).object();
|
||||
QStringList lines = configJson.value(config_key::config).toString().replace("\r", "").split("\n");
|
||||
for (const QString &line : lines) {
|
||||
m_config.append(line + "\n");
|
||||
}
|
||||
|
||||
qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(m_config.toUtf8(), qrcodegen::QrCode::Ecc::LOW);
|
||||
m_qrCodes << svgToBase64(QString::fromStdString(toSvgString(qr, 1)));
|
||||
|
||||
errorCode = m_clientManagementModel->appendClient(clientId, clientName, container, credentials);
|
||||
if (errorCode) {
|
||||
emit exportErrorOccurred(errorString(errorCode));
|
||||
return;
|
||||
}
|
||||
|
||||
emit exportConfigChanged();
|
||||
}
|
||||
|
||||
void ExportController::generateShadowSocksConfig()
|
||||
{
|
||||
clearPreviousConfig();
|
||||
|
||||
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
|
||||
int serverIndex = m_serversModel->getProcessedServerIndex();
|
||||
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
|
||||
|
||||
DockerContainer container = static_cast<DockerContainer>(m_containersModel->getCurrentlyProcessedContainerIndex());
|
||||
@@ -268,7 +308,7 @@ void ExportController::generateCloakConfig()
|
||||
{
|
||||
clearPreviousConfig();
|
||||
|
||||
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
|
||||
int serverIndex = m_serversModel->getProcessedServerIndex();
|
||||
ServerCredentials credentials = m_serversModel->getServerCredentials(serverIndex);
|
||||
|
||||
DockerContainer container = static_cast<DockerContainer>(m_containersModel->getCurrentlyProcessedContainerIndex());
|
||||
@@ -328,7 +368,7 @@ void ExportController::updateClientManagementModel(const DockerContainer contain
|
||||
void ExportController::revokeConfig(const int row, const DockerContainer container, ServerCredentials credentials)
|
||||
{
|
||||
ErrorCode errorCode = m_clientManagementModel->revokeClient(row, container, credentials,
|
||||
m_serversModel->getCurrentlyProcessedServerIndex());
|
||||
m_serversModel->getProcessedServerIndex());
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
emit exportErrorOccurred(errorString(errorCode));
|
||||
}
|
||||
@@ -378,4 +418,6 @@ void ExportController::clearPreviousConfig()
|
||||
m_config.clear();
|
||||
m_nativeConfigString.clear();
|
||||
m_qrCodes.clear();
|
||||
|
||||
emit exportConfigChanged();
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ public slots:
|
||||
void generateConnectionConfig(const QString &clientName);
|
||||
void generateOpenVpnConfig(const QString &clientName);
|
||||
void generateWireGuardConfig(const QString &clientName);
|
||||
void generateAwgConfig(const QString &clientName);
|
||||
void generateShadowSocksConfig();
|
||||
void generateCloakConfig();
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ void InstallController::installServer(DockerContainer container, QJsonObject &co
|
||||
|
||||
void InstallController::installContainer(DockerContainer container, QJsonObject &config)
|
||||
{
|
||||
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
|
||||
int serverIndex = m_serversModel->getProcessedServerIndex();
|
||||
ServerCredentials serverCredentials =
|
||||
qvariant_cast<ServerCredentials>(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole));
|
||||
|
||||
@@ -238,7 +238,7 @@ bool InstallController::isServerAlreadyExists()
|
||||
|
||||
void InstallController::scanServerForInstalledContainers()
|
||||
{
|
||||
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
|
||||
int serverIndex = m_serversModel->getProcessedServerIndex();
|
||||
ServerCredentials serverCredentials =
|
||||
qvariant_cast<ServerCredentials>(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole));
|
||||
|
||||
@@ -267,7 +267,7 @@ void InstallController::scanServerForInstalledContainers()
|
||||
|
||||
void InstallController::updateContainer(QJsonObject config)
|
||||
{
|
||||
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
|
||||
int serverIndex = m_serversModel->getProcessedServerIndex();
|
||||
ServerCredentials serverCredentials =
|
||||
qvariant_cast<ServerCredentials>(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole));
|
||||
|
||||
@@ -283,8 +283,8 @@ void InstallController::updateContainer(QJsonObject config)
|
||||
m_serversModel->updateContainerConfig(container, config);
|
||||
m_protocolModel->updateModel(config);
|
||||
|
||||
if ((serverIndex == m_serversModel->getDefaultServerIndex())
|
||||
&& (container == m_containersModel->getDefaultContainer())) {
|
||||
auto defaultContainer = qvariant_cast<DockerContainer>(m_serversModel->data(serverIndex, ServersModel::Roles::DefaultContainerRole));
|
||||
if ((serverIndex == m_serversModel->getDefaultServerIndex()) && (container == defaultContainer)) {
|
||||
emit currentContainerUpdated();
|
||||
} else {
|
||||
emit updateContainerFinished(tr("Settings updated successfully"));
|
||||
@@ -296,27 +296,27 @@ void InstallController::updateContainer(QJsonObject config)
|
||||
emit installationErrorOccurred(errorString(errorCode));
|
||||
}
|
||||
|
||||
void InstallController::rebootCurrentlyProcessedServer()
|
||||
void InstallController::rebootProcessedServer()
|
||||
{
|
||||
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
|
||||
int serverIndex = m_serversModel->getProcessedServerIndex();
|
||||
QString serverName = m_serversModel->data(serverIndex, ServersModel::Roles::NameRole).toString();
|
||||
|
||||
m_serversModel->rebootServer();
|
||||
emit rebootCurrentlyProcessedServerFinished(tr("Server '%1' was rebooted").arg(serverName));
|
||||
emit rebootProcessedServerFinished(tr("Server '%1' was rebooted").arg(serverName));
|
||||
}
|
||||
|
||||
void InstallController::removeCurrentlyProcessedServer()
|
||||
void InstallController::removeProcessedServer()
|
||||
{
|
||||
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
|
||||
int serverIndex = m_serversModel->getProcessedServerIndex();
|
||||
QString serverName = m_serversModel->data(serverIndex, ServersModel::Roles::NameRole).toString();
|
||||
|
||||
m_serversModel->removeServer();
|
||||
emit removeCurrentlyProcessedServerFinished(tr("Server '%1' was removed").arg(serverName));
|
||||
emit removeProcessedServerFinished(tr("Server '%1' was removed").arg(serverName));
|
||||
}
|
||||
|
||||
void InstallController::removeAllContainers()
|
||||
{
|
||||
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
|
||||
int serverIndex = m_serversModel->getProcessedServerIndex();
|
||||
QString serverName = m_serversModel->data(serverIndex, ServersModel::Roles::NameRole).toString();
|
||||
|
||||
ErrorCode errorCode = m_serversModel->removeAllContainers();
|
||||
@@ -329,7 +329,7 @@ void InstallController::removeAllContainers()
|
||||
|
||||
void InstallController::removeCurrentlyProcessedContainer()
|
||||
{
|
||||
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
|
||||
int serverIndex = m_serversModel->getProcessedServerIndex();
|
||||
QString serverName = m_serversModel->data(serverIndex, ServersModel::Roles::NameRole).toString();
|
||||
|
||||
int container = m_containersModel->getCurrentlyProcessedContainerIndex();
|
||||
@@ -377,7 +377,7 @@ void InstallController::mountSftpDrive(const QString &port, const QString &passw
|
||||
QString mountPath;
|
||||
QString cmd;
|
||||
|
||||
int serverIndex = m_serversModel->getCurrentlyProcessedServerIndex();
|
||||
int serverIndex = m_serversModel->getProcessedServerIndex();
|
||||
ServerCredentials serverCredentials =
|
||||
qvariant_cast<ServerCredentials>(m_serversModel->data(serverIndex, ServersModel::Roles::CredentialsRole));
|
||||
QString hostname = serverCredentials.hostName;
|
||||
|
||||
@@ -30,8 +30,8 @@ public slots:
|
||||
|
||||
void updateContainer(QJsonObject config);
|
||||
|
||||
void removeCurrentlyProcessedServer();
|
||||
void rebootCurrentlyProcessedServer();
|
||||
void removeProcessedServer();
|
||||
void rebootProcessedServer();
|
||||
void removeAllContainers();
|
||||
void removeCurrentlyProcessedContainer();
|
||||
|
||||
@@ -54,8 +54,8 @@ signals:
|
||||
|
||||
void scanServerFinished(bool isInstalledContainerFound);
|
||||
|
||||
void rebootCurrentlyProcessedServerFinished(const QString &finishedMessage);
|
||||
void removeCurrentlyProcessedServerFinished(const QString &finishedMessage);
|
||||
void rebootProcessedServerFinished(const QString &finishedMessage);
|
||||
void removeProcessedServerFinished(const QString &finishedMessage);
|
||||
void removeAllContainersFinished(const QString &finishedMessage);
|
||||
void removeCurrentlyProcessedContainerFinished(const QString &finishedMessage);
|
||||
|
||||
|
||||
@@ -77,7 +77,16 @@ void PageController::closeWindow()
|
||||
void PageController::keyPressEvent(Qt::Key key)
|
||||
{
|
||||
switch (key) {
|
||||
case Qt::Key_Back: emit closePage();
|
||||
case Qt::Key_Back:
|
||||
case Qt::Key_Escape: {
|
||||
if (m_drawerDepth) {
|
||||
emit closeTopDrawer();
|
||||
setDrawerDepth(getDrawerDepth() - 1);
|
||||
} else {
|
||||
emit escapePressed();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
@@ -118,42 +127,12 @@ void PageController::showOnStartup()
|
||||
}
|
||||
}
|
||||
|
||||
void PageController::updateDrawerRootPage(PageLoader::PageEnum page)
|
||||
{
|
||||
m_drawerLayer = 0;
|
||||
m_currentRootPage = page;
|
||||
}
|
||||
|
||||
void PageController::goToDrawerRootPage()
|
||||
{
|
||||
|
||||
m_drawerLayer = 0;
|
||||
|
||||
emit showTopCloseButton(false);
|
||||
emit forceCloseDrawer();
|
||||
}
|
||||
|
||||
void PageController::drawerOpen()
|
||||
{
|
||||
m_drawerLayer = m_drawerLayer + 1;
|
||||
emit showTopCloseButton(true);
|
||||
}
|
||||
|
||||
void PageController::drawerClose()
|
||||
{
|
||||
m_drawerLayer = m_drawerLayer -1;
|
||||
if (m_drawerLayer <= 0) {
|
||||
emit showTopCloseButton(false);
|
||||
m_drawerLayer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool PageController::isTriggeredByConnectButton()
|
||||
{
|
||||
return m_isTriggeredByConnectButton;
|
||||
}
|
||||
|
||||
void PageController::setTriggeredBtConnectButton(bool trigger)
|
||||
void PageController::setTriggeredByConnectButton(bool trigger)
|
||||
{
|
||||
m_isTriggeredByConnectButton = trigger;
|
||||
}
|
||||
@@ -162,3 +141,15 @@ void PageController::closeApplication()
|
||||
{
|
||||
qApp->quit();
|
||||
}
|
||||
|
||||
void PageController::setDrawerDepth(const int depth)
|
||||
{
|
||||
if (depth >= 0) {
|
||||
m_drawerDepth = depth;
|
||||
}
|
||||
}
|
||||
|
||||
int PageController::getDrawerDepth()
|
||||
{
|
||||
return m_drawerDepth;
|
||||
}
|
||||
|
||||
@@ -82,16 +82,14 @@ public slots:
|
||||
|
||||
void showOnStartup();
|
||||
|
||||
void updateDrawerRootPage(PageLoader::PageEnum page);
|
||||
void goToDrawerRootPage();
|
||||
void drawerOpen();
|
||||
void drawerClose();
|
||||
|
||||
bool isTriggeredByConnectButton();
|
||||
void setTriggeredBtConnectButton(bool trigger);
|
||||
void setTriggeredByConnectButton(bool trigger);
|
||||
|
||||
void closeApplication();
|
||||
|
||||
void setDrawerDepth(const int depth);
|
||||
int getDrawerDepth();
|
||||
|
||||
signals:
|
||||
void goToPage(PageLoader::PageEnum page, bool slide = true);
|
||||
void goToStartPage();
|
||||
@@ -110,7 +108,7 @@ signals:
|
||||
void showNotificationMessage(const QString &message);
|
||||
|
||||
void showBusyIndicator(bool visible);
|
||||
void enableTabBar(bool enabled);
|
||||
void disableControls(bool disabled);
|
||||
|
||||
void hideMainWindow();
|
||||
void raiseMainWindow();
|
||||
@@ -118,18 +116,17 @@ signals:
|
||||
void showPassphraseRequestDrawer();
|
||||
void passphraseRequestDrawerClosed(QString passphrase);
|
||||
|
||||
void showTopCloseButton(bool visible);
|
||||
void forceCloseDrawer();
|
||||
void escapePressed();
|
||||
void closeTopDrawer();
|
||||
|
||||
private:
|
||||
QSharedPointer<ServersModel> m_serversModel;
|
||||
|
||||
std::shared_ptr<Settings> m_settings;
|
||||
|
||||
PageLoader::PageEnum m_currentRootPage;
|
||||
int m_drawerLayer;
|
||||
|
||||
bool m_isTriggeredByConnectButton;
|
||||
|
||||
int m_drawerDepth = 0;
|
||||
};
|
||||
|
||||
#endif // PAGECONTROLLER_H
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
#include "ui/qautostart.h"
|
||||
#include "version.h"
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "platforms/android/android_utils.h"
|
||||
#include "platforms/android/android_controller.h"
|
||||
#include <QJniObject>
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
@@ -28,21 +26,7 @@ SettingsController::SettingsController(const QSharedPointer<ServersModel> &serve
|
||||
m_sitesModel(sitesModel),
|
||||
m_settings(settings)
|
||||
{
|
||||
m_appVersion = QString("%1: %2 (%3)").arg(tr("Software version"), QString(APP_VERSION), __DATE__);
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
if (!m_settings->isScreenshotsEnabled()) {
|
||||
// Set security screen for Android app
|
||||
AndroidUtils::runOnAndroidThreadSync([]() {
|
||||
QJniObject activity = AndroidUtils::getActivity();
|
||||
QJniObject window = activity.callObjectMethod("getWindow", "()Landroid/view/Window;");
|
||||
if (window.isValid()) {
|
||||
const int FLAG_SECURE = 8192;
|
||||
window.callMethod<void>("addFlags", "(I)V", FLAG_SECURE);
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
m_appVersion = QString("%1 (%2, %3)").arg(QString(APP_VERSION), __DATE__, GIT_COMMIT_HASH);
|
||||
}
|
||||
|
||||
void SettingsController::toggleAmneziaDns(bool enable)
|
||||
@@ -152,7 +136,12 @@ void SettingsController::clearSettings()
|
||||
m_languageModel->changeLanguage(
|
||||
static_cast<LanguageSettings::AvailableLanguageEnum>(m_languageModel->getCurrentLanguageIndex()));
|
||||
m_sitesModel->setRouteMode(Settings::RouteMode::VpnAllSites);
|
||||
|
||||
emit changeSettingsFinished(tr("All settings have been reset to default values"));
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
AmneziaVPN::clearSettings();
|
||||
#endif
|
||||
}
|
||||
|
||||
void SettingsController::clearCachedProfiles()
|
||||
@@ -199,19 +188,6 @@ bool SettingsController::isScreenshotsEnabled()
|
||||
void SettingsController::toggleScreenshotsEnabled(bool enable)
|
||||
{
|
||||
m_settings->setScreenshotsEnabled(enable);
|
||||
#ifdef Q_OS_ANDROID
|
||||
std::string command = enable ? "clearFlags" : "addFlags";
|
||||
|
||||
// Set security screen for Android app
|
||||
AndroidUtils::runOnAndroidThreadSync([&command]() {
|
||||
QJniObject activity = AndroidUtils::getActivity();
|
||||
QJniObject window = activity.callObjectMethod("getWindow", "()Landroid/view/Window;");
|
||||
if (window.isValid()) {
|
||||
const int FLAG_SECURE = 8192;
|
||||
window.callMethod<void>(command.c_str(), "(I)V", FLAG_SECURE);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SettingsController::isCameraPresent()
|
||||
|
||||
@@ -92,11 +92,11 @@ QString SystemController::getFileName(const QString &acceptLabel, const QString
|
||||
|
||||
mainFileDialog->setProperty("acceptLabel", QVariant::fromValue(acceptLabel));
|
||||
mainFileDialog->setProperty("nameFilters", QVariant::fromValue(QStringList(nameFilter)));
|
||||
if (!selectedFile.isEmpty()) {
|
||||
mainFileDialog->setProperty("selectedFile", QVariant::fromValue(selectedFile));
|
||||
}
|
||||
mainFileDialog->setProperty("isSaveMode", QVariant::fromValue(isSaveMode));
|
||||
mainFileDialog->setProperty("defaultSuffix", QVariant::fromValue(defaultSuffix));
|
||||
mainFileDialog->setProperty("isSaveMode", QVariant::fromValue(isSaveMode));
|
||||
if (!selectedFile.isEmpty()) {
|
||||
mainFileDialog->setProperty("selectedFile", QVariant::fromValue(QUrl(selectedFile)));
|
||||
}
|
||||
QMetaObject::invokeMethod(mainFileDialog, "open");
|
||||
|
||||
bool isFileDialogAccepted = false;
|
||||
|
||||
@@ -39,7 +39,6 @@ QVariant ContainersModel::data(const QModelIndex &index, int role) const
|
||||
case EasySetupOrderRole: return ContainerProps::easySetupOrder(container);
|
||||
case IsInstalledRole: return m_containers.contains(container);
|
||||
case IsCurrentlyProcessedRole: return container == static_cast<DockerContainer>(m_currentlyProcessedContainerIndex);
|
||||
case IsDefaultRole: return container == m_defaultContainerIndex;
|
||||
case IsSupportedRole: return ContainerProps::isSupportedByCurrentPlatform(container);
|
||||
case IsShareableRole: return ContainerProps::isShareable(container);
|
||||
}
|
||||
@@ -64,18 +63,6 @@ void ContainersModel::updateModel(const QJsonArray &containers)
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void ContainersModel::setDefaultContainer(const int containerIndex)
|
||||
{
|
||||
m_defaultContainerIndex = static_cast<DockerContainer>(containerIndex);
|
||||
emit dataChanged(index(containerIndex, 0), index(containerIndex, 0));
|
||||
}
|
||||
|
||||
|
||||
DockerContainer ContainersModel::getDefaultContainer()
|
||||
{
|
||||
return m_defaultContainerIndex;
|
||||
}
|
||||
|
||||
void ContainersModel::setCurrentlyProcessedContainerIndex(int index)
|
||||
{
|
||||
m_currentlyProcessedContainerIndex = index;
|
||||
@@ -96,20 +83,6 @@ QJsonObject ContainersModel::getContainerConfig(const int containerIndex)
|
||||
return qvariant_cast<QJsonObject>(data(index(containerIndex), ConfigRole));
|
||||
}
|
||||
|
||||
bool ContainersModel::isAnyContainerInstalled()
|
||||
{
|
||||
for (int row=0; row < rowCount(); row++) {
|
||||
QModelIndex idx = this->index(row, 0);
|
||||
|
||||
if (this->data(idx, IsInstalledRole).toBool() &&
|
||||
this->data(idx, ServiceTypeRole).toInt() == ServiceType::Vpn) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ContainersModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
@@ -127,7 +100,6 @@ QHash<int, QByteArray> ContainersModel::roleNames() const
|
||||
|
||||
roles[IsInstalledRole] = "isInstalled";
|
||||
roles[IsCurrentlyProcessedRole] = "isCurrentlyProcessed";
|
||||
roles[IsDefaultRole] = "isDefault";
|
||||
roles[IsSupportedRole] = "isSupported";
|
||||
roles[IsShareableRole] = "isShareable";
|
||||
return roles;
|
||||
|
||||
@@ -42,9 +42,6 @@ public:
|
||||
public slots:
|
||||
void updateModel(const QJsonArray &containers);
|
||||
|
||||
DockerContainer getDefaultContainer();
|
||||
void setDefaultContainer(const int containerIndex);
|
||||
|
||||
void setCurrentlyProcessedContainerIndex(int containerIndex);
|
||||
int getCurrentlyProcessedContainerIndex();
|
||||
|
||||
@@ -52,20 +49,16 @@ public slots:
|
||||
|
||||
QJsonObject getContainerConfig(const int containerIndex);
|
||||
|
||||
bool isAnyContainerInstalled();
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
signals:
|
||||
void defaultContainerChanged();
|
||||
void containersModelUpdated();
|
||||
|
||||
private:
|
||||
QMap<DockerContainer, QJsonObject> m_containers;
|
||||
|
||||
int m_currentlyProcessedContainerIndex;
|
||||
DockerContainer m_defaultContainerIndex;
|
||||
};
|
||||
|
||||
#endif // CONTAINERS_MODEL_H
|
||||
|
||||
@@ -45,6 +45,8 @@ QString LanguageModel::getLocalLanguageName(const LanguageSettings::AvailableLan
|
||||
case LanguageSettings::AvailableLanguageEnum::Russian: strLanguage = "Русский"; break;
|
||||
case LanguageSettings::AvailableLanguageEnum::China_cn: strLanguage = "\347\256\200\344\275\223\344\270\255\346\226\207"; break;
|
||||
case LanguageSettings::AvailableLanguageEnum::Persian: strLanguage = "فارسی"; break;
|
||||
case LanguageSettings::AvailableLanguageEnum::Arabic: strLanguage = "العربية"; break;
|
||||
case LanguageSettings::AvailableLanguageEnum::Burmese: strLanguage = "မြန်မာဘာသာ"; break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -59,6 +61,8 @@ void LanguageModel::changeLanguage(const LanguageSettings::AvailableLanguageEnum
|
||||
case LanguageSettings::AvailableLanguageEnum::Russian: emit updateTranslations(QLocale::Russian); break;
|
||||
case LanguageSettings::AvailableLanguageEnum::China_cn: emit updateTranslations(QLocale::Chinese); break;
|
||||
case LanguageSettings::AvailableLanguageEnum::Persian: emit updateTranslations(QLocale::Persian); break;
|
||||
case LanguageSettings::AvailableLanguageEnum::Arabic: emit updateTranslations(QLocale::Arabic); break;
|
||||
case LanguageSettings::AvailableLanguageEnum::Burmese: emit updateTranslations(QLocale::Burmese); break;
|
||||
default: emit updateTranslations(QLocale::English); break;
|
||||
}
|
||||
}
|
||||
@@ -71,6 +75,8 @@ int LanguageModel::getCurrentLanguageIndex()
|
||||
case QLocale::Russian: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::Russian); break;
|
||||
case QLocale::Chinese: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::China_cn); break;
|
||||
case QLocale::Persian: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::Persian); break;
|
||||
case QLocale::Arabic: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::Arabic); break;
|
||||
case QLocale::Burmese: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::Burmese); break;
|
||||
default: return static_cast<int>(LanguageSettings::AvailableLanguageEnum::English); break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ namespace LanguageSettings
|
||||
English,
|
||||
Russian,
|
||||
China_cn,
|
||||
Persian
|
||||
Persian,
|
||||
Arabic,
|
||||
Burmese
|
||||
};
|
||||
Q_ENUM_NS(AvailableLanguageEnum)
|
||||
|
||||
|
||||
@@ -5,19 +5,13 @@
|
||||
ServersModel::ServersModel(std::shared_ptr<Settings> settings, QObject *parent)
|
||||
: m_settings(settings), QAbstractListModel(parent)
|
||||
{
|
||||
m_servers = m_settings->serversArray();
|
||||
m_defaultServerIndex = m_settings->defaultServerIndex();
|
||||
m_currentlyProcessedServerIndex = m_defaultServerIndex;
|
||||
|
||||
connect(this, &ServersModel::defaultServerIndexChanged, this, &ServersModel::defaultServerNameChanged);
|
||||
connect(this, &ServersModel::defaultContainerChanged, this, &ServersModel::defaultServerDescriptionChanged);
|
||||
|
||||
connect(this, &ServersModel::defaultServerIndexChanged, this, [this](const int serverIndex) {
|
||||
auto defaultContainer = ContainerProps::containerFromString(m_servers.at(serverIndex).toObject().value(config_key::defaultContainer).toString());
|
||||
emit ServersModel::defaultContainerChanged(defaultContainer);
|
||||
});
|
||||
connect(this, &ServersModel::currentlyProcessedServerIndexChanged, this, [this](const int serverIndex) {
|
||||
auto defaultContainer = ContainerProps::containerFromString(m_servers.at(serverIndex).toObject().value(config_key::defaultContainer).toString());
|
||||
emit ServersModel::defaultContainerChanged(defaultContainer);
|
||||
emit ServersModel::defaultServerDefaultContainerChanged(defaultContainer);
|
||||
emit ServersModel::defaultServerNameChanged();
|
||||
updateDefaultServerContainersModel();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,16 +68,14 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
|
||||
return name;
|
||||
}
|
||||
case ServerDescriptionRole: {
|
||||
if (configVersion) {
|
||||
return server.value(config_key::description).toString();
|
||||
}
|
||||
return server.value(config_key::hostName).toString();
|
||||
auto description = getServerDescription(server, index.row());
|
||||
return configVersion ? description : description + server.value(config_key::hostName).toString();
|
||||
}
|
||||
case HostNameRole: return server.value(config_key::hostName).toString();
|
||||
case CredentialsRole: return QVariant::fromValue(serverCredentials(index.row()));
|
||||
case CredentialsLoginRole: return serverCredentials(index.row()).userName;
|
||||
case IsDefaultRole: return index.row() == m_defaultServerIndex;
|
||||
case IsCurrentlyProcessedRole: return index.row() == m_currentlyProcessedServerIndex;
|
||||
case IsCurrentlyProcessedRole: return index.row() == m_processedServerIndex;
|
||||
case HasWriteAccessRole: {
|
||||
auto credentials = serverCredentials(index.row());
|
||||
return (!credentials.userName.isEmpty() && !credentials.secretData.isEmpty());
|
||||
@@ -95,6 +87,16 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
|
||||
case DefaultContainerRole: {
|
||||
return ContainerProps::containerFromString(server.value(config_key::defaultContainer).toString());
|
||||
}
|
||||
case HasInstalledContainers: {
|
||||
return serverHasInstalledContainers(index.row());
|
||||
}
|
||||
case IsServerFromApiRole: {
|
||||
return server.value(config_key::configVersion).toInt();
|
||||
}
|
||||
case HasAmneziaDns: {
|
||||
QString primaryDns = server.value(config_key::dns1).toString();
|
||||
return primaryDns == protocols::dns::amneziaDnsIp;
|
||||
}
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
@@ -111,8 +113,9 @@ void ServersModel::resetModel()
|
||||
beginResetModel();
|
||||
m_servers = m_settings->serversArray();
|
||||
m_defaultServerIndex = m_settings->defaultServerIndex();
|
||||
m_currentlyProcessedServerIndex = m_defaultServerIndex;
|
||||
m_processedServerIndex = m_defaultServerIndex;
|
||||
endResetModel();
|
||||
emit defaultServerIndexChanged(m_defaultServerIndex);
|
||||
}
|
||||
|
||||
void ServersModel::setDefaultServerIndex(const int index)
|
||||
@@ -132,12 +135,7 @@ const QString ServersModel::getDefaultServerName()
|
||||
return qvariant_cast<QString>(data(m_defaultServerIndex, NameRole));
|
||||
}
|
||||
|
||||
const QString ServersModel::getDefaultServerHostName()
|
||||
{
|
||||
return qvariant_cast<QString>(data(m_defaultServerIndex, HostNameRole));
|
||||
}
|
||||
|
||||
QString ServersModel::getDefaultServerDescription(const QJsonObject &server)
|
||||
QString ServersModel::getServerDescription(const QJsonObject &server, const int index) const
|
||||
{
|
||||
const auto configVersion = server.value(config_key::configVersion).toInt();
|
||||
|
||||
@@ -145,13 +143,12 @@ QString ServersModel::getDefaultServerDescription(const QJsonObject &server)
|
||||
|
||||
if (configVersion) {
|
||||
return server.value(config_key::description).toString();
|
||||
} else if (isDefaultServerHasWriteAccess()) {
|
||||
if (m_isAmneziaDnsEnabled
|
||||
&& isAmneziaDnsContainerInstalled(m_defaultServerIndex)) {
|
||||
} else if (data(index, HasWriteAccessRole).toBool()) {
|
||||
if (m_isAmneziaDnsEnabled && isAmneziaDnsContainerInstalled(index)) {
|
||||
description += "Amnezia DNS | ";
|
||||
}
|
||||
} else {
|
||||
if (isDefaultServerConfigContainsAmneziaDns()) {
|
||||
if (data(index, HasAmneziaDns).toBool()) {
|
||||
description += "Amnezia DNS | ";
|
||||
}
|
||||
}
|
||||
@@ -162,7 +159,7 @@ const QString ServersModel::getDefaultServerDescriptionCollapsed()
|
||||
{
|
||||
const QJsonObject server = m_servers.at(m_defaultServerIndex).toObject();
|
||||
const auto configVersion = server.value(config_key::configVersion).toInt();
|
||||
auto description = getDefaultServerDescription(server);
|
||||
auto description = getServerDescription(server, m_defaultServerIndex);
|
||||
if (configVersion) {
|
||||
return description;
|
||||
}
|
||||
@@ -176,7 +173,7 @@ const QString ServersModel::getDefaultServerDescriptionExpanded()
|
||||
{
|
||||
const QJsonObject server = m_servers.at(m_defaultServerIndex).toObject();
|
||||
const auto configVersion = server.value(config_key::configVersion).toInt();
|
||||
auto description = getDefaultServerDescription(server);
|
||||
auto description = getServerDescription(server, m_defaultServerIndex);
|
||||
if (configVersion) {
|
||||
return description;
|
||||
}
|
||||
@@ -199,26 +196,21 @@ bool ServersModel::hasServerWithWriteAccess()
|
||||
return false;
|
||||
}
|
||||
|
||||
void ServersModel::setCurrentlyProcessedServerIndex(const int index)
|
||||
void ServersModel::setProcessedServerIndex(const int index)
|
||||
{
|
||||
m_currentlyProcessedServerIndex = index;
|
||||
m_processedServerIndex = index;
|
||||
updateContainersModel();
|
||||
emit currentlyProcessedServerIndexChanged(m_currentlyProcessedServerIndex);
|
||||
emit processedServerIndexChanged(m_processedServerIndex);
|
||||
}
|
||||
|
||||
int ServersModel::getCurrentlyProcessedServerIndex()
|
||||
int ServersModel::getProcessedServerIndex()
|
||||
{
|
||||
return m_currentlyProcessedServerIndex;
|
||||
return m_processedServerIndex;
|
||||
}
|
||||
|
||||
QString ServersModel::getCurrentlyProcessedServerHostName()
|
||||
const ServerCredentials ServersModel::getProcessedServerCredentials()
|
||||
{
|
||||
return qvariant_cast<QString>(data(m_currentlyProcessedServerIndex, HostNameRole));
|
||||
}
|
||||
|
||||
const ServerCredentials ServersModel::getCurrentlyProcessedServerCredentials()
|
||||
{
|
||||
return serverCredentials(m_currentlyProcessedServerIndex);
|
||||
return serverCredentials(m_processedServerIndex);
|
||||
}
|
||||
|
||||
const ServerCredentials ServersModel::getServerCredentials(const int index)
|
||||
@@ -228,12 +220,17 @@ const ServerCredentials ServersModel::getServerCredentials(const int index)
|
||||
|
||||
bool ServersModel::isDefaultServerCurrentlyProcessed()
|
||||
{
|
||||
return m_defaultServerIndex == m_currentlyProcessedServerIndex;
|
||||
return m_defaultServerIndex == m_processedServerIndex;
|
||||
}
|
||||
|
||||
bool ServersModel::isCurrentlyProcessedServerHasWriteAccess()
|
||||
bool ServersModel::isDefaultServerFromApi()
|
||||
{
|
||||
return qvariant_cast<bool>(data(m_currentlyProcessedServerIndex, HasWriteAccessRole));
|
||||
return qvariant_cast<bool>(data(m_defaultServerIndex, IsServerFromApiRole));
|
||||
}
|
||||
|
||||
bool ServersModel::isProcessedServerHasWriteAccess()
|
||||
{
|
||||
return qvariant_cast<bool>(data(m_processedServerIndex, HasWriteAccessRole));
|
||||
}
|
||||
|
||||
bool ServersModel::isDefaultServerHasWriteAccess()
|
||||
@@ -249,40 +246,42 @@ void ServersModel::addServer(const QJsonObject &server)
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void ServersModel::editServer(const QJsonObject &server)
|
||||
void ServersModel::editServer(const QJsonObject &server, const int serverIndex)
|
||||
{
|
||||
m_settings->editServer(m_currentlyProcessedServerIndex, server);
|
||||
m_servers.replace(m_currentlyProcessedServerIndex, m_settings->serversArray().at(m_currentlyProcessedServerIndex));
|
||||
emit dataChanged(index(m_currentlyProcessedServerIndex, 0), index(m_currentlyProcessedServerIndex, 0));
|
||||
m_settings->editServer(serverIndex, server);
|
||||
m_servers.replace(serverIndex, m_settings->serversArray().at(serverIndex));
|
||||
emit dataChanged(index(serverIndex, 0), index(serverIndex, 0));
|
||||
|
||||
if (serverIndex == m_defaultServerIndex) {
|
||||
updateDefaultServerContainersModel();
|
||||
}
|
||||
updateContainersModel();
|
||||
|
||||
if (serverIndex == m_defaultServerIndex) {
|
||||
auto defaultContainer = qvariant_cast<DockerContainer>(getDefaultServerData("defaultContainer"));
|
||||
emit defaultServerDefaultContainerChanged(defaultContainer);
|
||||
}
|
||||
}
|
||||
|
||||
void ServersModel::removeServer()
|
||||
{
|
||||
beginResetModel();
|
||||
m_settings->removeServer(m_currentlyProcessedServerIndex);
|
||||
m_settings->removeServer(m_processedServerIndex);
|
||||
m_servers = m_settings->serversArray();
|
||||
|
||||
if (m_settings->defaultServerIndex() == m_currentlyProcessedServerIndex) {
|
||||
if (m_settings->defaultServerIndex() == m_processedServerIndex) {
|
||||
setDefaultServerIndex(0);
|
||||
} else if (m_settings->defaultServerIndex() > m_currentlyProcessedServerIndex) {
|
||||
} else if (m_settings->defaultServerIndex() > m_processedServerIndex) {
|
||||
setDefaultServerIndex(m_settings->defaultServerIndex() - 1);
|
||||
}
|
||||
|
||||
if (m_settings->serversCount() == 0) {
|
||||
setDefaultServerIndex(-1);
|
||||
}
|
||||
setCurrentlyProcessedServerIndex(m_defaultServerIndex);
|
||||
setProcessedServerIndex(m_defaultServerIndex);
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
bool ServersModel::isDefaultServerConfigContainsAmneziaDns()
|
||||
{
|
||||
const QJsonObject server = m_servers.at(m_defaultServerIndex).toObject();
|
||||
QString primaryDns = server.value(config_key::dns1).toString();
|
||||
return primaryDns == protocols::dns::amneziaDnsIp;
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ServersModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
@@ -290,6 +289,8 @@ QHash<int, QByteArray> ServersModel::roleNames() const
|
||||
roles[NameRole] = "serverName";
|
||||
roles[NameRole] = "name";
|
||||
roles[ServerDescriptionRole] = "serverDescription";
|
||||
roles[CollapsedServerDescriptionRole] = "collapsedServerDescription";
|
||||
roles[ExpandedServerDescriptionRole] = "expandedServerDescription";
|
||||
|
||||
roles[HostNameRole] = "hostName";
|
||||
|
||||
@@ -304,6 +305,9 @@ QHash<int, QByteArray> ServersModel::roleNames() const
|
||||
roles[ContainsAmneziaDnsRole] = "containsAmneziaDns";
|
||||
|
||||
roles[DefaultContainerRole] = "defaultContainer";
|
||||
roles[HasInstalledContainers] = "hasInstalledContainers";
|
||||
|
||||
roles[IsServerFromApiRole] = "isServerFromApi";
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -322,28 +326,29 @@ ServerCredentials ServersModel::serverCredentials(int index) const
|
||||
|
||||
void ServersModel::updateContainersModel()
|
||||
{
|
||||
auto containers = m_servers.at(m_currentlyProcessedServerIndex).toObject().value(config_key::containers).toArray();
|
||||
auto containers = m_servers.at(m_processedServerIndex).toObject().value(config_key::containers).toArray();
|
||||
emit containersUpdated(containers);
|
||||
}
|
||||
|
||||
void ServersModel::updateDefaultServerContainersModel()
|
||||
{
|
||||
auto containers = m_servers.at(m_defaultServerIndex).toObject().value(config_key::containers).toArray();
|
||||
emit defaultServerContainersUpdated(containers);
|
||||
}
|
||||
|
||||
QJsonObject ServersModel::getDefaultServerConfig()
|
||||
{
|
||||
return m_servers.at(m_defaultServerIndex).toObject();
|
||||
}
|
||||
|
||||
QJsonObject ServersModel::getCurrentlyProcessedServerConfig()
|
||||
void ServersModel::reloadDefaultServerContainerConfig()
|
||||
{
|
||||
return m_servers.at(m_currentlyProcessedServerIndex).toObject();
|
||||
}
|
||||
|
||||
void ServersModel::reloadContainerConfig()
|
||||
{
|
||||
QJsonObject server = m_servers.at(m_currentlyProcessedServerIndex).toObject();
|
||||
QJsonObject server = m_servers.at(m_defaultServerIndex).toObject();
|
||||
auto container = ContainerProps::containerFromString(server.value(config_key::defaultContainer).toString());
|
||||
|
||||
auto containers = server.value(config_key::containers).toArray();
|
||||
|
||||
auto config = m_settings->containerConfig(m_currentlyProcessedServerIndex, container);
|
||||
auto config = m_settings->containerConfig(m_defaultServerIndex, container);
|
||||
for (auto i = 0; i < containers.size(); i++) {
|
||||
auto c = ContainerProps::containerFromString(containers.at(i).toObject().value(config_key::container).toString());
|
||||
if (c == container) {
|
||||
@@ -353,13 +358,13 @@ void ServersModel::reloadContainerConfig()
|
||||
}
|
||||
|
||||
server.insert(config_key::containers, containers);
|
||||
editServer(server);
|
||||
editServer(server, m_defaultServerIndex);
|
||||
}
|
||||
|
||||
void ServersModel::updateContainerConfig(const int containerIndex, const QJsonObject config)
|
||||
{
|
||||
auto container = static_cast<DockerContainer>(containerIndex);
|
||||
QJsonObject server = m_servers.at(m_currentlyProcessedServerIndex).toObject();
|
||||
QJsonObject server = m_servers.at(m_processedServerIndex).toObject();
|
||||
|
||||
auto containers = server.value(config_key::containers).toArray();
|
||||
for (auto i = 0; i < containers.size(); i++) {
|
||||
@@ -377,49 +382,38 @@ void ServersModel::updateContainerConfig(const int containerIndex, const QJsonOb
|
||||
server.insert(config_key::defaultContainer, ContainerProps::containerToString(container));
|
||||
}
|
||||
|
||||
editServer(server);
|
||||
editServer(server, m_processedServerIndex);
|
||||
}
|
||||
|
||||
void ServersModel::addContainerConfig(const int containerIndex, const QJsonObject config)
|
||||
{
|
||||
auto container = static_cast<DockerContainer>(containerIndex);
|
||||
QJsonObject server = m_servers.at(m_currentlyProcessedServerIndex).toObject();
|
||||
QJsonObject server = m_servers.at(m_processedServerIndex).toObject();
|
||||
|
||||
auto containers = server.value(config_key::containers).toArray();
|
||||
containers.push_back(config);
|
||||
|
||||
server.insert(config_key::containers, containers);
|
||||
|
||||
bool isDefaultContainerChanged = false;
|
||||
auto defaultContainer = server.value(config_key::defaultContainer).toString();
|
||||
if ((ContainerProps::containerFromString(defaultContainer) == DockerContainer::None || ContainerProps::containerService(container) != ServiceType::Other)) {
|
||||
server.insert(config_key::defaultContainer, ContainerProps::containerToString(container));
|
||||
isDefaultContainerChanged = true;
|
||||
}
|
||||
|
||||
editServer(server);
|
||||
if (isDefaultContainerChanged) {
|
||||
emit defaultContainerChanged(container);
|
||||
}
|
||||
editServer(server, m_processedServerIndex);
|
||||
}
|
||||
|
||||
void ServersModel::setDefaultContainer(const int containerIndex)
|
||||
void ServersModel::setDefaultContainer(const int serverIndex, const int containerIndex)
|
||||
{
|
||||
auto container = static_cast<DockerContainer>(containerIndex);
|
||||
QJsonObject s = m_servers.at(m_currentlyProcessedServerIndex).toObject();
|
||||
QJsonObject s = m_servers.at(serverIndex).toObject();
|
||||
s.insert(config_key::defaultContainer, ContainerProps::containerToString(container));
|
||||
editServer(s); //check
|
||||
emit defaultContainerChanged(container);
|
||||
editServer(s, serverIndex); //check
|
||||
}
|
||||
|
||||
DockerContainer ServersModel::getDefaultContainer()
|
||||
const QString ServersModel::getDefaultServerDefaultContainerName()
|
||||
{
|
||||
return qvariant_cast<DockerContainer>(data(m_currentlyProcessedServerIndex, DefaultContainerRole));
|
||||
}
|
||||
|
||||
const QString ServersModel::getDefaultContainerName()
|
||||
{
|
||||
auto defaultContainer = getDefaultContainer();
|
||||
auto defaultContainer = qvariant_cast<DockerContainer>(getDefaultServerData("defaultContainer"));
|
||||
return ContainerProps::containerHumanNames().value(defaultContainer);
|
||||
}
|
||||
|
||||
@@ -427,15 +421,14 @@ ErrorCode ServersModel::removeAllContainers()
|
||||
{
|
||||
ServerController serverController(m_settings);
|
||||
ErrorCode errorCode =
|
||||
serverController.removeAllContainers(m_settings->serverCredentials(m_currentlyProcessedServerIndex));
|
||||
serverController.removeAllContainers(m_settings->serverCredentials(m_processedServerIndex));
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
QJsonObject s = m_servers.at(m_currentlyProcessedServerIndex).toObject();
|
||||
QJsonObject s = m_servers.at(m_processedServerIndex).toObject();
|
||||
s.insert(config_key::containers, {});
|
||||
s.insert(config_key::defaultContainer, ContainerProps::containerToString(DockerContainer::None));
|
||||
|
||||
editServer(s);
|
||||
emit defaultContainerChanged(DockerContainer::None);
|
||||
editServer(s, m_processedServerIndex);
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
@@ -443,7 +436,7 @@ ErrorCode ServersModel::removeAllContainers()
|
||||
ErrorCode ServersModel::rebootServer()
|
||||
{
|
||||
ServerController serverController(m_settings);
|
||||
auto credentials = m_settings->serverCredentials(m_currentlyProcessedServerIndex);
|
||||
auto credentials = m_settings->serverCredentials(m_processedServerIndex);
|
||||
|
||||
ErrorCode errorCode = serverController.rebootServer(credentials);
|
||||
return errorCode;
|
||||
@@ -452,13 +445,13 @@ ErrorCode ServersModel::rebootServer()
|
||||
ErrorCode ServersModel::removeContainer(const int containerIndex)
|
||||
{
|
||||
ServerController serverController(m_settings);
|
||||
auto credentials = m_settings->serverCredentials(m_currentlyProcessedServerIndex);
|
||||
auto credentials = m_settings->serverCredentials(m_processedServerIndex);
|
||||
auto dockerContainer = static_cast<DockerContainer>(containerIndex);
|
||||
|
||||
ErrorCode errorCode = serverController.removeContainer(credentials, dockerContainer);
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
QJsonObject server = m_servers.at(m_currentlyProcessedServerIndex).toObject();
|
||||
QJsonObject server = m_servers.at(m_processedServerIndex).toObject();
|
||||
|
||||
auto containers = server.value(config_key::containers).toArray();
|
||||
for (auto it = containers.begin(); it != containers.end(); it++) {
|
||||
@@ -480,32 +473,37 @@ ErrorCode ServersModel::removeContainer(const int containerIndex)
|
||||
server.insert(config_key::defaultContainer, ContainerProps::containerToString(defaultContainer));
|
||||
}
|
||||
|
||||
editServer(server);
|
||||
emit defaultContainerChanged(defaultContainer);
|
||||
editServer(server, m_processedServerIndex);
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
void ServersModel::clearCachedProfiles()
|
||||
{
|
||||
const auto &containers = m_settings->containers(m_currentlyProcessedServerIndex);
|
||||
const auto &containers = m_settings->containers(m_processedServerIndex);
|
||||
for (DockerContainer container : containers.keys()) {
|
||||
m_settings->clearLastConnectionConfig(m_currentlyProcessedServerIndex, container);
|
||||
m_settings->clearLastConnectionConfig(m_processedServerIndex, container);
|
||||
}
|
||||
|
||||
m_servers.replace(m_currentlyProcessedServerIndex, m_settings->server(m_currentlyProcessedServerIndex));
|
||||
m_servers.replace(m_processedServerIndex, m_settings->server(m_processedServerIndex));
|
||||
if (m_processedServerIndex == m_defaultServerIndex) {
|
||||
updateDefaultServerContainersModel();
|
||||
}
|
||||
updateContainersModel();
|
||||
}
|
||||
|
||||
void ServersModel::clearCachedProfile(const DockerContainer container)
|
||||
{
|
||||
m_settings->clearLastConnectionConfig(m_currentlyProcessedServerIndex, container);
|
||||
m_settings->clearLastConnectionConfig(m_processedServerIndex, container);
|
||||
|
||||
m_servers.replace(m_currentlyProcessedServerIndex, m_settings->server(m_currentlyProcessedServerIndex));
|
||||
m_servers.replace(m_processedServerIndex, m_settings->server(m_processedServerIndex));
|
||||
if (m_processedServerIndex == m_defaultServerIndex) {
|
||||
updateDefaultServerContainersModel();
|
||||
}
|
||||
updateContainersModel();
|
||||
}
|
||||
|
||||
bool ServersModel::isAmneziaDnsContainerInstalled(const int serverIndex)
|
||||
bool ServersModel::isAmneziaDnsContainerInstalled(const int serverIndex) const
|
||||
{
|
||||
QJsonObject server = m_servers.at(serverIndex).toObject();
|
||||
auto containers = server.value(config_key::containers).toArray();
|
||||
@@ -544,16 +542,6 @@ void ServersModel::toggleAmneziaDns(bool enabled)
|
||||
emit defaultServerDescriptionChanged();
|
||||
}
|
||||
|
||||
bool ServersModel::isDefaultServerFromApi()
|
||||
{
|
||||
return m_settings->server(m_defaultServerIndex).value(config_key::configVersion).toInt();
|
||||
}
|
||||
|
||||
bool ServersModel::isCurrentlyProcessedServerFromApi()
|
||||
{
|
||||
return m_settings->server(m_currentlyProcessedServerIndex).value(config_key::configVersion).toInt();
|
||||
}
|
||||
|
||||
bool ServersModel::isServerFromApiAlreadyExists(const quint16 crc)
|
||||
{
|
||||
for (const auto &server : qAsConst(m_servers)) {
|
||||
@@ -564,3 +552,55 @@ bool ServersModel::isServerFromApiAlreadyExists(const quint16 crc)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ServersModel::serverHasInstalledContainers(const int serverIndex) const
|
||||
{
|
||||
QJsonObject server = m_servers.at(serverIndex).toObject();
|
||||
const auto containers = server.value(config_key::containers).toArray();
|
||||
for (auto it = containers.begin(); it != containers.end(); it++) {
|
||||
auto container = ContainerProps::containerFromString(it->toObject().value(config_key::container).toString());
|
||||
if (ContainerProps::containerService(container) == ServiceType::Vpn) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QVariant ServersModel::getDefaultServerData(const QString roleString)
|
||||
{
|
||||
auto roles = roleNames();
|
||||
for (auto it = roles.begin(); it != roles.end(); it++) {
|
||||
if (QString(it.value()) == roleString) {
|
||||
return data(m_defaultServerIndex, it.key());
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QVariant ServersModel::getProcessedServerData(const QString roleString)
|
||||
{
|
||||
auto roles = roleNames();
|
||||
for (auto it = roles.begin(); it != roles.end(); it++) {
|
||||
if (QString(it.value()) == roleString) {
|
||||
return data(m_processedServerIndex, it.key());
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling()
|
||||
{
|
||||
auto server = m_servers.at(m_defaultServerIndex).toObject();
|
||||
auto defaultContainer = ContainerProps::containerFromString(server.value(config_key::defaultContainer).toString());
|
||||
auto containerConfig = server.value(config_key::containers).toArray().at(defaultContainer).toObject();
|
||||
auto protocolConfig = containerConfig.value(ContainerProps::containerTypeToString(defaultContainer)).toObject();
|
||||
|
||||
if (defaultContainer == DockerContainer::Awg || defaultContainer == DockerContainer::WireGuard) {
|
||||
return !(protocolConfig.value(config_key::last_config).toString().contains("AllowedIPs = 0.0.0.0/0, ::/0"));
|
||||
} else if (defaultContainer == DockerContainer::Cloak || defaultContainer == DockerContainer::OpenVpn || defaultContainer == DockerContainer::ShadowSocks) {
|
||||
return !(protocolConfig.value(config_key::last_config).toString().contains("redirect-gateway"));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ public:
|
||||
enum Roles {
|
||||
NameRole = Qt::UserRole + 1,
|
||||
ServerDescriptionRole,
|
||||
|
||||
CollapsedServerDescriptionRole,
|
||||
ExpandedServerDescriptionRole,
|
||||
HostNameRole,
|
||||
|
||||
CredentialsRole,
|
||||
@@ -25,7 +26,12 @@ public:
|
||||
|
||||
ContainsAmneziaDnsRole,
|
||||
|
||||
DefaultContainerRole
|
||||
DefaultContainerRole,
|
||||
|
||||
HasInstalledContainers,
|
||||
IsServerFromApiRole,
|
||||
|
||||
HasAmneziaDns
|
||||
};
|
||||
|
||||
ServersModel(std::shared_ptr<Settings> settings, QObject *parent = nullptr);
|
||||
@@ -40,47 +46,43 @@ public:
|
||||
|
||||
Q_PROPERTY(int defaultIndex READ getDefaultServerIndex WRITE setDefaultServerIndex NOTIFY defaultServerIndexChanged)
|
||||
Q_PROPERTY(QString defaultServerName READ getDefaultServerName NOTIFY defaultServerNameChanged)
|
||||
Q_PROPERTY(QString defaultServerHostName READ getDefaultServerHostName NOTIFY defaultServerIndexChanged)
|
||||
Q_PROPERTY(QString defaultContainerName READ getDefaultContainerName NOTIFY defaultContainerChanged)
|
||||
Q_PROPERTY(QString defaultServerDescriptionCollapsed READ getDefaultServerDescriptionCollapsed NOTIFY defaultServerDescriptionChanged)
|
||||
Q_PROPERTY(QString defaultServerDescriptionExpanded READ getDefaultServerDescriptionExpanded NOTIFY defaultServerDescriptionChanged)
|
||||
Q_PROPERTY(QString defaultServerDefaultContainerName READ getDefaultServerDefaultContainerName NOTIFY defaultServerDefaultContainerChanged)
|
||||
Q_PROPERTY(QString defaultServerDescriptionCollapsed READ getDefaultServerDescriptionCollapsed NOTIFY defaultServerDefaultContainerChanged)
|
||||
Q_PROPERTY(QString defaultServerDescriptionExpanded READ getDefaultServerDescriptionExpanded NOTIFY defaultServerDefaultContainerChanged)
|
||||
Q_PROPERTY(bool isDefaultServerDefaultContainerHasSplitTunneling READ isDefaultServerDefaultContainerHasSplitTunneling NOTIFY defaultServerDefaultContainerChanged)
|
||||
Q_PROPERTY(bool isDefaultServerFromApi READ isDefaultServerFromApi NOTIFY defaultServerIndexChanged)
|
||||
|
||||
Q_PROPERTY(int currentlyProcessedIndex READ getCurrentlyProcessedServerIndex WRITE setCurrentlyProcessedServerIndex
|
||||
NOTIFY currentlyProcessedServerIndexChanged)
|
||||
Q_PROPERTY(int processedIndex READ getProcessedServerIndex WRITE setProcessedServerIndex NOTIFY processedServerIndexChanged)
|
||||
|
||||
public slots:
|
||||
void setDefaultServerIndex(const int index);
|
||||
const int getDefaultServerIndex();
|
||||
const QString getDefaultServerName();
|
||||
const QString getDefaultServerHostName();
|
||||
const QString getDefaultServerDescriptionCollapsed();
|
||||
const QString getDefaultServerDescriptionExpanded();
|
||||
const QString getDefaultServerDefaultContainerName();
|
||||
bool isDefaultServerCurrentlyProcessed();
|
||||
bool isDefaultServerFromApi();
|
||||
|
||||
bool isCurrentlyProcessedServerHasWriteAccess();
|
||||
bool isProcessedServerHasWriteAccess();
|
||||
bool isDefaultServerHasWriteAccess();
|
||||
bool hasServerWithWriteAccess();
|
||||
|
||||
const int getServersCount();
|
||||
|
||||
void setCurrentlyProcessedServerIndex(const int index);
|
||||
int getCurrentlyProcessedServerIndex();
|
||||
void setProcessedServerIndex(const int index);
|
||||
int getProcessedServerIndex();
|
||||
|
||||
QString getCurrentlyProcessedServerHostName();
|
||||
const ServerCredentials getCurrentlyProcessedServerCredentials();
|
||||
const ServerCredentials getProcessedServerCredentials();
|
||||
const ServerCredentials getServerCredentials(const int index);
|
||||
|
||||
void addServer(const QJsonObject &server);
|
||||
void editServer(const QJsonObject &server);
|
||||
void editServer(const QJsonObject &server, const int serverIndex);
|
||||
void removeServer();
|
||||
|
||||
bool isDefaultServerConfigContainsAmneziaDns();
|
||||
bool isAmneziaDnsContainerInstalled(const int serverIndex);
|
||||
|
||||
QJsonObject getDefaultServerConfig();
|
||||
QJsonObject getCurrentlyProcessedServerConfig();
|
||||
|
||||
void reloadContainerConfig();
|
||||
void reloadDefaultServerContainerConfig();
|
||||
void updateContainerConfig(const int containerIndex, const QJsonObject config);
|
||||
void addContainerConfig(const int containerIndex, const QJsonObject config);
|
||||
|
||||
@@ -91,43 +93,51 @@ public slots:
|
||||
ErrorCode removeAllContainers();
|
||||
ErrorCode rebootServer();
|
||||
|
||||
void setDefaultContainer(const int containerIndex);
|
||||
DockerContainer getDefaultContainer();
|
||||
const QString getDefaultContainerName();
|
||||
void setDefaultContainer(const int serverIndex, const int containerIndex);
|
||||
|
||||
QStringList getAllInstalledServicesName(const int serverIndex);
|
||||
|
||||
void toggleAmneziaDns(bool enabled);
|
||||
|
||||
bool isDefaultServerFromApi();
|
||||
bool isCurrentlyProcessedServerFromApi();
|
||||
|
||||
bool isServerFromApiAlreadyExists(const quint16 crc);
|
||||
|
||||
QVariant getDefaultServerData(const QString roleString);
|
||||
|
||||
QVariant getProcessedServerData(const QString roleString);
|
||||
|
||||
bool isDefaultServerDefaultContainerHasSplitTunneling();
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
signals:
|
||||
void currentlyProcessedServerIndexChanged(const int index);
|
||||
void processedServerIndexChanged(const int index);
|
||||
void defaultServerIndexChanged(const int index);
|
||||
void defaultServerNameChanged();
|
||||
void defaultServerDescriptionChanged();
|
||||
|
||||
void containersUpdated(const QJsonArray &containers);
|
||||
void defaultContainerChanged(const int containerIndex);
|
||||
void defaultServerContainersUpdated(const QJsonArray &containers);
|
||||
void defaultServerDefaultContainerChanged(const int containerIndex);
|
||||
|
||||
private:
|
||||
ServerCredentials serverCredentials(int index) const;
|
||||
void updateContainersModel();
|
||||
|
||||
QString getDefaultServerDescription(const QJsonObject &server);
|
||||
void updateContainersModel();
|
||||
void updateDefaultServerContainersModel();
|
||||
|
||||
QString getServerDescription(const QJsonObject &server, const int index) const;
|
||||
|
||||
bool isAmneziaDnsContainerInstalled(const int serverIndex) const;
|
||||
|
||||
bool serverHasInstalledContainers(const int serverIndex) const;
|
||||
|
||||
QJsonArray m_servers;
|
||||
|
||||
std::shared_ptr<Settings> m_settings;
|
||||
|
||||
int m_defaultServerIndex;
|
||||
int m_currentlyProcessedServerIndex;
|
||||
int m_processedServerIndex;
|
||||
|
||||
bool m_isAmneziaDnsEnabled = m_settings->useAmneziaDns();
|
||||
};
|
||||
|
||||
@@ -113,6 +113,7 @@ void SitesModel::toggleSplitTunneling(bool enabled)
|
||||
m_settings->setRouteMode(Settings::RouteMode::VpnAllSites);
|
||||
}
|
||||
m_isSplitTunnelingEnabled = enabled;
|
||||
emit splitTunnelingToggled();
|
||||
}
|
||||
|
||||
QVector<QPair<QString, QString> > SitesModel::getCurrentSites()
|
||||
|
||||
@@ -22,6 +22,7 @@ public:
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
Q_PROPERTY(int routeMode READ getRouteMode WRITE setRouteMode NOTIFY routeModeChanged)
|
||||
Q_PROPERTY(bool isTunnelingEnabled READ isSplitTunnelingEnabled NOTIFY splitTunnelingToggled)
|
||||
|
||||
public slots:
|
||||
bool addSite(const QString &hostname, const QString &ip);
|
||||
@@ -38,6 +39,7 @@ public slots:
|
||||
|
||||
signals:
|
||||
void routeModeChanged();
|
||||
void splitTunnelingToggled();
|
||||
|
||||
protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
@@ -138,8 +138,7 @@ Button {
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (!ConnectionController.isConnectionInProgress) {
|
||||
ApiController.updateServerConfigFromApi()
|
||||
}
|
||||
ServersModel.setProcessedServerIndex(ServersModel.defaultIndex)
|
||||
ApiController.updateServerConfigFromApi()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,18 +8,32 @@ import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
|
||||
DrawerType {
|
||||
DrawerType2 {
|
||||
id: root
|
||||
|
||||
width: parent.width
|
||||
height: parent.height * 0.4375
|
||||
height: parent.height
|
||||
|
||||
expandedContent: ColumnLayout {
|
||||
id: content
|
||||
|
||||
ColumnLayout {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: 0
|
||||
|
||||
Component.onCompleted: {
|
||||
root.expandedHeight = content.implicitHeight + 32
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
enabled: !GC.isMobile()
|
||||
function onOpened() {
|
||||
config.rightButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Header2Type {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
@@ -31,7 +45,7 @@ DrawerType {
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
id: ip
|
||||
id: config
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
|
||||
@@ -40,13 +54,16 @@ DrawerType {
|
||||
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageSetupWizardCredentials)
|
||||
root.visible = false
|
||||
root.close()
|
||||
}
|
||||
|
||||
KeyNavigation.tab: configFromFile.rightButton
|
||||
}
|
||||
|
||||
DividerType {}
|
||||
|
||||
LabelWithButtonType {
|
||||
id: configFromFile
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("Open config file, key or QR code")
|
||||
@@ -54,8 +71,10 @@ DrawerType {
|
||||
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageSetupWizardConfigSource)
|
||||
root.visible = false
|
||||
root.close()
|
||||
}
|
||||
|
||||
KeyNavigation.tab: config.rightButton
|
||||
}
|
||||
|
||||
DividerType {}
|
||||
|
||||
@@ -15,6 +15,7 @@ ListView {
|
||||
id: menuContent
|
||||
|
||||
property var rootWidth
|
||||
property var selectedText
|
||||
|
||||
width: rootWidth
|
||||
height: menuContent.contentItem.height
|
||||
@@ -26,24 +27,6 @@ ListView {
|
||||
id: containersRadioButtonGroup
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ServersModel
|
||||
|
||||
function onCurrentlyProcessedServerIndexChanged() {
|
||||
if (ContainersModel.getDefaultContainer()) {
|
||||
menuContent.checkCurrentItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkCurrentItem() {
|
||||
var item = menuContent.itemAtIndex(currentIndex)
|
||||
if (item !== null) {
|
||||
var radioButton = item.children[0].children[0]
|
||||
radioButton.checked = true
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
implicitWidth: rootWidth
|
||||
implicitHeight: content.implicitHeight
|
||||
@@ -69,7 +52,7 @@ ListView {
|
||||
showImage: !isInstalled
|
||||
|
||||
checkable: isInstalled && !ConnectionController.isConnected && isSupported
|
||||
checked: isDefault
|
||||
checked: proxyDefaultServerContainersModel.mapToSource(index) === ServersModel.getDefaultServerData("defaultContainer")
|
||||
|
||||
onClicked: {
|
||||
if (ConnectionController.isConnected && isInstalled) {
|
||||
@@ -78,18 +61,18 @@ ListView {
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
containersDropDown.menuVisible = false
|
||||
ServersModel.setDefaultContainer(proxyContainersModel.mapToSource(index))
|
||||
containersDropDown.close()
|
||||
ServersModel.setDefaultContainer(ServersModel.defaultIndex, proxyDefaultServerContainersModel.mapToSource(index))
|
||||
} else {
|
||||
if (!isSupported && isInstalled) {
|
||||
PageController.showErrorMessage(qsTr("The selected protocol is not supported on the current platform"))
|
||||
return
|
||||
}
|
||||
|
||||
ContainersModel.setCurrentlyProcessedContainerIndex(proxyContainersModel.mapToSource(index))
|
||||
ContainersModel.setCurrentlyProcessedContainerIndex(proxyDefaultServerContainersModel.mapToSource(index))
|
||||
InstallController.setShouldCreateServer(false)
|
||||
PageController.goToPage(PageEnum.PageSetupWizardProtocolSettings)
|
||||
containersDropDown.menuVisible = false
|
||||
containersDropDown.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
112
client/ui/qml/Components/HomeSplitTunnelingDrawer.qml
Normal file
112
client/ui/qml/Components/HomeSplitTunnelingDrawer.qml
Normal file
@@ -0,0 +1,112 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import PageEnum 1.0
|
||||
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
|
||||
DrawerType2 {
|
||||
id: root
|
||||
|
||||
anchors.fill: parent
|
||||
expandedHeight: parent.height * 0.7
|
||||
|
||||
expandedContent: ColumnLayout {
|
||||
id: content
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: 0
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
enabled: !GC.isMobile()
|
||||
function onOpened() {
|
||||
if (splitTunneling.visible) {
|
||||
splitTunneling.rightButton.forceActiveFocus()
|
||||
} else {
|
||||
splitTunnelingSiteBased.rightButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Header2Type {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
|
||||
headerText: qsTr("Split tunneling")
|
||||
descriptionText: qsTr("Allows you to connect to some sites or applications through a VPN connection and bypass others")
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
id: splitTunneling
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
|
||||
visible: ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && ServersModel.getDefaultServerData("isServerFromApi")
|
||||
|
||||
text: qsTr("Split tunneling on the server")
|
||||
descriptionText: qsTr("Enabled \nCan't be disabled for current server")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
clickedFunction: function() {
|
||||
// PageController.goToPage(PageEnum.PageSettingsSplitTunneling)
|
||||
// root.close()
|
||||
}
|
||||
|
||||
KeyNavigation.tab: splitTunnelingSiteBased.rightButton
|
||||
}
|
||||
|
||||
DividerType {
|
||||
visible: ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && ServersModel.getDefaultServerData("isServerFromApi")
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
id: splitTunnelingSiteBased
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
|
||||
enabled: ! ServersModel.isDefaultServerDefaultContainerHasSplitTunneling || !ServersModel.getDefaultServerData("isServerFromApi")
|
||||
|
||||
text: qsTr("Site-based split tunneling")
|
||||
descriptionText: enabled && SitesModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageSettingsSplitTunneling)
|
||||
root.close()
|
||||
}
|
||||
KeyNavigation.tab: appBasedSplitTunneling.visible ? appBasedSplitTunneling.rightButton : null
|
||||
}
|
||||
|
||||
DividerType {
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
id: appBasedSplitTunneling
|
||||
Layout.fillWidth: true
|
||||
visible: false
|
||||
|
||||
text: qsTr("App-based split tunneling")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
clickedFunction: function() {
|
||||
// PageController.goToPage(PageEnum.PageSetupWizardConfigSource)
|
||||
root.close()
|
||||
}
|
||||
|
||||
KeyNavigation.tab: splitTunneling.rightButton
|
||||
}
|
||||
|
||||
DividerType {
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,9 @@ import QtQuick.Layouts
|
||||
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
|
||||
DrawerType {
|
||||
DrawerType2 {
|
||||
id: root
|
||||
|
||||
property string headerText
|
||||
@@ -16,23 +17,32 @@ DrawerType {
|
||||
property var yesButtonFunction
|
||||
property var noButtonFunction
|
||||
|
||||
width: parent.width
|
||||
height: content.implicitHeight + 32
|
||||
|
||||
ColumnLayout {
|
||||
expandedContent: ColumnLayout {
|
||||
id: content
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
anchors.leftMargin: 16
|
||||
|
||||
spacing: 8
|
||||
|
||||
onImplicitHeightChanged: {
|
||||
root.expandedHeight = content.implicitHeight + 32
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
enabled: !GC.isMobile()
|
||||
function onOpened() {
|
||||
yesButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Header2TextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
text: headerText
|
||||
}
|
||||
@@ -40,25 +50,34 @@ DrawerType {
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
text: descriptionText
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: yesButton
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
text: yesButtonText
|
||||
|
||||
onClicked: {
|
||||
clickedFunc: function() {
|
||||
if (yesButtonFunction && typeof yesButtonFunction === "function") {
|
||||
yesButtonFunction()
|
||||
}
|
||||
}
|
||||
KeyNavigation.tab: noButton
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: noButton
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
defaultColor: "transparent"
|
||||
hoveredColor: Qt.rgba(1, 1, 1, 0.08)
|
||||
@@ -69,11 +88,12 @@ DrawerType {
|
||||
|
||||
text: noButtonText
|
||||
|
||||
onClicked: {
|
||||
clickedFunc: function() {
|
||||
if (noButtonFunction && typeof noButtonFunction === "function") {
|
||||
noButtonFunction()
|
||||
}
|
||||
}
|
||||
KeyNavigation.tab: yesButton
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,129 +5,136 @@ import QtQuick.Layouts
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
|
||||
DrawerType {
|
||||
DrawerType2 {
|
||||
id: root
|
||||
|
||||
width: parent.width
|
||||
height: parent.height * 0.9
|
||||
expandedContent: Item {
|
||||
id: container
|
||||
|
||||
ColumnLayout {
|
||||
id: backButton
|
||||
implicitHeight: root.height * 0.9
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 16
|
||||
|
||||
BackButtonType {
|
||||
backButtonImage: "qrc:/images/controls/arrow-left.svg"
|
||||
backButtonFunction: function() {
|
||||
root.close()
|
||||
}
|
||||
Component.onCompleted: {
|
||||
root.expandedHeight = container.implicitHeight
|
||||
}
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
anchors.top: backButton.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
contentHeight: content.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
id: backButton
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 16
|
||||
|
||||
Header2Type {
|
||||
id: header
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
headerText: qsTr("Choose language")
|
||||
BackButtonType {
|
||||
backButtonImage: "qrc:/images/controls/arrow-left.svg"
|
||||
backButtonFunction: function() {
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
FlickableType {
|
||||
anchors.top: backButton.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
contentHeight: content.implicitHeight
|
||||
|
||||
Layout.fillWidth: true
|
||||
height: listView.contentItem.height
|
||||
ColumnLayout {
|
||||
id: content
|
||||
|
||||
clip: true
|
||||
interactive: false
|
||||
anchors.fill: parent
|
||||
|
||||
model: LanguageModel
|
||||
currentIndex: LanguageModel.currentLanguageIndex
|
||||
Header2Type {
|
||||
id: header
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
ButtonGroup {
|
||||
id: buttonGroup
|
||||
headerText: qsTr("Choose language")
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
implicitWidth: root.width
|
||||
implicitHeight: delegateContent.implicitHeight
|
||||
ListView {
|
||||
id: listView
|
||||
|
||||
ColumnLayout {
|
||||
id: delegateContent
|
||||
Layout.fillWidth: true
|
||||
height: listView.contentItem.height
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
interactive: false
|
||||
|
||||
RadioButton {
|
||||
id: radioButton
|
||||
model: LanguageModel
|
||||
currentIndex: LanguageModel.currentLanguageIndex
|
||||
|
||||
implicitWidth: parent.width
|
||||
implicitHeight: radioButtonContent.implicitHeight
|
||||
ButtonGroup {
|
||||
id: buttonGroup
|
||||
}
|
||||
|
||||
hoverEnabled: true
|
||||
delegate: Item {
|
||||
implicitWidth: root.width
|
||||
implicitHeight: delegateContent.implicitHeight
|
||||
|
||||
indicator: Rectangle {
|
||||
anchors.fill: parent
|
||||
color: radioButton.hovered ? "#2C2D30" : "#1C1D21"
|
||||
ColumnLayout {
|
||||
id: delegateContent
|
||||
|
||||
Behavior on color {
|
||||
PropertyAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
anchors.fill: parent
|
||||
|
||||
RowLayout {
|
||||
id: radioButtonContent
|
||||
anchors.fill: parent
|
||||
RadioButton {
|
||||
id: radioButton
|
||||
|
||||
anchors.rightMargin: 16
|
||||
anchors.leftMargin: 16
|
||||
implicitWidth: parent.width
|
||||
implicitHeight: radioButtonContent.implicitHeight
|
||||
|
||||
spacing: 0
|
||||
hoverEnabled: true
|
||||
|
||||
z: 1
|
||||
indicator: Rectangle {
|
||||
anchors.fill: parent
|
||||
color: radioButton.hovered ? "#2C2D30" : "#1C1D21"
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 20
|
||||
Layout.bottomMargin: 20
|
||||
|
||||
text: languageName
|
||||
Behavior on color {
|
||||
PropertyAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
source: "qrc:/images/controls/check.svg"
|
||||
visible: radioButton.checked
|
||||
RowLayout {
|
||||
id: radioButtonContent
|
||||
anchors.fill: parent
|
||||
|
||||
width: 24
|
||||
height: 24
|
||||
anchors.rightMargin: 16
|
||||
anchors.leftMargin: 16
|
||||
|
||||
Layout.rightMargin: 8
|
||||
spacing: 0
|
||||
|
||||
z: 1
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 20
|
||||
Layout.bottomMargin: 20
|
||||
|
||||
text: languageName
|
||||
}
|
||||
|
||||
Image {
|
||||
source: "qrc:/images/controls/check.svg"
|
||||
visible: radioButton.checked
|
||||
|
||||
width: 24
|
||||
height: 24
|
||||
|
||||
Layout.rightMargin: 8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ButtonGroup.group: buttonGroup
|
||||
checked: listView.currentIndex === index
|
||||
ButtonGroup.group: buttonGroup
|
||||
checked: listView.currentIndex === index
|
||||
|
||||
onClicked: {
|
||||
listView.currentIndex = index
|
||||
LanguageModel.changeLanguage(languageIndex)
|
||||
root.close()
|
||||
onClicked: {
|
||||
listView.currentIndex = index
|
||||
LanguageModel.changeLanguage(languageIndex)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,19 +16,18 @@ import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
DrawerType {
|
||||
DrawerType2 {
|
||||
id: root
|
||||
|
||||
property alias headerText: header.headerText
|
||||
property alias configContentHeaderText: configContentHeader.headerText
|
||||
property alias contentVisible: content.visible
|
||||
property string headerText
|
||||
property string configContentHeaderText
|
||||
property string contentVisible
|
||||
|
||||
property string configExtension: ".vpn"
|
||||
property string configCaption: qsTr("Save AmneziaVPN config")
|
||||
property string configFileName: "amnezia_config"
|
||||
|
||||
width: parent.width
|
||||
height: parent.height * 0.9
|
||||
expandedHeight: parent.height * 0.9
|
||||
|
||||
onClosed: {
|
||||
configExtension = ".vpn"
|
||||
@@ -36,8 +35,16 @@ DrawerType {
|
||||
configFileName = "amnezia_config"
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
expandedContent: Item {
|
||||
implicitHeight: root.expandedHeight
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
enabled: !GC.isMobile()
|
||||
function onOpened() {
|
||||
shareButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Header2Type {
|
||||
id: header
|
||||
@@ -47,6 +54,8 @@ DrawerType {
|
||||
anchors.topMargin: 20
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
|
||||
headerText: root.headerText
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
@@ -64,14 +73,17 @@ DrawerType {
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
|
||||
visible: root.contentVisible
|
||||
|
||||
BasicButtonType {
|
||||
id: shareButton
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
|
||||
text: qsTr("Share")
|
||||
imageSource: "qrc:/images/controls/share-2.svg"
|
||||
|
||||
onClicked: {
|
||||
clickedFunc: function() {
|
||||
var fileName = ""
|
||||
if (GC.isMobile()) {
|
||||
fileName = configFileName + configExtension
|
||||
@@ -88,9 +100,12 @@ DrawerType {
|
||||
PageController.showBusyIndicator(false)
|
||||
}
|
||||
}
|
||||
|
||||
KeyNavigation.tab: copyConfigTextButton
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: copyConfigTextButton
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
|
||||
@@ -104,19 +119,15 @@ DrawerType {
|
||||
text: qsTr("Copy")
|
||||
imageSource: "qrc:/images/controls/copy.svg"
|
||||
|
||||
onClicked: {
|
||||
configText.selectAll()
|
||||
configText.copy()
|
||||
configText.select(0, 0)
|
||||
PageController.showNotificationMessage(qsTr("Copied"))
|
||||
}
|
||||
KeyNavigation.tab: copyNativeConfigStringButton.visible ? copyNativeConfigStringButton : showSettingsButton
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: copyNativeConfigStringButton
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
|
||||
visible: nativeConfigString.text !== ""
|
||||
visible: false
|
||||
|
||||
defaultColor: "transparent"
|
||||
hoveredColor: Qt.rgba(1, 1, 1, 0.08)
|
||||
@@ -128,15 +139,12 @@ DrawerType {
|
||||
text: qsTr("Copy config string")
|
||||
imageSource: "qrc:/images/controls/copy.svg"
|
||||
|
||||
onClicked: {
|
||||
nativeConfigString.selectAll()
|
||||
nativeConfigString.copy()
|
||||
nativeConfigString.select(0, 0)
|
||||
PageController.showNotificationMessage(qsTr("Copied"))
|
||||
}
|
||||
KeyNavigation.tab: showSettingsButton
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: showSettingsButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
|
||||
@@ -149,83 +157,119 @@ DrawerType {
|
||||
|
||||
text: qsTr("Show connection settings")
|
||||
|
||||
onClicked: {
|
||||
configContentDrawer.visible = true
|
||||
clickedFunc: function() {
|
||||
configContentDrawer.open()
|
||||
}
|
||||
|
||||
KeyNavigation.tab: shareButton
|
||||
}
|
||||
|
||||
DrawerType {
|
||||
DrawerType2 {
|
||||
id: configContentDrawer
|
||||
|
||||
width: parent.width
|
||||
height: parent.height * 0.9
|
||||
parent: root.parent
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
anchors.fill: parent
|
||||
expandedHeight: parent.height * 0.9
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 16
|
||||
expandedContent: Item {
|
||||
id: configContentContainer
|
||||
|
||||
backButtonFunction: function() {
|
||||
configContentDrawer.visible = false
|
||||
implicitHeight: configContentDrawer.expandedHeight
|
||||
|
||||
Connections {
|
||||
target: copyNativeConfigStringButton
|
||||
function onClicked() {
|
||||
nativeConfigString.selectAll()
|
||||
nativeConfigString.copy()
|
||||
nativeConfigString.select(0, 0)
|
||||
PageController.showNotificationMessage(qsTr("Copied"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
anchors.top: backButton.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
contentHeight: configContent.implicitHeight + configContent.anchors.topMargin + configContent.anchors.bottomMargin
|
||||
|
||||
ColumnLayout {
|
||||
id: configContent
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 16
|
||||
anchors.leftMargin: 16
|
||||
|
||||
Header2Type {
|
||||
id: configContentHeader
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Connections {
|
||||
target: copyConfigTextButton
|
||||
function onClicked() {
|
||||
configText.selectAll()
|
||||
configText.copy()
|
||||
configText.select(0, 0)
|
||||
PageController.showNotificationMessage(qsTr("Copied"))
|
||||
}
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: nativeConfigString
|
||||
visible: false
|
||||
text: ExportController.nativeConfigString
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 16
|
||||
|
||||
backButtonFunction: function() {
|
||||
configContentDrawer.close()
|
||||
}
|
||||
}
|
||||
|
||||
TextArea {
|
||||
id: configText
|
||||
FlickableType {
|
||||
anchors.top: backButton.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
contentHeight: configContent.implicitHeight + configContent.anchors.topMargin + configContent.anchors.bottomMargin
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
ColumnLayout {
|
||||
id: configContent
|
||||
|
||||
padding: 0
|
||||
leftPadding: 0
|
||||
height: 24
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 16
|
||||
anchors.leftMargin: 16
|
||||
|
||||
readOnly: true
|
||||
Header2Type {
|
||||
id: configContentHeader
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
|
||||
color: "#D7D8DB"
|
||||
selectionColor: "#633303"
|
||||
selectedTextColor: "#D7D8DB"
|
||||
headerText: root.configContentHeaderText
|
||||
}
|
||||
|
||||
font.pixelSize: 16
|
||||
font.weight: Font.Medium
|
||||
font.family: "PT Root UI VF"
|
||||
TextField {
|
||||
id: nativeConfigString
|
||||
visible: false
|
||||
text: ExportController.nativeConfigString
|
||||
|
||||
text: ExportController.config
|
||||
onTextChanged: {
|
||||
copyNativeConfigStringButton.visible = nativeConfigString.text !== ""
|
||||
}
|
||||
}
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
TextArea {
|
||||
id: configText
|
||||
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
|
||||
padding: 0
|
||||
leftPadding: 0
|
||||
height: 24
|
||||
|
||||
readOnly: true
|
||||
|
||||
color: "#D7D8DB"
|
||||
selectionColor: "#633303"
|
||||
selectedTextColor: "#D7D8DB"
|
||||
|
||||
font.pixelSize: 16
|
||||
font.weight: Font.Medium
|
||||
font.family: "PT Root UI VF"
|
||||
|
||||
text: ExportController.config
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,6 +277,8 @@ DrawerType {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: qrCodeContainer
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width
|
||||
Layout.topMargin: 20
|
||||
|
||||
@@ -16,47 +16,40 @@ Button {
|
||||
property string textColor: "#0E0E11"
|
||||
|
||||
property string borderColor: "#D7D8DB"
|
||||
property string borderFocusedColor: "#D7D8DB"
|
||||
property int borderWidth: 0
|
||||
property int borderFocusedWidth: 1
|
||||
|
||||
property string imageSource
|
||||
property string rightImageSource
|
||||
property string leftImageColor: textColor
|
||||
|
||||
property bool squareLeftSide: false
|
||||
|
||||
property var clickedFunc
|
||||
|
||||
implicitHeight: 56
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
background: Rectangle {
|
||||
id: background
|
||||
anchors.fill: parent
|
||||
radius: 16
|
||||
color: {
|
||||
if (root.enabled) {
|
||||
if (root.pressed) {
|
||||
return pressedColor
|
||||
}
|
||||
return root.hovered ? hoveredColor : defaultColor
|
||||
} else {
|
||||
return disabledColor
|
||||
}
|
||||
}
|
||||
border.color: borderColor
|
||||
border.width: borderWidth
|
||||
id: focusBorder
|
||||
|
||||
Behavior on color {
|
||||
PropertyAnimation { duration: 200 }
|
||||
}
|
||||
color: "transparent"
|
||||
border.color: root.activeFocus ? root.borderFocusedColor : "transparent"
|
||||
border.width: root.activeFocus ? root.borderFocusedWidth : "transparent"
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
radius: 16
|
||||
|
||||
Rectangle {
|
||||
visible: root.squareLeftSide
|
||||
id: background
|
||||
|
||||
z: 1
|
||||
anchors.fill: focusBorder
|
||||
anchors.margins: root.activeFocus ? 2 : 0
|
||||
|
||||
width: parent.radius
|
||||
height: parent.radius
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
radius: root.activeFocus ? 14 : 16
|
||||
color: {
|
||||
if (root.enabled) {
|
||||
if (root.pressed) {
|
||||
@@ -67,24 +60,53 @@ Button {
|
||||
return disabledColor
|
||||
}
|
||||
}
|
||||
border.color: borderColor
|
||||
border.width: borderWidth
|
||||
|
||||
Behavior on color {
|
||||
PropertyAnimation { duration: 200 }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.squareLeftSide
|
||||
|
||||
z: 1
|
||||
|
||||
width: parent.radius
|
||||
height: parent.radius
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
color: {
|
||||
if (root.enabled) {
|
||||
if (root.pressed) {
|
||||
return pressedColor
|
||||
}
|
||||
return root.hovered ? hoveredColor : defaultColor
|
||||
} else {
|
||||
return disabledColor
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
PropertyAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: background
|
||||
anchors.fill: focusBorder
|
||||
enabled: false
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
anchors.fill: background
|
||||
anchors.fill: focusBorder
|
||||
|
||||
implicitWidth: content.implicitWidth
|
||||
implicitHeight: content.implicitHeight
|
||||
|
||||
RowLayout {
|
||||
id: content
|
||||
anchors.centerIn: parent
|
||||
@@ -99,7 +121,7 @@ Button {
|
||||
layer {
|
||||
enabled: true
|
||||
effect: ColorOverlay {
|
||||
color: textColor
|
||||
color: leftImageColor
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,6 +134,39 @@ Button {
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.preferredHeight: 20
|
||||
Layout.preferredWidth: 20
|
||||
|
||||
source: root.rightImageSource
|
||||
visible: root.rightImageSource === "" ? false : true
|
||||
|
||||
layer {
|
||||
enabled: true
|
||||
effect: ColorOverlay {
|
||||
color: textColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
if (root.clickedFunc && typeof root.clickedFunc === "function") {
|
||||
root.clickedFunc()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
if (root.clickedFunc && typeof root.clickedFunc === "function") {
|
||||
root.clickedFunc()
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (root.clickedFunc && typeof root.clickedFunc === "function") {
|
||||
root.clickedFunc()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import "../Config"
|
||||
|
||||
Drawer {
|
||||
id: drawer
|
||||
property bool needCloseButton: true
|
||||
|
||||
Connections {
|
||||
target: PageController
|
||||
|
||||
function onForceCloseDrawer() {
|
||||
visible = false
|
||||
}
|
||||
}
|
||||
|
||||
edge: Qt.BottomEdge
|
||||
|
||||
clip: true
|
||||
modal: true
|
||||
dragMargin: -10
|
||||
|
||||
enter: Transition {
|
||||
SmoothedAnimation {
|
||||
velocity: 4
|
||||
}
|
||||
}
|
||||
|
||||
exit: Transition {
|
||||
SmoothedAnimation {
|
||||
velocity: 4
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: -radius
|
||||
radius: 16
|
||||
color: "#1C1D21"
|
||||
|
||||
border.color: "#2C2D30"
|
||||
border.width: 1
|
||||
|
||||
Rectangle {
|
||||
visible: GC.isMobile()
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: 10
|
||||
|
||||
width: 20
|
||||
height: 2
|
||||
color: "#2C2D30"
|
||||
}
|
||||
}
|
||||
|
||||
Overlay.modal: Rectangle {
|
||||
color: Qt.rgba(14/255, 14/255, 17/255, 0.8)
|
||||
}
|
||||
|
||||
onAboutToShow: {
|
||||
if (PageController.getInitialPageNavigationBarColor() !== 0xFF1C1D21) {
|
||||
PageController.updateNavigationBarColor(0xFF1C1D21)
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
if (needCloseButton) {
|
||||
PageController.drawerOpen()
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
if (needCloseButton) {
|
||||
PageController.drawerClose()
|
||||
}
|
||||
|
||||
var initialPageNavigationBarColor = PageController.getInitialPageNavigationBarColor()
|
||||
if (initialPageNavigationBarColor !== 0xFF1C1D21) {
|
||||
PageController.updateNavigationBarColor(initialPageNavigationBarColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
266
client/ui/qml/Controls2/DrawerType2.qml
Normal file
266
client/ui/qml/Controls2/DrawerType2.qml
Normal file
@@ -0,0 +1,266 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "TextTypes"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property string drawerExpanded: "expanded"
|
||||
readonly property string drawerCollapsed: "collapsed"
|
||||
|
||||
readonly property bool isOpened: drawerContent.state === root.drawerExpanded || (drawerContent.state === root.drawerCollapsed && dragArea.drag.active === true)
|
||||
readonly property bool isClosed: drawerContent.state === root.drawerCollapsed && dragArea.drag.active === false
|
||||
|
||||
readonly property bool isExpanded: drawerContent.state === root.drawerExpanded
|
||||
readonly property bool isCollapsed: drawerContent.state === root.drawerCollapsed
|
||||
|
||||
property Component collapsedContent
|
||||
property Component expandedContent
|
||||
|
||||
property string defaultColor: "#1C1D21"
|
||||
property string borderColor: "#2C2D30"
|
||||
|
||||
property real expandedHeight
|
||||
property real collapsedHeight: 0
|
||||
|
||||
property int depthIndex: 0
|
||||
|
||||
signal entered
|
||||
signal exited
|
||||
signal pressed(bool pressed, bool entered)
|
||||
|
||||
signal aboutToHide
|
||||
signal aboutToShow
|
||||
signal close
|
||||
signal open
|
||||
signal closed
|
||||
signal opened
|
||||
|
||||
Connections {
|
||||
target: PageController
|
||||
|
||||
function onCloseTopDrawer() {
|
||||
if (depthIndex === PageController.getDrawerDepth()) {
|
||||
if (isCollapsed) {
|
||||
return
|
||||
}
|
||||
|
||||
aboutToHide()
|
||||
|
||||
drawerContent.state = root.drawerCollapsed
|
||||
depthIndex = 0
|
||||
closed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
|
||||
function onClose() {
|
||||
if (isCollapsed) {
|
||||
return
|
||||
}
|
||||
|
||||
aboutToHide()
|
||||
|
||||
drawerContent.state = root.drawerCollapsed
|
||||
depthIndex = 0
|
||||
PageController.setDrawerDepth(PageController.getDrawerDepth() - 1)
|
||||
closed()
|
||||
}
|
||||
|
||||
function onOpen() {
|
||||
if (isExpanded) {
|
||||
return
|
||||
}
|
||||
|
||||
aboutToShow()
|
||||
|
||||
drawerContent.state = root.drawerExpanded
|
||||
depthIndex = PageController.getDrawerDepth() + 1
|
||||
PageController.setDrawerDepth(depthIndex)
|
||||
opened()
|
||||
}
|
||||
}
|
||||
|
||||
/** Set once based on first implicit height change once all children are layed out */
|
||||
Component.onCompleted: {
|
||||
if (root.isCollapsed && root.collapsedHeight == 0) {
|
||||
root.collapsedHeight = drawerContent.implicitHeight
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: background
|
||||
|
||||
anchors.fill: parent
|
||||
color: root.isCollapsed ? "transparent" : Qt.rgba(14/255, 14/255, 17/255, 0.8)
|
||||
|
||||
Behavior on color {
|
||||
PropertyAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: emptyArea
|
||||
anchors.fill: parent
|
||||
enabled: root.isExpanded
|
||||
visible: enabled
|
||||
onClicked: {
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dragArea
|
||||
|
||||
anchors.fill: drawerContentBackground
|
||||
cursorShape: root.isCollapsed ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
hoverEnabled: true
|
||||
|
||||
enabled: drawerContent.implicitHeight > 0
|
||||
|
||||
drag.target: drawerContent
|
||||
drag.axis: Drag.YAxis
|
||||
drag.maximumY: root.height - root.collapsedHeight
|
||||
drag.minimumY: root.height - root.expandedHeight
|
||||
|
||||
/** If drag area is released at any point other than min or max y, transition to the other state */
|
||||
onReleased: {
|
||||
if (root.isCollapsed && drawerContent.y < dragArea.drag.maximumY) {
|
||||
root.open()
|
||||
return
|
||||
}
|
||||
if (root.isExpanded && drawerContent.y > dragArea.drag.minimumY) {
|
||||
root.close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
root.entered()
|
||||
}
|
||||
onExited: {
|
||||
root.exited()
|
||||
}
|
||||
onPressedChanged: {
|
||||
root.pressed(pressed, entered)
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (root.isCollapsed) {
|
||||
root.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: drawerContentBackground
|
||||
|
||||
anchors { left: drawerContent.left; right: drawerContent.right; top: drawerContent.top }
|
||||
height: root.height
|
||||
radius: 16
|
||||
color: root.defaultColor
|
||||
border.color: root.borderColor
|
||||
border.width: 1
|
||||
|
||||
Rectangle {
|
||||
width: parent.radius
|
||||
height: parent.radius
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
color: parent.color
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: drawerContent
|
||||
|
||||
Drag.active: dragArea.drag.active
|
||||
anchors.right: root.right
|
||||
anchors.left: root.left
|
||||
y: root.height - drawerContent.height
|
||||
state: root.drawerCollapsed
|
||||
|
||||
implicitHeight: root.isCollapsed ? collapsedLoader.implicitHeight : expandedLoader.implicitHeight
|
||||
|
||||
onStateChanged: {
|
||||
if (root.isCollapsed) {
|
||||
var initialPageNavigationBarColor = PageController.getInitialPageNavigationBarColor()
|
||||
if (initialPageNavigationBarColor !== 0xFF1C1D21) {
|
||||
PageController.updateNavigationBarColor(initialPageNavigationBarColor)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (root.isExpanded) {
|
||||
if (PageController.getInitialPageNavigationBarColor() !== 0xFF1C1D21) {
|
||||
PageController.updateNavigationBarColor(0xFF1C1D21)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: root.drawerCollapsed
|
||||
PropertyChanges {
|
||||
target: drawerContent
|
||||
y: root.height - root.collapsedHeight
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: root.drawerExpanded
|
||||
PropertyChanges {
|
||||
target: drawerContent
|
||||
y: dragArea.drag.minimumY
|
||||
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: root.drawerCollapsed
|
||||
to: root.drawerExpanded
|
||||
PropertyAnimation {
|
||||
target: drawerContent
|
||||
properties: "y"
|
||||
duration: 200
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: root.drawerExpanded
|
||||
to: root.drawerCollapsed
|
||||
PropertyAnimation {
|
||||
target: drawerContent
|
||||
properties: "y"
|
||||
duration: 200
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Loader {
|
||||
id: collapsedLoader
|
||||
|
||||
visible: root.isCollapsed
|
||||
sourceComponent: root.collapsedContent
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: expandedLoader
|
||||
|
||||
visible: root.isExpanded
|
||||
sourceComponent: root.expandedContent
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,19 +36,23 @@ Item {
|
||||
property int rootButtonTextBottomMargin: 16
|
||||
|
||||
property real drawerHeight: 0.9
|
||||
property Item drawerParent
|
||||
property Component listView
|
||||
|
||||
property alias menuVisible: menu.visible
|
||||
signal open
|
||||
signal close
|
||||
|
||||
implicitWidth: rootButtonContent.implicitWidth
|
||||
implicitHeight: rootButtonContent.implicitHeight
|
||||
|
||||
onMenuVisibleChanged: {
|
||||
if (menuVisible) {
|
||||
rootButtonBackground.border.color = rootButtonPressedBorderColor
|
||||
} else {
|
||||
rootButtonBackground.border.color = rootButtonDefaultBorderColor
|
||||
}
|
||||
onOpen: {
|
||||
menu.open()
|
||||
rootButtonBackground.border.color = rootButtonPressedBorderColor
|
||||
}
|
||||
|
||||
onClose: {
|
||||
menu.close()
|
||||
rootButtonBackground.border.color = rootButtonDefaultBorderColor
|
||||
}
|
||||
|
||||
onEnabledChanged: {
|
||||
@@ -133,21 +137,21 @@ Item {
|
||||
hoverEnabled: root.enabled ? true : false
|
||||
|
||||
onEntered: {
|
||||
if (menu.visible === false) {
|
||||
if (menu.isClosed) {
|
||||
rootButtonBackground.border.color = rootButtonHoveredBorderColor
|
||||
rootButtonBackground.color = rootButtonBackgroundHoveredColor
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
if (menu.visible === false) {
|
||||
if (menu.isClosed) {
|
||||
rootButtonBackground.border.color = rootButtonDefaultBorderColor
|
||||
rootButtonBackground.color = rootButtonBackgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: {
|
||||
if (menu.visible === false) {
|
||||
if (menu.isClosed) {
|
||||
rootButtonBackground.color = pressed ? rootButtonBackgroundPressedColor : entered ? rootButtonHoveredBorderColor : rootButtonDefaultBorderColor
|
||||
}
|
||||
}
|
||||
@@ -156,60 +160,68 @@ Item {
|
||||
if (rootButtonClickedFunction && typeof rootButtonClickedFunction === "function") {
|
||||
rootButtonClickedFunction()
|
||||
} else {
|
||||
menu.visible = true
|
||||
menu.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DrawerType {
|
||||
DrawerType2 {
|
||||
id: menu
|
||||
|
||||
width: parent.width
|
||||
height: parent.height * drawerHeight
|
||||
parent: drawerParent
|
||||
|
||||
ColumnLayout {
|
||||
id: header
|
||||
anchors.fill: parent
|
||||
expandedHeight: drawerParent.height * drawerHeight
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 16
|
||||
expandedContent: Item {
|
||||
id: container
|
||||
|
||||
BackButtonType {
|
||||
backButtonImage: root.headerBackButtonImage
|
||||
backButtonFunction: function() {
|
||||
root.menuVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
implicitHeight: menu.expandedHeight
|
||||
|
||||
FlickableType {
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: 16
|
||||
contentHeight: col.implicitHeight
|
||||
ColumnLayout {
|
||||
id: header
|
||||
|
||||
Column {
|
||||
id: col
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 16
|
||||
|
||||
spacing: 16
|
||||
BackButtonType {
|
||||
backButtonImage: root.headerBackButtonImage
|
||||
backButtonFunction: function() {
|
||||
menu.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Header2Type {
|
||||
FlickableType {
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: 16
|
||||
contentHeight: col.implicitHeight
|
||||
|
||||
Column {
|
||||
id: col
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
|
||||
headerText: root.headerText
|
||||
spacing: 16
|
||||
|
||||
width: parent.width
|
||||
}
|
||||
Header2Type {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
|
||||
Loader {
|
||||
id: listViewLoader
|
||||
sourceComponent: root.listView
|
||||
headerText: root.headerText
|
||||
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: listViewLoader
|
||||
sourceComponent: root.listView
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ Button {
|
||||
property string pressedColor: Qt.rgba(1, 1, 1, 0.12)
|
||||
property string disableColor: "#2C2D30"
|
||||
|
||||
property string borderFocusedColor: "#D7D8DB"
|
||||
property int borderFocusedWidth: 1
|
||||
|
||||
property string imageColor: "#878B91"
|
||||
property string disableImageColor: "#2C2D30"
|
||||
|
||||
@@ -31,6 +34,9 @@ Button {
|
||||
id: background
|
||||
|
||||
anchors.fill: parent
|
||||
border.color: root.activeFocus ? root.borderFocusedColor : "transparent"
|
||||
border.width: root.activeFocus ? root.borderFocusedWidth : "transparent"
|
||||
|
||||
color: {
|
||||
if (root.enabled) {
|
||||
if (root.pressed) {
|
||||
|
||||
@@ -27,6 +27,8 @@ Item {
|
||||
|
||||
property string rightImageColor: "#d7d8db"
|
||||
|
||||
property alias rightButton: rightImage
|
||||
|
||||
property bool descriptionOnTop: false
|
||||
|
||||
implicitWidth: content.implicitWidth + content.anchors.topMargin + content.anchors.bottomMargin
|
||||
@@ -207,4 +209,16 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
if (clickedFunction && typeof clickedFunction === "function") {
|
||||
clickedFunction()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
if (clickedFunction && typeof clickedFunction === "function") {
|
||||
clickedFunction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,21 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import "../Config"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property StackView stackView: StackView.view
|
||||
|
||||
property var defaultActiveFocusItem: null
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible && !GC.isMobile()) {
|
||||
timer.start()
|
||||
}
|
||||
}
|
||||
|
||||
// MouseArea {
|
||||
// id: globalMouseArea
|
||||
// z: 99
|
||||
@@ -19,4 +29,17 @@ Item {
|
||||
// mouse.accepted = false
|
||||
// }
|
||||
// }
|
||||
|
||||
// Set a timer to set focus after a short delay
|
||||
Timer {
|
||||
id: timer
|
||||
interval: 100 // Milliseconds
|
||||
onTriggered: {
|
||||
if (defaultActiveFocusItem) {
|
||||
defaultActiveFocusItem.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
repeat: false // Stop the timer after one trigger
|
||||
running: !GC.isMobile() // Start the timer
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ Popup {
|
||||
borderWidth: 0
|
||||
|
||||
text: qsTr("Close")
|
||||
onClicked: {
|
||||
clickedFunc: function() {
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,10 +69,13 @@ Item {
|
||||
|
||||
TextField {
|
||||
id: textField
|
||||
activeFocusOnTab: false
|
||||
|
||||
enabled: root.textFieldEditable
|
||||
color: root.enabled ? root.textFieldTextColor : root.textFieldTextDisabledColor
|
||||
|
||||
inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhSensitiveData | Qt.ImhNoPredictiveText
|
||||
|
||||
placeholderText: root.textFieldPlaceholderText
|
||||
placeholderTextColor: "#494B50"
|
||||
|
||||
@@ -134,6 +137,7 @@ Item {
|
||||
// textColor: "#D7D8DB"
|
||||
// borderWidth: 0
|
||||
|
||||
focusPolicy: Qt.NoFocus
|
||||
text: root.buttonText
|
||||
imageSource: root.buttonImageSource
|
||||
|
||||
@@ -142,7 +146,7 @@ Item {
|
||||
Layout.preferredWidth: content.implicitHeight
|
||||
squareLeftSide: true
|
||||
|
||||
onClicked: {
|
||||
clickedFunc: function() {
|
||||
if (root.clickedFunc && typeof root.clickedFunc === "function") {
|
||||
root.clickedFunc()
|
||||
}
|
||||
@@ -186,4 +190,14 @@ Item {
|
||||
function getBackgroundBorderColor(noneFocusedColor) {
|
||||
return textField.focus ? root.borderFocusedColor : noneFocusedColor
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
if (KeyNavigation.tab)
|
||||
KeyNavigation.tab.forceActiveFocus();
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
if (KeyNavigation.tab)
|
||||
KeyNavigation.tab.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import "../Config"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
Component.onCompleted: PageController.enableTabBar(false)
|
||||
Component.onDestruction: PageController.enableTabBar(true)
|
||||
Component.onCompleted: PageController.disableControls(true)
|
||||
Component.onDestruction: PageController.disableControls(false)
|
||||
|
||||
SortFilterProxyModel {
|
||||
id: proxyServersModel
|
||||
|
||||
@@ -18,484 +18,382 @@ import "../Components"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property string defaultColor: "#1C1D21"
|
||||
|
||||
property string borderColor: "#2C2D30"
|
||||
|
||||
Connections {
|
||||
target: PageController
|
||||
|
||||
function onRestorePageHomeState(isContainerInstalled) {
|
||||
buttonContent.state = "expanded"
|
||||
drawer.open()
|
||||
if (isContainerInstalled) {
|
||||
containersDropDown.rootButtonClickedFunction()
|
||||
}
|
||||
}
|
||||
|
||||
function onForceCloseDrawer() {
|
||||
buttonContent.state = "collapsed"
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: buttonContent.state === "expanded"
|
||||
onClicked: {
|
||||
buttonContent.state = "collapsed"
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: buttonContent.collapsedHeight
|
||||
anchors.bottomMargin: drawer.collapsedHeight
|
||||
|
||||
ConnectButton {
|
||||
id: connectButton
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dragArea
|
||||
|
||||
anchors.fill: buttonBackground
|
||||
cursorShape: buttonContent.state === "collapsed" ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
hoverEnabled: true
|
||||
|
||||
drag.target: buttonContent
|
||||
drag.axis: Drag.YAxis
|
||||
drag.maximumY: root.height - buttonContent.collapsedHeight
|
||||
drag.minimumY: root.height - root.height * 0.9
|
||||
|
||||
/** If drag area is released at any point other than min or max y, transition to the other state */
|
||||
onReleased: {
|
||||
if (buttonContent.state === "collapsed" && buttonContent.y < dragArea.drag.maximumY) {
|
||||
buttonContent.state = "expanded"
|
||||
return
|
||||
}
|
||||
if (buttonContent.state === "expanded" && buttonContent.y > dragArea.drag.minimumY) {
|
||||
buttonContent.state = "collapsed"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
collapsedButtonChevron.backgroundColor = collapsedButtonChevron.hoveredColor
|
||||
collapsedButtonHeader.opacity = 0.8
|
||||
}
|
||||
onExited: {
|
||||
collapsedButtonChevron.backgroundColor = collapsedButtonChevron.defaultColor
|
||||
collapsedButtonHeader.opacity = 1
|
||||
}
|
||||
onPressedChanged: {
|
||||
collapsedButtonChevron.backgroundColor = pressed ? collapsedButtonChevron.pressedColor : entered ? collapsedButtonChevron.hoveredColor : collapsedButtonChevron.defaultColor
|
||||
collapsedButtonHeader.opacity = 0.7
|
||||
}
|
||||
|
||||
|
||||
onClicked: {
|
||||
if (buttonContent.state === "collapsed") {
|
||||
buttonContent.state = "expanded"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: buttonBackground
|
||||
|
||||
anchors { left: buttonContent.left; right: buttonContent.right; top: buttonContent.top }
|
||||
height: root.height
|
||||
radius: 16
|
||||
color: root.defaultColor
|
||||
border.color: root.borderColor
|
||||
border.width: 1
|
||||
|
||||
Rectangle {
|
||||
width: parent.radius
|
||||
height: parent.radius
|
||||
BasicButtonType {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
color: parent.color
|
||||
anchors.bottomMargin: 34
|
||||
leftPadding: 16
|
||||
rightPadding: 16
|
||||
|
||||
implicitHeight: 36
|
||||
|
||||
defaultColor: "transparent"
|
||||
hoveredColor: Qt.rgba(1, 1, 1, 0.08)
|
||||
pressedColor: Qt.rgba(1, 1, 1, 0.12)
|
||||
disabledColor: "#878B91"
|
||||
textColor: "#878B91"
|
||||
leftImageColor: "transparent"
|
||||
borderWidth: 0
|
||||
|
||||
property bool isSplitTunnelingEnabled: SitesModel.isTunnelingEnabled ||
|
||||
(ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && ServersModel.getDefaultServerData("isServerFromApi"))
|
||||
|
||||
text: isSplitTunnelingEnabled ? qsTr("Split tunneling enabled") : qsTr("Split tunneling disabled")
|
||||
|
||||
imageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : ""
|
||||
rightImageSource: "qrc:/images/controls/chevron-down.svg"
|
||||
|
||||
onClicked: {
|
||||
homeSplitTunnelingDrawer.open()
|
||||
}
|
||||
|
||||
HomeSplitTunnelingDrawer {
|
||||
id: homeSplitTunnelingDrawer
|
||||
|
||||
parent: root
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: buttonContent
|
||||
|
||||
/** Initial height of button content */
|
||||
property int collapsedHeight: 0
|
||||
/** True when expanded objects should be visible */
|
||||
property bool expandedVisibility: buttonContent.state === "expanded" || (buttonContent.state === "collapsed" && dragArea.drag.active === true)
|
||||
/** True when collapsed objects should be visible */
|
||||
property bool collapsedVisibility: buttonContent.state === "collapsed" && dragArea.drag.active === false
|
||||
DrawerType2 {
|
||||
id: drawer
|
||||
anchors.fill: parent
|
||||
|
||||
Drag.active: dragArea.drag.active
|
||||
anchors.right: root.right
|
||||
anchors.left: root.left
|
||||
y: root.height - buttonContent.height
|
||||
|
||||
Component.onCompleted: {
|
||||
buttonContent.state = "collapsed"
|
||||
}
|
||||
|
||||
/** Set once based on first implicit height change once all children are layed out */
|
||||
onImplicitHeightChanged: {
|
||||
if (buttonContent.state === "collapsed" && collapsedHeight == 0) {
|
||||
collapsedHeight = implicitHeight
|
||||
}
|
||||
}
|
||||
|
||||
onStateChanged: {
|
||||
if (buttonContent.state === "collapsed") {
|
||||
var initialPageNavigationBarColor = PageController.getInitialPageNavigationBarColor()
|
||||
if (initialPageNavigationBarColor !== 0xFF1C1D21) {
|
||||
PageController.updateNavigationBarColor(initialPageNavigationBarColor)
|
||||
}
|
||||
PageController.drawerClose()
|
||||
return
|
||||
}
|
||||
if (buttonContent.state === "expanded") {
|
||||
if (PageController.getInitialPageNavigationBarColor() !== 0xFF1C1D21) {
|
||||
PageController.updateNavigationBarColor(0xFF1C1D21)
|
||||
}
|
||||
PageController.drawerOpen()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/** Two states of buttonContent, great place to add any future animations for the drawer */
|
||||
states: [
|
||||
State {
|
||||
name: "collapsed"
|
||||
PropertyChanges {
|
||||
target: buttonContent
|
||||
y: root.height - collapsedHeight
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "expanded"
|
||||
PropertyChanges {
|
||||
target: buttonContent
|
||||
y: dragArea.drag.minimumY
|
||||
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: "collapsed"
|
||||
to: "expanded"
|
||||
PropertyAnimation {
|
||||
target: buttonContent
|
||||
properties: "y"
|
||||
duration: 200
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "expanded"
|
||||
to: "collapsed"
|
||||
PropertyAnimation {
|
||||
target: buttonContent
|
||||
properties: "y"
|
||||
duration: 200
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
DividerType {
|
||||
Layout.topMargin: 10
|
||||
Layout.fillWidth: false
|
||||
Layout.preferredWidth: 20
|
||||
Layout.preferredHeight: 2
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
|
||||
visible: (buttonContent.collapsedVisibility || buttonContent.expandedVisibility)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: 14
|
||||
Layout.leftMargin: 24
|
||||
Layout.rightMargin: 24
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
visible: buttonContent.collapsedVisibility
|
||||
|
||||
spacing: 0
|
||||
|
||||
Header1TextType {
|
||||
id: collapsedButtonHeader
|
||||
Layout.maximumWidth: buttonContent.width - 48 - 18 - 12 // todo
|
||||
|
||||
maximumLineCount: 2
|
||||
elide: Qt.ElideRight
|
||||
|
||||
text: ServersModel.defaultServerName
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
|
||||
Behavior on opacity {
|
||||
PropertyAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
id: collapsedButtonChevron
|
||||
|
||||
Layout.leftMargin: 8
|
||||
|
||||
hoverEnabled: false
|
||||
image: "qrc:/images/controls/chevron-down.svg"
|
||||
imageColor: "#d7d8db"
|
||||
|
||||
icon.width: 18
|
||||
icon.height: 18
|
||||
backgroundRadius: 16
|
||||
horizontalPadding: 4
|
||||
topPadding: 4
|
||||
bottomPadding: 3
|
||||
|
||||
onClicked: {
|
||||
if (buttonContent.state === "collapsed") {
|
||||
buttonContent.state = "expanded"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LabelTextType {
|
||||
id: collapsedServerMenuDescription
|
||||
Layout.bottomMargin: 44
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
visible: buttonContent.collapsedVisibility
|
||||
text: ServersModel.defaultServerDescriptionCollapsed
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: serversMenuHeader
|
||||
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
visible: buttonContent.expandedVisibility
|
||||
|
||||
Header1TextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 14
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: ServersModel.defaultServerName
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
maximumLineCount: 2
|
||||
elide: Qt.ElideRight
|
||||
}
|
||||
|
||||
LabelTextType {
|
||||
id: expandedServersMenuDescription
|
||||
Layout.bottomMargin: 24
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
text: ServersModel.defaultServerDescriptionExpanded
|
||||
collapsedContent: ColumnLayout {
|
||||
DividerType {
|
||||
Layout.topMargin: 10
|
||||
Layout.fillWidth: false
|
||||
Layout.preferredWidth: 20
|
||||
Layout.preferredHeight: 2
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: 14
|
||||
Layout.leftMargin: 24
|
||||
Layout.rightMargin: 24
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
spacing: 8
|
||||
|
||||
DropDownType {
|
||||
id: containersDropDown
|
||||
spacing: 0
|
||||
|
||||
rootButtonImageColor: "#0E0E11"
|
||||
rootButtonBackgroundColor: "#D7D8DB"
|
||||
rootButtonBackgroundHoveredColor: Qt.rgba(215, 216, 219, 0.8)
|
||||
rootButtonBackgroundPressedColor: Qt.rgba(215, 216, 219, 0.65)
|
||||
rootButtonHoveredBorderColor: "transparent"
|
||||
rootButtonDefaultBorderColor: "transparent"
|
||||
rootButtonTextTopMargin: 8
|
||||
rootButtonTextBottomMargin: 8
|
||||
|
||||
text: ServersModel.defaultContainerName
|
||||
textColor: "#0E0E11"
|
||||
headerText: qsTr("VPN protocol")
|
||||
headerBackButtonImage: "qrc:/images/controls/arrow-left.svg"
|
||||
|
||||
rootButtonClickedFunction: function() {
|
||||
ServersModel.currentlyProcessedIndex = serversMenuContent.currentIndex
|
||||
containersDropDown.menuVisible = true
|
||||
Connections {
|
||||
target: drawer
|
||||
function onEntered() {
|
||||
collapsedButtonChevron.backgroundColor = collapsedButtonChevron.hoveredColor
|
||||
collapsedButtonHeader.opacity = 0.8
|
||||
}
|
||||
|
||||
listView: HomeContainersListView {
|
||||
rootWidth: root.width
|
||||
function onExited() {
|
||||
collapsedButtonChevron.backgroundColor = collapsedButtonChevron.defaultColor
|
||||
collapsedButtonHeader.opacity = 1
|
||||
}
|
||||
|
||||
function onPressed(pressed, entered) {
|
||||
collapsedButtonChevron.backgroundColor = pressed ? collapsedButtonChevron.pressedColor : entered ? collapsedButtonChevron.hoveredColor : collapsedButtonChevron.defaultColor
|
||||
collapsedButtonHeader.opacity = 0.7
|
||||
}
|
||||
}
|
||||
|
||||
Header1TextType {
|
||||
id: collapsedButtonHeader
|
||||
Layout.maximumWidth: drawer.width - 48 - 18 - 12 // todo
|
||||
|
||||
maximumLineCount: 2
|
||||
elide: Qt.ElideRight
|
||||
|
||||
text: ServersModel.defaultServerName
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
|
||||
Behavior on opacity {
|
||||
PropertyAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
id: collapsedButtonChevron
|
||||
|
||||
Layout.leftMargin: 8
|
||||
|
||||
hoverEnabled: false
|
||||
image: "qrc:/images/controls/chevron-down.svg"
|
||||
imageColor: "#d7d8db"
|
||||
|
||||
icon.width: 18
|
||||
icon.height: 18
|
||||
backgroundRadius: 16
|
||||
horizontalPadding: 4
|
||||
topPadding: 4
|
||||
bottomPadding: 3
|
||||
|
||||
onClicked: {
|
||||
if (drawer.isCollapsed) {
|
||||
drawer.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LabelTextType {
|
||||
id: collapsedServerMenuDescription
|
||||
Layout.bottomMargin: 44
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
text: ServersModel.defaultServerDescriptionCollapsed
|
||||
}
|
||||
}
|
||||
expandedContent: Item {
|
||||
id: serverMenuContainer
|
||||
|
||||
implicitHeight: Qt.platform.os !== "ios" ? root.height * 0.9 : screen.height * 0.77
|
||||
|
||||
Component.onCompleted: {
|
||||
drawer.expandedHeight = serverMenuContainer.implicitHeight
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: serversMenuHeader
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
|
||||
|
||||
Header1TextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 14
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: ServersModel.defaultServerName
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
maximumLineCount: 2
|
||||
elide: Qt.ElideRight
|
||||
}
|
||||
|
||||
LabelTextType {
|
||||
id: expandedServersMenuDescription
|
||||
Layout.bottomMargin: ServersModel.isDefaultServerFromApi ? 69 : 24
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
text: ServersModel.defaultServerDescriptionExpanded
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
spacing: 8
|
||||
|
||||
visible: !ServersModel.isDefaultServerFromApi
|
||||
onVisibleChanged: expandedServersMenuDescription.Layout
|
||||
|
||||
DropDownType {
|
||||
id: containersDropDown
|
||||
|
||||
rootButtonImageColor: "#0E0E11"
|
||||
rootButtonBackgroundColor: "#D7D8DB"
|
||||
rootButtonBackgroundHoveredColor: Qt.rgba(215, 216, 219, 0.8)
|
||||
rootButtonBackgroundPressedColor: Qt.rgba(215, 216, 219, 0.65)
|
||||
rootButtonHoveredBorderColor: "transparent"
|
||||
rootButtonDefaultBorderColor: "transparent"
|
||||
rootButtonTextTopMargin: 8
|
||||
rootButtonTextBottomMargin: 8
|
||||
|
||||
text: ServersModel.defaultServerDefaultContainerName
|
||||
textColor: "#0E0E11"
|
||||
headerText: qsTr("VPN protocol")
|
||||
headerBackButtonImage: "qrc:/images/controls/arrow-left.svg"
|
||||
|
||||
rootButtonClickedFunction: function() {
|
||||
containersDropDown.open()
|
||||
}
|
||||
|
||||
drawerParent: root
|
||||
|
||||
listView: HomeContainersListView {
|
||||
rootWidth: root.width
|
||||
|
||||
Connections {
|
||||
target: ServersModel
|
||||
|
||||
function onDefaultServerIndexChanged() {
|
||||
updateContainersModelFilters()
|
||||
}
|
||||
}
|
||||
|
||||
function updateContainersModelFilters() {
|
||||
if (ServersModel.isDefaultServerHasWriteAccess()) {
|
||||
proxyDefaultServerContainersModel.filters = ContainersModelFilters.getWriteAccessProtocolsListFilters()
|
||||
} else {
|
||||
proxyDefaultServerContainersModel.filters = ContainersModelFilters.getReadAccessProtocolsListFilters()
|
||||
}
|
||||
}
|
||||
|
||||
model: SortFilterProxyModel {
|
||||
id: proxyDefaultServerContainersModel
|
||||
sourceModel: DefaultServerContainersModel
|
||||
|
||||
sorters: [
|
||||
RoleSorter { roleName: "isInstalled"; sortOrder: Qt.DescendingOrder }
|
||||
]
|
||||
}
|
||||
|
||||
Component.onCompleted: updateContainersModelFilters()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Header2Type {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 48
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
headerText: qsTr("Servers")
|
||||
}
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: serversContainer
|
||||
|
||||
anchors.top: serversMenuHeader.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
anchors.topMargin: 16
|
||||
|
||||
contentHeight: col.height + col.anchors.bottomMargin
|
||||
implicitHeight: parent.height - serversMenuHeader.implicitHeight
|
||||
clip: true
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
id: scrollBar
|
||||
policy: serversContainer.height >= serversContainer.contentHeight ? ScrollBar.AlwaysOff : ScrollBar.AlwaysOn
|
||||
}
|
||||
|
||||
Keys.onUpPressed: scrollBar.decrease()
|
||||
Keys.onDownPressed: scrollBar.increase()
|
||||
|
||||
Column {
|
||||
id: col
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottomMargin: 32
|
||||
|
||||
spacing: 16
|
||||
|
||||
ButtonGroup {
|
||||
id: serversRadioButtonGroup
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: serversMenuContent
|
||||
width: parent.width
|
||||
height: serversMenuContent.contentItem.height
|
||||
|
||||
model: ServersModel
|
||||
currentIndex: ServersModel.defaultIndex
|
||||
|
||||
Connections {
|
||||
target: ServersModel
|
||||
|
||||
function onCurrentlyProcessedServerIndexChanged() {
|
||||
updateContainersModelFilters()
|
||||
function onDefaultServerIndexChanged(serverIndex) {
|
||||
serversMenuContent.currentIndex = serverIndex
|
||||
}
|
||||
}
|
||||
|
||||
function updateContainersModelFilters() {
|
||||
if (ServersModel.isCurrentlyProcessedServerHasWriteAccess()) {
|
||||
proxyContainersModel.filters = ContainersModelFilters.getWriteAccessProtocolsListFilters()
|
||||
} else {
|
||||
proxyContainersModel.filters = ContainersModelFilters.getReadAccessProtocolsListFilters()
|
||||
}
|
||||
}
|
||||
clip: true
|
||||
interactive: false
|
||||
|
||||
model: SortFilterProxyModel {
|
||||
id: proxyContainersModel
|
||||
sourceModel: ContainersModel
|
||||
}
|
||||
delegate: Item {
|
||||
id: menuContentDelegate
|
||||
|
||||
Component.onCompleted: updateContainersModelFilters()
|
||||
}
|
||||
}
|
||||
}
|
||||
property variant delegateData: model
|
||||
|
||||
Header2Type {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 48
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
visible: buttonContent.expandedVisibility
|
||||
implicitWidth: serversMenuContent.width
|
||||
implicitHeight: serverRadioButtonContent.implicitHeight
|
||||
|
||||
headerText: qsTr("Servers")
|
||||
}
|
||||
}
|
||||
ColumnLayout {
|
||||
id: serverRadioButtonContent
|
||||
|
||||
Flickable {
|
||||
id: serversContainer
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
contentHeight: col.implicitHeight
|
||||
implicitHeight: root.height - (root.height * 0.1) - serversMenuHeader.implicitHeight - 52 //todo 52 is tabbar height
|
||||
visible: buttonContent.expandedVisibility
|
||||
clip: true
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 16
|
||||
anchors.leftMargin: 16
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
id: scrollBar
|
||||
policy: serversContainer.height >= serversContainer.contentHeight ? ScrollBar.AlwaysOff : ScrollBar.AlwaysOn
|
||||
}
|
||||
spacing: 0
|
||||
|
||||
Keys.onUpPressed: scrollBar.decrease()
|
||||
Keys.onDownPressed: scrollBar.increase()
|
||||
RowLayout {
|
||||
VerticalRadioButton {
|
||||
id: serverRadioButton
|
||||
|
||||
Column {
|
||||
id: col
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 16
|
||||
text: name
|
||||
descriptionText: serverDescription
|
||||
|
||||
ButtonGroup {
|
||||
id: serversRadioButtonGroup
|
||||
}
|
||||
checked: index === serversMenuContent.currentIndex
|
||||
checkable: !ConnectionController.isConnected
|
||||
|
||||
ListView {
|
||||
id: serversMenuContent
|
||||
width: parent.width
|
||||
height: serversMenuContent.contentItem.height
|
||||
ButtonGroup.group: serversRadioButtonGroup
|
||||
|
||||
model: ServersModel
|
||||
currentIndex: ServersModel.defaultIndex
|
||||
onClicked: {
|
||||
if (ConnectionController.isConnected) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change server while there is an active connection"))
|
||||
return
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ServersModel
|
||||
function onDefaultServerIndexChanged(serverIndex) {
|
||||
serversMenuContent.currentIndex = serverIndex
|
||||
}
|
||||
}
|
||||
serversMenuContent.currentIndex = index
|
||||
|
||||
clip: true
|
||||
interactive: false
|
||||
ServersModel.defaultIndex = index
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: menuContentDelegate
|
||||
MouseArea {
|
||||
anchors.fill: serverRadioButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
|
||||
property variant delegateData: model
|
||||
ImageButtonType {
|
||||
image: "qrc:/images/controls/settings.svg"
|
||||
imageColor: "#D7D8DB"
|
||||
|
||||
implicitWidth: serversMenuContent.width
|
||||
implicitHeight: serverRadioButtonContent.implicitHeight
|
||||
implicitWidth: 56
|
||||
implicitHeight: 56
|
||||
|
||||
ColumnLayout {
|
||||
id: serverRadioButtonContent
|
||||
z: 1
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 16
|
||||
anchors.leftMargin: 16
|
||||
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
VerticalRadioButton {
|
||||
id: serverRadioButton
|
||||
onClicked: function() {
|
||||
ServersModel.processedIndex = index
|
||||
PageController.goToPage(PageEnum.PageSettingsServerInfo)
|
||||
drawer.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: name
|
||||
descriptionText: {
|
||||
var fullDescription = ""
|
||||
if (hasWriteAccess) {
|
||||
if (SettingsController.isAmneziaDnsEnabled()
|
||||
&& ServersModel.isAmneziaDnsContainerInstalled(index)) {
|
||||
fullDescription += "Amnezia DNS | "
|
||||
}
|
||||
} else {
|
||||
if (containsAmneziaDns) {
|
||||
fullDescription += "Amnezia DNS | "
|
||||
}
|
||||
}
|
||||
|
||||
return fullDescription += serverDescription
|
||||
}
|
||||
|
||||
checked: index === serversMenuContent.currentIndex
|
||||
checkable: !ConnectionController.isConnected
|
||||
|
||||
ButtonGroup.group: serversRadioButtonGroup
|
||||
|
||||
onClicked: {
|
||||
if (ConnectionController.isConnected) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change server while there is an active connection"))
|
||||
return
|
||||
}
|
||||
|
||||
serversMenuContent.currentIndex = index
|
||||
|
||||
ServersModel.currentlyProcessedIndex = index
|
||||
ServersModel.defaultIndex = index
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: serverRadioButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: false
|
||||
}
|
||||
Layout.leftMargin: 0
|
||||
Layout.rightMargin: 0
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
image: "qrc:/images/controls/settings.svg"
|
||||
imageColor: "#D7D8DB"
|
||||
|
||||
implicitWidth: 56
|
||||
implicitHeight: 56
|
||||
|
||||
z: 1
|
||||
|
||||
onClicked: function() {
|
||||
ServersModel.currentlyProcessedIndex = index
|
||||
PageController.goToPage(PageEnum.PageSettingsServerInfo)
|
||||
buttonContent.state = "collapsed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 0
|
||||
Layout.rightMargin: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,12 @@ import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
defaultActiveFocusItem: listview.currentItem.portTextField.textField
|
||||
|
||||
ColumnLayout {
|
||||
id: backButton
|
||||
|
||||
@@ -41,9 +44,11 @@ PageType {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
enabled: ServersModel.isCurrentlyProcessedServerHasWriteAccess()
|
||||
enabled: ServersModel.isProcessedServerHasWriteAccess()
|
||||
|
||||
ListView {
|
||||
|
||||
|
||||
id: listview
|
||||
|
||||
width: parent.width
|
||||
@@ -55,9 +60,13 @@ PageType {
|
||||
model: AwgConfigModel
|
||||
|
||||
delegate: Item {
|
||||
id: _delegate
|
||||
|
||||
implicitWidth: listview.width
|
||||
implicitHeight: col.implicitHeight
|
||||
|
||||
property alias portTextField:portTextField
|
||||
|
||||
ColumnLayout {
|
||||
id: col
|
||||
|
||||
@@ -93,6 +102,8 @@ PageType {
|
||||
}
|
||||
|
||||
checkEmptyText: true
|
||||
|
||||
KeyNavigation.tab: junkPacketCountTextField.textField
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -100,7 +111,7 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
|
||||
headerText: qsTr("Junk packet count")
|
||||
headerText: "Jc - Junk packet count"
|
||||
textFieldText: junkPacketCount
|
||||
textField.validator: IntValidator { bottom: 0 }
|
||||
|
||||
@@ -116,6 +127,8 @@ PageType {
|
||||
}
|
||||
|
||||
checkEmptyText: true
|
||||
|
||||
KeyNavigation.tab: junkPacketMinSizeTextField.textField
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -123,7 +136,7 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
|
||||
headerText: qsTr("Junk packet minimum size")
|
||||
headerText: "Jmin - Junk packet minimum size"
|
||||
textFieldText: junkPacketMinSize
|
||||
textField.validator: IntValidator { bottom: 0 }
|
||||
|
||||
@@ -134,6 +147,8 @@ PageType {
|
||||
}
|
||||
|
||||
checkEmptyText: true
|
||||
|
||||
KeyNavigation.tab: junkPacketMaxSizeTextField.textField
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -141,7 +156,7 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
|
||||
headerText: qsTr("Junk packet maximum size")
|
||||
headerText: "Jmax - Junk packet maximum size"
|
||||
textFieldText: junkPacketMaxSize
|
||||
textField.validator: IntValidator { bottom: 0 }
|
||||
|
||||
@@ -152,6 +167,8 @@ PageType {
|
||||
}
|
||||
|
||||
checkEmptyText: true
|
||||
|
||||
KeyNavigation.tab: initPacketJunkSizeTextField.textField
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -159,7 +176,7 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
|
||||
headerText: qsTr("Init packet junk size")
|
||||
headerText: "S1 - Init packet junk size"
|
||||
textFieldText: initPacketJunkSize
|
||||
textField.validator: IntValidator { bottom: 0 }
|
||||
|
||||
@@ -170,6 +187,8 @@ PageType {
|
||||
}
|
||||
|
||||
checkEmptyText: true
|
||||
|
||||
KeyNavigation.tab: responsePacketJunkSizeTextField.textField
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -177,7 +196,7 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
|
||||
headerText: qsTr("Response packet junk size")
|
||||
headerText: "S2 - Response packet junk size"
|
||||
textFieldText: responsePacketJunkSize
|
||||
textField.validator: IntValidator { bottom: 0 }
|
||||
|
||||
@@ -188,6 +207,8 @@ PageType {
|
||||
}
|
||||
|
||||
checkEmptyText: true
|
||||
|
||||
KeyNavigation.tab: initPacketMagicHeaderTextField.textField
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -195,7 +216,7 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
|
||||
headerText: qsTr("Init packet magic header")
|
||||
headerText: "H1 - Init packet magic header"
|
||||
textFieldText: initPacketMagicHeader
|
||||
textField.validator: IntValidator { bottom: 0 }
|
||||
|
||||
@@ -206,6 +227,8 @@ PageType {
|
||||
}
|
||||
|
||||
checkEmptyText: true
|
||||
|
||||
KeyNavigation.tab: responsePacketMagicHeaderTextField.textField
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -213,7 +236,7 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
|
||||
headerText: qsTr("Response packet magic header")
|
||||
headerText: "H2 - Response packet magic header"
|
||||
textFieldText: responsePacketMagicHeader
|
||||
textField.validator: IntValidator { bottom: 0 }
|
||||
|
||||
@@ -224,6 +247,8 @@ PageType {
|
||||
}
|
||||
|
||||
checkEmptyText: true
|
||||
|
||||
KeyNavigation.tab: transportPacketMagicHeaderTextField.textField
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -231,7 +256,7 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
|
||||
headerText: qsTr("Transport packet magic header")
|
||||
headerText: "H4 - Transport packet magic header"
|
||||
textFieldText: transportPacketMagicHeader
|
||||
textField.validator: IntValidator { bottom: 0 }
|
||||
|
||||
@@ -242,6 +267,8 @@ PageType {
|
||||
}
|
||||
|
||||
checkEmptyText: true
|
||||
|
||||
KeyNavigation.tab: underloadPacketMagicHeaderTextField.textField
|
||||
}
|
||||
|
||||
TextFieldWithHeaderType {
|
||||
@@ -249,7 +276,7 @@ PageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
|
||||
headerText: qsTr("Underload packet magic header")
|
||||
headerText: "H3 - Underload packet magic header"
|
||||
textFieldText: underloadPacketMagicHeader
|
||||
textField.validator: IntValidator { bottom: 0 }
|
||||
|
||||
@@ -260,6 +287,8 @@ PageType {
|
||||
}
|
||||
|
||||
checkEmptyText: true
|
||||
|
||||
KeyNavigation.tab: saveRestartButton
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
@@ -275,24 +304,24 @@ PageType {
|
||||
text: qsTr("Remove AmneziaWG")
|
||||
|
||||
onClicked: {
|
||||
questionDrawer.headerText = qsTr("Remove AmneziaWG from server?")
|
||||
questionDrawer.descriptionText = qsTr("All users with whom you shared a connection will no longer be able to connect to it.")
|
||||
questionDrawer.yesButtonText = qsTr("Continue")
|
||||
questionDrawer.noButtonText = qsTr("Cancel")
|
||||
var headerText = qsTr("Remove AmneziaWG from server?")
|
||||
var descriptionText = qsTr("All users with whom you shared a connection will no longer be able to connect to it.")
|
||||
var yesButtonText = qsTr("Continue")
|
||||
var noButtonText = qsTr("Cancel")
|
||||
|
||||
questionDrawer.yesButtonFunction = function() {
|
||||
questionDrawer.visible = false
|
||||
var yesButtonFunction = function() {
|
||||
PageController.goToPage(PageEnum.PageDeinstalling)
|
||||
InstallController.removeCurrentlyProcessedContainer()
|
||||
}
|
||||
questionDrawer.noButtonFunction = function() {
|
||||
questionDrawer.visible = false
|
||||
var noButtonFunction = function() {
|
||||
}
|
||||
questionDrawer.visible = true
|
||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: saveRestartButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 24
|
||||
@@ -310,7 +339,7 @@ PageType {
|
||||
|
||||
text: qsTr("Save and Restart Amnezia")
|
||||
|
||||
onClicked: {
|
||||
clickedFunc: function() {
|
||||
forceActiveFocus()
|
||||
PageController.goToPage(PageEnum.PageSetupWizardInstalling);
|
||||
InstallController.updateContainer(AwgConfigModel.getConfig())
|
||||
@@ -318,11 +347,8 @@ PageType {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QuestionDrawer {
|
||||
id: questionDrawer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user