mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-17 16:26:08 +03:00
Compare commits
186 Commits
bugfix/fix
...
improve_na
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25a24bf38c | ||
|
|
5e492e8392 | ||
|
|
b25d911328 | ||
|
|
caf29fb982 | ||
|
|
32ef1eae84 | ||
|
|
b0fdbe6c4f | ||
|
|
dccfcde6f6 | ||
|
|
26ba520022 | ||
|
|
0f98fc955f | ||
|
|
2c1f3eaf3a | ||
|
|
867ad8ee3c | ||
|
|
f8f4d75cb7 | ||
|
|
b88ab8e432 | ||
|
|
13e680f6e1 | ||
|
|
8767f487b8 | ||
|
|
0cdd2ff826 | ||
|
|
c973b8868b | ||
|
|
53aa14a2ee | ||
|
|
b29b5962d2 | ||
|
|
441a2f6df6 | ||
|
|
fa64c8d68d | ||
|
|
48f6cf904e | ||
|
|
ae31bafeb9 | ||
|
|
14d67503a8 | ||
|
|
1029142b47 | ||
|
|
7670d9dace | ||
|
|
2266702a0b | ||
|
|
11d20974f0 | ||
|
|
a57e94e4f2 | ||
|
|
9620a24d6a | ||
|
|
62be292833 | ||
|
|
10e91b06f4 | ||
|
|
3a62f1f20b | ||
|
|
3021066180 | ||
|
|
378db1a9e5 | ||
|
|
906cf7b939 | ||
|
|
8bc31b60a1 | ||
|
|
149c9d12d3 | ||
|
|
b33b97802e | ||
|
|
367789bda2 | ||
|
|
d18f50a3e0 | ||
|
|
ef2ffce47a | ||
|
|
53f41408b8 | ||
|
|
d06924c59d | ||
|
|
5b9ba8c027 | ||
|
|
2db99715b1 | ||
|
|
882c288d92 | ||
|
|
ea39447708 | ||
|
|
3cc70790bd | ||
|
|
f331c11f51 | ||
|
|
39cbe6de28 | ||
|
|
9688a8e52d | ||
|
|
b2c429f74d | ||
|
|
6ea6ab1bd9 | ||
|
|
c5aa070bf4 | ||
|
|
d67201ede9 | ||
|
|
4323fb2063 | ||
|
|
6d5452b8ee | ||
|
|
569d63ef0f | ||
|
|
87d00116e1 | ||
|
|
ea910ba300 | ||
|
|
1c1e74d06f | ||
|
|
5dc16c06f1 | ||
|
|
f992252c6e | ||
|
|
4efaf20a1c | ||
|
|
9d96b1cd13 | ||
|
|
1d721ffb9a | ||
|
|
2130131a9d | ||
|
|
e0b091b474 | ||
|
|
c9205afbd4 | ||
|
|
09b1f322ba | ||
|
|
4d2d7e4071 | ||
|
|
c72d76aec7 | ||
|
|
50bd364f29 | ||
|
|
3eeab2f2e7 | ||
|
|
2987e03a86 | ||
|
|
392cacaabc | ||
|
|
51092e91cf | ||
|
|
d02a6dfb22 | ||
|
|
4e1862a802 | ||
|
|
53e766f6d3 | ||
|
|
341d6f569b | ||
|
|
a5abab8caf | ||
|
|
7df050371e | ||
|
|
65870ad5c9 | ||
|
|
940806a6fa | ||
|
|
0620b4536a | ||
|
|
d956be901d | ||
|
|
942805cca2 | ||
|
|
ed6fc27e52 | ||
|
|
416421cba7 | ||
|
|
45b8235a81 | ||
|
|
a92f706524 | ||
|
|
5e9202f6ac | ||
|
|
dc6c1cdf49 | ||
|
|
f020bdb6e8 | ||
|
|
88958c042f | ||
|
|
e4d21dc4d7 | ||
|
|
2e896ed34f | ||
|
|
163399816f | ||
|
|
3d7209ee7a | ||
|
|
42645a98f8 | ||
|
|
7c3d08d80e | ||
|
|
c77e01fb37 | ||
|
|
b6c59b08a1 | ||
|
|
766e1c92fc | ||
|
|
21755cbd54 | ||
|
|
dac45a9f7f | ||
|
|
db5d289edc | ||
|
|
c962211068 | ||
|
|
9cfa4c1389 | ||
|
|
2c9fa10b8b | ||
|
|
626b9e1e76 | ||
|
|
ada3f9a7fa | ||
|
|
063851445a | ||
|
|
852e90e317 | ||
|
|
89ac585e07 | ||
|
|
f3df9eb5f5 | ||
|
|
75f189e256 | ||
|
|
3c655d0051 | ||
|
|
b4f4ec4ac9 | ||
|
|
f189f7b6af | ||
|
|
78a4caa47e | ||
|
|
01e31b4b4d | ||
|
|
cecee3769e | ||
|
|
02bbcd3a31 | ||
|
|
8547de82ea | ||
|
|
aa871bd1c9 | ||
|
|
23806e1def | ||
|
|
31867993ce | ||
|
|
7b7a922d92 | ||
|
|
09bd958d8d | ||
|
|
576e2226fe | ||
|
|
1533270e4e | ||
|
|
e7b25719e4 | ||
|
|
7183e8541c | ||
|
|
9e71e64cbd | ||
|
|
4f3bae4a9a | ||
|
|
990059f8a6 | ||
|
|
af55af5e76 | ||
|
|
82d96a9691 | ||
|
|
9f3f215452 | ||
|
|
2dfc6a87b8 | ||
|
|
7261a86c48 | ||
|
|
2946dd2278 | ||
|
|
5065262aac | ||
|
|
4685d3b543 | ||
|
|
7a389e8755 | ||
|
|
4e5daf22a3 | ||
|
|
3bf9c10d7d | ||
|
|
2e175cb9fc | ||
|
|
823c1b5d3a | ||
|
|
92bc1a6f09 | ||
|
|
d511220f8b | ||
|
|
923e358aaa | ||
|
|
92b19eccf6 | ||
|
|
5358aaeb00 | ||
|
|
e31a2066c0 | ||
|
|
928c4f18c9 | ||
|
|
628e22869d | ||
|
|
c9cd860654 | ||
|
|
17984adae5 | ||
|
|
5601bc4fdf | ||
|
|
e14681801e | ||
|
|
f106b4d367 | ||
|
|
74802f30ed | ||
|
|
d63bf15011 | ||
|
|
60de146f03 | ||
|
|
c4f32eed31 | ||
|
|
2c9067b0de | ||
|
|
6844a2375b | ||
|
|
7b838e77a0 | ||
|
|
694e781beb | ||
|
|
399a8c6d28 | ||
|
|
dce08b3ecc | ||
|
|
2763da960f | ||
|
|
d4fff4af3c | ||
|
|
f0903c32f3 | ||
|
|
ea8875478e | ||
|
|
4c08e9f3bc | ||
|
|
e8736102bf | ||
|
|
371cadcc02 | ||
|
|
02a98b9d68 | ||
|
|
1542adba82 | ||
|
|
3aa8a46f6e | ||
|
|
1f08d78b43 |
39
.clang-format
Normal file
39
.clang-format
Normal file
@@ -0,0 +1,39 @@
|
||||
BasedOnStyle: WebKit
|
||||
AccessModifierOffset: '-4'
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveMacros: 'true'
|
||||
AlignTrailingComments: 'true'
|
||||
AllowAllArgumentsOnNextLine: 'true'
|
||||
AllowAllParametersOfDeclarationOnNextLine: 'true'
|
||||
AllowShortBlocksOnASingleLine: 'false'
|
||||
AllowShortCaseLabelsOnASingleLine: 'true'
|
||||
AllowShortEnumsOnASingleLine: 'false'
|
||||
AllowShortFunctionsOnASingleLine: None
|
||||
AlwaysBreakTemplateDeclarations: 'No'
|
||||
BreakBeforeBinaryOperators: NonAssignment
|
||||
BreakBeforeBraces: Custom
|
||||
BraceWrapping:
|
||||
AfterClass: true
|
||||
AfterControlStatement: false
|
||||
AfterEnum: false
|
||||
AfterFunction: true
|
||||
AfterNamespace: true
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: true
|
||||
AfterUnion: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
IndentBraces: false
|
||||
BreakConstructorInitializers: BeforeColon
|
||||
ColumnLimit: '120'
|
||||
CommentPragmas: '"^!|^:"'
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: 'true'
|
||||
ConstructorInitializerIndentWidth: '4'
|
||||
ContinuationIndentWidth: '8'
|
||||
IndentPPDirectives: BeforeHash
|
||||
NamespaceIndentation: All
|
||||
PenaltyExcessCharacter: '10'
|
||||
PointerAlignment: Right
|
||||
SortIncludes: 'true'
|
||||
SpaceAfterTemplateKeyword: 'false'
|
||||
Standard: Auto
|
||||
20
.clang-format-ignore
Normal file
20
.clang-format-ignore
Normal file
@@ -0,0 +1,20 @@
|
||||
/client/3rd
|
||||
/client/3rd-prebuild
|
||||
/client/android
|
||||
/client/cmake
|
||||
/client/core/serialization
|
||||
/client/daemon
|
||||
/client/fonts
|
||||
/client/images
|
||||
/client/ios
|
||||
/client/mozilla
|
||||
/client/platforms/dummy
|
||||
/client/platforms/linux
|
||||
/client/platforms/macos
|
||||
/client/platforms/windows
|
||||
/client/server_scripts
|
||||
/client/translations
|
||||
/deploy
|
||||
/docs
|
||||
/metadata
|
||||
/service/src
|
||||
40
.github/workflows/deploy.yml
vendored
40
.github/workflows/deploy.yml
vendored
@@ -16,7 +16,10 @@ jobs:
|
||||
QT_VERSION: 6.6.2
|
||||
QIF_VERSION: 4.7
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
|
||||
steps:
|
||||
- name: 'Install Qt'
|
||||
@@ -83,7 +86,10 @@ jobs:
|
||||
QIF_VERSION: 4.7
|
||||
BUILD_ARCH: 64
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
|
||||
steps:
|
||||
- name: 'Get sources'
|
||||
@@ -146,7 +152,10 @@ jobs:
|
||||
CC: cc
|
||||
CXX: c++
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
|
||||
steps:
|
||||
- name: 'Setup xcode'
|
||||
@@ -208,7 +217,11 @@ jobs:
|
||||
export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/ios/bin"
|
||||
export QT_MACOS_ROOT_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos"
|
||||
export PATH=$PATH:~/go/bin
|
||||
sh deploy/build_ios.sh
|
||||
sh deploy/build_ios.sh | \
|
||||
sed -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/d' | \
|
||||
sed -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/d' | \
|
||||
sed -e '/-DPROD_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DPROD_AGW_PUBLIC_KEY/d' | \
|
||||
sed -e '/-DDEV_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DDEV_AGW_PUBLIC_KEY/d'
|
||||
env:
|
||||
IOS_TRUST_CERT_BASE64: ${{ secrets.IOS_TRUST_CERT_BASE64 }}
|
||||
IOS_SIGNING_CERT_BASE64: ${{ secrets.IOS_SIGNING_CERT_BASE64 }}
|
||||
@@ -238,13 +251,16 @@ jobs:
|
||||
QT_VERSION: 6.4.3
|
||||
QIF_VERSION: 4.6
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
|
||||
steps:
|
||||
- name: 'Setup xcode'
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '14.3.1'
|
||||
xcode-version: '15.4.0'
|
||||
|
||||
- name: 'Install Qt'
|
||||
uses: jurplel/install-qt-action@v3
|
||||
@@ -301,10 +317,13 @@ jobs:
|
||||
|
||||
env:
|
||||
ANDROID_BUILD_PLATFORM: android-34
|
||||
QT_VERSION: 6.7.2
|
||||
QT_VERSION: 6.7.3
|
||||
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
|
||||
steps:
|
||||
- name: 'Install desktop Qt'
|
||||
@@ -316,7 +335,8 @@ jobs:
|
||||
arch: 'linux_gcc_64'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
dir: ${{ runner.temp }}
|
||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
||||
py7zrversion: '==0.22.*'
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install android_x86_64 Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
@@ -327,7 +347,8 @@ jobs:
|
||||
arch: 'android_x86_64'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
dir: ${{ runner.temp }}
|
||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
||||
py7zrversion: '==0.22.*'
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install android_x86 Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
@@ -338,7 +359,8 @@ jobs:
|
||||
arch: 'android_x86'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
dir: ${{ runner.temp }}
|
||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
||||
py7zrversion: '==0.22.*'
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install android_armv7 Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
@@ -349,7 +371,8 @@ jobs:
|
||||
arch: 'android_armv7'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
dir: ${{ runner.temp }}
|
||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
||||
py7zrversion: '==0.22.*'
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Install android_arm64_v8a Qt'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
@@ -360,7 +383,8 @@ jobs:
|
||||
arch: 'android_arm64_v8a'
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
dir: ${{ runner.temp }}
|
||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
||||
py7zrversion: '==0.22.*'
|
||||
extra: '--base ${{ env.QT_MIRROR }}'
|
||||
|
||||
- name: 'Grant execute permission for qt-cmake'
|
||||
shell: bash
|
||||
|
||||
3
.github/workflows/tag-deploy.yml
vendored
3
.github/workflows/tag-deploy.yml
vendored
@@ -16,7 +16,10 @@ jobs:
|
||||
QT_VERSION: 6.4.1
|
||||
QIF_VERSION: 4.5
|
||||
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
|
||||
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}
|
||||
DEV_AGW_PUBLIC_KEY: ${{ secrets.DEV_AGW_PUBLIC_KEY }}
|
||||
DEV_AGW_ENDPOINT: ${{ secrets.DEV_AGW_ENDPOINT }}
|
||||
DEV_S3_ENDPOINT: ${{ secrets.DEV_S3_ENDPOINT }}
|
||||
|
||||
steps:
|
||||
- name: 'Install desktop Qt'
|
||||
|
||||
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
|
||||
project(${PROJECT} VERSION 4.8.1.8
|
||||
project(${PROJECT} VERSION 4.8.3.0
|
||||
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 63)
|
||||
set(APP_ANDROID_VERSION_CODE 2072)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
|
||||
32
README.md
32
README.md
@@ -1,30 +1,31 @@
|
||||
# Amnezia VPN
|
||||
## _The best client for self-hosted VPN_
|
||||
|
||||
### _The best client for self-hosted VPN_
|
||||
|
||||
|
||||
[](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev)
|
||||
[](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client)
|
||||
|
||||
Amnezia is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server.
|
||||
### [English]([https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md](https://github.com/amnezia-vpn/amnezia-client/tree/dev?tab=readme-ov-file#)) | [Русский](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README_RU.md)
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
[Amnezia](https://amnezia.org) is an open-source VPN client, with a key feature that enables you to deploy your own VPN server on your server.
|
||||
|
||||
<a href="https://github.com/amnezia-vpn/amnezia-client/releases/download/4.7.0.0/AmneziaVPN_4.7.0.0_x64.exe"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/win.png" width="150" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/amnezia-vpn/amnezia-client/releases/download/4.7.0.0/AmneziaVPN_4.7.0.0.dmg"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/mac.png" width="150" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/amnezia-vpn/amnezia-client/releases/download/4.7.0.0/AmneziaVPN_Linux_4.7.0.0.tar.zip"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/lin.png" width="150" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/amnezia-vpn/amnezia-client/releases/tag/4.7.0.0"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/andr.png" width="150" style="max-width: 100%;"></a>
|
||||
[](https://amnezia.org)
|
||||
|
||||
<br>
|
||||
### [Website](https://amnezia.org) | [Alt website link](https://storage.googleapis.com/kldscp/amnezia.org) | [Documentation](https://docs.amnezia.org) | [Troubleshooting](https://docs.amnezia.org/troubleshooting)
|
||||
|
||||
<a href="https://play.google.com/store/search?q=amnezia+vpn&c=apps"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/play.png" width="150" style="max-width: 100%;"></a>
|
||||
<a href="https://apps.apple.com/us/app/amneziavpn/id1600529900"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/apl.png" width="150" style="max-width: 100%;"></a>
|
||||
> [!TIP]
|
||||
> If the [Amnezia website](https://amnezia.org) is blocked in your region, you can use an [Alternative website link](https://storage.googleapis.com/kldscp/amnezia.org).
|
||||
|
||||
<a href="https://amnezia.org/downloads"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/download-website.svg" width="150" style="max-width: 100%; margin-right: 10px"></a>
|
||||
<a href="https://storage.googleapis.com/kldscp/amnezia.org/downloads"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/download-alt.svg" width="150" style="max-width: 100%;"></a>
|
||||
|
||||
[All releases](https://github.com/amnezia-vpn/amnezia-client/releases)
|
||||
|
||||
<br>
|
||||
<br/>
|
||||
|
||||
<a href="https://www.testiny.io"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/testiny.png" height="28px"></a>
|
||||
|
||||
## Features
|
||||
|
||||
@@ -37,7 +38,8 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep
|
||||
|
||||
## Links
|
||||
|
||||
- [https://amnezia.org](https://amnezia.org) - project website
|
||||
- [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org)
|
||||
- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation
|
||||
- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
|
||||
- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English)
|
||||
- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi)
|
||||
@@ -186,8 +188,8 @@ Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn
|
||||
Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p <br>
|
||||
USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4 <br>
|
||||
USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d <br>
|
||||
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
|
||||
|
||||
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3 <br>
|
||||
TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns
|
||||
## Acknowledgments
|
||||
|
||||
This project is tested with BrowserStack.
|
||||
|
||||
181
README_RU.md
Normal file
181
README_RU.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# Amnezia VPN
|
||||
|
||||
### _Лучший клиент для создания VPN на собственном сервере_
|
||||
|
||||
[](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev)
|
||||
[](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client)
|
||||
|
||||
### [English](https://github.com/amnezia-vpn/amnezia-client/blob/dev/README.md) | Русский
|
||||
[AmneziaVPN](https://amnezia.org) — это open sourse VPN-клиент, ключевая особенность которого заключается в возможности развернуть собственный VPN на вашем сервере.
|
||||
|
||||
[](https://amnezia.org)
|
||||
|
||||
### [Сайт](https://amnezia.org) | [Зеркало на сайт](https://storage.googleapis.com/kldscp/amnezia.org) | [Документация](https://docs.amnezia.org) | [Решение проблем](https://docs.amnezia.org/troubleshooting)
|
||||
|
||||
> [!TIP]
|
||||
> Если [сайт Amnezia](https://amnezia.org) заблокирован в вашем регионе, вы можете воспользоваться [ссылкой на зеркало](https://storage.googleapis.com/kldscp/amnezia.org).
|
||||
|
||||
<a href="https://storage.googleapis.com/kldscp/amnezia.org/downloads"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/download-website-ru.svg" width="150" style="max-width: 100%; margin-right: 10px"></a>
|
||||
|
||||
|
||||
[Все релизы](https://github.com/amnezia-vpn/amnezia-client/releases)
|
||||
|
||||
<br/>
|
||||
|
||||
<a href="https://www.testiny.io"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/testiny.png" height="28px"></a>
|
||||
|
||||
## Особенности
|
||||
|
||||
- Простой в использовании — введите IP-адрес, SSH-логин и пароль, и Amnezia автоматически установит VPN-контейнеры Docker на ваш сервер и подключится к VPN.
|
||||
- Классические VPN-протоколы: OpenVPN, WireGuard и IKEv2.
|
||||
- Протоколы с маскировкой трафика (обфускацией): OpenVPN с плагином [Cloak](https://github.com/cbeuw/Cloak), Shadowsocks (OpenVPN over Shadowsocks), [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) and XRay.
|
||||
- Поддержка Split Tunneling — добавляйте любые сайты или приложения в список, чтобы включить VPN только для них.
|
||||
- Поддерживает платформы: Windows, MacOS, Linux, Android, iOS.
|
||||
- Поддержка конфигурации протокола AmneziaWG на [бета-прошивке Keenetic](https://docs.keenetic.com/ua/air/kn-1611/en/6319-latest-development-release.html#UUID-186c4108-5afd-c10b-f38a-cdff6c17fab3_section-idm33192196168192-improved).
|
||||
|
||||
## Ссылки
|
||||
|
||||
- [https://amnezia.org](https://amnezia.org) - Веб-сайт проекта | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/kldscp/amnezia.org)
|
||||
- [https://docs.amnezia.org](https://docs.amnezia.org) - Документация
|
||||
- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
|
||||
- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Канал поддржки в Telegram (Английский)
|
||||
- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Канал поддржки в Telegram (Фарси)
|
||||
- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Канал поддржки в Telegram (Мьянма)
|
||||
- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Канал поддржки в Telegram (Русский)
|
||||
- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium | [Зеркало](https://storage.googleapis.com/kldscp/vpnpay.io/ru/amnezia-premium\)
|
||||
|
||||
## Технологии
|
||||
|
||||
AmneziaVPN использует несколько проектов с открытым исходным кодом:
|
||||
|
||||
- [OpenSSL](https://www.openssl.org/)
|
||||
- [OpenVPN](https://openvpn.net/)
|
||||
- [Shadowsocks](https://shadowsocks.org/)
|
||||
- [Qt](https://www.qt.io/)
|
||||
- [LibSsh](https://libssh.org)
|
||||
- и другие...
|
||||
|
||||
## Проверка исходного кода
|
||||
После клонирования репозитория обязательно загрузите все подмодули.
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
|
||||
## Разработка
|
||||
Хотите внести свой вклад? Добро пожаловать!
|
||||
|
||||
### Помощь с переводами
|
||||
|
||||
Загрузите самые актуальные файлы перевода.
|
||||
|
||||
Перейдите на [вкладку "Actions"](https://github.com/amnezia-vpn/amnezia-client/actions?query=is%3Asuccess+branch%3Adev), нажмите на первую строку. Затем прокрутите вниз до раздела "Artifacts" и скачайте "AmneziaVPN_translations".
|
||||
|
||||
Распакуйте этот файл. Каждый файл с расширением *.ts содержит строки для соответствующего языка.
|
||||
|
||||
Переведите или исправьте строки в одном или нескольких файлах *.ts и загрузите их обратно в этот репозиторий в папку ``client/translations``. Это можно сделать через веб-интерфейс или любым другим знакомым вам способом.
|
||||
|
||||
### Сборка исходного кода и деплой
|
||||
Проверьте папку deploy для скриптов сборки.
|
||||
|
||||
### Как собрать iOS-приложение из исходного кода на MacOS
|
||||
1. Убедитесь, что у вас установлен XCode версии 14 или выше.
|
||||
2. Для генерации проекта XCode используется QT. Требуется версия QT 6.6.2. Установите QT для MacOS здесь или через QT Online Installer. Необходимые модули:
|
||||
- MacOS
|
||||
- iOS
|
||||
- Модуль совместимости с Qt 5
|
||||
- Qt Shader Tools
|
||||
- Дополнительные библиотеки:
|
||||
- Qt Image Formats
|
||||
- Qt Multimedia
|
||||
- Qt Remote Objects
|
||||
|
||||
|
||||
3. Установите CMake, если это необходимо. Рекомендуемая версия — 3.25. Скачать CMake можно здесь.
|
||||
4. Установите Go версии >= v1.16. Если Go ещё не установлен, скачайте его с [официального сайта](https://golang.org/dl/) или используйте Homebrew. Установите gomobile:
|
||||
|
||||
```bash
|
||||
export PATH=$PATH:~/go/bin
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
gomobile init
|
||||
```
|
||||
|
||||
5. Соберите проект:
|
||||
```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"
|
||||
export QT_IOS_BIN=$QT_BIN_DIR
|
||||
export PATH=$PATH:~/go/bin
|
||||
mkdir build-ios
|
||||
$QT_IOS_BIN/qt-cmake . -B build-ios -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR
|
||||
```
|
||||
Замените <PATH-TO-QT-FOLDER> и <QT-VERSION> на ваши значения.
|
||||
|
||||
Если появляется ошибка gomobile: command not found, убедитесь, что PATH настроен на папку bin, где установлен gomobile:
|
||||
```bash
|
||||
export PATH=$(PATH):/path/to/GOPATH/bin
|
||||
```
|
||||
|
||||
6. Откройте проект в XCode. Теперь вы можете тестировать, архивировать или публиковать приложение.
|
||||
|
||||
Если сборка завершится с ошибкой:
|
||||
```
|
||||
make: ***
|
||||
[$(PROJECTDIR)/client/build/AmneziaVPN.build/Debug-iphoneos/wireguard-go-bridge/goroot/.prepared]
|
||||
Error 1
|
||||
```
|
||||
Добавьте пользовательскую переменную PATH в настройки сборки для целей AmneziaVPN и WireGuardNetworkExtension с ключом `PATH` и значением `${PATH}/path/to/bin/folder/with/go/executable`, e.g. `${PATH}:/usr/local/go/bin`.
|
||||
|
||||
Если ошибка повторяется на Mac с M1, установите версию CMake для архитектуры ARM:
|
||||
```
|
||||
arch -arm64 brew install cmake
|
||||
```
|
||||
|
||||
При первой попытке сборка может завершиться с ошибкой source files not found. Это происходит из-за параллельной компиляции зависимостей в XCode. Просто перезапустите сборку.
|
||||
|
||||
|
||||
## Как собрать Android-приложение
|
||||
Сборка тестировалась на MacOS. Требования:
|
||||
- JDK 11
|
||||
- Android SDK 33
|
||||
- CMake 3.25.0
|
||||
|
||||
Установите QT, QT Creator и Android Studio.
|
||||
Настройте QT Creator:
|
||||
|
||||
- В меню QT Creator перейдите в `QT Creator` -> `Preferences` -> `Devices` ->`Android`.
|
||||
- Укажите путь к JDK 11.
|
||||
- Укажите путь к Android SDK (`$ANDROID_HOME`)
|
||||
|
||||
Если вы сталкиваетесь с ошибками, связанными с отсутствием SDK или сообщением «SDK manager not running», их нельзя исправить просто корректировкой путей. Если у вас есть несколько свободных гигабайт на диске, вы можете позволить Qt Creator установить все необходимые компоненты, выбрав пустую папку для расположения Android SDK и нажав кнопку **Set Up SDK**. Учтите: это установит второй Android SDK и NDK на вашем компьютере!
|
||||
|
||||
Убедитесь, что настроена правильная версия CMake: перейдите в **Qt Creator -> Preferences** и в боковом меню выберите пункт **Kits**. В центральной части окна, на вкладке **Kits**, найдите запись для инструмента **CMake Tool**. Если выбранная по умолчанию версия CMake ниже 3.25.0, установите на свою систему CMake версии 3.25.0 или выше, а затем выберите опцию **System CMake at <путь>** из выпадающего списка. Если этот пункт отсутствует, это может означать, что вы еще не установили CMake, или Qt Creator не смог найти путь к нему. В таком случае в окне **Preferences** перейдите в боковое меню **CMake**, затем во вкладку **Tools** в центральной части окна и нажмите кнопку **Add**, чтобы указать путь к установленному CMake.
|
||||
|
||||
Убедитесь, что для вашего проекта выбрана Android Platform SDK 33: в главном окне на боковой панели выберите пункт **Projects**, и слева вы увидите раздел **Build & Run**, показывающий различные целевые Android-платформы. Вы можете выбрать любую из них, так как настройка проекта Amnezia VPN разработана таким образом, чтобы все Android-цели могли быть собраны. Перейдите в подраздел **Build** и прокрутите центральную часть окна до раздела **Build Steps**. Нажмите **Details** в заголовке **Build Android APK** (кнопка **Details** может быть скрыта, если окно Qt Creator не запущено в полноэкранном режиме!). Вот здесь выберите **android-33** в качестве Android Build Platform SDK.
|
||||
|
||||
### Разработка Android-компонентов
|
||||
|
||||
После сборки QT Creator копирует проект в отдельную папку, например, `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>`. Для разработки Android-компонентов откройте сгенерированный проект в Android Studio, указав папку `build-amnezia-client-Android_Qt_<version>_Clang_<architecture>-<BuildType>/client/android-build` в качестве корневой.
|
||||
Изменения в сгенерированном проекте нужно вручную перенести в репозиторий. После этого можно коммитить изменения.
|
||||
Если возникают проблемы со сборкой в QT Creator после работы в Android Studio, выполните команду `./gradlew clean` в корневой папке сгенерированного проекта (`<path>/client/android-build/.`).
|
||||
|
||||
|
||||
## Лицензия
|
||||
|
||||
GPL v3.0
|
||||
|
||||
## Донаты
|
||||
|
||||
Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn)
|
||||
|
||||
Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p <br>
|
||||
USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4 <br>
|
||||
USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d <br>
|
||||
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3 <br>
|
||||
TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns
|
||||
|
||||
## Благодарности
|
||||
|
||||
Этот проект тестируется с помощью BrowserStack.
|
||||
Мы выражаем благодарность [BrowserStack](https://www.browserstack.com) за поддержку нашего проекта.
|
||||
@@ -1,25 +0,0 @@
|
||||
include_directories(${CMAKE_CURRENT_LIST_DIR})
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS
|
||||
Core Network
|
||||
)
|
||||
set(LIBS ${LIBS} Qt6::Core Qt6::Network)
|
||||
|
||||
|
||||
set(HEADERS ${HEADERS}
|
||||
${CMAKE_CURRENT_LIST_DIR}/singleapplication.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/singleapplication_p.h
|
||||
)
|
||||
|
||||
set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_LIST_DIR}/singleapplication.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/singleapplication_p.cpp
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
if(MSVC)
|
||||
set(LIBS ${LIBS} Advapi32.lib)
|
||||
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
|
||||
set(LIBS ${LIBS} advapi32)
|
||||
endif()
|
||||
endif()
|
||||
@@ -1,274 +0,0 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) Itay Grudev 2015 - 2020
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#include <QtCore/QElapsedTimer>
|
||||
#include <QtCore/QByteArray>
|
||||
#include <QtCore/QSharedMemory>
|
||||
|
||||
#include "singleapplication.h"
|
||||
#include "singleapplication_p.h"
|
||||
|
||||
/**
|
||||
* @brief Constructor. Checks and fires up LocalServer or closes the program
|
||||
* if another instance already exists
|
||||
* @param argc
|
||||
* @param argv
|
||||
* @param allowSecondary Whether to enable secondary instance support
|
||||
* @param options Optional flags to toggle specific behaviour
|
||||
* @param timeout Maximum time blocking functions are allowed during app load
|
||||
*/
|
||||
SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary, Options options, int timeout, const QString &userData )
|
||||
: app_t( argc, argv ), d_ptr( new SingleApplicationPrivate( this ) )
|
||||
{
|
||||
Q_D( SingleApplication );
|
||||
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
// On Android and iOS since the library is not supported fallback to
|
||||
// standard QApplication behaviour by simply returning at this point.
|
||||
qWarning() << "SingleApplication is not supported on Android and iOS systems.";
|
||||
return;
|
||||
#endif
|
||||
|
||||
// Store the current mode of the program
|
||||
d->options = options;
|
||||
|
||||
// Add any unique user data
|
||||
if ( ! userData.isEmpty() )
|
||||
d->addAppData( userData );
|
||||
|
||||
// Generating an application ID used for identifying the shared memory
|
||||
// block and QLocalServer
|
||||
d->genBlockServerName();
|
||||
|
||||
// To mitigate QSharedMemory issues with large amount of processes
|
||||
// attempting to attach at the same time
|
||||
SingleApplicationPrivate::randomSleep();
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
// By explicitly attaching it and then deleting it we make sure that the
|
||||
// memory is deleted even after the process has crashed on Unix.
|
||||
d->memory = new QSharedMemory( d->blockServerName );
|
||||
d->memory->attach();
|
||||
delete d->memory;
|
||||
#endif
|
||||
// Guarantee thread safe behaviour with a shared memory block.
|
||||
d->memory = new QSharedMemory( d->blockServerName );
|
||||
|
||||
// Create a shared memory block
|
||||
if( d->memory->create( sizeof( InstancesInfo ) )){
|
||||
// Initialize the shared memory block
|
||||
if( ! d->memory->lock() ){
|
||||
qCritical() << "SingleApplication: Unable to lock memory block after create.";
|
||||
abortSafely();
|
||||
}
|
||||
d->initializeMemoryBlock();
|
||||
} else {
|
||||
if( d->memory->error() == QSharedMemory::AlreadyExists ){
|
||||
// Attempt to attach to the memory segment
|
||||
if( ! d->memory->attach() ){
|
||||
qCritical() << "SingleApplication: Unable to attach to shared memory block.";
|
||||
abortSafely();
|
||||
}
|
||||
if( ! d->memory->lock() ){
|
||||
qCritical() << "SingleApplication: Unable to lock memory block after attach.";
|
||||
abortSafely();
|
||||
}
|
||||
} else {
|
||||
qCritical() << "SingleApplication: Unable to create block.";
|
||||
abortSafely();
|
||||
}
|
||||
}
|
||||
|
||||
auto *inst = static_cast<InstancesInfo*>( d->memory->data() );
|
||||
QElapsedTimer time;
|
||||
time.start();
|
||||
|
||||
// Make sure the shared memory block is initialised and in consistent state
|
||||
while( true ){
|
||||
// If the shared memory block's checksum is valid continue
|
||||
if( d->blockChecksum() == inst->checksum ) break;
|
||||
|
||||
// If more than 5s have elapsed, assume the primary instance crashed and
|
||||
// assume it's position
|
||||
if( time.elapsed() > 5000 ){
|
||||
qWarning() << "SingleApplication: Shared memory block has been in an inconsistent state from more than 5s. Assuming primary instance failure.";
|
||||
d->initializeMemoryBlock();
|
||||
}
|
||||
|
||||
// Otherwise wait for a random period and try again. The random sleep here
|
||||
// limits the probability of a collision between two racing apps and
|
||||
// allows the app to initialise faster
|
||||
if( ! d->memory->unlock() ){
|
||||
qDebug() << "SingleApplication: Unable to unlock memory for random wait.";
|
||||
qDebug() << d->memory->errorString();
|
||||
}
|
||||
SingleApplicationPrivate::randomSleep();
|
||||
if( ! d->memory->lock() ){
|
||||
qCritical() << "SingleApplication: Unable to lock memory after random wait.";
|
||||
abortSafely();
|
||||
}
|
||||
}
|
||||
|
||||
if( inst->primary == false ){
|
||||
d->startPrimary();
|
||||
if( ! d->memory->unlock() ){
|
||||
qDebug() << "SingleApplication: Unable to unlock memory after primary start.";
|
||||
qDebug() << d->memory->errorString();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if another instance can be started
|
||||
if( allowSecondary ){
|
||||
d->startSecondary();
|
||||
if( d->options & Mode::SecondaryNotification ){
|
||||
d->connectToPrimary( timeout, SingleApplicationPrivate::SecondaryInstance );
|
||||
}
|
||||
if( ! d->memory->unlock() ){
|
||||
qDebug() << "SingleApplication: Unable to unlock memory after secondary start.";
|
||||
qDebug() << d->memory->errorString();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if( ! d->memory->unlock() ){
|
||||
qDebug() << "SingleApplication: Unable to unlock memory at end of execution.";
|
||||
qDebug() << d->memory->errorString();
|
||||
}
|
||||
|
||||
d->connectToPrimary( timeout, SingleApplicationPrivate::NewInstance );
|
||||
|
||||
delete d;
|
||||
|
||||
::exit( EXIT_SUCCESS );
|
||||
}
|
||||
|
||||
SingleApplication::~SingleApplication()
|
||||
{
|
||||
Q_D( SingleApplication );
|
||||
delete d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current application instance is primary.
|
||||
* @return Returns true if the instance is primary, false otherwise.
|
||||
*/
|
||||
bool SingleApplication::isPrimary() const
|
||||
{
|
||||
Q_D( const SingleApplication );
|
||||
return d->server != nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current application instance is secondary.
|
||||
* @return Returns true if the instance is secondary, false otherwise.
|
||||
*/
|
||||
bool SingleApplication::isSecondary() const
|
||||
{
|
||||
Q_D( const SingleApplication );
|
||||
return d->server == nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows you to identify an instance by returning unique consecutive instance
|
||||
* ids. It is reset when the first (primary) instance of your app starts and
|
||||
* only incremented afterwards.
|
||||
* @return Returns a unique instance id.
|
||||
*/
|
||||
quint32 SingleApplication::instanceId() const
|
||||
{
|
||||
Q_D( const SingleApplication );
|
||||
return d->instanceNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OS PID (Process Identifier) of the process running the primary
|
||||
* instance. Especially useful when SingleApplication is coupled with OS.
|
||||
* specific APIs.
|
||||
* @return Returns the primary instance PID.
|
||||
*/
|
||||
qint64 SingleApplication::primaryPid() const
|
||||
{
|
||||
Q_D( const SingleApplication );
|
||||
return d->primaryPid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username the primary instance is running as.
|
||||
* @return Returns the username the primary instance is running as.
|
||||
*/
|
||||
QString SingleApplication::primaryUser() const
|
||||
{
|
||||
Q_D( const SingleApplication );
|
||||
return d->primaryUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username the current instance is running as.
|
||||
* @return Returns the username the current instance is running as.
|
||||
*/
|
||||
QString SingleApplication::currentUser() const
|
||||
{
|
||||
return SingleApplicationPrivate::getUsername();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends message to the Primary Instance.
|
||||
* @param message The message to send.
|
||||
* @param timeout the maximum timeout in milliseconds for blocking functions.
|
||||
* @return true if the message was sent successfuly, false otherwise.
|
||||
*/
|
||||
bool SingleApplication::sendMessage( const QByteArray &message, int timeout )
|
||||
{
|
||||
Q_D( SingleApplication );
|
||||
|
||||
// Nobody to connect to
|
||||
if( isPrimary() ) return false;
|
||||
|
||||
// Make sure the socket is connected
|
||||
if( ! d->connectToPrimary( timeout, SingleApplicationPrivate::Reconnect ) )
|
||||
return false;
|
||||
|
||||
d->socket->write( message );
|
||||
bool dataWritten = d->socket->waitForBytesWritten( timeout );
|
||||
d->socket->flush();
|
||||
return dataWritten;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the shared memory block and exits with a failure.
|
||||
* This function halts program execution.
|
||||
*/
|
||||
void SingleApplication::abortSafely()
|
||||
{
|
||||
Q_D( SingleApplication );
|
||||
|
||||
qCritical() << "SingleApplication: " << d->memory->error() << d->memory->errorString();
|
||||
delete d;
|
||||
::exit( EXIT_FAILURE );
|
||||
}
|
||||
|
||||
QStringList SingleApplication::userData() const
|
||||
{
|
||||
Q_D( const SingleApplication );
|
||||
return d->appData();
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) Itay Grudev 2015 - 2018
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
#ifndef SINGLE_APPLICATION_H
|
||||
#define SINGLE_APPLICATION_H
|
||||
|
||||
#include <QtCore/QtGlobal>
|
||||
#include <QtNetwork/QLocalSocket>
|
||||
|
||||
#ifndef QAPPLICATION_CLASS
|
||||
#define QAPPLICATION_CLASS QApplication
|
||||
#endif
|
||||
|
||||
#include QT_STRINGIFY(QAPPLICATION_CLASS)
|
||||
|
||||
class SingleApplicationPrivate;
|
||||
|
||||
/**
|
||||
* @brief The SingleApplication class handles multiple instances of the same
|
||||
* Application
|
||||
* @see QCoreApplication
|
||||
*/
|
||||
class SingleApplication : public QAPPLICATION_CLASS
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
using app_t = QAPPLICATION_CLASS;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Mode of operation of SingleApplication.
|
||||
* Whether the block should be user-wide or system-wide and whether the
|
||||
* primary instance should be notified when a secondary instance had been
|
||||
* started.
|
||||
* @note Operating system can restrict the shared memory blocks to the same
|
||||
* user, in which case the User/System modes will have no effect and the
|
||||
* block will be user wide.
|
||||
* @enum
|
||||
*/
|
||||
enum Mode {
|
||||
User = 1 << 0,
|
||||
System = 1 << 1,
|
||||
SecondaryNotification = 1 << 2,
|
||||
ExcludeAppVersion = 1 << 3,
|
||||
ExcludeAppPath = 1 << 4
|
||||
};
|
||||
Q_DECLARE_FLAGS(Options, Mode)
|
||||
|
||||
/**
|
||||
* @brief Intitializes a SingleApplication instance with argc command line
|
||||
* arguments in argv
|
||||
* @arg {int &} argc - Number of arguments in argv
|
||||
* @arg {const char *[]} argv - Supplied command line arguments
|
||||
* @arg {bool} allowSecondary - Whether to start the instance as secondary
|
||||
* if there is already a primary instance.
|
||||
* @arg {Mode} mode - Whether for the SingleApplication block to be applied
|
||||
* User wide or System wide.
|
||||
* @arg {int} timeout - Timeout to wait in milliseconds.
|
||||
* @note argc and argv may be changed as Qt removes arguments that it
|
||||
* recognizes
|
||||
* @note Mode::SecondaryNotification only works if set on both the primary
|
||||
* instance and the secondary instance.
|
||||
* @note The timeout is just a hint for the maximum time of blocking
|
||||
* operations. It does not guarantee that the SingleApplication
|
||||
* initialisation will be completed in given time, though is a good hint.
|
||||
* Usually 4*timeout would be the worst case (fail) scenario.
|
||||
* @see See the corresponding QAPPLICATION_CLASS constructor for reference
|
||||
*/
|
||||
explicit SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 1000, const QString &userData = {} );
|
||||
~SingleApplication() override;
|
||||
|
||||
/**
|
||||
* @brief Returns if the instance is the primary instance
|
||||
* @returns {bool}
|
||||
*/
|
||||
bool isPrimary() const;
|
||||
|
||||
/**
|
||||
* @brief Returns if the instance is a secondary instance
|
||||
* @returns {bool}
|
||||
*/
|
||||
bool isSecondary() const;
|
||||
|
||||
/**
|
||||
* @brief Returns a unique identifier for the current instance
|
||||
* @returns {qint32}
|
||||
*/
|
||||
quint32 instanceId() const;
|
||||
|
||||
/**
|
||||
* @brief Returns the process ID (PID) of the primary instance
|
||||
* @returns {qint64}
|
||||
*/
|
||||
qint64 primaryPid() const;
|
||||
|
||||
/**
|
||||
* @brief Returns the username of the user running the primary instance
|
||||
* @returns {QString}
|
||||
*/
|
||||
QString primaryUser() const;
|
||||
|
||||
/**
|
||||
* @brief Returns the username of the current user
|
||||
* @returns {QString}
|
||||
*/
|
||||
QString currentUser() const;
|
||||
|
||||
/**
|
||||
* @brief Sends a message to the primary instance. Returns true on success.
|
||||
* @param {int} timeout - Timeout for connecting
|
||||
* @returns {bool}
|
||||
* @note sendMessage() will return false if invoked from the primary
|
||||
* instance.
|
||||
*/
|
||||
bool sendMessage( const QByteArray &message, int timeout = 100 );
|
||||
|
||||
/**
|
||||
* @brief Get the set user data.
|
||||
* @returns {QStringList}
|
||||
*/
|
||||
QStringList userData() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void instanceStarted();
|
||||
void receivedMessage( quint32 instanceId, QByteArray message );
|
||||
|
||||
private:
|
||||
SingleApplicationPrivate *d_ptr;
|
||||
Q_DECLARE_PRIVATE(SingleApplication)
|
||||
void abortSafely();
|
||||
};
|
||||
|
||||
Q_DECLARE_OPERATORS_FOR_FLAGS(SingleApplication::Options)
|
||||
|
||||
#endif // SINGLE_APPLICATION_H
|
||||
@@ -1,15 +0,0 @@
|
||||
QT += core network
|
||||
CONFIG += c++11
|
||||
|
||||
HEADERS += \
|
||||
$$PWD/singleapplication.h \
|
||||
$$PWD/singleapplication_p.h
|
||||
SOURCES += $$PWD/singleapplication.cpp \
|
||||
$$PWD/singleapplication_p.cpp
|
||||
|
||||
INCLUDEPATH += $$PWD
|
||||
|
||||
win32 {
|
||||
msvc:LIBS += Advapi32.lib
|
||||
gcc:LIBS += -ladvapi32
|
||||
}
|
||||
@@ -1,486 +0,0 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) Itay Grudev 2015 - 2020
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
//
|
||||
// W A R N I N G !!!
|
||||
// -----------------
|
||||
//
|
||||
// This file is not part of the SingleApplication API. It is used purely as an
|
||||
// implementation detail. This header file may change from version to
|
||||
// version without notice, or may even be removed.
|
||||
//
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstddef>
|
||||
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QThread>
|
||||
#include <QtCore/QByteArray>
|
||||
#include <QtCore/QDataStream>
|
||||
#include <QtCore/QElapsedTimer>
|
||||
#include <QtCore/QCryptographicHash>
|
||||
#include <QtNetwork/QLocalServer>
|
||||
#include <QtNetwork/QLocalSocket>
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
||||
#include <QtCore/QRandomGenerator>
|
||||
#else
|
||||
#include <QtCore/QDateTime>
|
||||
#endif
|
||||
|
||||
#include "singleapplication.h"
|
||||
#include "singleapplication_p.h"
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
#include <unistd.h>
|
||||
#include <sys/types.h>
|
||||
#include <pwd.h>
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX 1
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <lmcons.h>
|
||||
#endif
|
||||
|
||||
SingleApplicationPrivate::SingleApplicationPrivate( SingleApplication *q_ptr )
|
||||
: q_ptr( q_ptr )
|
||||
{
|
||||
server = nullptr;
|
||||
socket = nullptr;
|
||||
memory = nullptr;
|
||||
instanceNumber = 0;
|
||||
}
|
||||
|
||||
SingleApplicationPrivate::~SingleApplicationPrivate()
|
||||
{
|
||||
if( socket != nullptr ){
|
||||
socket->close();
|
||||
delete socket;
|
||||
}
|
||||
|
||||
if( memory != nullptr ){
|
||||
memory->lock();
|
||||
auto *inst = static_cast<InstancesInfo*>(memory->data());
|
||||
if( server != nullptr ){
|
||||
server->close();
|
||||
delete server;
|
||||
inst->primary = false;
|
||||
inst->primaryPid = -1;
|
||||
inst->primaryUser[0] = '\0';
|
||||
inst->checksum = blockChecksum();
|
||||
}
|
||||
memory->unlock();
|
||||
|
||||
delete memory;
|
||||
}
|
||||
}
|
||||
|
||||
QString SingleApplicationPrivate::getUsername()
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
wchar_t username[UNLEN + 1];
|
||||
// Specifies size of the buffer on input
|
||||
DWORD usernameLength = UNLEN + 1;
|
||||
if( GetUserNameW( username, &usernameLength ) )
|
||||
return QString::fromWCharArray( username );
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
|
||||
return QString::fromLocal8Bit( qgetenv( "USERNAME" ) );
|
||||
#else
|
||||
return qEnvironmentVariable( "USERNAME" );
|
||||
#endif
|
||||
#endif
|
||||
#ifdef Q_OS_UNIX
|
||||
QString username;
|
||||
uid_t uid = geteuid();
|
||||
struct passwd *pw = getpwuid( uid );
|
||||
if( pw )
|
||||
username = QString::fromLocal8Bit( pw->pw_name );
|
||||
if ( username.isEmpty() ){
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
|
||||
username = QString::fromLocal8Bit( qgetenv( "USER" ) );
|
||||
#else
|
||||
username = qEnvironmentVariable( "USER" );
|
||||
#endif
|
||||
}
|
||||
return username;
|
||||
#endif
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::genBlockServerName()
|
||||
{
|
||||
QCryptographicHash appData( QCryptographicHash::Sha256 );
|
||||
appData.addData( "SingleApplication", 17 );
|
||||
appData.addData( SingleApplication::app_t::applicationName().toUtf8() );
|
||||
appData.addData( SingleApplication::app_t::organizationName().toUtf8() );
|
||||
appData.addData( SingleApplication::app_t::organizationDomain().toUtf8() );
|
||||
|
||||
if ( ! appDataList.isEmpty() )
|
||||
appData.addData( appDataList.join( "" ).toUtf8() );
|
||||
|
||||
if( ! (options & SingleApplication::Mode::ExcludeAppVersion) ){
|
||||
appData.addData( SingleApplication::app_t::applicationVersion().toUtf8() );
|
||||
}
|
||||
|
||||
if( ! (options & SingleApplication::Mode::ExcludeAppPath) ){
|
||||
#ifdef Q_OS_WIN
|
||||
appData.addData( SingleApplication::app_t::applicationFilePath().toLower().toUtf8() );
|
||||
#else
|
||||
appData.addData( SingleApplication::app_t::applicationFilePath().toUtf8() );
|
||||
#endif
|
||||
}
|
||||
|
||||
// User level block requires a user specific data in the hash
|
||||
if( options & SingleApplication::Mode::User ){
|
||||
appData.addData( getUsername().toUtf8() );
|
||||
}
|
||||
|
||||
// Replace the backslash in RFC 2045 Base64 [a-zA-Z0-9+/=] to comply with
|
||||
// server naming requirements.
|
||||
blockServerName = appData.result().toBase64().replace("/", "_");
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::initializeMemoryBlock() const
|
||||
{
|
||||
auto *inst = static_cast<InstancesInfo*>( memory->data() );
|
||||
inst->primary = false;
|
||||
inst->secondary = 0;
|
||||
inst->primaryPid = -1;
|
||||
inst->primaryUser[0] = '\0';
|
||||
inst->checksum = blockChecksum();
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::startPrimary()
|
||||
{
|
||||
// Reset the number of connections
|
||||
auto *inst = static_cast <InstancesInfo*>( memory->data() );
|
||||
|
||||
inst->primary = true;
|
||||
inst->primaryPid = QCoreApplication::applicationPid();
|
||||
qstrncpy( inst->primaryUser, getUsername().toUtf8().data(), sizeof(inst->primaryUser) );
|
||||
inst->checksum = blockChecksum();
|
||||
instanceNumber = 0;
|
||||
// Successful creation means that no main process exists
|
||||
// So we start a QLocalServer to listen for connections
|
||||
QLocalServer::removeServer( blockServerName );
|
||||
server = new QLocalServer();
|
||||
|
||||
// Restrict access to the socket according to the
|
||||
// SingleApplication::Mode::User flag on User level or no restrictions
|
||||
if( options & SingleApplication::Mode::User ){
|
||||
server->setSocketOptions( QLocalServer::UserAccessOption );
|
||||
} else {
|
||||
server->setSocketOptions( QLocalServer::WorldAccessOption );
|
||||
}
|
||||
|
||||
server->listen( blockServerName );
|
||||
QObject::connect(
|
||||
server,
|
||||
&QLocalServer::newConnection,
|
||||
this,
|
||||
&SingleApplicationPrivate::slotConnectionEstablished
|
||||
);
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::startSecondary()
|
||||
{
|
||||
auto *inst = static_cast <InstancesInfo*>( memory->data() );
|
||||
|
||||
inst->secondary += 1;
|
||||
inst->checksum = blockChecksum();
|
||||
instanceNumber = inst->secondary;
|
||||
}
|
||||
|
||||
bool SingleApplicationPrivate::connectToPrimary( int msecs, ConnectionType connectionType )
|
||||
{
|
||||
QElapsedTimer time;
|
||||
time.start();
|
||||
|
||||
// Connect to the Local Server of the Primary Instance if not already
|
||||
// connected.
|
||||
if( socket == nullptr ){
|
||||
socket = new QLocalSocket();
|
||||
}
|
||||
|
||||
if( socket->state() == QLocalSocket::ConnectedState ) return true;
|
||||
|
||||
if( socket->state() != QLocalSocket::ConnectedState ){
|
||||
|
||||
while( true ){
|
||||
randomSleep();
|
||||
|
||||
if( socket->state() != QLocalSocket::ConnectingState )
|
||||
socket->connectToServer( blockServerName );
|
||||
|
||||
if( socket->state() == QLocalSocket::ConnectingState ){
|
||||
socket->waitForConnected( static_cast<int>(msecs - time.elapsed()) );
|
||||
}
|
||||
|
||||
// If connected break out of the loop
|
||||
if( socket->state() == QLocalSocket::ConnectedState ) break;
|
||||
|
||||
// If elapsed time since start is longer than the method timeout return
|
||||
if( time.elapsed() >= msecs ) return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisation message according to the SingleApplication protocol
|
||||
QByteArray initMsg;
|
||||
QDataStream writeStream(&initMsg, QIODevice::WriteOnly);
|
||||
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
|
||||
writeStream.setVersion(QDataStream::Qt_5_6);
|
||||
#endif
|
||||
|
||||
writeStream << blockServerName.toLatin1();
|
||||
writeStream << static_cast<quint8>(connectionType);
|
||||
writeStream << instanceNumber;
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
quint16 checksum = qChecksum(QByteArray(initMsg, static_cast<quint32>(initMsg.length())));
|
||||
#else
|
||||
quint16 checksum = qChecksum(initMsg.constData(), static_cast<quint32>(initMsg.length()));
|
||||
#endif
|
||||
writeStream << checksum;
|
||||
|
||||
// The header indicates the message length that follows
|
||||
QByteArray header;
|
||||
QDataStream headerStream(&header, QIODevice::WriteOnly);
|
||||
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
|
||||
headerStream.setVersion(QDataStream::Qt_5_6);
|
||||
#endif
|
||||
headerStream << static_cast <quint64>( initMsg.length() );
|
||||
|
||||
socket->write( header );
|
||||
socket->write( initMsg );
|
||||
bool result = socket->waitForBytesWritten( static_cast<int>(msecs - time.elapsed()) );
|
||||
socket->flush();
|
||||
return result;
|
||||
}
|
||||
|
||||
quint16 SingleApplicationPrivate::blockChecksum() const
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
quint16 checksum = qChecksum(QByteArray(static_cast<const char*>(memory->constData()), offsetof(InstancesInfo, checksum)));
|
||||
#else
|
||||
quint16 checksum = qChecksum(static_cast<const char*>(memory->constData()), offsetof(InstancesInfo, checksum));
|
||||
#endif
|
||||
return checksum;
|
||||
}
|
||||
|
||||
qint64 SingleApplicationPrivate::primaryPid() const
|
||||
{
|
||||
qint64 pid;
|
||||
|
||||
memory->lock();
|
||||
auto *inst = static_cast<InstancesInfo*>( memory->data() );
|
||||
pid = inst->primaryPid;
|
||||
memory->unlock();
|
||||
|
||||
return pid;
|
||||
}
|
||||
|
||||
QString SingleApplicationPrivate::primaryUser() const
|
||||
{
|
||||
QByteArray username;
|
||||
|
||||
memory->lock();
|
||||
auto *inst = static_cast<InstancesInfo*>( memory->data() );
|
||||
username = inst->primaryUser;
|
||||
memory->unlock();
|
||||
|
||||
return QString::fromUtf8( username );
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Executed when a connection has been made to the LocalServer
|
||||
*/
|
||||
void SingleApplicationPrivate::slotConnectionEstablished()
|
||||
{
|
||||
QLocalSocket *nextConnSocket = server->nextPendingConnection();
|
||||
connectionMap.insert(nextConnSocket, ConnectionInfo());
|
||||
|
||||
QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose,
|
||||
[nextConnSocket, this](){
|
||||
auto &info = connectionMap[nextConnSocket];
|
||||
Q_EMIT this->slotClientConnectionClosed( nextConnSocket, info.instanceId );
|
||||
}
|
||||
);
|
||||
|
||||
QObject::connect(nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater);
|
||||
|
||||
QObject::connect(nextConnSocket, &QLocalSocket::destroyed,
|
||||
[nextConnSocket, this](){
|
||||
connectionMap.remove(nextConnSocket);
|
||||
}
|
||||
);
|
||||
|
||||
QObject::connect(nextConnSocket, &QLocalSocket::readyRead,
|
||||
[nextConnSocket, this](){
|
||||
auto &info = connectionMap[nextConnSocket];
|
||||
switch(info.stage){
|
||||
case StageHeader:
|
||||
readInitMessageHeader(nextConnSocket);
|
||||
break;
|
||||
case StageBody:
|
||||
readInitMessageBody(nextConnSocket);
|
||||
break;
|
||||
case StageConnected:
|
||||
Q_EMIT this->slotDataAvailable( nextConnSocket, info.instanceId );
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::readInitMessageHeader( QLocalSocket *sock )
|
||||
{
|
||||
if (!connectionMap.contains( sock )){
|
||||
return;
|
||||
}
|
||||
|
||||
if( sock->bytesAvailable() < ( qint64 )sizeof( quint64 ) ){
|
||||
return;
|
||||
}
|
||||
|
||||
QDataStream headerStream( sock );
|
||||
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
|
||||
headerStream.setVersion( QDataStream::Qt_5_6 );
|
||||
#endif
|
||||
|
||||
// Read the header to know the message length
|
||||
quint64 msgLen = 0;
|
||||
headerStream >> msgLen;
|
||||
ConnectionInfo &info = connectionMap[sock];
|
||||
info.stage = StageBody;
|
||||
info.msgLen = msgLen;
|
||||
|
||||
if ( sock->bytesAvailable() >= (qint64) msgLen ){
|
||||
readInitMessageBody( sock );
|
||||
}
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock )
|
||||
{
|
||||
Q_Q(SingleApplication);
|
||||
|
||||
if (!connectionMap.contains( sock )){
|
||||
return;
|
||||
}
|
||||
|
||||
ConnectionInfo &info = connectionMap[sock];
|
||||
if( sock->bytesAvailable() < ( qint64 )info.msgLen ){
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the message body
|
||||
QByteArray msgBytes = sock->read(info.msgLen);
|
||||
QDataStream readStream(msgBytes);
|
||||
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
|
||||
readStream.setVersion( QDataStream::Qt_5_6 );
|
||||
#endif
|
||||
|
||||
// server name
|
||||
QByteArray latin1Name;
|
||||
readStream >> latin1Name;
|
||||
|
||||
// connection type
|
||||
ConnectionType connectionType = InvalidConnection;
|
||||
quint8 connTypeVal = InvalidConnection;
|
||||
readStream >> connTypeVal;
|
||||
connectionType = static_cast <ConnectionType>( connTypeVal );
|
||||
|
||||
// instance id
|
||||
quint32 instanceId = 0;
|
||||
readStream >> instanceId;
|
||||
|
||||
// checksum
|
||||
quint16 msgChecksum = 0;
|
||||
readStream >> msgChecksum;
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
const quint16 actualChecksum = qChecksum(QByteArray(msgBytes, static_cast<quint32>(msgBytes.length() - sizeof(quint16))));
|
||||
#else
|
||||
const quint16 actualChecksum = qChecksum(msgBytes.constData(), static_cast<quint32>(msgBytes.length() - sizeof(quint16)));
|
||||
#endif
|
||||
|
||||
bool isValid = readStream.status() == QDataStream::Ok &&
|
||||
QLatin1String(latin1Name) == blockServerName &&
|
||||
msgChecksum == actualChecksum;
|
||||
|
||||
if( !isValid ){
|
||||
sock->close();
|
||||
return;
|
||||
}
|
||||
|
||||
info.instanceId = instanceId;
|
||||
info.stage = StageConnected;
|
||||
|
||||
if( connectionType == NewInstance ||
|
||||
( connectionType == SecondaryInstance &&
|
||||
options & SingleApplication::Mode::SecondaryNotification ) )
|
||||
{
|
||||
Q_EMIT q->instanceStarted();
|
||||
}
|
||||
|
||||
if (sock->bytesAvailable() > 0){
|
||||
Q_EMIT this->slotDataAvailable( sock, instanceId );
|
||||
}
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::slotDataAvailable( QLocalSocket *dataSocket, quint32 instanceId )
|
||||
{
|
||||
Q_Q(SingleApplication);
|
||||
Q_EMIT q->receivedMessage( instanceId, dataSocket->readAll() );
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::slotClientConnectionClosed( QLocalSocket *closedSocket, quint32 instanceId )
|
||||
{
|
||||
if( closedSocket->bytesAvailable() > 0 )
|
||||
Q_EMIT slotDataAvailable( closedSocket, instanceId );
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::randomSleep()
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK( 5, 10, 0 )
|
||||
QThread::msleep( QRandomGenerator::global()->bounded( 8u, 18u ));
|
||||
#else
|
||||
qsrand( QDateTime::currentMSecsSinceEpoch() % std::numeric_limits<uint>::max() );
|
||||
QThread::msleep( 8 + static_cast <unsigned long>( static_cast <float>( qrand() ) / RAND_MAX * 10 ));
|
||||
#endif
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::addAppData(const QString &data)
|
||||
{
|
||||
appDataList.push_back(data);
|
||||
}
|
||||
|
||||
QStringList SingleApplicationPrivate::appData() const
|
||||
{
|
||||
return appDataList;
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) Itay Grudev 2015 - 2020
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
//
|
||||
// W A R N I N G !!!
|
||||
// -----------------
|
||||
//
|
||||
// This file is not part of the SingleApplication API. It is used purely as an
|
||||
// implementation detail. This header file may change from version to
|
||||
// version without notice, or may even be removed.
|
||||
//
|
||||
|
||||
#ifndef SINGLEAPPLICATION_P_H
|
||||
#define SINGLEAPPLICATION_P_H
|
||||
|
||||
#include <QtCore/QSharedMemory>
|
||||
#include <QtNetwork/QLocalServer>
|
||||
#include <QtNetwork/QLocalSocket>
|
||||
#include "singleapplication.h"
|
||||
|
||||
struct InstancesInfo {
|
||||
bool primary;
|
||||
quint32 secondary;
|
||||
qint64 primaryPid;
|
||||
char primaryUser[128];
|
||||
quint16 checksum; // Must be the last field
|
||||
};
|
||||
|
||||
struct ConnectionInfo {
|
||||
qint64 msgLen = 0;
|
||||
quint32 instanceId = 0;
|
||||
quint8 stage = 0;
|
||||
};
|
||||
|
||||
class SingleApplicationPrivate : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum ConnectionType : quint8 {
|
||||
InvalidConnection = 0,
|
||||
NewInstance = 1,
|
||||
SecondaryInstance = 2,
|
||||
Reconnect = 3
|
||||
};
|
||||
enum ConnectionStage : quint8 {
|
||||
StageHeader = 0,
|
||||
StageBody = 1,
|
||||
StageConnected = 2,
|
||||
};
|
||||
Q_DECLARE_PUBLIC(SingleApplication)
|
||||
|
||||
SingleApplicationPrivate( SingleApplication *q_ptr );
|
||||
~SingleApplicationPrivate() override;
|
||||
|
||||
static QString getUsername();
|
||||
void genBlockServerName();
|
||||
void initializeMemoryBlock() const;
|
||||
void startPrimary();
|
||||
void startSecondary();
|
||||
bool connectToPrimary( int msecs, ConnectionType connectionType );
|
||||
quint16 blockChecksum() const;
|
||||
qint64 primaryPid() const;
|
||||
QString primaryUser() const;
|
||||
void readInitMessageHeader(QLocalSocket *socket);
|
||||
void readInitMessageBody(QLocalSocket *socket);
|
||||
static void randomSleep();
|
||||
void addAppData(const QString &data);
|
||||
QStringList appData() const;
|
||||
|
||||
SingleApplication *q_ptr;
|
||||
QSharedMemory *memory;
|
||||
QLocalSocket *socket;
|
||||
QLocalServer *server;
|
||||
quint32 instanceNumber;
|
||||
QString blockServerName;
|
||||
SingleApplication::Options options;
|
||||
QMap<QLocalSocket*, ConnectionInfo> connectionMap;
|
||||
QStringList appDataList;
|
||||
|
||||
public Q_SLOTS:
|
||||
void slotConnectionEstablished();
|
||||
void slotDataAvailable( QLocalSocket*, quint32 );
|
||||
void slotClientConnectionClosed( QLocalSocket*, quint32 );
|
||||
};
|
||||
|
||||
#endif // SINGLEAPPLICATION_P_H
|
||||
@@ -25,10 +25,11 @@ execute_process(
|
||||
add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}")
|
||||
|
||||
add_definitions(-DPROD_AGW_PUBLIC_KEY="$ENV{PROD_AGW_PUBLIC_KEY}")
|
||||
add_definitions(-DPROD_PROXY_STORAGE_KEY="$ENV{PROD_PROXY_STORAGE_KEY}")
|
||||
add_definitions(-DPROD_S3_ENDPOINT="$ENV{PROD_S3_ENDPOINT}")
|
||||
|
||||
add_definitions(-DDEV_AGW_PUBLIC_KEY="$ENV{DEV_AGW_PUBLIC_KEY}")
|
||||
add_definitions(-DDEV_AGW_ENDPOINT="$ENV{DEV_AGW_ENDPOINT}")
|
||||
add_definitions(-DDEV_S3_ENDPOINT="$ENV{DEV_S3_ENDPOINT}")
|
||||
|
||||
if(IOS)
|
||||
set(PACKAGES ${PACKAGES} Multimedia)
|
||||
@@ -145,6 +146,7 @@ set(HEADERS ${HEADERS}
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/serialization/transfer.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/enums/apiEnums.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/../common/logger/logger.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/utils/qmlUtils.h
|
||||
)
|
||||
|
||||
# Mozilla headres
|
||||
@@ -196,6 +198,7 @@ set(SOURCES ${SOURCES}
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess_new.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/../common/logger/logger.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/utils/qmlUtils.cpp
|
||||
)
|
||||
|
||||
# Mozilla sources
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include <QTextDocument>
|
||||
#include <QTimer>
|
||||
#include <QTranslator>
|
||||
#include <QLocalSocket>
|
||||
#include <QLocalServer>
|
||||
|
||||
#include "logger.h"
|
||||
#include "ui/models/installedAppsModel.h"
|
||||
@@ -28,13 +30,7 @@
|
||||
#include <AmneziaVPN-Swift.h>
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
AmneziaApplication::AmneziaApplication(int &argc, char *argv[]) : AMNEZIA_BASE_CLASS(argc, argv)
|
||||
#else
|
||||
AmneziaApplication::AmneziaApplication(int &argc, char *argv[], bool allowSecondary, SingleApplication::Options options, int timeout,
|
||||
const QString &userData)
|
||||
: SingleApplication(argc, argv, allowSecondary, options, timeout, userData)
|
||||
#endif
|
||||
{
|
||||
setQuitOnLastWindowClosed(false);
|
||||
|
||||
@@ -115,10 +111,11 @@ void AmneziaApplication::init()
|
||||
qFatal("Android controller initialization failed");
|
||||
}
|
||||
|
||||
connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, [this](QString data) {
|
||||
m_pageController->goToPageHome();
|
||||
connect(AndroidController::instance(), &AndroidController::importConfigFromOutside, this, [this](QString data) {
|
||||
emit m_pageController->goToPageHome();
|
||||
m_importController->extractConfigFromData(data);
|
||||
m_pageController->goToPageViewConfig();
|
||||
data.clear();
|
||||
emit m_pageController->goToPageViewConfig();
|
||||
});
|
||||
|
||||
m_engine->addImageProvider(QLatin1String("installedAppImage"), new InstalledAppsImageProvider);
|
||||
@@ -126,16 +123,16 @@ void AmneziaApplication::init()
|
||||
|
||||
#ifdef Q_OS_IOS
|
||||
IosController::Instance()->initialize();
|
||||
connect(IosController::Instance(), &IosController::importConfigFromOutside, [this](QString data) {
|
||||
m_pageController->goToPageHome();
|
||||
connect(IosController::Instance(), &IosController::importConfigFromOutside, this, [this](QString data) {
|
||||
emit m_pageController->goToPageHome();
|
||||
m_importController->extractConfigFromData(data);
|
||||
m_pageController->goToPageViewConfig();
|
||||
emit m_pageController->goToPageViewConfig();
|
||||
});
|
||||
|
||||
connect(IosController::Instance(), &IosController::importBackupFromOutside, [this](QString filePath) {
|
||||
m_pageController->goToPageHome();
|
||||
connect(IosController::Instance(), &IosController::importBackupFromOutside, this, [this](QString filePath) {
|
||||
emit m_pageController->goToPageHome();
|
||||
m_pageController->goToPageSettingsBackup();
|
||||
m_settingsController->importBackupFromOutside(filePath);
|
||||
emit m_settingsController->importBackupFromOutside(filePath);
|
||||
});
|
||||
|
||||
QTimer::singleShot(0, this, [this]() { AmneziaVPN::toggleScreenshots(m_settings->isScreenshotsEnabled()); });
|
||||
@@ -180,16 +177,6 @@ void AmneziaApplication::init()
|
||||
m_pageController->showOnStartup();
|
||||
#endif
|
||||
|
||||
// TODO - fix
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
if (isPrimary()) {
|
||||
QObject::connect(this, &SingleApplication::instanceStarted, m_pageController.get(), [this]() {
|
||||
qDebug() << "Secondary instance started, showing this window instead";
|
||||
emit m_pageController->raiseMainWindow();
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
// Android TextArea clipboard workaround
|
||||
// Text from TextArea always has "text/html" mime-type:
|
||||
// /qt/6.6.1/Src/qtdeclarative/src/quick/items/qquicktextcontrol.cpp:1865
|
||||
@@ -294,6 +281,24 @@ bool AmneziaApplication::parseCommands()
|
||||
return true;
|
||||
}
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
void AmneziaApplication::startLocalServer() {
|
||||
const QString serverName("AmneziaVPNInstance");
|
||||
QLocalServer::removeServer(serverName);
|
||||
|
||||
QLocalServer* server = new QLocalServer(this);
|
||||
server->listen(serverName);
|
||||
|
||||
QObject::connect(server, &QLocalServer::newConnection, this, [server, this]() {
|
||||
if (server) {
|
||||
QLocalSocket* clientConnection = server->nextPendingConnection();
|
||||
clientConnection->deleteLater();
|
||||
}
|
||||
emit m_pageController->raiseMainWindow();
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
QQmlApplicationEngine *AmneziaApplication::qmlEngine() const
|
||||
{
|
||||
return m_engine;
|
||||
@@ -399,6 +404,9 @@ void AmneziaApplication::initControllers()
|
||||
m_pageController.reset(new PageController(m_serversModel, m_settings));
|
||||
m_engine->rootContext()->setContextProperty("PageController", m_pageController.get());
|
||||
|
||||
m_focusController.reset(new FocusController(m_engine, this));
|
||||
m_engine->rootContext()->setContextProperty("FocusController", m_focusController.get());
|
||||
|
||||
m_installController.reset(new InstallController(m_serversModel, m_containersModel, m_protocolsModel, m_clientManagementModel,
|
||||
m_apiServicesModel, m_settings));
|
||||
m_engine->rootContext()->setContextProperty("InstallController", m_installController.get());
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "ui/controllers/exportController.h"
|
||||
#include "ui/controllers/importController.h"
|
||||
#include "ui/controllers/installController.h"
|
||||
#include "ui/controllers/focusController.h"
|
||||
#include "ui/controllers/pageController.h"
|
||||
#include "ui/controllers/settingsController.h"
|
||||
#include "ui/controllers/sitesController.h"
|
||||
@@ -53,22 +54,14 @@
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
#define AMNEZIA_BASE_CLASS QGuiApplication
|
||||
#else
|
||||
#define AMNEZIA_BASE_CLASS SingleApplication
|
||||
#define QAPPLICATION_CLASS QApplication
|
||||
#include "singleapplication.h"
|
||||
#define AMNEZIA_BASE_CLASS QApplication
|
||||
#endif
|
||||
|
||||
class AmneziaApplication : public AMNEZIA_BASE_CLASS
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
AmneziaApplication(int &argc, char *argv[]);
|
||||
#else
|
||||
AmneziaApplication(int &argc, char *argv[], bool allowSecondary = false,
|
||||
SingleApplication::Options options = SingleApplication::User, int timeout = 1000,
|
||||
const QString &userData = {});
|
||||
#endif
|
||||
virtual ~AmneziaApplication();
|
||||
|
||||
void init();
|
||||
@@ -78,6 +71,10 @@ public:
|
||||
void updateTranslator(const QLocale &locale);
|
||||
bool parseCommands();
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
void startLocalServer();
|
||||
#endif
|
||||
|
||||
QQmlApplicationEngine *qmlEngine() const;
|
||||
QNetworkAccessManager *manager() { return m_nam; }
|
||||
|
||||
@@ -128,6 +125,7 @@ private:
|
||||
#endif
|
||||
|
||||
QScopedPointer<ConnectionController> m_connectionController;
|
||||
QScopedPointer<FocusController> m_focusController;
|
||||
QScopedPointer<PageController> m_pageController;
|
||||
QScopedPointer<InstallController> m_installController;
|
||||
QScopedPointer<ImportController> m_importController;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
<!-- for TV -->
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
<uses-feature android:name="android.software.leanback" android:required="true" />
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
|
||||
<!-- The following comment will be replaced upon deployment with default features based on the dependencies
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- To request network state -->
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
@@ -91,6 +91,13 @@
|
||||
android:exported="false"
|
||||
android:theme="@style/Translucent" />
|
||||
|
||||
<activity android:name=".TvFilePicker"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:exported="false"
|
||||
android:theme="@style/Translucent" />
|
||||
|
||||
<activity
|
||||
android:name=".ImportConfigActivity"
|
||||
android:excludeFromRecents="true"
|
||||
|
||||
@@ -33,7 +33,7 @@ android.library.defaults.buildfeatures.androidresources=false
|
||||
# For development copy and set local values for these parameters in local.properties
|
||||
#androidCompileSdkVersion=android-34
|
||||
#androidBuildToolsVersion=34.0.0
|
||||
#qtMinSdkVersion=24
|
||||
#qtMinSdkVersion=26
|
||||
#qtTargetSdkVersion=34
|
||||
#androidNdkVersion=26.1.10909125
|
||||
#qtTargetAbiList=x86_64
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.amnezia.vpn.protocol
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.net.IpPrefix
|
||||
import android.net.VpnService
|
||||
@@ -8,9 +7,6 @@ import android.net.VpnService.Builder
|
||||
import android.os.Build
|
||||
import android.system.OsConstants
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.zip.ZipFile
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.amnezia.vpn.util.Log
|
||||
import org.amnezia.vpn.util.net.InetNetwork
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_banner_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_banner_foreground"/>
|
||||
</adaptive-icon>
|
||||
BIN
client/android/res/mipmap-hdpi/ic_banner.png
Normal file
BIN
client/android/res/mipmap-hdpi/ic_banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
client/android/res/mipmap-mdpi/ic_banner.png
Normal file
BIN
client/android/res/mipmap-mdpi/ic_banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
@@ -23,4 +23,6 @@
|
||||
<string name="notificationSettingsDialogTitle">Настройки уведомлений</string>
|
||||
<string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string>
|
||||
<string name="openNotificationSettings">Открыть настройки уведомлений</string>
|
||||
|
||||
<string name="tvNoFileBrowser">Пожалуйста, установите приложение для просмотра файлов</string>
|
||||
</resources>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_banner_background">#1E1E1F</color>
|
||||
</resources>
|
||||
@@ -23,4 +23,6 @@
|
||||
<string name="notificationSettingsDialogTitle">Notification settings</string>
|
||||
<string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system settings</string>
|
||||
<string name="openNotificationSettings">Open notification settings</string>
|
||||
|
||||
<string name="tvNoFileBrowser">Please install a file management utility to browse files</string>
|
||||
</resources>
|
||||
@@ -4,6 +4,7 @@ import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.app.NotificationManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
@@ -12,6 +13,7 @@ import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -20,7 +22,13 @@ import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.os.Message
|
||||
import android.os.Messenger
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.SystemClock
|
||||
import android.provider.OpenableColumns
|
||||
import android.provider.Settings
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager.LayoutParams
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
@@ -29,6 +37,7 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import java.io.IOException
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.text.RegexOption.IGNORE_CASE
|
||||
import AppListProvider
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
@@ -70,6 +79,7 @@ class AmneziaActivity : QtActivity() {
|
||||
private var isInBoundState = false
|
||||
private var notificationStateReceiver: BroadcastReceiver? = null
|
||||
private lateinit var vpnServiceMessenger: IpcMessenger
|
||||
private var pfd: ParcelFileDescriptor? = null
|
||||
|
||||
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
|
||||
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
|
||||
@@ -158,7 +168,7 @@ class AmneziaActivity : QtActivity() {
|
||||
*/
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.d(TAG, "Create Amnezia activity: $intent")
|
||||
Log.d(TAG, "Create Amnezia activity")
|
||||
loadLibs()
|
||||
window.apply {
|
||||
addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
||||
@@ -200,7 +210,7 @@ class AmneziaActivity : QtActivity() {
|
||||
NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED
|
||||
)
|
||||
) {
|
||||
Log.d(
|
||||
Log.v(
|
||||
TAG, "Notification state changed: ${it?.action}, blocked = " +
|
||||
"${it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)}"
|
||||
)
|
||||
@@ -214,7 +224,7 @@ class AmneziaActivity : QtActivity() {
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
Log.d(TAG, "onNewIntent: $intent")
|
||||
Log.v(TAG, "onNewIntent: $intent")
|
||||
intent?.let(::processIntent)
|
||||
}
|
||||
|
||||
@@ -403,7 +413,7 @@ class AmneziaActivity : QtActivity() {
|
||||
@MainThread
|
||||
private fun startVpn(vpnConfig: String) {
|
||||
getVpnProto(vpnConfig)?.let { proto ->
|
||||
Log.d(TAG, "Proto from config: $proto, current proto: $vpnProto")
|
||||
Log.v(TAG, "Proto from config: $proto, current proto: $vpnProto")
|
||||
if (isServiceConnected) {
|
||||
if (proto.serviceClass == vpnProto?.serviceClass) {
|
||||
vpnProto = proto
|
||||
@@ -513,21 +523,25 @@ class AmneziaActivity : QtActivity() {
|
||||
type = "text/*"
|
||||
putExtra(Intent.EXTRA_TITLE, fileName)
|
||||
}.also {
|
||||
startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler(
|
||||
onSuccess = {
|
||||
it?.data?.let { uri ->
|
||||
Log.d(TAG, "Save file to $uri")
|
||||
try {
|
||||
contentResolver.openOutputStream(uri)?.use { os ->
|
||||
os.bufferedWriter().use { it.write(data) }
|
||||
try {
|
||||
startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler(
|
||||
onSuccess = {
|
||||
it?.data?.let { uri ->
|
||||
Log.v(TAG, "Save file to $uri")
|
||||
try {
|
||||
contentResolver.openOutputStream(uri)?.use { os ->
|
||||
os.bufferedWriter().use { it.write(data) }
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to save file $uri: $e")
|
||||
// todo: send error to Qt
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to save file $uri: $e")
|
||||
// todo: send error to Qt
|
||||
}
|
||||
}
|
||||
}
|
||||
))
|
||||
))
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(this@AmneziaActivity, "Unsupported", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -536,46 +550,115 @@ class AmneziaActivity : QtActivity() {
|
||||
fun openFile(filter: String?) {
|
||||
Log.v(TAG, "Open file with filter: $filter")
|
||||
mainScope.launch {
|
||||
val mimeTypes = if (!filter.isNullOrEmpty()) {
|
||||
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
|
||||
val mime = MimeTypeMap.getSingleton()
|
||||
extensionRegex.findAll(filter).map {
|
||||
it.groups[1]?.value?.let { mime.getMimeTypeFromExtension(it) } ?: "*/*"
|
||||
}.toSet()
|
||||
} else emptySet()
|
||||
val intent = if (!isOnTv()) {
|
||||
val mimeTypes = if (!filter.isNullOrEmpty()) {
|
||||
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
|
||||
val mime = MimeTypeMap.getSingleton()
|
||||
extensionRegex.findAll(filter).map {
|
||||
it.groups[1]?.value?.let { mime.getMimeTypeFromExtension(it) } ?: "*/*"
|
||||
}.toSet()
|
||||
} else emptySet()
|
||||
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
Log.v(TAG, "File mimyType filter: $mimeTypes")
|
||||
if ("*/*" in mimeTypes) {
|
||||
type = "*/*"
|
||||
} else {
|
||||
when (mimeTypes.size) {
|
||||
1 -> type = mimeTypes.first()
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
Log.v(TAG, "File mimyType filter: $mimeTypes")
|
||||
if ("*/*" in mimeTypes) {
|
||||
type = "*/*"
|
||||
} else {
|
||||
when (mimeTypes.size) {
|
||||
1 -> type = mimeTypes.first()
|
||||
|
||||
in 2..Int.MAX_VALUE -> {
|
||||
type = "*/*"
|
||||
putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
|
||||
in 2..Int.MAX_VALUE -> {
|
||||
type = "*/*"
|
||||
putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
|
||||
}
|
||||
|
||||
else -> type = "*/*"
|
||||
}
|
||||
|
||||
else -> type = "*/*"
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
startActivityForResult(it, OPEN_FILE_ACTION_CODE, ActivityResultHandler(
|
||||
} else {
|
||||
Intent(this@AmneziaActivity, TvFilePicker::class.java)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivityForResult(intent, OPEN_FILE_ACTION_CODE, ActivityResultHandler(
|
||||
onAny = {
|
||||
val uri = it?.data?.toString() ?: ""
|
||||
Log.d(TAG, "Open file: $uri")
|
||||
if (isOnTv() && it?.hasExtra("activityNotFound") == true) {
|
||||
showNoFileBrowserAlertDialog()
|
||||
}
|
||||
val uri = it?.data?.apply {
|
||||
grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}?.toString() ?: ""
|
||||
Log.v(TAG, "Open file: $uri")
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onFileOpened(uri)
|
||||
}
|
||||
}
|
||||
))
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
showNoFileBrowserAlertDialog()
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
QtAndroidController.onFileOpened("")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNoFileBrowserAlertDialog() {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.tvNoFileBrowser)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
try {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://webstoreredirect")))
|
||||
} catch (_: Throwable) {}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun getFd(fileName: String): Int {
|
||||
Log.v(TAG, "Get fd for $fileName")
|
||||
return blockingCall {
|
||||
try {
|
||||
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
|
||||
pfd?.fd ?: -1
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to get fd: $e")
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun closeFd() {
|
||||
Log.v(TAG, "Close fd")
|
||||
mainScope.launch {
|
||||
pfd?.close()
|
||||
pfd = null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun getFileName(uri: String): String {
|
||||
Log.v(TAG, "Get file name for uri: $uri")
|
||||
return blockingCall {
|
||||
try {
|
||||
contentResolver.query(Uri.parse(uri), arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst() && !cursor.isNull(0)) {
|
||||
return@blockingCall cursor.getString(0) ?: ""
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to get file name: $e")
|
||||
}
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@SuppressLint("UnsupportedChromeOsCameraSystemFeature")
|
||||
fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
|
||||
@@ -720,9 +803,121 @@ class AmneziaActivity : QtActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// method to workaround Qt's problem with calling the keyboard on TVs
|
||||
@Suppress("unused")
|
||||
fun sendTouch(x: Float, y: Float) {
|
||||
Log.v(TAG, "Send touch: $x, $y")
|
||||
blockingCall {
|
||||
findQtWindow(window.decorView)?.let {
|
||||
Log.v(TAG, "Send touch to $it")
|
||||
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN))
|
||||
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun findQtWindow(view: View): View? {
|
||||
Log.v(TAG, "findQtWindow: process $view")
|
||||
if (view::class.simpleName == "QtWindow") return view
|
||||
else if (view is ViewGroup) {
|
||||
for (i in 0 until view.childCount) {
|
||||
val result = findQtWindow(view.getChildAt(i))
|
||||
if (result != null) return result
|
||||
}
|
||||
return null
|
||||
} else return null
|
||||
}
|
||||
|
||||
private fun createEvent(x: Float, y: Float, eventTime: Long, action: Int): MotionEvent =
|
||||
MotionEvent.obtain(
|
||||
eventTime,
|
||||
eventTime,
|
||||
action,
|
||||
1,
|
||||
arrayOf(MotionEvent.PointerProperties().apply {
|
||||
id = 0
|
||||
toolType = MotionEvent.TOOL_TYPE_FINGER
|
||||
}),
|
||||
arrayOf(MotionEvent.PointerCoords().apply {
|
||||
this.x = x
|
||||
this.y = y
|
||||
pressure = 1f
|
||||
size = 1f
|
||||
}),
|
||||
0, 0, 1.0f, 1.0f, 0, 0, 0,0
|
||||
)
|
||||
|
||||
// workaround for a bug in Qt that causes the mouse click event not to be handled
|
||||
// also disable right-click, as it causes the application to crash
|
||||
private var lastButtonState = 0
|
||||
private fun MotionEvent.fixCopy(): MotionEvent = MotionEvent.obtain(
|
||||
downTime,
|
||||
eventTime,
|
||||
action,
|
||||
pointerCount,
|
||||
(0 until pointerCount).map { i ->
|
||||
MotionEvent.PointerProperties().apply {
|
||||
getPointerProperties(i, this)
|
||||
}
|
||||
}.toTypedArray(),
|
||||
(0 until pointerCount).map { i ->
|
||||
MotionEvent.PointerCoords().apply {
|
||||
getPointerCoords(i, this)
|
||||
}
|
||||
}.toTypedArray(),
|
||||
metaState,
|
||||
MotionEvent.BUTTON_PRIMARY,
|
||||
xPrecision,
|
||||
yPrecision,
|
||||
deviceId,
|
||||
edgeFlags,
|
||||
source,
|
||||
flags
|
||||
)
|
||||
|
||||
private fun handleMouseEvent(ev: MotionEvent, superDispatch: (MotionEvent?) -> Boolean): Boolean {
|
||||
when (ev.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
lastButtonState = ev.buttonState
|
||||
if (ev.buttonState == MotionEvent.BUTTON_SECONDARY) return true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
when (lastButtonState) {
|
||||
MotionEvent.BUTTON_SECONDARY -> return true
|
||||
MotionEvent.BUTTON_PRIMARY -> {
|
||||
val modEvent = ev.fixCopy()
|
||||
return superDispatch(modEvent).apply { modEvent.recycle() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return superDispatch(ev)
|
||||
}
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
||||
Log.v(TAG, "dispatchTouch: $ev")
|
||||
if (ev != null && ev.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
|
||||
return handleMouseEvent(ev) { super.dispatchTouchEvent(it) }
|
||||
}
|
||||
return super.dispatchTouchEvent(ev)
|
||||
}
|
||||
|
||||
override fun dispatchTrackballEvent(ev: MotionEvent?): Boolean {
|
||||
ev?.let { return handleMouseEvent(ev) { super.dispatchTrackballEvent(it) }}
|
||||
return super.dispatchTrackballEvent(ev)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utils methods
|
||||
*/
|
||||
private fun <T> blockingCall(
|
||||
context: CoroutineContext = Dispatchers.Main.immediate,
|
||||
block: suspend () -> T
|
||||
) = runBlocking {
|
||||
mainScope.async(context) { block() }.await()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun actionCodeToString(actionCode: Int): String =
|
||||
when (actionCode) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.annotation.MainThread
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import java.net.UnknownHostException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
@@ -127,6 +128,8 @@ open class AmneziaVpnService : VpnService() {
|
||||
|
||||
is LoadLibraryException -> onError("${e.message}. Caused: ${e.cause?.message}")
|
||||
|
||||
is UnknownHostException -> onError("Unknown host")
|
||||
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
@@ -297,7 +300,7 @@ open class AmneziaVpnService : VpnService() {
|
||||
arrayOf(ACTION_CONNECT, ACTION_DISCONNECT), ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
) {
|
||||
it?.action?.let { action ->
|
||||
Log.d(TAG, "Broadcast request received: $action")
|
||||
Log.v(TAG, "Broadcast request received: $action")
|
||||
when (action) {
|
||||
ACTION_CONNECT -> connect()
|
||||
ACTION_DISCONNECT -> disconnect()
|
||||
@@ -314,7 +317,7 @@ open class AmneziaVpnService : VpnService() {
|
||||
)
|
||||
) {
|
||||
val state = it?.getBooleanExtra(NotificationManager.EXTRA_BLOCKED_STATE, false)
|
||||
Log.d(TAG, "Notification state changed: ${it?.action}, blocked = $state")
|
||||
Log.v(TAG, "Notification state changed: ${it?.action}, blocked = $state")
|
||||
if (state == false) {
|
||||
enableNotification()
|
||||
} else {
|
||||
@@ -447,7 +450,7 @@ open class AmneziaVpnService : VpnService() {
|
||||
serviceNotification.isNotificationEnabled() &&
|
||||
getSystemService<PowerManager>()?.isInteractive != false
|
||||
) {
|
||||
Log.d(TAG, "Launch traffic stats update")
|
||||
Log.v(TAG, "Launch traffic stats update")
|
||||
trafficStats.reset()
|
||||
startTrafficStatsUpdateJob()
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ class AuthActivity : FragmentActivity() {
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: AuthenticationResult) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
Log.d(TAG, "Authentication succeeded")
|
||||
Log.v(TAG, "Authentication succeeded")
|
||||
QtAndroidController.onAuthResult(true)
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -29,20 +29,20 @@ class ImportConfigActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.d(TAG, "Create Import Config Activity: $intent")
|
||||
Log.v(TAG, "Create Import Config Activity: $intent")
|
||||
intent?.let(::readConfig)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
Log.d(TAG, "onNewIntent: $intent")
|
||||
Log.v(TAG, "onNewIntent: $intent")
|
||||
intent.let(::readConfig)
|
||||
}
|
||||
|
||||
private fun readConfig(intent: Intent) {
|
||||
when (intent.action) {
|
||||
ACTION_SEND -> {
|
||||
Log.d(TAG, "Process SEND action, type: ${intent.type}")
|
||||
Log.v(TAG, "Process SEND action, type: ${intent.type}")
|
||||
when (intent.type) {
|
||||
"application/octet-stream" -> {
|
||||
intent.getUriCompat()?.let { uri ->
|
||||
@@ -60,7 +60,7 @@ class ImportConfigActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
ACTION_VIEW -> {
|
||||
Log.d(TAG, "Process VIEW action, scheme: ${intent.scheme}")
|
||||
Log.v(TAG, "Process VIEW action, scheme: ${intent.scheme}")
|
||||
when (intent.scheme) {
|
||||
"file", "content" -> {
|
||||
intent.data?.let { uri ->
|
||||
|
||||
@@ -62,7 +62,7 @@ class ServiceNotification(private val context: Context) {
|
||||
fun buildNotification(serverName: String?, protocol: String?, state: ProtocolState): Notification {
|
||||
val speedString = if (state == CONNECTED) zeroSpeed else null
|
||||
|
||||
Log.d(TAG, "Build notification: $serverName, $state")
|
||||
Log.v(TAG, "Build notification: $serverName, $state")
|
||||
|
||||
return notificationBuilder
|
||||
.setSmallIcon(R.drawable.ic_amnezia_round)
|
||||
@@ -88,17 +88,15 @@ class ServiceNotification(private val context: Context) {
|
||||
fun isNotificationEnabled(): Boolean {
|
||||
if (!context.isNotificationPermissionGranted()) return false
|
||||
if (!notificationManager.areNotificationsEnabled()) return false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
return notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID)
|
||||
?.let { it.importance != NotificationManager.IMPORTANCE_NONE } ?: true
|
||||
}
|
||||
return true
|
||||
return notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID)?.let {
|
||||
it.importance != NotificationManager.IMPORTANCE_NONE
|
||||
} ?: true
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun updateNotification(serverName: String?, protocol: String?, state: ProtocolState) {
|
||||
if (context.isNotificationPermissionGranted()) {
|
||||
Log.d(TAG, "Update notification: $serverName, $state")
|
||||
Log.v(TAG, "Update notification: $serverName, $state")
|
||||
notificationManager.notify(NOTIFICATION_ID, buildNotification(serverName, protocol, state))
|
||||
}
|
||||
}
|
||||
|
||||
45
client/android/src/org/amnezia/vpn/TvFilePicker.kt
Normal file
45
client/android/src/org/amnezia/vpn/TvFilePicker.kt
Normal file
@@ -0,0 +1,45 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import org.amnezia.vpn.util.Log
|
||||
|
||||
private const val TAG = "TvFilePicker"
|
||||
|
||||
class TvFilePicker : ComponentActivity() {
|
||||
|
||||
private val fileChooseResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) {
|
||||
setResult(RESULT_OK, Intent().apply { data = it })
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.v(TAG, "onCreate")
|
||||
getFile()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
Log.v(TAG, "onNewIntent")
|
||||
getFile()
|
||||
}
|
||||
|
||||
private fun getFile() {
|
||||
try {
|
||||
Log.v(TAG, "getFile")
|
||||
fileChooseResultLauncher.launch("*/*")
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Log.w(TAG, "Activity not found")
|
||||
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })
|
||||
finish()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to get file: $e")
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ object LibraryLoader {
|
||||
System.loadLibrary(libraryName)
|
||||
return
|
||||
} catch (_: UnsatisfiedLinkError) {
|
||||
Log.d(TAG, "Failed to load library, try to extract it from apk")
|
||||
Log.w(TAG, "Failed to load library, try to extract it from apk")
|
||||
}
|
||||
var tempFile: File? = null
|
||||
try {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.amnezia.vpn.util
|
||||
|
||||
import android.content.Context
|
||||
import android.icu.text.DateFormat
|
||||
import android.icu.text.SimpleDateFormat
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
import java.io.File
|
||||
@@ -12,8 +10,6 @@ import java.nio.channels.FileChannel
|
||||
import java.nio.channels.FileLock
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import org.amnezia.vpn.util.Log.Priority.D
|
||||
import org.amnezia.vpn.util.Log.Priority.E
|
||||
@@ -41,11 +37,7 @@ private const val LOG_MAX_FILE_SIZE = 1024 * 1024
|
||||
* | | | create a report and/or terminate the process |
|
||||
*/
|
||||
object Log {
|
||||
private val dateTimeFormat: Any =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)
|
||||
else object : ThreadLocal<DateFormat>() {
|
||||
override fun initialValue(): DateFormat = SimpleDateFormat(DATE_TIME_PATTERN, Locale.US)
|
||||
}
|
||||
private val dateTimeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)
|
||||
|
||||
private lateinit var logDir: File
|
||||
private val logFile: File by lazy { File(logDir, LOG_FILE_NAME) }
|
||||
@@ -143,12 +135,7 @@ object Log {
|
||||
}
|
||||
|
||||
private fun formatLogMsg(tag: String, msg: String, priority: Priority): String {
|
||||
val date = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
LocalDateTime.now().format(dateTimeFormat as DateTimeFormatter)
|
||||
} else {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(dateTimeFormat as ThreadLocal<DateFormat>).get()?.format(Date())
|
||||
}
|
||||
val date = LocalDateTime.now().format(dateTimeFormat)
|
||||
return "$date ${Process.myPid()} ${Process.myTid()} $priority [${Thread.currentThread().name}] " +
|
||||
"$tag: $msg\n"
|
||||
}
|
||||
|
||||
@@ -42,18 +42,12 @@ class NetworkState(
|
||||
private val networkCallback: NetworkCallback by lazy(NONE) {
|
||||
object : NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
Log.d(TAG, "onAvailable: $network")
|
||||
Log.v(TAG, "onAvailable: $network")
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
Log.d(TAG, "onCapabilitiesChanged: $network, $networkCapabilities")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
checkNetworkState(network, networkCapabilities)
|
||||
} else {
|
||||
handler.post {
|
||||
checkNetworkState(network, networkCapabilities)
|
||||
}
|
||||
}
|
||||
Log.v(TAG, "onCapabilitiesChanged: $network, $networkCapabilities")
|
||||
checkNetworkState(network, networkCapabilities)
|
||||
}
|
||||
|
||||
private fun checkNetworkState(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
@@ -73,11 +67,11 @@ class NetworkState(
|
||||
}
|
||||
|
||||
override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
|
||||
Log.d(TAG, "onBlockedStatusChanged: $network, $blocked")
|
||||
Log.v(TAG, "onBlockedStatusChanged: $network, $blocked")
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
Log.d(TAG, "onLost: $network")
|
||||
Log.v(TAG, "onLost: $network")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +81,7 @@ class NetworkState(
|
||||
Log.d(TAG, "Bind network listener")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
connectivityManager.registerBestMatchingNetworkCallback(networkRequest, networkCallback, handler)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
} else {
|
||||
val numberAttempts = 300
|
||||
var attemptCount = 0
|
||||
while(true) {
|
||||
@@ -108,8 +102,6 @@ class NetworkState(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
connectivityManager.requestNetwork(networkRequest, networkCallback)
|
||||
}
|
||||
isListenerBound = true
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package org.amnezia.vpn.protocol.wireguard
|
||||
|
||||
import android.net.VpnService.Builder
|
||||
import java.io.IOException
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.launch
|
||||
import org.amnezia.awg.GoBackend
|
||||
import org.amnezia.vpn.protocol.Protocol
|
||||
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
|
||||
@@ -27,6 +28,8 @@ open class Wireguard : Protocol() {
|
||||
|
||||
private var tunnelHandle: Int = -1
|
||||
protected open val ifName: String = "amn0"
|
||||
private lateinit var scope: CoroutineScope
|
||||
private var statusJob: Job? = null
|
||||
|
||||
override val statistics: Statistics
|
||||
get() {
|
||||
@@ -49,46 +52,17 @@ open class Wireguard : Protocol() {
|
||||
|
||||
override fun internalInit() {
|
||||
if (!isInitialized) loadSharedLibrary(context, "wg-go")
|
||||
if (this::scope.isInitialized) {
|
||||
scope.cancel()
|
||||
}
|
||||
scope = CoroutineScope(Dispatchers.IO)
|
||||
}
|
||||
|
||||
override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) {
|
||||
val wireguardConfig = parseConfig(config)
|
||||
val startTime = System.currentTimeMillis()
|
||||
start(wireguardConfig, vpnBuilder, protect)
|
||||
waitForConnection(startTime)
|
||||
state.value = CONNECTED
|
||||
}
|
||||
|
||||
private suspend fun waitForConnection(startTime: Long) {
|
||||
Log.d(TAG, "Waiting for connection")
|
||||
withContext(Dispatchers.IO) {
|
||||
val time = String.format(Locale.ROOT,"%.3f", startTime / 1000.0)
|
||||
try {
|
||||
delay(1000)
|
||||
var log = getLogcat(time)
|
||||
Log.d(TAG, "First waiting log: $log")
|
||||
// check that there is a connection log,
|
||||
// to avoid infinite connection
|
||||
if (!log.contains("Attaching to interface")) {
|
||||
Log.w(TAG, "Logs do not contain a connection log")
|
||||
return@withContext
|
||||
}
|
||||
while (!log.contains("Received handshake response")) {
|
||||
delay(1000)
|
||||
log = getLogcat(time)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to get logcat: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLogcat(time: String): String =
|
||||
ProcessBuilder("logcat", "--buffer=main", "--format=raw", "*:S AmneziaWG/awg0", "-t", time)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
.inputStream.reader().readText()
|
||||
|
||||
protected open fun parseConfig(config: JSONObject): WireguardConfig {
|
||||
val configData = config.getJSONObject("wireguard_config_data")
|
||||
return WireguardConfig.build {
|
||||
@@ -178,6 +152,43 @@ open class Wireguard : Protocol() {
|
||||
tunnelHandle = -1
|
||||
throw VpnStartException("Protect VPN interface: permission not granted or revoked")
|
||||
}
|
||||
launchStatusJob()
|
||||
}
|
||||
|
||||
private fun launchStatusJob() {
|
||||
Log.d(TAG, "Launch status job")
|
||||
statusJob = scope.launch {
|
||||
while (true) {
|
||||
val lastHandshake = getLastHandshake()
|
||||
Log.v(TAG, "lastHandshake=$lastHandshake")
|
||||
if (lastHandshake == 0L) {
|
||||
delay(1000)
|
||||
continue
|
||||
}
|
||||
if (lastHandshake == -2L || lastHandshake > 0L) state.value = CONNECTED
|
||||
else if (lastHandshake == -1L) state.value = DISCONNECTED
|
||||
statusJob = null
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLastHandshake(): Long {
|
||||
if (tunnelHandle == -1) {
|
||||
Log.e(TAG, "Trying to get config of a non-existent tunnel")
|
||||
return -1
|
||||
}
|
||||
val config = GoBackend.awgGetConfig(tunnelHandle)
|
||||
if (config == null) {
|
||||
Log.e(TAG, "Failed to get tunnel config")
|
||||
return -2
|
||||
}
|
||||
val lastHandshake = config.lines().find { it.startsWith("last_handshake_time_sec=") }?.substring(24)?.toLong()
|
||||
if (lastHandshake == null) {
|
||||
Log.e(TAG, "Failed to get last_handshake_time_sec")
|
||||
return -2
|
||||
}
|
||||
return lastHandshake
|
||||
}
|
||||
|
||||
override fun stopVpn() {
|
||||
@@ -185,6 +196,8 @@ open class Wireguard : Protocol() {
|
||||
Log.w(TAG, "Tunnel already down")
|
||||
return
|
||||
}
|
||||
statusJob?.cancel()
|
||||
statusJob = null
|
||||
val handleToClose = tunnelHandle
|
||||
tunnelHandle = -1
|
||||
GoBackend.awgTurnOff(handleToClose)
|
||||
|
||||
@@ -130,8 +130,8 @@ class Xray : Protocol() {
|
||||
LibXray.initXray(assetsPath)
|
||||
val geoDir = File(assetsPath, "geo").absolutePath
|
||||
val configPath = File(context.cacheDir, "config.json")
|
||||
Log.d(TAG, "xray.location.asset: $geoDir")
|
||||
Log.d(TAG, "config: $configPath")
|
||||
Log.v(TAG, "xray.location.asset: $geoDir")
|
||||
Log.v(TAG, "config: $configPath")
|
||||
try {
|
||||
configPath.writeText(configJson)
|
||||
} catch (e: IOException) {
|
||||
|
||||
@@ -2,10 +2,6 @@ set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
|
||||
|
||||
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/Modules;${CMAKE_MODULE_PATH}")
|
||||
|
||||
if(NOT IOS AND NOT ANDROID)
|
||||
include(${CLIENT_ROOT_DIR}/3rd/SingleApplication/singleapplication.cmake)
|
||||
endif()
|
||||
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/SortFilterProxyModel)
|
||||
set(LIBS ${LIBS} SortFilterProxyModel)
|
||||
include(${CLIENT_ROOT_DIR}/cmake/QSimpleCrypto.cmake)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
message("Client android ${CMAKE_ANDROID_ARCH_ABI} build")
|
||||
|
||||
set(APP_ANDROID_MIN_SDK 24)
|
||||
set(APP_ANDROID_MIN_SDK 26)
|
||||
set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING
|
||||
"The minimum API level supported by the application or library" FORCE)
|
||||
|
||||
|
||||
@@ -3,38 +3,169 @@
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include "logger.h"
|
||||
|
||||
#include "containers/containers_defs.h"
|
||||
#include "core/controllers/serverController.h"
|
||||
#include "core/scripts_registry.h"
|
||||
|
||||
namespace {
|
||||
Logger logger("XrayConfigurator");
|
||||
}
|
||||
|
||||
XrayConfigurator::XrayConfigurator(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController, QObject *parent)
|
||||
: ConfiguratorBase(settings, serverController, parent)
|
||||
{
|
||||
}
|
||||
|
||||
QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
|
||||
ErrorCode &errorCode)
|
||||
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const QJsonObject &containerConfig, ErrorCode &errorCode)
|
||||
{
|
||||
QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container),
|
||||
m_serverController->genVarsForScript(credentials, container, containerConfig));
|
||||
|
||||
QString xrayPublicKey =
|
||||
m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode);
|
||||
xrayPublicKey.replace("\n", "");
|
||||
|
||||
QString xrayUuid = m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, errorCode);
|
||||
xrayUuid.replace("\n", "");
|
||||
|
||||
QString xrayShortId =
|
||||
m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode);
|
||||
xrayShortId.replace("\n", "");
|
||||
|
||||
// Generate new UUID for client
|
||||
QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
|
||||
// Get current server config
|
||||
QString currentConfig = m_serverController->getTextFileFromContainer(
|
||||
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
|
||||
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to get server config file";
|
||||
return "";
|
||||
}
|
||||
|
||||
config.replace("$XRAY_CLIENT_ID", xrayUuid);
|
||||
// Parse current config as JSON
|
||||
QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8());
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
logger.error() << "Failed to parse server config JSON";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
QJsonObject serverConfig = doc.object();
|
||||
|
||||
// Validate server config structure
|
||||
if (!serverConfig.contains("inbounds")) {
|
||||
logger.error() << "Server config missing 'inbounds' field";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
QJsonArray inbounds = serverConfig["inbounds"].toArray();
|
||||
if (inbounds.isEmpty()) {
|
||||
logger.error() << "Server config has empty 'inbounds' array";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
QJsonObject inbound = inbounds[0].toObject();
|
||||
if (!inbound.contains("settings")) {
|
||||
logger.error() << "Inbound missing 'settings' field";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
QJsonObject settings = inbound["settings"].toObject();
|
||||
if (!settings.contains("clients")) {
|
||||
logger.error() << "Settings missing 'clients' field";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
QJsonArray clients = settings["clients"].toArray();
|
||||
|
||||
// Create configuration for new client
|
||||
QJsonObject clientConfig {
|
||||
{"id", clientId},
|
||||
{"flow", "xtls-rprx-vision"}
|
||||
};
|
||||
|
||||
clients.append(clientConfig);
|
||||
|
||||
// Update config
|
||||
settings["clients"] = clients;
|
||||
inbound["settings"] = settings;
|
||||
inbounds[0] = inbound;
|
||||
serverConfig["inbounds"] = inbounds;
|
||||
|
||||
// Save updated config to server
|
||||
QString updatedConfig = QJsonDocument(serverConfig).toJson();
|
||||
errorCode = m_serverController->uploadTextFileToContainer(
|
||||
container,
|
||||
credentials,
|
||||
updatedConfig,
|
||||
amnezia::protocols::xray::serverConfigPath,
|
||||
libssh::ScpOverwriteMode::ScpOverwriteExisting
|
||||
);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to upload updated config";
|
||||
return "";
|
||||
}
|
||||
|
||||
// Restart container
|
||||
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
|
||||
errorCode = m_serverController->runScript(
|
||||
credentials,
|
||||
m_serverController->replaceVars(restartScript, m_serverController->genVarsForScript(credentials, container))
|
||||
);
|
||||
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to restart container";
|
||||
return "";
|
||||
}
|
||||
|
||||
return clientId;
|
||||
}
|
||||
|
||||
QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||
const QJsonObject &containerConfig, ErrorCode &errorCode)
|
||||
{
|
||||
// Get client ID from prepareServerConfig
|
||||
QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) {
|
||||
logger.error() << "Failed to prepare server config";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container),
|
||||
m_serverController->genVarsForScript(credentials, container, containerConfig));
|
||||
|
||||
if (config.isEmpty()) {
|
||||
logger.error() << "Failed to get config template";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
QString xrayPublicKey =
|
||||
m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) {
|
||||
logger.error() << "Failed to get public key";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
xrayPublicKey.replace("\n", "");
|
||||
|
||||
QString xrayShortId =
|
||||
m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode);
|
||||
if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) {
|
||||
logger.error() << "Failed to get short ID";
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
xrayShortId.replace("\n", "");
|
||||
|
||||
// Validate all required variables are present
|
||||
if (!config.contains("$XRAY_CLIENT_ID") || !config.contains("$XRAY_PUBLIC_KEY") || !config.contains("$XRAY_SHORT_ID")) {
|
||||
logger.error() << "Config template missing required variables:"
|
||||
<< "XRAY_CLIENT_ID:" << !config.contains("$XRAY_CLIENT_ID")
|
||||
<< "XRAY_PUBLIC_KEY:" << !config.contains("$XRAY_PUBLIC_KEY")
|
||||
<< "XRAY_SHORT_ID:" << !config.contains("$XRAY_SHORT_ID");
|
||||
errorCode = ErrorCode::InternalError;
|
||||
return "";
|
||||
}
|
||||
|
||||
config.replace("$XRAY_CLIENT_ID", xrayClientId);
|
||||
config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey);
|
||||
config.replace("$XRAY_SHORT_ID", xrayShortId);
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ public:
|
||||
|
||||
QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
|
||||
ErrorCode &errorCode);
|
||||
|
||||
private:
|
||||
QString prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
|
||||
ErrorCode &errorCode);
|
||||
};
|
||||
|
||||
#endif // XRAY_CONFIGURATOR_H
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "apiController.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
@@ -11,6 +14,7 @@
|
||||
#include "amnezia_application.h"
|
||||
#include "configurators/wireguard_configurator.h"
|
||||
#include "core/enums/apiEnums.h"
|
||||
#include "utilities.h"
|
||||
#include "version.h"
|
||||
|
||||
namespace
|
||||
@@ -33,6 +37,7 @@ namespace
|
||||
constexpr char userCountryCode[] = "user_country_code";
|
||||
constexpr char serverCountryCode[] = "server_country_code";
|
||||
constexpr char serviceType[] = "service_type";
|
||||
constexpr char serviceInfo[] = "service_info";
|
||||
|
||||
constexpr char aesKey[] = "aes_key";
|
||||
constexpr char aesIv[] = "aes_iv";
|
||||
@@ -40,9 +45,12 @@ namespace
|
||||
|
||||
constexpr char apiPayload[] = "api_payload";
|
||||
constexpr char keyPayload[] = "key_payload";
|
||||
|
||||
constexpr char apiConfig[] = "api_config";
|
||||
constexpr char authData[] = "auth_data";
|
||||
}
|
||||
|
||||
const QStringList proxyStorageUrl = { "" };
|
||||
const int requestTimeoutMsecs = 12 * 1000; // 12 secs
|
||||
|
||||
ErrorCode checkErrors(const QList<QSslError> &sslErrors, QNetworkReply *reply)
|
||||
{
|
||||
@@ -63,6 +71,28 @@ namespace
|
||||
return ErrorCode::ApiConfigDownloadError;
|
||||
}
|
||||
}
|
||||
|
||||
bool shouldBypassProxy(QNetworkReply *reply, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key = "",
|
||||
const QByteArray &iv = "", const QByteArray &salt = "")
|
||||
{
|
||||
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError
|
||||
|| reply->error() == QNetworkReply::NetworkError::TimeoutError) {
|
||||
qDebug() << "Timeout occurred";
|
||||
return true;
|
||||
} else if (responseBody.contains("html")) {
|
||||
qDebug() << "The response contains an html tag";
|
||||
return true;
|
||||
} else if (checkEncryption) {
|
||||
try {
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
static_cast<void>(blockCipher.decryptAesBlockCipher(responseBody, key, iv, "", salt));
|
||||
} catch (...) {
|
||||
qDebug() << "Failed to decrypt the data";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ApiController::ApiController(const QString &gatewayEndpoint, bool isDevEnvironment, QObject *parent)
|
||||
@@ -94,8 +124,8 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle
|
||||
configStr.replace("$OPENVPN_PRIV_KEY", apiPayloadData.certRequest.privKey);
|
||||
} else if (protocol == configKey::awg) {
|
||||
configStr.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", apiPayloadData.wireGuardClientPrivKey);
|
||||
auto serverConfig = QJsonDocument::fromJson(configStr.toUtf8()).object();
|
||||
auto containers = serverConfig.value(config_key::containers).toArray();
|
||||
auto newServerConfig = QJsonDocument::fromJson(configStr.toUtf8()).object();
|
||||
auto containers = newServerConfig.value(config_key::containers).toArray();
|
||||
if (containers.isEmpty()) {
|
||||
return; // todo process error
|
||||
}
|
||||
@@ -114,38 +144,57 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle
|
||||
containerConfig[config_key::transportPacketMagicHeader] = protocolConfig.value(config_key::transportPacketMagicHeader);
|
||||
container[containerName] = containerConfig;
|
||||
containers.replace(0, container);
|
||||
serverConfig[config_key::containers] = containers;
|
||||
configStr = QString(QJsonDocument(serverConfig).toJson());
|
||||
newServerConfig[config_key::containers] = containers;
|
||||
configStr = QString(QJsonDocument(newServerConfig).toJson());
|
||||
}
|
||||
|
||||
QJsonObject apiConfig = QJsonDocument::fromJson(configStr.toUtf8()).object();
|
||||
serverConfig[config_key::dns1] = apiConfig.value(config_key::dns1);
|
||||
serverConfig[config_key::dns2] = apiConfig.value(config_key::dns2);
|
||||
serverConfig[config_key::containers] = apiConfig.value(config_key::containers);
|
||||
serverConfig[config_key::hostName] = apiConfig.value(config_key::hostName);
|
||||
QJsonObject newServerConfig = QJsonDocument::fromJson(configStr.toUtf8()).object();
|
||||
serverConfig[config_key::dns1] = newServerConfig.value(config_key::dns1);
|
||||
serverConfig[config_key::dns2] = newServerConfig.value(config_key::dns2);
|
||||
serverConfig[config_key::containers] = newServerConfig.value(config_key::containers);
|
||||
serverConfig[config_key::hostName] = newServerConfig.value(config_key::hostName);
|
||||
|
||||
if (apiConfig.value(config_key::configVersion).toInt() == ApiConfigSources::AmneziaGateway) {
|
||||
serverConfig[config_key::configVersion] = apiConfig.value(config_key::configVersion);
|
||||
serverConfig[config_key::description] = apiConfig.value(config_key::description);
|
||||
serverConfig[config_key::name] = apiConfig.value(config_key::name);
|
||||
if (newServerConfig.value(config_key::configVersion).toInt() == ApiConfigSources::AmneziaGateway) {
|
||||
serverConfig[config_key::configVersion] = newServerConfig.value(config_key::configVersion);
|
||||
serverConfig[config_key::description] = newServerConfig.value(config_key::description);
|
||||
serverConfig[config_key::name] = newServerConfig.value(config_key::name);
|
||||
}
|
||||
|
||||
auto defaultContainer = apiConfig.value(config_key::defaultContainer).toString();
|
||||
auto defaultContainer = newServerConfig.value(config_key::defaultContainer).toString();
|
||||
serverConfig[config_key::defaultContainer] = defaultContainer;
|
||||
|
||||
QVariantMap map = serverConfig.value(configKey::apiConfig).toObject().toVariantMap();
|
||||
map.insert(newServerConfig.value(configKey::apiConfig).toObject().toVariantMap());
|
||||
auto apiConfig = QJsonObject::fromVariantMap(map);
|
||||
|
||||
if (newServerConfig.value(config_key::configVersion).toInt() == ApiConfigSources::AmneziaGateway) {
|
||||
apiConfig.insert(configKey::serviceInfo, QJsonDocument::fromJson(apiResponseBody).object().value(configKey::serviceInfo).toObject());
|
||||
}
|
||||
|
||||
serverConfig[configKey::apiConfig] = apiConfig;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList ApiController::getProxyUrls()
|
||||
{
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(7000);
|
||||
request.setTransferTimeout(requestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QEventLoop wait;
|
||||
QList<QSslError> sslErrors;
|
||||
QNetworkReply *reply;
|
||||
|
||||
QStringList proxyStorageUrl;
|
||||
if (m_isDevEnvironment) {
|
||||
proxyStorageUrl = QStringList { DEV_S3_ENDPOINT };
|
||||
} else {
|
||||
proxyStorageUrl = QStringList { PROD_S3_ENDPOINT };
|
||||
}
|
||||
|
||||
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
|
||||
for (const auto &proxyStorageUrl : proxyStorageUrl) {
|
||||
request.setUrl(proxyStorageUrl);
|
||||
reply = amnApp->manager()->get(request);
|
||||
@@ -166,11 +215,23 @@ QStringList ApiController::getProxyUrls()
|
||||
EVP_PKEY *privateKey = nullptr;
|
||||
QByteArray responseBody;
|
||||
try {
|
||||
QByteArray key = PROD_PROXY_STORAGE_KEY;
|
||||
QSimpleCrypto::QRsa rsa;
|
||||
privateKey = rsa.getPrivateKeyFromByteArray(key, "");
|
||||
responseBody = rsa.decrypt(encryptedResponseBody, privateKey, RSA_PKCS1_PADDING);
|
||||
if (!m_isDevEnvironment) {
|
||||
QCryptographicHash hash(QCryptographicHash::Sha512);
|
||||
hash.addData(key);
|
||||
QByteArray hashResult = hash.result().toHex();
|
||||
|
||||
QByteArray key = QByteArray::fromHex(hashResult.left(64));
|
||||
QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32));
|
||||
|
||||
QByteArray ba = QByteArray::fromBase64(encryptedResponseBody);
|
||||
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
responseBody = blockCipher.decryptAesBlockCipher(ba, key, iv);
|
||||
} else {
|
||||
responseBody = encryptedResponseBody;
|
||||
}
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
qCritical() << "error loading private key from environment variables or decrypting payload";
|
||||
return {};
|
||||
}
|
||||
@@ -221,7 +282,7 @@ void ApiController::updateServerConfigFromApi(const QString &installationUuid, c
|
||||
|
||||
if (serverConfig.value(config_key::configVersion).toInt()) {
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(7000);
|
||||
request.setTransferTimeout(requestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("Authorization", "Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8());
|
||||
QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString();
|
||||
@@ -277,7 +338,7 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody)
|
||||
#endif
|
||||
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(7000);
|
||||
request.setTransferTimeout(requestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
request.setUrl(QString("%1v1/services").arg(m_gatewayEndpoint));
|
||||
@@ -292,40 +353,53 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody)
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec();
|
||||
|
||||
if (reply->error() == QNetworkReply::NetworkError::TimeoutError || reply->error() == QNetworkReply::NetworkError::OperationCanceledError) {
|
||||
responseBody = reply->readAll();
|
||||
|
||||
if (sslErrors.isEmpty() && shouldBypassProxy(reply, responseBody, false)) {
|
||||
m_proxyUrls = getProxyUrls();
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(m_proxyUrls.begin(), m_proxyUrls.end(), generator);
|
||||
for (const QString &proxyUrl : m_proxyUrls) {
|
||||
qDebug() << "Go to the next endpoint";
|
||||
request.setUrl(QString("%1v1/services").arg(proxyUrl));
|
||||
reply->deleteLater(); // delete the previous reply
|
||||
reply = amnApp->manager()->get(request);
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec();
|
||||
if (reply->error() != QNetworkReply::NetworkError::TimeoutError
|
||||
&& reply->error() != QNetworkReply::NetworkError::OperationCanceledError) {
|
||||
|
||||
responseBody = reply->readAll();
|
||||
if (!sslErrors.isEmpty() || !shouldBypassProxy(reply, responseBody, false)) {
|
||||
break;
|
||||
}
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
responseBody = reply->readAll();
|
||||
auto errorCode = checkErrors(sslErrors, reply);
|
||||
reply->deleteLater();
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
if (!responseBody.contains("services")) {
|
||||
return ErrorCode::ApiServicesMissingError;
|
||||
}
|
||||
}
|
||||
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
ErrorCode ApiController::getConfigForService(const QString &installationUuid, const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &protocol, const QString &serverCountryCode, QJsonObject &serverConfig)
|
||||
const QString &protocol, const QString &serverCountryCode, const QJsonObject &authData,
|
||||
QJsonObject &serverConfig)
|
||||
{
|
||||
#ifdef Q_OS_IOS
|
||||
IosController::Instance()->requestInetAccess();
|
||||
QThread::msleep(10);
|
||||
#endif
|
||||
|
||||
QNetworkAccessManager manager;
|
||||
QNetworkRequest request;
|
||||
request.setTransferTimeout(7000);
|
||||
request.setTransferTimeout(requestTimeoutMsecs);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
request.setUrl(QString("%1v1/config").arg(m_gatewayEndpoint));
|
||||
@@ -339,6 +413,9 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co
|
||||
}
|
||||
apiPayload[configKey::serviceType] = serviceType;
|
||||
apiPayload[configKey::uuid] = installationUuid;
|
||||
if (!authData.isEmpty()) {
|
||||
apiPayload[configKey::authData] = authData;
|
||||
}
|
||||
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
QByteArray key = blockCipher.generatePrivateSalt(32);
|
||||
@@ -361,6 +438,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co
|
||||
QSimpleCrypto::QRsa rsa;
|
||||
publicKey = rsa.getPublicKeyFromByteArray(rsaKey);
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
qCritical() << "error loading public key from environment variables";
|
||||
return ErrorCode::ApiMissingAgwPublicKey;
|
||||
}
|
||||
@@ -370,14 +448,16 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co
|
||||
|
||||
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), key, iv, "", salt);
|
||||
} catch (...) { // todo change error handling in QSimpleCrypto?
|
||||
Utils::logException();
|
||||
qCritical() << "error when encrypting the request body";
|
||||
return ErrorCode::ApiConfigDecryptionError;
|
||||
}
|
||||
|
||||
QJsonObject requestBody;
|
||||
requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64());
|
||||
requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64());
|
||||
|
||||
QNetworkReply *reply = manager.post(request, QJsonDocument(requestBody).toJson());
|
||||
QNetworkReply *reply = amnApp->manager()->post(request, QJsonDocument(requestBody).toJson());
|
||||
|
||||
QEventLoop wait;
|
||||
connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
@@ -386,37 +466,43 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec();
|
||||
|
||||
if (reply->error() == QNetworkReply::NetworkError::TimeoutError || reply->error() == QNetworkReply::NetworkError::OperationCanceledError) {
|
||||
if (m_proxyUrls.isEmpty()) {
|
||||
m_proxyUrls = getProxyUrls();
|
||||
}
|
||||
auto encryptedResponseBody = reply->readAll();
|
||||
|
||||
if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) {
|
||||
m_proxyUrls = getProxyUrls();
|
||||
std::random_device randomDevice;
|
||||
std::mt19937 generator(randomDevice());
|
||||
std::shuffle(m_proxyUrls.begin(), m_proxyUrls.end(), generator);
|
||||
for (const QString &proxyUrl : m_proxyUrls) {
|
||||
qDebug() << "Go to the next endpoint";
|
||||
request.setUrl(QString("%1v1/config").arg(proxyUrl));
|
||||
reply = manager.post(request, QJsonDocument(requestBody).toJson());
|
||||
reply->deleteLater(); // delete the previous reply
|
||||
reply = amnApp->manager()->post(request, QJsonDocument(requestBody).toJson());
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList<QSslError> &errors) { sslErrors = errors; });
|
||||
wait.exec();
|
||||
if (reply->error() != QNetworkReply::NetworkError::TimeoutError
|
||||
&& reply->error() != QNetworkReply::NetworkError::OperationCanceledError) {
|
||||
|
||||
encryptedResponseBody = reply->readAll();
|
||||
if (!sslErrors.isEmpty() || !shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) {
|
||||
break;
|
||||
}
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
auto errorCode = checkErrors(sslErrors, reply);
|
||||
reply->deleteLater();
|
||||
if (errorCode) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
auto encryptedResponseBody = reply->readAll();
|
||||
reply->deleteLater();
|
||||
try {
|
||||
auto responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
|
||||
fillServerConfig(protocol, apiPayloadData, responseBody, serverConfig);
|
||||
} catch (...) { // todo change error handling in QSimpleCrypto?
|
||||
Utils::logException();
|
||||
qCritical() << "error when decrypting the request body";
|
||||
return ErrorCode::ApiConfigDecryptionError;
|
||||
}
|
||||
|
||||
return errorCode;
|
||||
|
||||
@@ -21,7 +21,7 @@ public slots:
|
||||
|
||||
ErrorCode getServicesList(QByteArray &responseBody);
|
||||
ErrorCode getConfigForService(const QString &installationUuid, const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &protocol, const QString &serverCountryCode, QJsonObject &serverConfig);
|
||||
const QString &protocol, const QString &serverCountryCode, const QJsonObject &authData, QJsonObject &serverConfig);
|
||||
|
||||
signals:
|
||||
void errorOccurred(ErrorCode errorCode);
|
||||
|
||||
@@ -96,6 +96,7 @@ namespace amnezia
|
||||
|
||||
// import and install errors
|
||||
ImportInvalidConfigError = 900,
|
||||
ImportOpenConfigError = 901,
|
||||
|
||||
// Android errors
|
||||
AndroidError = 1000,
|
||||
@@ -107,6 +108,8 @@ namespace amnezia
|
||||
ApiConfigTimeoutError = 1103,
|
||||
ApiConfigSslError = 1104,
|
||||
ApiMissingAgwPublicKey = 1105,
|
||||
ApiConfigDecryptionError = 1106,
|
||||
ApiServicesMissingError = 1107,
|
||||
|
||||
// QFile errors
|
||||
OpenError = 1200,
|
||||
|
||||
@@ -50,6 +50,7 @@ QString errorString(ErrorCode code) {
|
||||
case (ErrorCode::AddressPoolError): errorMessage = QObject::tr("VPN pool error: no available addresses"); break;
|
||||
|
||||
case (ErrorCode::ImportInvalidConfigError): errorMessage = QObject::tr("The config does not contain any containers and credentials for connecting to the server"); break;
|
||||
case (ErrorCode::ImportOpenConfigError): errorMessage = QObject::tr("Unable to open config file"); break;
|
||||
|
||||
// Android errors
|
||||
case (ErrorCode::AndroidError): errorMessage = QObject::tr("VPN connection error"); break;
|
||||
@@ -61,7 +62,9 @@ QString errorString(ErrorCode code) {
|
||||
case (ErrorCode::ApiConfigSslError): errorMessage = QObject::tr("SSL error occurred"); break;
|
||||
case (ErrorCode::ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break;
|
||||
case (ErrorCode::ApiMissingAgwPublicKey): errorMessage = QObject::tr("Missing AGW public key"); break;
|
||||
|
||||
case (ErrorCode::ApiConfigDecryptionError): errorMessage = QObject::tr("Failed to decrypt response payload"); break;
|
||||
case (ErrorCode::ApiServicesMissingError): errorMessage = QObject::tr("Missing list of available services"); break;
|
||||
|
||||
// QFile errors
|
||||
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
||||
case(ErrorCode::ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break;
|
||||
|
||||
@@ -78,7 +78,7 @@ bool Daemon::activate(const InterfaceConfig& config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (supportDnsUtils() && !dnsutils()->restoreResolvers()) {
|
||||
if (!dnsutils()->restoreResolvers()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -165,10 +165,6 @@ bool Daemon::activate(const InterfaceConfig& config) {
|
||||
}
|
||||
|
||||
bool Daemon::maybeUpdateResolvers(const InterfaceConfig& config) {
|
||||
if (!supportDnsUtils()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((config.m_hopType == InterfaceConfig::MultiHopExit) ||
|
||||
(config.m_hopType == InterfaceConfig::SingleHop)) {
|
||||
QList<QHostAddress> resolvers;
|
||||
@@ -423,13 +419,8 @@ bool Daemon::deactivate(bool emitSignals) {
|
||||
}
|
||||
|
||||
// Cleanup DNS
|
||||
if (supportDnsUtils() && !dnsutils()->restoreResolvers()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!wgutils()->interfaceExists()) {
|
||||
logger.warning() << "Wireguard interface does not exist.";
|
||||
return false;
|
||||
if (!dnsutils()->restoreResolvers()) {
|
||||
logger.warning() << "Failed to restore DNS resolvers.";
|
||||
}
|
||||
|
||||
// Cleanup peers and routing
|
||||
@@ -449,13 +440,9 @@ bool Daemon::deactivate(bool emitSignals) {
|
||||
}
|
||||
m_excludedAddrSet.clear();
|
||||
|
||||
// Delete the interface
|
||||
if (!wgutils()->deleteInterface()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_connections.clear();
|
||||
return true;
|
||||
// Delete the interface
|
||||
return wgutils()->deleteInterface();
|
||||
}
|
||||
|
||||
QString Daemon::logs() {
|
||||
|
||||
@@ -69,7 +69,6 @@ class Daemon : public QObject {
|
||||
virtual WireguardUtils* wgutils() const = 0;
|
||||
virtual bool supportIPUtils() const { return false; }
|
||||
virtual IPUtils* iputils() { return nullptr; }
|
||||
virtual bool supportDnsUtils() const { return false; }
|
||||
virtual DnsUtils* dnsutils() { return nullptr; }
|
||||
|
||||
static bool parseStringList(const QJsonObject& obj, const QString& name,
|
||||
|
||||
@@ -92,6 +92,17 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) {
|
||||
|
||||
logger.debug() << "Command received:" << type;
|
||||
|
||||
// It is expected that sometimes the client will request backend logs
|
||||
// before the first authentication. In these cases we just return empty
|
||||
// logs.
|
||||
if (type == "logs") {
|
||||
QJsonObject obj;
|
||||
obj.insert("type", "logs");
|
||||
obj.insert("logs", "");
|
||||
write(obj);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == "activate") {
|
||||
InterfaceConfig config;
|
||||
if (!Daemon::parseConfig(obj, config)) {
|
||||
@@ -115,8 +126,7 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) {
|
||||
if (type == "status") {
|
||||
QJsonObject obj = Daemon::instance()->getStatus();
|
||||
obj.insert("type", "status");
|
||||
m_socket->write(QJsonDocument(obj).toJson(QJsonDocument::Compact));
|
||||
m_socket->write("\n");
|
||||
write(obj);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -124,8 +134,7 @@ void DaemonLocalServerConnection::parseCommand(const QByteArray& data) {
|
||||
QJsonObject obj;
|
||||
obj.insert("type", "logs");
|
||||
obj.insert("logs", Daemon::instance()->logs().replace("\n", "|"));
|
||||
m_socket->write(QJsonDocument(obj).toJson(QJsonDocument::Compact));
|
||||
m_socket->write("\n");
|
||||
write(obj);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,13 +15,24 @@
|
||||
#include "platforms/ios/QtAppDelegate-C-Interface.h"
|
||||
#endif
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
bool isAnotherInstanceRunning()
|
||||
{
|
||||
QLocalSocket socket;
|
||||
socket.connectToServer("AmneziaVPNInstance");
|
||||
if (socket.waitForConnected(500)) {
|
||||
qWarning() << "AmneziaVPN is already running";
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
Migrations migrationsManager;
|
||||
migrationsManager.doMigrations();
|
||||
|
||||
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true);
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
AllowSetForegroundWindow(ASFW_ANY);
|
||||
#endif
|
||||
@@ -32,16 +43,14 @@ int main(int argc, char *argv[])
|
||||
qputenv("ANDROID_OPENSSL_SUFFIX", "_3");
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
AmneziaApplication app(argc, argv);
|
||||
#else
|
||||
AmneziaApplication app(argc, argv, true,
|
||||
SingleApplication::Mode::User | SingleApplication::Mode::SecondaryNotification);
|
||||
|
||||
if (!app.isPrimary()) {
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
if (isAnotherInstanceRunning()) {
|
||||
QTimer::singleShot(1000, &app, [&]() { app.quit(); });
|
||||
return app.exec();
|
||||
}
|
||||
app.startLocalServer();
|
||||
#endif
|
||||
|
||||
// Allow to raise app window if secondary instance launched
|
||||
|
||||
@@ -34,8 +34,8 @@ LocalSocketController::LocalSocketController() {
|
||||
m_socket = new QLocalSocket(this);
|
||||
connect(m_socket, &QLocalSocket::connected, this,
|
||||
&LocalSocketController::daemonConnected);
|
||||
connect(m_socket, &QLocalSocket::disconnected, this,
|
||||
&LocalSocketController::disconnected);
|
||||
connect(m_socket, &QLocalSocket::disconnected, this,
|
||||
[&] { errorOccurred(QLocalSocket::PeerClosedError); });
|
||||
connect(m_socket, &QLocalSocket::errorOccurred, this,
|
||||
&LocalSocketController::errorOccurred);
|
||||
connect(m_socket, &QLocalSocket::readyRead, this,
|
||||
|
||||
@@ -163,9 +163,7 @@ QString AndroidController::openFile(const QString &filter)
|
||||
QString fileName;
|
||||
connect(this, &AndroidController::fileOpened, this,
|
||||
[&fileName, &wait](const QString &uri) {
|
||||
qDebug() << "Android event: file opened; uri:" << uri;
|
||||
fileName = QQmlFile::urlToLocalFileOrQrc(uri);
|
||||
qDebug() << "Android opened filename:" << fileName;
|
||||
fileName = uri;
|
||||
wait.quit();
|
||||
},
|
||||
static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::SingleShotConnection));
|
||||
@@ -175,6 +173,25 @@ QString AndroidController::openFile(const QString &filter)
|
||||
return fileName;
|
||||
}
|
||||
|
||||
int AndroidController::getFd(const QString &fileName)
|
||||
{
|
||||
return callActivityMethod<jint>("getFd", "(Ljava/lang/String;)I",
|
||||
QJniObject::fromString(fileName).object<jstring>());
|
||||
}
|
||||
|
||||
void AndroidController::closeFd()
|
||||
{
|
||||
callActivityMethod("closeFd", "()V");
|
||||
}
|
||||
|
||||
QString AndroidController::getFileName(const QString &uri)
|
||||
{
|
||||
auto fileName = callActivityMethod<jstring, jstring>("getFileName", "(Ljava/lang/String;)Ljava/lang/String;",
|
||||
QJniObject::fromString(uri).object<jstring>());
|
||||
QJniEnvironment env;
|
||||
return AndroidUtils::convertJString(env.jniEnv(), fileName.object<jstring>());
|
||||
}
|
||||
|
||||
bool AndroidController::isCameraPresent()
|
||||
{
|
||||
return callActivityMethod<jboolean>("isCameraPresent", "()Z");
|
||||
@@ -287,6 +304,11 @@ bool AndroidController::requestAuthentication()
|
||||
return result;
|
||||
}
|
||||
|
||||
void AndroidController::sendTouch(float x, float y)
|
||||
{
|
||||
callActivityMethod("sendTouch", "(FF)V", x, y);
|
||||
}
|
||||
|
||||
// Moving log processing to the Android side
|
||||
jclass AndroidController::log;
|
||||
jmethodID AndroidController::logDebug;
|
||||
|
||||
@@ -34,6 +34,9 @@ public:
|
||||
void resetLastServer(int serverIndex);
|
||||
void saveFile(const QString &fileName, const QString &data);
|
||||
QString openFile(const QString &filter);
|
||||
int getFd(const QString &fileName);
|
||||
void closeFd();
|
||||
QString getFileName(const QString &uri);
|
||||
bool isCameraPresent();
|
||||
bool isOnTv();
|
||||
void startQrReaderActivity();
|
||||
@@ -48,6 +51,7 @@ public:
|
||||
bool isNotificationPermissionGranted();
|
||||
void requestNotificationPermission();
|
||||
bool requestAuthentication();
|
||||
void sendTouch(float x, float y);
|
||||
|
||||
static bool initLogging();
|
||||
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
|
||||
|
||||
@@ -22,7 +22,6 @@ class LinuxDaemon final : public Daemon {
|
||||
|
||||
protected:
|
||||
WireguardUtils* wgutils() const override { return m_wgutils; }
|
||||
bool supportDnsUtils() const override { return true; }
|
||||
DnsUtils* dnsutils() override { return m_dnsutils; }
|
||||
bool supportIPUtils() const override { return true; }
|
||||
IPUtils* iputils() override { return m_iputils; }
|
||||
|
||||
@@ -21,7 +21,6 @@ class MacOSDaemon final : public Daemon {
|
||||
|
||||
protected:
|
||||
WireguardUtils* wgutils() const override { return m_wgutils; }
|
||||
bool supportDnsUtils() const override { return true; }
|
||||
DnsUtils* dnsutils() override { return m_dnsutils; }
|
||||
bool supportIPUtils() const override { return true; }
|
||||
IPUtils* iputils() override { return m_iputils; }
|
||||
|
||||
@@ -26,7 +26,6 @@ class WindowsDaemon final : public Daemon {
|
||||
protected:
|
||||
bool run(Op op, const InterfaceConfig& config) override;
|
||||
WireguardUtils* wgutils() const override { return m_wgutils; }
|
||||
bool supportDnsUtils() const override { return true; }
|
||||
DnsUtils* dnsutils() override { return m_dnsutils; }
|
||||
|
||||
private:
|
||||
|
||||
@@ -502,7 +502,7 @@ QString WindowsSplitTunnel::convertPath(const QString& path) {
|
||||
// device should contain : for e.g C:
|
||||
return "";
|
||||
}
|
||||
QByteArray buffer(2048, 0xFF);
|
||||
QByteArray buffer(2048, 0xFFu);
|
||||
auto ok = QueryDosDeviceW(qUtf16Printable(driveLetter),
|
||||
(wchar_t*)buffer.data(), buffer.size() / 2);
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ bool WireguardUtilsWindows::updateRoutePrefix(const IPAddress& prefix) {
|
||||
}
|
||||
if (result != NO_ERROR) {
|
||||
logger.error() << "Failed to create route to"
|
||||
<< logger.sensitive(prefix.toString())
|
||||
<< prefix.toString()
|
||||
<< "result:" << result;
|
||||
}
|
||||
return result == NO_ERROR;
|
||||
@@ -265,7 +265,7 @@ bool WireguardUtilsWindows::deleteRoutePrefix(const IPAddress& prefix) {
|
||||
}
|
||||
if (result != NO_ERROR) {
|
||||
logger.error() << "Failed to delete route to"
|
||||
<< logger.sensitive(prefix.toString())
|
||||
<< prefix.toString()
|
||||
<< "result:" << result;
|
||||
}
|
||||
return result == NO_ERROR;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
#include "platforms/windows/windowsutils.h"
|
||||
|
||||
constexpr const char* VPN_NAME = "AmneziaVPN";
|
||||
constexpr const char* WIREGUARD_DIR = "WireGuard";
|
||||
constexpr const char* WIREGUARD_DIR = "AmneziaWG";
|
||||
constexpr const char* DATA_DIR = "Data";
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -1,225 +1,227 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>fonts/pt-root-ui_vf.ttf</file>
|
||||
<file>images/amneziaBigLogo.png</file>
|
||||
<file>images/AmneziaVPN.png</file>
|
||||
<file>images/controls/alert-circle.svg</file>
|
||||
<file>images/controls/amnezia.svg</file>
|
||||
<file>images/controls/app.svg</file>
|
||||
<file>images/controls/archive-restore.svg</file>
|
||||
<file>images/controls/arrow-left.svg</file>
|
||||
<file>images/controls/arrow-right.svg</file>
|
||||
<file>images/controls/bug.svg</file>
|
||||
<file>images/controls/check.svg</file>
|
||||
<file>images/controls/chevron-down.svg</file>
|
||||
<file>images/controls/chevron-right.svg</file>
|
||||
<file>images/controls/chevron-up.svg</file>
|
||||
<file>images/controls/close.svg</file>
|
||||
<file>images/controls/copy.svg</file>
|
||||
<file>images/controls/delete.svg</file>
|
||||
<file>images/controls/download.svg</file>
|
||||
<file>images/controls/edit-3.svg</file>
|
||||
<file>images/controls/eye-off.svg</file>
|
||||
<file>images/controls/eye.svg</file>
|
||||
<file>images/controls/file-check-2.svg</file>
|
||||
<file>images/controls/file-cog-2.svg</file>
|
||||
<file>images/controls/folder-open.svg</file>
|
||||
<file>images/controls/folder-search-2.svg</file>
|
||||
<file>images/controls/gauge.svg</file>
|
||||
<file>images/controls/github.svg</file>
|
||||
<file>images/controls/help-circle.svg</file>
|
||||
<file>images/controls/history.svg</file>
|
||||
<file>images/controls/home.svg</file>
|
||||
<file>images/controls/info.svg</file>
|
||||
<file>images/controls/mail.svg</file>
|
||||
<file>images/controls/map-pin.svg</file>
|
||||
<file>images/controls/more-vertical.svg</file>
|
||||
<file>images/controls/plus.svg</file>
|
||||
<file>images/controls/qr-code.svg</file>
|
||||
<file>images/controls/radio-button-inner-circle-pressed.png</file>
|
||||
<file>images/controls/radio-button-inner-circle.png</file>
|
||||
<file>images/controls/radio-button-pressed.svg</file>
|
||||
<file>images/controls/radio-button.svg</file>
|
||||
<file>images/controls/radio.svg</file>
|
||||
<file>images/controls/refresh-cw.svg</file>
|
||||
<file>images/controls/save.svg</file>
|
||||
<file>images/controls/scan-line.svg</file>
|
||||
<file>images/controls/search.svg</file>
|
||||
<file>images/controls/server.svg</file>
|
||||
<file>images/controls/settings-2.svg</file>
|
||||
<file>images/controls/settings.svg</file>
|
||||
<file>images/controls/share-2.svg</file>
|
||||
<file>images/controls/split-tunneling.svg</file>
|
||||
<file>images/controls/tag.svg</file>
|
||||
<file>images/controls/telegram.svg</file>
|
||||
<file>images/controls/text-cursor.svg</file>
|
||||
<file>images/controls/trash.svg</file>
|
||||
<file>images/controls/x-circle.svg</file>
|
||||
<file>images/tray/active.png</file>
|
||||
<file>images/tray/default.png</file>
|
||||
<file>images/tray/error.png</file>
|
||||
<file>images/AmneziaVPN.png</file>
|
||||
<file>server_scripts/remove_container.sh</file>
|
||||
<file>server_scripts/setup_host_firewall.sh</file>
|
||||
<file>server_scripts/openvpn_cloak/Dockerfile</file>
|
||||
<file>server_scripts/awg/configure_container.sh</file>
|
||||
<file>server_scripts/awg/Dockerfile</file>
|
||||
<file>server_scripts/awg/run_container.sh</file>
|
||||
<file>server_scripts/awg/start.sh</file>
|
||||
<file>server_scripts/awg/template.conf</file>
|
||||
<file>server_scripts/build_container.sh</file>
|
||||
<file>server_scripts/check_connection.sh</file>
|
||||
<file>server_scripts/check_server_is_busy.sh</file>
|
||||
<file>server_scripts/check_user_in_sudo.sh</file>
|
||||
<file>server_scripts/dns/configure_container.sh</file>
|
||||
<file>server_scripts/dns/Dockerfile</file>
|
||||
<file>server_scripts/dns/run_container.sh</file>
|
||||
<file>server_scripts/install_docker.sh</file>
|
||||
<file>server_scripts/ipsec/configure_container.sh</file>
|
||||
<file>server_scripts/ipsec/Dockerfile</file>
|
||||
<file>server_scripts/ipsec/mobileconfig.plist</file>
|
||||
<file>server_scripts/ipsec/run_container.sh</file>
|
||||
<file>server_scripts/ipsec/start.sh</file>
|
||||
<file>server_scripts/ipsec/strongswan.profile</file>
|
||||
<file>server_scripts/openvpn_cloak/configure_container.sh</file>
|
||||
<file>server_scripts/openvpn_cloak/Dockerfile</file>
|
||||
<file>server_scripts/openvpn_cloak/run_container.sh</file>
|
||||
<file>server_scripts/openvpn_cloak/start.sh</file>
|
||||
<file>server_scripts/openvpn_cloak/template.ovpn</file>
|
||||
<file>server_scripts/install_docker.sh</file>
|
||||
<file>server_scripts/build_container.sh</file>
|
||||
<file>server_scripts/prepare_host.sh</file>
|
||||
<file>server_scripts/check_connection.sh</file>
|
||||
<file>server_scripts/remove_all_containers.sh</file>
|
||||
<file>server_scripts/openvpn_cloak/run_container.sh</file>
|
||||
<file>server_scripts/openvpn/configure_container.sh</file>
|
||||
<file>server_scripts/openvpn/run_container.sh</file>
|
||||
<file>server_scripts/openvpn/template.ovpn</file>
|
||||
<file>server_scripts/openvpn/Dockerfile</file>
|
||||
<file>server_scripts/openvpn/start.sh</file>
|
||||
<file>server_scripts/openvpn_shadowsocks/configure_container.sh</file>
|
||||
<file>server_scripts/openvpn_shadowsocks/Dockerfile</file>
|
||||
<file>server_scripts/openvpn_shadowsocks/run_container.sh</file>
|
||||
<file>server_scripts/openvpn_shadowsocks/start.sh</file>
|
||||
<file>server_scripts/openvpn_shadowsocks/template.ovpn</file>
|
||||
<file>server_scripts/openvpn/configure_container.sh</file>
|
||||
<file>server_scripts/openvpn/Dockerfile</file>
|
||||
<file>server_scripts/openvpn/run_container.sh</file>
|
||||
<file>server_scripts/openvpn/start.sh</file>
|
||||
<file>server_scripts/openvpn/template.ovpn</file>
|
||||
<file>server_scripts/prepare_host.sh</file>
|
||||
<file>server_scripts/remove_all_containers.sh</file>
|
||||
<file>server_scripts/remove_container.sh</file>
|
||||
<file>server_scripts/setup_host_firewall.sh</file>
|
||||
<file>server_scripts/sftp/configure_container.sh</file>
|
||||
<file>server_scripts/sftp/Dockerfile</file>
|
||||
<file>server_scripts/sftp/run_container.sh</file>
|
||||
<file>server_scripts/socks5_proxy/configure_container.sh</file>
|
||||
<file>server_scripts/socks5_proxy/Dockerfile</file>
|
||||
<file>server_scripts/socks5_proxy/run_container.sh</file>
|
||||
<file>server_scripts/socks5_proxy/start.sh</file>
|
||||
<file>server_scripts/website_tor/configure_container.sh</file>
|
||||
<file>server_scripts/website_tor/Dockerfile</file>
|
||||
<file>server_scripts/website_tor/run_container.sh</file>
|
||||
<file>server_scripts/wireguard/configure_container.sh</file>
|
||||
<file>server_scripts/wireguard/Dockerfile</file>
|
||||
<file>server_scripts/wireguard/run_container.sh</file>
|
||||
<file>server_scripts/wireguard/start.sh</file>
|
||||
<file>server_scripts/wireguard/template.conf</file>
|
||||
<file>server_scripts/website_tor/configure_container.sh</file>
|
||||
<file>server_scripts/website_tor/run_container.sh</file>
|
||||
<file>ui/qml/Config/GlobalConfig.qml</file>
|
||||
<file>ui/qml/Config/qmldir</file>
|
||||
<file>server_scripts/check_server_is_busy.sh</file>
|
||||
<file>server_scripts/dns/configure_container.sh</file>
|
||||
<file>server_scripts/dns/Dockerfile</file>
|
||||
<file>server_scripts/dns/run_container.sh</file>
|
||||
<file>server_scripts/sftp/configure_container.sh</file>
|
||||
<file>server_scripts/sftp/Dockerfile</file>
|
||||
<file>server_scripts/sftp/run_container.sh</file>
|
||||
<file>server_scripts/ipsec/configure_container.sh</file>
|
||||
<file>server_scripts/ipsec/Dockerfile</file>
|
||||
<file>server_scripts/ipsec/run_container.sh</file>
|
||||
<file>server_scripts/ipsec/start.sh</file>
|
||||
<file>server_scripts/ipsec/mobileconfig.plist</file>
|
||||
<file>server_scripts/ipsec/strongswan.profile</file>
|
||||
<file>server_scripts/website_tor/Dockerfile</file>
|
||||
<file>server_scripts/check_user_in_sudo.sh</file>
|
||||
<file>ui/qml/Controls2/BasicButtonType.qml</file>
|
||||
<file>ui/qml/Controls2/TextFieldWithHeaderType.qml</file>
|
||||
<file>ui/qml/Controls2/LabelWithButtonType.qml</file>
|
||||
<file>images/controls/arrow-right.svg</file>
|
||||
<file>images/controls/chevron-right.svg</file>
|
||||
<file>ui/qml/Controls2/ImageButtonType.qml</file>
|
||||
<file>ui/qml/Controls2/CardType.qml</file>
|
||||
<file>ui/qml/Controls2/CheckBoxType.qml</file>
|
||||
<file>images/controls/check.svg</file>
|
||||
<file>ui/qml/Controls2/DropDownType.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardStart.qml</file>
|
||||
<file>ui/qml/main2.qml</file>
|
||||
<file>images/amneziaBigLogo.png</file>
|
||||
<file>ui/qml/Controls2/FlickableType.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardCredentials.qml</file>
|
||||
<file>ui/qml/Controls2/HeaderType.qml</file>
|
||||
<file>images/controls/arrow-left.svg</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardProtocols.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardEasy.qml</file>
|
||||
<file>images/controls/chevron-down.svg</file>
|
||||
<file>images/controls/chevron-up.svg</file>
|
||||
<file>ui/qml/Controls2/TextTypes/ParagraphTextType.qml</file>
|
||||
<file>ui/qml/Controls2/TextTypes/Header2TextType.qml</file>
|
||||
<file>ui/qml/Controls2/HorizontalRadioButton.qml</file>
|
||||
<file>ui/qml/Controls2/VerticalRadioButton.qml</file>
|
||||
<file>ui/qml/Controls2/SwitcherType.qml</file>
|
||||
<file>ui/qml/Controls2/TabButtonType.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardProtocolSettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardInstalling.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardConfigSource.qml</file>
|
||||
<file>images/controls/folder-open.svg</file>
|
||||
<file>images/controls/qr-code.svg</file>
|
||||
<file>images/controls/text-cursor.svg</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardTextKey.qml</file>
|
||||
<file>ui/qml/Pages2/PageStart.qml</file>
|
||||
<file>ui/qml/Controls2/TabImageButtonType.qml</file>
|
||||
<file>images/controls/home.svg</file>
|
||||
<file>images/controls/settings-2.svg</file>
|
||||
<file>images/controls/share-2.svg</file>
|
||||
<file>ui/qml/Pages2/PageHome.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsServersList.qml</file>
|
||||
<file>ui/qml/Pages2/PageShare.qml</file>
|
||||
<file>ui/qml/Controls2/TextTypes/Header1TextType.qml</file>
|
||||
<file>ui/qml/Controls2/TextTypes/LabelTextType.qml</file>
|
||||
<file>ui/qml/Controls2/TextTypes/ButtonTextType.qml</file>
|
||||
<file>ui/qml/Controls2/Header2Type.qml</file>
|
||||
<file>images/controls/plus.svg</file>
|
||||
<file>ui/qml/Components/ConnectButton.qml</file>
|
||||
<file>images/controls/download.svg</file>
|
||||
<file>ui/qml/Controls2/ProgressBarType.qml</file>
|
||||
<file>ui/qml/Components/ConnectionTypeSelectionDrawer.qml</file>
|
||||
<file>ui/qml/Components/HomeContainersListView.qml</file>
|
||||
<file>ui/qml/Controls2/TextTypes/CaptionTextType.qml</file>
|
||||
<file>images/controls/settings.svg</file>
|
||||
<file>ui/qml/Pages2/PageSettingsServerInfo.qml</file>
|
||||
<file>ui/qml/Controls2/PageType.qml</file>
|
||||
<file>ui/qml/Controls2/PopupType.qml</file>
|
||||
<file>images/controls/edit-3.svg</file>
|
||||
<file>ui/qml/Pages2/PageSettingsServerData.qml</file>
|
||||
<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/StackViewType.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettings.qml</file>
|
||||
<file>images/controls/amnezia.svg</file>
|
||||
<file>images/controls/app.svg</file>
|
||||
<file>images/controls/radio.svg</file>
|
||||
<file>images/controls/save.svg</file>
|
||||
<file>images/controls/server.svg</file>
|
||||
<file>ui/qml/Pages2/PageSettingsServerProtocols.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsServerServices.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardViewConfig.qml</file>
|
||||
<file>images/controls/file-cog-2.svg</file>
|
||||
<file>ui/qml/Components/QuestionDrawer.qml</file>
|
||||
<file>ui/qml/Pages2/PageDeinstalling.qml</file>
|
||||
<file>ui/qml/Controls2/BackButtonType.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsServerProtocol.qml</file>
|
||||
<file>ui/qml/Components/TransportProtoSelector.qml</file>
|
||||
<file>ui/qml/Controls2/ListViewWithRadioButtonType.qml</file>
|
||||
<file>images/controls/radio-button.svg</file>
|
||||
<file>images/controls/radio-button-inner-circle.png</file>
|
||||
<file>images/controls/radio-button-pressed.svg</file>
|
||||
<file>images/controls/radio-button-inner-circle-pressed.png</file>
|
||||
<file>ui/qml/Components/ShareConnectionDrawer.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsConnection.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsDns.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsApplication.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsBackup.qml</file>
|
||||
<file>images/controls/delete.svg</file>
|
||||
<file>ui/qml/Pages2/PageSettingsAbout.qml</file>
|
||||
<file>images/controls/github.svg</file>
|
||||
<file>images/controls/mail.svg</file>
|
||||
<file>images/controls/telegram.svg</file>
|
||||
<file>ui/qml/Controls2/TextTypes/SmallTextType.qml</file>
|
||||
<file>ui/qml/Filters/ContainersModelFilters.qml</file>
|
||||
<file>ui/qml/Components/SelectLanguageDrawer.qml</file>
|
||||
<file>ui/qml/Controls2/BusyIndicatorType.qml</file>
|
||||
<file>ui/qml/Pages2/PageProtocolOpenVpnSettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageProtocolShadowSocksSettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageProtocolCloakSettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageProtocolXraySettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageProtocolRaw.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsLogging.qml</file>
|
||||
<file>ui/qml/Pages2/PageServiceSftpSettings.qml</file>
|
||||
<file>images/controls/copy.svg</file>
|
||||
<file>ui/qml/Pages2/PageServiceTorWebsiteSettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardQrReader.qml</file>
|
||||
<file>images/controls/eye.svg</file>
|
||||
<file>images/controls/eye-off.svg</file>
|
||||
<file>ui/qml/Pages2/PageSettingsSplitTunneling.qml</file>
|
||||
<file>ui/qml/Controls2/ContextMenuType.qml</file>
|
||||
<file>ui/qml/Controls2/TextAreaType.qml</file>
|
||||
<file>images/controls/trash.svg</file>
|
||||
<file>images/controls/more-vertical.svg</file>
|
||||
<file>ui/qml/Controls2/ListViewWithLabelsType.qml</file>
|
||||
<file>ui/qml/Pages2/PageServiceDnsSettings.qml</file>
|
||||
<file>ui/qml/Controls2/TopCloseButtonType.qml</file>
|
||||
<file>images/controls/x-circle.svg</file>
|
||||
<file>ui/qml/Pages2/PageProtocolAwgSettings.qml</file>
|
||||
<file>server_scripts/awg/template.conf</file>
|
||||
<file>server_scripts/awg/start.sh</file>
|
||||
<file>server_scripts/awg/configure_container.sh</file>
|
||||
<file>server_scripts/awg/run_container.sh</file>
|
||||
<file>server_scripts/awg/Dockerfile</file>
|
||||
<file>ui/qml/Pages2/PageShareFullAccess.qml</file>
|
||||
<file>images/controls/close.svg</file>
|
||||
<file>images/controls/search.svg</file>
|
||||
<file>server_scripts/xray/configure_container.sh</file>
|
||||
<file>server_scripts/xray/Dockerfile</file>
|
||||
<file>server_scripts/xray/run_container.sh</file>
|
||||
<file>server_scripts/xray/start.sh</file>
|
||||
<file>server_scripts/xray/template.json</file>
|
||||
<file>ui/qml/Pages2/PageProtocolWireGuardSettings.qml</file>
|
||||
<file>ui/qml/Components/ConnectButton.qml</file>
|
||||
<file>ui/qml/Components/ConnectionTypeSelectionDrawer.qml</file>
|
||||
<file>ui/qml/Components/HomeContainersListView.qml</file>
|
||||
<file>ui/qml/Components/HomeSplitTunnelingDrawer.qml</file>
|
||||
<file>images/controls/split-tunneling.svg</file>
|
||||
<file>ui/qml/Controls2/DrawerType2.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsAppSplitTunneling.qml</file>
|
||||
<file>ui/qml/Components/InstalledAppsDrawer.qml</file>
|
||||
<file>images/controls/alert-circle.svg</file>
|
||||
<file>images/controls/file-check-2.svg</file>
|
||||
<file>ui/qml/Components/QuestionDrawer.qml</file>
|
||||
<file>ui/qml/Components/SelectLanguageDrawer.qml</file>
|
||||
<file>ui/qml/Components/ServersListView.qml</file>
|
||||
<file>ui/qml/Components/SettingsContainersListView.qml</file>
|
||||
<file>ui/qml/Components/ShareConnectionDrawer.qml</file>
|
||||
<file>ui/qml/Components/TransportProtoSelector.qml</file>
|
||||
<file>ui/qml/Config/GlobalConfig.qml</file>
|
||||
<file>ui/qml/Config/qmldir</file>
|
||||
<file>ui/qml/Controls2/BackButtonType.qml</file>
|
||||
<file>ui/qml/Controls2/BasicButtonType.qml</file>
|
||||
<file>ui/qml/Controls2/BusyIndicatorType.qml</file>
|
||||
<file>ui/qml/Controls2/CardType.qml</file>
|
||||
<file>ui/qml/Controls2/CardWithIconsType.qml</file>
|
||||
<file>ui/qml/Controls2/CheckBoxType.qml</file>
|
||||
<file>ui/qml/Controls2/ContextMenuType.qml</file>
|
||||
<file>ui/qml/Controls2/DividerType.qml</file>
|
||||
<file>ui/qml/Controls2/DrawerType2.qml</file>
|
||||
<file>ui/qml/Controls2/DropDownType.qml</file>
|
||||
<file>ui/qml/Controls2/FlickableType.qml</file>
|
||||
<file>ui/qml/Controls2/Header2Type.qml</file>
|
||||
<file>ui/qml/Controls2/HeaderType.qml</file>
|
||||
<file>ui/qml/Controls2/HorizontalRadioButton.qml</file>
|
||||
<file>ui/qml/Controls2/ImageButtonType.qml</file>
|
||||
<file>ui/qml/Controls2/LabelWithButtonType.qml</file>
|
||||
<file>ui/qml/Controls2/LabelWithImageType.qml</file>
|
||||
<file>ui/qml/Controls2/ListViewWithLabelsType.qml</file>
|
||||
<file>ui/qml/Controls2/ListViewWithRadioButtonType.qml</file>
|
||||
<file>ui/qml/Controls2/PageType.qml</file>
|
||||
<file>ui/qml/Controls2/PopupType.qml</file>
|
||||
<file>ui/qml/Controls2/ProgressBarType.qml</file>
|
||||
<file>ui/qml/Controls2/ScrollBarType.qml</file>
|
||||
<file>ui/qml/Controls2/StackViewType.qml</file>
|
||||
<file>ui/qml/Controls2/SwitcherType.qml</file>
|
||||
<file>ui/qml/Controls2/TabButtonType.qml</file>
|
||||
<file>ui/qml/Controls2/TabImageButtonType.qml</file>
|
||||
<file>ui/qml/Controls2/TextAreaType.qml</file>
|
||||
<file>ui/qml/Controls2/TextAreaWithFooterType.qml</file>
|
||||
<file>ui/qml/Controls2/TextFieldWithHeaderType.qml</file>
|
||||
<file>ui/qml/Controls2/TextTypes/ButtonTextType.qml</file>
|
||||
<file>ui/qml/Controls2/TextTypes/CaptionTextType.qml</file>
|
||||
<file>ui/qml/Controls2/TextTypes/Header1TextType.qml</file>
|
||||
<file>ui/qml/Controls2/TextTypes/Header2TextType.qml</file>
|
||||
<file>ui/qml/Controls2/TextTypes/LabelTextType.qml</file>
|
||||
<file>ui/qml/Controls2/TextTypes/ListItemTitleType.qml</file>
|
||||
<file>ui/qml/Controls2/TextTypes/ParagraphTextType.qml</file>
|
||||
<file>ui/qml/Controls2/TextTypes/SmallTextType.qml</file>
|
||||
<file>ui/qml/Controls2/TopCloseButtonType.qml</file>
|
||||
<file>ui/qml/Controls2/VerticalRadioButton.qml</file>
|
||||
<file>ui/qml/Controls2/WarningType.qml</file>
|
||||
<file>fonts/pt-root-ui_vf.ttf</file>
|
||||
<file>ui/qml/Modules/Style/qmldir</file>
|
||||
<file>ui/qml/Filters/ContainersModelFilters.qml</file>
|
||||
<file>ui/qml/main2.qml</file>
|
||||
<file>ui/qml/Modules/Style/AmneziaStyle.qml</file>
|
||||
<file>ui/qml/Modules/Style/qmldir</file>
|
||||
<file>ui/qml/Pages2/PageDeinstalling.qml</file>
|
||||
<file>ui/qml/Pages2/PageDevMenu.qml</file>
|
||||
<file>ui/qml/Pages2/PageHome.qml</file>
|
||||
<file>ui/qml/Pages2/PageProtocolAwgSettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageProtocolCloakSettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageProtocolOpenVpnSettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageProtocolRaw.qml</file>
|
||||
<file>ui/qml/Pages2/PageProtocolShadowSocksSettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageProtocolWireGuardSettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageProtocolXraySettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageServiceDnsSettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageServiceSftpSettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageServiceSocksProxySettings.qml</file>
|
||||
<file>server_scripts/socks5_proxy/run_container.sh</file>
|
||||
<file>server_scripts/socks5_proxy/Dockerfile</file>
|
||||
<file>server_scripts/socks5_proxy/configure_container.sh</file>
|
||||
<file>server_scripts/socks5_proxy/start.sh</file>
|
||||
<file>ui/qml/Pages2/PageServiceTorWebsiteSettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsAbout.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsApiLanguageList.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsApiServerInfo.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsApplication.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsAppSplitTunneling.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsBackup.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsConnection.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsDns.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsLogging.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsServerData.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsServerInfo.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsServerProtocol.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsServerProtocols.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsServerServices.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsServersList.qml</file>
|
||||
<file>ui/qml/Pages2/PageSettingsSplitTunneling.qml</file>
|
||||
<file>ui/qml/Pages2/PageProtocolAwgClientSettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardApiServicesList.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml</file>
|
||||
<file>ui/qml/Controls2/CardWithIconsType.qml</file>
|
||||
<file>images/controls/tag.svg</file>
|
||||
<file>images/controls/history.svg</file>
|
||||
<file>images/controls/gauge.svg</file>
|
||||
<file>images/controls/map-pin.svg</file>
|
||||
<file>ui/qml/Controls2/LabelWithImageType.qml</file>
|
||||
<file>images/controls/info.svg</file>
|
||||
<file>ui/qml/Controls2/TextAreaWithFooterType.qml</file>
|
||||
<file>images/controls/scan-line.svg</file>
|
||||
<file>images/controls/folder-search-2.svg</file>
|
||||
<file>ui/qml/Pages2/PageSettingsApiServerInfo.qml</file>
|
||||
<file>images/controls/bug.svg</file>
|
||||
<file>ui/qml/Pages2/PageDevMenu.qml</file>
|
||||
<file>images/controls/refresh-cw.svg</file>
|
||||
<file>ui/qml/Pages2/PageSettingsApiLanguageList.qml</file>
|
||||
<file>images/controls/archive-restore.svg</file>
|
||||
<file>images/controls/help-circle.svg</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardApiServicesList.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardConfigSource.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardCredentials.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardEasy.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardInstalling.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardProtocols.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardProtocolSettings.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardQrReader.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardStart.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardTextKey.qml</file>
|
||||
<file>ui/qml/Pages2/PageSetupWizardViewConfig.qml</file>
|
||||
<file>ui/qml/Pages2/PageShare.qml</file>
|
||||
<file>ui/qml/Pages2/PageShareFullAccess.qml</file>
|
||||
<file>ui/qml/Pages2/PageStart.qml</file>
|
||||
</qresource>
|
||||
<qresource prefix="/countriesFlags">
|
||||
<file>images/flagKit/ZW.svg</file>
|
||||
|
||||
@@ -2679,7 +2679,7 @@ and will not be shared or disclosed to the Amnezia or any third parties</source>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="154"/>
|
||||
<source>Where to get connection data, step-by-step instructions for buying a VPS</source>
|
||||
<translation>Где взять данные для подключения, пошаговые инстуркции по покупке VPS</translation>
|
||||
<translation>Где взять данные для подключения, пошаговые инструкции по покупке VPS</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="170"/>
|
||||
|
||||
@@ -34,13 +34,13 @@ ConnectionController::ConnectionController(const QSharedPointer<ServersModel> &s
|
||||
|
||||
void ConnectionController::openConnection()
|
||||
{
|
||||
// #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
// if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true))
|
||||
// {
|
||||
// emit connectionErrorOccurred(ErrorCode::AmneziaServiceNotRunning);
|
||||
// return;
|
||||
// }
|
||||
// #endif
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
if (!Utils::processIsRunning(Utils::executable(SERVICE_NAME, false), true))
|
||||
{
|
||||
emit connectionErrorOccurred(ErrorCode::AmneziaServiceNotRunning);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
int serverIndex = m_serversModel->getDefaultServerIndex();
|
||||
QJsonObject serverConfig = m_serversModel->getServerConfig(serverIndex);
|
||||
@@ -51,8 +51,11 @@ void ConnectionController::openConnection()
|
||||
if (configVersion == ApiConfigSources::Telegram
|
||||
&& !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
|
||||
emit updateApiConfigFromTelegram();
|
||||
} else if (configVersion == ApiConfigSources::AmneziaGateway
|
||||
&& !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
|
||||
emit updateApiConfigFromGateway();
|
||||
} else if (configVersion && m_serversModel->isApiKeyExpired(serverIndex)) {
|
||||
qDebug() << "attempt to update api config by end_date event";
|
||||
qDebug() << "attempt to update api config by expires_at event";
|
||||
if (configVersion == ApiConfigSources::Telegram) {
|
||||
emit updateApiConfigFromTelegram();
|
||||
} else {
|
||||
|
||||
@@ -121,9 +121,8 @@ ErrorCode ExportController::generateNativeConfig(const DockerContainer container
|
||||
|
||||
jsonNativeConfig = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object();
|
||||
|
||||
if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg) {
|
||||
auto clientId = jsonNativeConfig.value(config_key::clientId).toString();
|
||||
errorCode = m_clientManagementModel->appendClient(clientId, clientName, container, credentials, serverController);
|
||||
if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg || protocol == Proto::Xray) {
|
||||
errorCode = m_clientManagementModel->appendClient(jsonNativeConfig, clientName, container, credentials, serverController);
|
||||
}
|
||||
return errorCode;
|
||||
}
|
||||
@@ -248,10 +247,10 @@ void ExportController::generateCloakConfig()
|
||||
emit exportConfigChanged();
|
||||
}
|
||||
|
||||
void ExportController::generateXrayConfig()
|
||||
void ExportController::generateXrayConfig(const QString &clientName)
|
||||
{
|
||||
QJsonObject nativeConfig;
|
||||
ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, "", Proto::Xray, nativeConfig);
|
||||
ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, clientName, Proto::Xray, nativeConfig);
|
||||
if (errorCode) {
|
||||
emit exportErrorOccurred(errorCode);
|
||||
return;
|
||||
|
||||
@@ -28,7 +28,7 @@ public slots:
|
||||
void generateAwgConfig(const QString &clientName);
|
||||
void generateShadowSocksConfig();
|
||||
void generateCloakConfig();
|
||||
void generateXrayConfig();
|
||||
void generateXrayConfig(const QString &clientName);
|
||||
|
||||
QString getConfig();
|
||||
QString getNativeConfigString();
|
||||
|
||||
210
client/ui/controllers/focusController.cpp
Normal file
210
client/ui/controllers/focusController.cpp
Normal file
@@ -0,0 +1,210 @@
|
||||
#include "focusController.h"
|
||||
#include "utils/qmlUtils.h"
|
||||
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQuickWindow>
|
||||
|
||||
FocusController::FocusController(QQmlApplicationEngine *engine, QObject *parent)
|
||||
: QObject { parent },
|
||||
m_engine { engine },
|
||||
m_focusChain {},
|
||||
m_focusedItem { nullptr },
|
||||
m_rootObjects {},
|
||||
m_defaultFocusItem { QSharedPointer<QQuickItem>() },
|
||||
m_lvfc { nullptr }
|
||||
{
|
||||
QObject::connect(m_engine.get(), &QQmlApplicationEngine::objectCreated, this,
|
||||
[this](QObject *object, const QUrl &url) {
|
||||
QQuickItem *newDefaultFocusItem = object->findChild<QQuickItem *>("defaultFocusItem");
|
||||
if (newDefaultFocusItem && m_defaultFocusItem != newDefaultFocusItem) {
|
||||
m_defaultFocusItem.reset(newDefaultFocusItem);
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(this, &FocusController::focusedItemChanged, this,
|
||||
[this]() { m_focusedItem->forceActiveFocus(Qt::TabFocusReason); });
|
||||
}
|
||||
|
||||
void FocusController::nextKeyTabItem()
|
||||
{
|
||||
nextItem(Direction::Forward);
|
||||
}
|
||||
|
||||
void FocusController::previousKeyTabItem()
|
||||
{
|
||||
nextItem(Direction::Backward);
|
||||
}
|
||||
|
||||
void FocusController::nextKeyUpItem()
|
||||
{
|
||||
nextItem(Direction::Backward);
|
||||
}
|
||||
|
||||
void FocusController::nextKeyDownItem()
|
||||
{
|
||||
nextItem(Direction::Forward);
|
||||
}
|
||||
|
||||
void FocusController::nextKeyLeftItem()
|
||||
{
|
||||
nextItem(Direction::Backward);
|
||||
}
|
||||
|
||||
void FocusController::nextKeyRightItem()
|
||||
{
|
||||
nextItem(Direction::Forward);
|
||||
}
|
||||
|
||||
void FocusController::setFocusItem(QQuickItem *item)
|
||||
{
|
||||
if (m_focusedItem != item) {
|
||||
m_focusedItem = item;
|
||||
}
|
||||
emit focusedItemChanged();
|
||||
}
|
||||
|
||||
void FocusController::setFocusOnDefaultItem()
|
||||
{
|
||||
setFocusItem(m_defaultFocusItem.get());
|
||||
}
|
||||
|
||||
void FocusController::pushRootObject(QObject *object)
|
||||
{
|
||||
m_rootObjects.push(object);
|
||||
dropListView();
|
||||
// setFocusOnDefaultItem();
|
||||
}
|
||||
|
||||
void FocusController::dropRootObject(QObject *object)
|
||||
{
|
||||
if (m_rootObjects.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_rootObjects.top() == object) {
|
||||
m_rootObjects.pop();
|
||||
dropListView();
|
||||
setFocusOnDefaultItem();
|
||||
} else {
|
||||
qWarning() << "===>> TRY TO DROP WRONG ROOT OBJECT: " << m_rootObjects.top() << " SHOULD BE: " << object;
|
||||
}
|
||||
}
|
||||
|
||||
void FocusController::resetRootObject()
|
||||
{
|
||||
m_rootObjects.clear();
|
||||
}
|
||||
|
||||
void FocusController::reload(Direction direction)
|
||||
{
|
||||
m_focusChain.clear();
|
||||
|
||||
QObject *rootObject = (m_rootObjects.empty() ? m_engine->rootObjects().value(0) : m_rootObjects.top());
|
||||
|
||||
if (!rootObject) {
|
||||
qCritical() << "No ROOT OBJECT found!";
|
||||
resetRootObject();
|
||||
dropListView();
|
||||
return;
|
||||
}
|
||||
|
||||
m_focusChain.append(FocusControl::getSubChain(rootObject));
|
||||
|
||||
std::sort(m_focusChain.begin(), m_focusChain.end(),
|
||||
direction == Direction::Forward ? FocusControl::isLess : FocusControl::isMore);
|
||||
|
||||
if (m_focusChain.empty()) {
|
||||
qWarning() << "Focus chain is empty!";
|
||||
resetRootObject();
|
||||
dropListView();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void FocusController::nextItem(Direction direction)
|
||||
{
|
||||
reload(direction);
|
||||
|
||||
if (m_lvfc && FocusControl::isListView(m_focusedItem)) {
|
||||
direction == Direction::Forward ? focusNextListViewItem() : focusPreviousListViewItem();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_focusChain.empty()) {
|
||||
qWarning() << "There are no items to navigate";
|
||||
setFocusOnDefaultItem();
|
||||
return;
|
||||
}
|
||||
|
||||
auto focusedItemIndex = m_focusChain.indexOf(m_focusedItem);
|
||||
|
||||
if (focusedItemIndex == -1) {
|
||||
focusedItemIndex = 0;
|
||||
} else if (focusedItemIndex == (m_focusChain.size() - 1)) {
|
||||
focusedItemIndex = 0;
|
||||
} else {
|
||||
focusedItemIndex++;
|
||||
}
|
||||
|
||||
const auto focusedItem = qobject_cast<QQuickItem *>(m_focusChain.at(focusedItemIndex));
|
||||
|
||||
if (focusedItem == nullptr) {
|
||||
qWarning() << "Failed to get item to focus on. Setting focus on default";
|
||||
setFocusOnDefaultItem();
|
||||
return;
|
||||
}
|
||||
|
||||
if (FocusControl::isListView(focusedItem)) {
|
||||
m_lvfc = new ListViewFocusController(focusedItem, this);
|
||||
m_focusedItem = focusedItem;
|
||||
if (direction == Direction::Forward) {
|
||||
m_lvfc->nextDelegate();
|
||||
focusNextListViewItem();
|
||||
} else {
|
||||
m_lvfc->previousDelegate();
|
||||
focusPreviousListViewItem();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setFocusItem(focusedItem);
|
||||
}
|
||||
|
||||
void FocusController::focusNextListViewItem()
|
||||
{
|
||||
m_lvfc->reloadFocusChain();
|
||||
if (m_lvfc->isLastFocusItemInListView() || m_lvfc->isReturnNeeded()) {
|
||||
dropListView();
|
||||
nextItem(Direction::Forward);
|
||||
return;
|
||||
} else if (m_lvfc->isLastFocusItemInDelegate()) {
|
||||
m_lvfc->resetFocusChain();
|
||||
m_lvfc->nextDelegate();
|
||||
}
|
||||
|
||||
m_lvfc->focusNextItem();
|
||||
}
|
||||
|
||||
void FocusController::focusPreviousListViewItem()
|
||||
{
|
||||
m_lvfc->reloadFocusChain();
|
||||
if (m_lvfc->isFirstFocusItemInListView() || m_lvfc->isReturnNeeded()) {
|
||||
dropListView();
|
||||
nextItem(Direction::Backward);
|
||||
return;
|
||||
} else if (m_lvfc->isFirstFocusItemInDelegate()) {
|
||||
m_lvfc->resetFocusChain();
|
||||
m_lvfc->previousDelegate();
|
||||
}
|
||||
|
||||
m_lvfc->focusPreviousItem();
|
||||
}
|
||||
|
||||
void FocusController::dropListView()
|
||||
{
|
||||
if (m_lvfc) {
|
||||
delete m_lvfc;
|
||||
m_lvfc = nullptr;
|
||||
}
|
||||
}
|
||||
57
client/ui/controllers/focusController.h
Normal file
57
client/ui/controllers/focusController.h
Normal file
@@ -0,0 +1,57 @@
|
||||
#ifndef FOCUSCONTROLLER_H
|
||||
#define FOCUSCONTROLLER_H
|
||||
|
||||
#include "ui/controllers/listViewFocusController.h"
|
||||
|
||||
#include <QQmlApplicationEngine>
|
||||
|
||||
/*!
|
||||
* \brief The FocusController class makes focus control more straightforward
|
||||
* \details Focus is handled only for visible and enabled items which have
|
||||
* `isFocused` property from top left to bottom right.
|
||||
* \note There are items handled differently (e.g. ListView)
|
||||
*/
|
||||
class FocusController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit FocusController(QQmlApplicationEngine *engine, QObject *parent = nullptr);
|
||||
~FocusController() override = default;
|
||||
|
||||
Q_INVOKABLE void nextKeyTabItem();
|
||||
Q_INVOKABLE void previousKeyTabItem();
|
||||
Q_INVOKABLE void nextKeyUpItem();
|
||||
Q_INVOKABLE void nextKeyDownItem();
|
||||
Q_INVOKABLE void nextKeyLeftItem();
|
||||
Q_INVOKABLE void nextKeyRightItem();
|
||||
Q_INVOKABLE void setFocusItem(QQuickItem *item);
|
||||
Q_INVOKABLE void setFocusOnDefaultItem();
|
||||
Q_INVOKABLE void pushRootObject(QObject *object);
|
||||
Q_INVOKABLE void dropRootObject(QObject *object);
|
||||
Q_INVOKABLE void resetRootObject();
|
||||
|
||||
private:
|
||||
enum class Direction {
|
||||
Forward,
|
||||
Backward,
|
||||
};
|
||||
|
||||
void reload(Direction direction);
|
||||
void nextItem(Direction direction);
|
||||
void focusNextListViewItem();
|
||||
void focusPreviousListViewItem();
|
||||
void dropListView();
|
||||
|
||||
QSharedPointer<QQmlApplicationEngine> m_engine; // Pointer to engine to get root object
|
||||
QList<QObject *> m_focusChain; // List of current objects to be focused
|
||||
QQuickItem *m_focusedItem; // Pointer to the active focus item
|
||||
QStack<QObject *> m_rootObjects;
|
||||
QSharedPointer<QQuickItem> m_defaultFocusItem;
|
||||
|
||||
ListViewFocusController *m_lvfc; // ListView focus manager
|
||||
|
||||
signals:
|
||||
void focusedItemChanged();
|
||||
};
|
||||
|
||||
#endif // FOCUSCONTROLLER_H
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "core/errorstrings.h"
|
||||
#include "core/serialization/serialization.h"
|
||||
#include "systemController.h"
|
||||
#include "utilities.h"
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
@@ -39,11 +40,12 @@ namespace
|
||||
const QString amneziaConfigPatternUserName = "userName";
|
||||
const QString amneziaConfigPatternPassword = "password";
|
||||
const QString amneziaFreeConfigPattern = "api_key";
|
||||
const QString amneziaPremiumConfigPattern = "auth_data";
|
||||
const QString backupPattern = "Servers/serversList";
|
||||
|
||||
if (config.contains(backupPattern)) {
|
||||
return ConfigTypes::Backup;
|
||||
} else if (config.contains(amneziaConfigPattern) || config.contains(amneziaFreeConfigPattern)
|
||||
} else if (config.contains(amneziaConfigPattern) || config.contains(amneziaFreeConfigPattern) || config.contains(amneziaPremiumConfigPattern)
|
||||
|| (config.contains(amneziaConfigPatternHostName) && config.contains(amneziaConfigPatternUserName)
|
||||
&& config.contains(amneziaConfigPatternPassword))) {
|
||||
return ConfigTypes::Amnezia;
|
||||
@@ -75,17 +77,18 @@ ImportController::ImportController(const QSharedPointer<ServersModel> &serversMo
|
||||
|
||||
bool ImportController::extractConfigFromFile(const QString &fileName)
|
||||
{
|
||||
QFile file(fileName);
|
||||
|
||||
if (file.open(QIODevice::ReadOnly)) {
|
||||
QString data = file.readAll();
|
||||
|
||||
m_configFileName = QFileInfo(file.fileName()).fileName();
|
||||
return extractConfigFromData(data);
|
||||
QString data;
|
||||
if (!SystemController::readFile(fileName, data)) {
|
||||
emit importErrorOccurred(ErrorCode::ImportOpenConfigError, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
emit importErrorOccurred(tr("Unable to open file"), false);
|
||||
return false;
|
||||
m_configFileName = QFileInfo(QFile(fileName).fileName()).fileName();
|
||||
#ifdef Q_OS_ANDROID
|
||||
if (m_configFileName.isEmpty()) {
|
||||
m_configFileName = AndroidController::instance()->getFileName(fileName);
|
||||
}
|
||||
#endif
|
||||
return extractConfigFromData(data);
|
||||
}
|
||||
|
||||
bool ImportController::extractConfigFromData(QString data)
|
||||
@@ -188,12 +191,12 @@ bool ImportController::extractConfigFromData(QString data)
|
||||
if (!m_serversModel->getServersCount()) {
|
||||
emit restoreAppConfig(config.toUtf8());
|
||||
} else {
|
||||
emit importErrorOccurred(tr("Invalid configuration file"), false);
|
||||
emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ConfigTypes::Invalid: {
|
||||
emit importErrorOccurred(tr("Invalid configuration file"), false);
|
||||
emit importErrorOccurred(ErrorCode::ImportInvalidConfigError, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ public slots:
|
||||
|
||||
signals:
|
||||
void importFinished();
|
||||
void importErrorOccurred(const QString &errorMessage, bool goToPageHome);
|
||||
void importErrorOccurred(ErrorCode errorCode, bool goToPageHome);
|
||||
|
||||
void qrDecodingFinished();
|
||||
|
||||
47
client/ui/controllers/installController.cpp
Normal file → Executable file
47
client/ui/controllers/installController.cpp
Normal file → Executable file
@@ -32,32 +32,8 @@ namespace
|
||||
constexpr char availableCountries[] = "available_countries";
|
||||
|
||||
constexpr char apiConfig[] = "api_config";
|
||||
constexpr char authData[] = "auth_data";
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WINDOWS
|
||||
QString getNextDriverLetter()
|
||||
{
|
||||
QProcess drivesProc;
|
||||
drivesProc.start("wmic logicaldisk get caption");
|
||||
drivesProc.waitForFinished();
|
||||
QString drives = drivesProc.readAll();
|
||||
qDebug() << drives;
|
||||
|
||||
QString letters = "CFGHIJKLMNOPQRSTUVWXYZ";
|
||||
QString letter;
|
||||
for (int i = letters.size() - 1; i > 0; i--) {
|
||||
letter = letters.at(i);
|
||||
if (!drives.contains(letter + ":"))
|
||||
break;
|
||||
}
|
||||
if (letter == "C:") {
|
||||
// set err info
|
||||
qDebug() << "Can't find free drive letter";
|
||||
return "";
|
||||
}
|
||||
return letter;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
InstallController::InstallController(const QSharedPointer<ServersModel> &serversModel, const QSharedPointer<ContainersModel> &containersModel,
|
||||
@@ -135,10 +111,10 @@ void InstallController::install(DockerContainer container, int port, TransportPr
|
||||
containerConfig[config_key::transportPacketMagicHeader] = transportPacketMagicHeader;
|
||||
} else if (container == DockerContainer::Sftp) {
|
||||
containerConfig.insert(config_key::userName, protocols::sftp::defaultUserName);
|
||||
containerConfig.insert(config_key::password, Utils::getRandomString(10));
|
||||
containerConfig.insert(config_key::password, Utils::getRandomString(16));
|
||||
} else if (container == DockerContainer::Socks5Proxy) {
|
||||
containerConfig.insert(config_key::userName, protocols::socks5Proxy::defaultUserName);
|
||||
containerConfig.insert(config_key::password, Utils::getRandomString(10));
|
||||
containerConfig.insert(config_key::password, Utils::getRandomString(16));
|
||||
}
|
||||
|
||||
config.insert(config_key::container, ContainerProps::containerToString(container));
|
||||
@@ -667,7 +643,7 @@ void InstallController::mountSftpDrive(const QString &port, const QString &passw
|
||||
QString hostname = serverCredentials.hostName;
|
||||
|
||||
#ifdef Q_OS_WINDOWS
|
||||
mountPath = getNextDriverLetter() + ":";
|
||||
mountPath = Utils::getNextDriverLetter() + ":";
|
||||
// QString cmd = QString("net use \\\\sshfs\\%1@x.x.x.x!%2 /USER:%1 %3")
|
||||
// .arg(labelTftpUserNameText())
|
||||
// .arg(labelTftpPortText())
|
||||
@@ -768,7 +744,7 @@ bool InstallController::checkSshConnection(QSharedPointer<ServerController> serv
|
||||
} else {
|
||||
if (output.contains(tr("Please login as the user"))) {
|
||||
output.replace("\n", "");
|
||||
emit installationErrorOccurred(output);
|
||||
emit wrongInstallationUser(output);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -826,7 +802,7 @@ bool InstallController::installServiceFromApi()
|
||||
|
||||
ErrorCode errorCode = apiController.getConfigForService(m_settings->getInstallationUuid(true), m_apiServicesModel->getCountryCode(),
|
||||
m_apiServicesModel->getSelectedServiceType(),
|
||||
m_apiServicesModel->getSelectedServiceProtocol(), "", serverConfig);
|
||||
m_apiServicesModel->getSelectedServiceProtocol(), "", QJsonObject(), serverConfig);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
emit installationErrorOccurred(errorCode);
|
||||
return false;
|
||||
@@ -853,24 +829,25 @@ bool InstallController::updateServiceFromApi(const int serverIndex, const QStrin
|
||||
|
||||
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
|
||||
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
|
||||
auto authData = serverConfig.value(configKey::authData).toObject();
|
||||
|
||||
QJsonObject newServerConfig;
|
||||
ErrorCode errorCode =
|
||||
apiController.getConfigForService(m_settings->getInstallationUuid(true), apiConfig.value(configKey::userCountryCode).toString(),
|
||||
apiConfig.value(configKey::serviceType).toString(),
|
||||
apiConfig.value(configKey::serviceProtocol).toString(), newCountryCode, newServerConfig);
|
||||
ErrorCode errorCode = apiController.getConfigForService(
|
||||
m_settings->getInstallationUuid(true), apiConfig.value(configKey::userCountryCode).toString(),
|
||||
apiConfig.value(configKey::serviceType).toString(), apiConfig.value(configKey::serviceProtocol).toString(), newCountryCode,
|
||||
authData, newServerConfig);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
emit installationErrorOccurred(errorCode);
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject newApiConfig = newServerConfig.value(configKey::apiConfig).toObject();
|
||||
newApiConfig.insert(configKey::serviceInfo, apiConfig.value(configKey::serviceInfo));
|
||||
newApiConfig.insert(configKey::userCountryCode, apiConfig.value(configKey::userCountryCode));
|
||||
newApiConfig.insert(configKey::serviceType, apiConfig.value(configKey::serviceType));
|
||||
newApiConfig.insert(configKey::serviceProtocol, apiConfig.value(configKey::serviceProtocol));
|
||||
|
||||
newServerConfig.insert(configKey::apiConfig, newApiConfig);
|
||||
newServerConfig.insert(configKey::authData, authData);
|
||||
m_serversModel->editServer(newServerConfig, serverIndex);
|
||||
|
||||
if (reloadServiceConfig) {
|
||||
|
||||
@@ -75,8 +75,8 @@ signals:
|
||||
void removeAllContainersFinished(const QString &finishedMessage);
|
||||
void removeProcessedContainerFinished(const QString &finishedMessage);
|
||||
|
||||
void installationErrorOccurred(const QString &errorMessage);
|
||||
void installationErrorOccurred(ErrorCode errorCode);
|
||||
void wrongInstallationUser(const QString &message);
|
||||
|
||||
void serverAlreadyExists(int serverIndex);
|
||||
|
||||
|
||||
309
client/ui/controllers/listViewFocusController.cpp
Normal file
309
client/ui/controllers/listViewFocusController.cpp
Normal file
@@ -0,0 +1,309 @@
|
||||
#include "listViewFocusController.h"
|
||||
#include "utils/qmlUtils.h"
|
||||
|
||||
#include <QQuickWindow>
|
||||
|
||||
ListViewFocusController::ListViewFocusController(QQuickItem *listView, QObject *parent)
|
||||
: QObject { parent },
|
||||
m_listView { listView },
|
||||
m_focusChain {},
|
||||
m_currentSection { Section::Default },
|
||||
m_header { nullptr },
|
||||
m_footer { nullptr },
|
||||
m_focusedItem { nullptr },
|
||||
m_focusedItemIndex { -1 },
|
||||
m_delegateIndex { 0 },
|
||||
m_isReturnNeeded { false },
|
||||
m_currentSectionString { "Default", "Header", "Delegate", "Footer" }
|
||||
{
|
||||
QVariant headerItemProperty = m_listView->property("headerItem");
|
||||
m_header = headerItemProperty.canConvert<QQuickItem *>() ? headerItemProperty.value<QQuickItem *>() : nullptr;
|
||||
|
||||
QVariant footerItemProperty = m_listView->property("footerItem");
|
||||
m_footer = footerItemProperty.canConvert<QQuickItem *>() ? footerItemProperty.value<QQuickItem *>() : nullptr;
|
||||
}
|
||||
|
||||
ListViewFocusController::~ListViewFocusController()
|
||||
{
|
||||
}
|
||||
|
||||
void ListViewFocusController::viewAtCurrentIndex() const
|
||||
{
|
||||
switch (m_currentSection) {
|
||||
case Section::Default: [[fallthrough]];
|
||||
case Section::Header: {
|
||||
QMetaObject::invokeMethod(m_listView, "positionViewAtBeginning");
|
||||
break;
|
||||
}
|
||||
case Section::Delegate: {
|
||||
QMetaObject::invokeMethod(m_listView, "positionViewAtIndex", Q_ARG(int, m_delegateIndex), // Index
|
||||
Q_ARG(int, 2)); // PositionMode (0 = Visible)
|
||||
break;
|
||||
}
|
||||
case Section::Footer: {
|
||||
QMetaObject::invokeMethod(m_listView, "positionViewAtEnd");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int ListViewFocusController::size() const
|
||||
{
|
||||
return m_listView->property("count").toInt();
|
||||
}
|
||||
|
||||
int ListViewFocusController::currentIndex() const
|
||||
{
|
||||
return m_delegateIndex;
|
||||
}
|
||||
|
||||
void ListViewFocusController::setDelegateIndex(int index)
|
||||
{
|
||||
m_delegateIndex = index;
|
||||
m_listView->setProperty("currentIndex", index);
|
||||
}
|
||||
|
||||
void ListViewFocusController::nextDelegate()
|
||||
{
|
||||
switch (m_currentSection) {
|
||||
case Section::Default: {
|
||||
if (hasHeader()) {
|
||||
m_currentSection = Section::Header;
|
||||
viewAtCurrentIndex();
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
}
|
||||
case Section::Header: {
|
||||
if (size() > 0) {
|
||||
m_currentSection = Section::Delegate;
|
||||
viewAtCurrentIndex();
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
}
|
||||
case Section::Delegate:
|
||||
if (m_delegateIndex < (size() - 1)) {
|
||||
setDelegateIndex(m_delegateIndex + 1);
|
||||
viewAtCurrentIndex();
|
||||
break;
|
||||
} else if (hasFooter()) {
|
||||
m_currentSection = Section::Footer;
|
||||
viewAtCurrentIndex();
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
case Section::Footer: {
|
||||
m_isReturnNeeded = true;
|
||||
m_currentSection = Section::Default;
|
||||
viewAtCurrentIndex();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
qCritical() << "Current section is invalid!";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ListViewFocusController::previousDelegate()
|
||||
{
|
||||
switch (m_currentSection) {
|
||||
case Section::Default: {
|
||||
if (hasFooter()) {
|
||||
m_currentSection = Section::Footer;
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
}
|
||||
case Section::Footer: {
|
||||
if (size() > 0) {
|
||||
m_currentSection = Section::Delegate;
|
||||
setDelegateIndex(size() - 1);
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
}
|
||||
case Section::Delegate: {
|
||||
if (m_delegateIndex > 0) {
|
||||
setDelegateIndex(m_delegateIndex - 1);
|
||||
break;
|
||||
} else if (hasHeader()) {
|
||||
m_currentSection = Section::Header;
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
}
|
||||
case Section::Header: {
|
||||
m_isReturnNeeded = true;
|
||||
m_currentSection = Section::Default;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
qCritical() << "Current section is invalid!";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ListViewFocusController::decrementIndex()
|
||||
{
|
||||
m_delegateIndex--;
|
||||
}
|
||||
|
||||
QQuickItem *ListViewFocusController::itemAtIndex(const int index) const
|
||||
{
|
||||
QQuickItem *item { nullptr };
|
||||
|
||||
QMetaObject::invokeMethod(m_listView, "itemAtIndex", Q_RETURN_ARG(QQuickItem *, item), Q_ARG(int, index));
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
QQuickItem *ListViewFocusController::currentDelegate() const
|
||||
{
|
||||
QQuickItem *result { nullptr };
|
||||
|
||||
switch (m_currentSection) {
|
||||
case Section::Default: {
|
||||
qWarning() << "No elements...";
|
||||
break;
|
||||
}
|
||||
case Section::Header: {
|
||||
result = m_header;
|
||||
break;
|
||||
}
|
||||
case Section::Delegate: {
|
||||
result = itemAtIndex(m_delegateIndex);
|
||||
break;
|
||||
}
|
||||
case Section::Footer: {
|
||||
result = m_footer;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QQuickItem *ListViewFocusController::focusedItem() const
|
||||
{
|
||||
return m_focusedItem;
|
||||
}
|
||||
|
||||
void ListViewFocusController::focusNextItem()
|
||||
{
|
||||
if (m_isReturnNeeded) {
|
||||
return;
|
||||
}
|
||||
|
||||
reloadFocusChain();
|
||||
|
||||
if (m_focusChain.empty()) {
|
||||
qWarning() << "No elements found in the delegate. Going to next delegate...";
|
||||
nextDelegate();
|
||||
focusNextItem();
|
||||
return;
|
||||
}
|
||||
m_focusedItemIndex++;
|
||||
m_focusedItem = qobject_cast<QQuickItem *>(m_focusChain.at(m_focusedItemIndex));
|
||||
m_focusedItem->forceActiveFocus(Qt::TabFocusReason);
|
||||
}
|
||||
|
||||
void ListViewFocusController::focusPreviousItem()
|
||||
{
|
||||
if (m_isReturnNeeded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_focusChain.empty()) {
|
||||
qInfo() << "Empty focusChain with current delegate: " << currentDelegate() << "Scanning for elements...";
|
||||
reloadFocusChain();
|
||||
}
|
||||
if (m_focusChain.empty()) {
|
||||
qWarning() << "No elements found in the delegate. Going to next delegate...";
|
||||
previousDelegate();
|
||||
focusPreviousItem();
|
||||
return;
|
||||
}
|
||||
if (m_focusedItemIndex == -1) {
|
||||
m_focusedItemIndex = m_focusChain.size();
|
||||
}
|
||||
m_focusedItemIndex--;
|
||||
m_focusedItem = qobject_cast<QQuickItem *>(m_focusChain.at(m_focusedItemIndex));
|
||||
m_focusedItem->forceActiveFocus(Qt::TabFocusReason);
|
||||
}
|
||||
|
||||
void ListViewFocusController::resetFocusChain()
|
||||
{
|
||||
m_focusChain.clear();
|
||||
m_focusedItem = nullptr;
|
||||
m_focusedItemIndex = -1;
|
||||
}
|
||||
|
||||
void ListViewFocusController::reloadFocusChain()
|
||||
{
|
||||
m_focusChain = FocusControl::getItemsChain(currentDelegate());
|
||||
}
|
||||
|
||||
bool ListViewFocusController::isFirstFocusItemInDelegate() const
|
||||
{
|
||||
return m_focusedItem && (m_focusedItem == m_focusChain.first());
|
||||
}
|
||||
|
||||
bool ListViewFocusController::isLastFocusItemInDelegate() const
|
||||
{
|
||||
return m_focusedItem && (m_focusedItem == m_focusChain.last());
|
||||
}
|
||||
|
||||
bool ListViewFocusController::hasHeader() const
|
||||
{
|
||||
return m_header && !FocusControl::getItemsChain(m_header).isEmpty();
|
||||
}
|
||||
|
||||
bool ListViewFocusController::hasFooter() const
|
||||
{
|
||||
return m_footer && !FocusControl::getItemsChain(m_footer).isEmpty();
|
||||
}
|
||||
|
||||
bool ListViewFocusController::isFirstFocusItemInListView() const
|
||||
{
|
||||
switch (m_currentSection) {
|
||||
case Section::Footer: {
|
||||
return isFirstFocusItemInDelegate() && !hasHeader() && (size() == 0);
|
||||
}
|
||||
case Section::Delegate: {
|
||||
return isFirstFocusItemInDelegate() && (m_delegateIndex == 0) && !hasHeader();
|
||||
}
|
||||
case Section::Header: {
|
||||
isFirstFocusItemInDelegate();
|
||||
}
|
||||
case Section::Default: {
|
||||
return true;
|
||||
}
|
||||
default: qWarning() << "Wrong section"; return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool ListViewFocusController::isLastFocusItemInListView() const
|
||||
{
|
||||
switch (m_currentSection) {
|
||||
case Section::Default: {
|
||||
return !hasHeader() && (size() == 0) && !hasFooter();
|
||||
}
|
||||
case Section::Header: {
|
||||
return isLastFocusItemInDelegate() && (size() == 0) && !hasFooter();
|
||||
}
|
||||
case Section::Delegate: {
|
||||
return isLastFocusItemInDelegate() && (m_delegateIndex == size() - 1) && !hasFooter();
|
||||
}
|
||||
case Section::Footer: {
|
||||
return isLastFocusItemInDelegate();
|
||||
}
|
||||
default: qWarning() << "Wrong section"; return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool ListViewFocusController::isReturnNeeded() const
|
||||
{
|
||||
return m_isReturnNeeded;
|
||||
}
|
||||
70
client/ui/controllers/listViewFocusController.h
Normal file
70
client/ui/controllers/listViewFocusController.h
Normal file
@@ -0,0 +1,70 @@
|
||||
#ifndef LISTVIEWFOCUSCONTROLLER_H
|
||||
#define LISTVIEWFOCUSCONTROLLER_H
|
||||
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QQuickItem>
|
||||
#include <QSharedPointer>
|
||||
#include <QStack>
|
||||
|
||||
/*!
|
||||
* \brief The ListViewFocusController class manages the focus of elements in ListView
|
||||
* \details This class object moving focus to ListView's controls since ListView stores
|
||||
* it's data implicitly and it could be got one by one.
|
||||
*
|
||||
* This class was made to store as less as possible data getting it from QML
|
||||
* when it's needed.
|
||||
*/
|
||||
class ListViewFocusController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ListViewFocusController(QQuickItem *listView, QObject *parent = nullptr);
|
||||
~ListViewFocusController();
|
||||
|
||||
void nextDelegate();
|
||||
void previousDelegate();
|
||||
void decrementIndex();
|
||||
void focusNextItem();
|
||||
void focusPreviousItem();
|
||||
void resetFocusChain();
|
||||
void reloadFocusChain();
|
||||
bool isFirstFocusItemInListView() const;
|
||||
bool isFirstFocusItemInDelegate() const;
|
||||
bool isLastFocusItemInListView() const;
|
||||
bool isLastFocusItemInDelegate() const;
|
||||
bool isReturnNeeded() const;
|
||||
|
||||
private:
|
||||
enum class Section {
|
||||
Default,
|
||||
Header,
|
||||
Delegate,
|
||||
Footer,
|
||||
};
|
||||
|
||||
int size() const;
|
||||
int currentIndex() const;
|
||||
void setDelegateIndex(int index);
|
||||
void viewAtCurrentIndex() const;
|
||||
QQuickItem *itemAtIndex(const int index) const;
|
||||
QQuickItem *currentDelegate() const;
|
||||
QQuickItem *focusedItem() const;
|
||||
|
||||
bool hasHeader() const;
|
||||
bool hasFooter() const;
|
||||
|
||||
QQuickItem *m_listView;
|
||||
QList<QObject *> m_focusChain;
|
||||
Section m_currentSection;
|
||||
QQuickItem *m_header;
|
||||
QQuickItem *m_footer;
|
||||
QQuickItem *m_focusedItem; // Pointer to focused item on Delegate
|
||||
qsizetype m_focusedItemIndex;
|
||||
qsizetype m_delegateIndex;
|
||||
bool m_isReturnNeeded;
|
||||
|
||||
QList<QString> m_currentSectionString;
|
||||
};
|
||||
|
||||
#endif // LISTVIEWFOCUSCONTROLLER_H
|
||||
@@ -81,7 +81,7 @@ void PageController::keyPressEvent(Qt::Key key)
|
||||
case Qt::Key_Escape: {
|
||||
if (m_drawerDepth) {
|
||||
emit closeTopDrawer();
|
||||
setDrawerDepth(getDrawerDepth() - 1);
|
||||
decrementDrawerDepth();
|
||||
} else {
|
||||
emit escapePressed();
|
||||
}
|
||||
@@ -142,11 +142,25 @@ void PageController::setDrawerDepth(const int depth)
|
||||
}
|
||||
}
|
||||
|
||||
int PageController::getDrawerDepth()
|
||||
int PageController::getDrawerDepth() const
|
||||
{
|
||||
return m_drawerDepth;
|
||||
}
|
||||
|
||||
int PageController::incrementDrawerDepth()
|
||||
{
|
||||
return ++m_drawerDepth;
|
||||
}
|
||||
|
||||
int PageController::decrementDrawerDepth()
|
||||
{
|
||||
if (m_drawerDepth == 0) {
|
||||
return m_drawerDepth;
|
||||
} else {
|
||||
return --m_drawerDepth;
|
||||
}
|
||||
}
|
||||
|
||||
void PageController::onShowErrorMessage(ErrorCode errorCode)
|
||||
{
|
||||
const auto fullErrorMessage = errorString(errorCode);
|
||||
|
||||
@@ -100,7 +100,9 @@ public slots:
|
||||
void closeApplication();
|
||||
|
||||
void setDrawerDepth(const int depth);
|
||||
int getDrawerDepth();
|
||||
int getDrawerDepth() const;
|
||||
int incrementDrawerDepth();
|
||||
int decrementDrawerDepth();
|
||||
|
||||
private slots:
|
||||
void onShowErrorMessage(amnezia::ErrorCode errorCode);
|
||||
@@ -135,9 +137,6 @@ signals:
|
||||
void escapePressed();
|
||||
void closeTopDrawer();
|
||||
|
||||
void forceTabBarActiveFocus();
|
||||
void forceStackActiveFocus();
|
||||
|
||||
private:
|
||||
QSharedPointer<ServersModel> m_serversModel;
|
||||
|
||||
|
||||
@@ -131,12 +131,8 @@ void SettingsController::backupAppConfig(const QString &fileName)
|
||||
|
||||
void SettingsController::restoreAppConfig(const QString &fileName)
|
||||
{
|
||||
QFile file(fileName);
|
||||
|
||||
file.open(QIODevice::ReadOnly);
|
||||
|
||||
QByteArray data = file.readAll();
|
||||
|
||||
QByteArray data;
|
||||
SystemController::readFile(fileName, data);
|
||||
restoreAppConfigFromData(data);
|
||||
}
|
||||
|
||||
|
||||
@@ -82,14 +82,12 @@ void SitesController::removeSite(int index)
|
||||
|
||||
void SitesController::importSites(const QString &fileName, bool replaceExisting)
|
||||
{
|
||||
QFile file(fileName);
|
||||
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
QByteArray jsonData;
|
||||
if (!SystemController::readFile(fileName, jsonData)) {
|
||||
emit errorOccurred(tr("Can't open file: %1").arg(fileName));
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray jsonData = file.readAll();
|
||||
QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonData);
|
||||
if (jsonDocument.isNull()) {
|
||||
emit errorOccurred(tr("Failed to parse JSON data from file: %1").arg(fileName));
|
||||
|
||||
@@ -24,7 +24,7 @@ SystemController::SystemController(const std::shared_ptr<Settings> &settings, QO
|
||||
{
|
||||
}
|
||||
|
||||
void SystemController::saveFile(QString fileName, const QString &data)
|
||||
void SystemController::saveFile(const QString &fileName, const QString &data)
|
||||
{
|
||||
#if defined Q_OS_ANDROID
|
||||
AndroidController::instance()->saveFile(fileName, data);
|
||||
@@ -62,6 +62,31 @@ void SystemController::saveFile(QString fileName, const QString &data)
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SystemController::readFile(const QString &fileName, QByteArray &data)
|
||||
{
|
||||
#ifdef Q_OS_ANDROID
|
||||
int fd = AndroidController::instance()->getFd(fileName);
|
||||
if (fd == -1) return false;
|
||||
QFile file;
|
||||
if(!file.open(fd, QIODevice::ReadOnly)) return false;
|
||||
data = file.readAll();
|
||||
AndroidController::instance()->closeFd();
|
||||
#else
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::ReadOnly)) return false;
|
||||
data = file.readAll();
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SystemController::readFile(const QString &fileName, QString &data)
|
||||
{
|
||||
QByteArray byteArray;
|
||||
if(!readFile(fileName, byteArray)) return false;
|
||||
data = byteArray;
|
||||
return true;
|
||||
}
|
||||
|
||||
QString SystemController::getFileName(const QString &acceptLabel, const QString &nameFilter,
|
||||
const QString &selectedFile, const bool isSaveMode, const QString &defaultSuffix)
|
||||
{
|
||||
@@ -134,3 +159,10 @@ bool SystemController::isAuthenticated()
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void SystemController::sendTouch(float x, float y)
|
||||
{
|
||||
#ifdef Q_OS_ANDROID
|
||||
AndroidController::instance()->sendTouch(x, y);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ class SystemController : public QObject
|
||||
public:
|
||||
explicit SystemController(const std::shared_ptr<Settings> &setting, QObject *parent = nullptr);
|
||||
|
||||
static void saveFile(QString fileName, const QString &data);
|
||||
static void saveFile(const QString &fileName, const QString &data);
|
||||
static bool readFile(const QString &fileName, QByteArray &data);
|
||||
static bool readFile(const QString &fileName, QString &data);
|
||||
|
||||
public slots:
|
||||
QString getFileName(const QString &acceptLabel, const QString &nameFilter, const QString &selectedFile = "",
|
||||
@@ -20,6 +22,8 @@ public slots:
|
||||
void setQmlRoot(QObject *qmlRoot);
|
||||
|
||||
bool isAuthenticated();
|
||||
void sendTouch(float x, float y);
|
||||
|
||||
signals:
|
||||
void fileDialogClosed(const bool isAccepted);
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ QVariant ApiCountryModel::data(const QModelIndex &index, int role) const
|
||||
case CountryNameRole: {
|
||||
return countryInfo.value(configKey::serverCountryName).toString();
|
||||
}
|
||||
case CountryImageCodeRole: {
|
||||
return countryInfo.value(configKey::serverCountryCode).toString().toUpper();
|
||||
}
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
@@ -76,5 +79,6 @@ QHash<int, QByteArray> ApiCountryModel::roleNames() const
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[CountryNameRole] = "countryName";
|
||||
roles[CountryCodeRole] = "countryCode";
|
||||
roles[CountryImageCodeRole] = "countryImageCode";
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ class ApiCountryModel : public QAbstractListModel
|
||||
public:
|
||||
enum Roles {
|
||||
CountryNameRole = Qt::UserRole + 1,
|
||||
CountryCodeRole
|
||||
CountryCodeRole,
|
||||
CountryImageCodeRole
|
||||
};
|
||||
|
||||
explicit ApiCountryModel(QObject *parent = nullptr);
|
||||
|
||||
@@ -27,6 +27,9 @@ namespace
|
||||
constexpr char storeEndpoint[] = "store_endpoint";
|
||||
|
||||
constexpr char isAvailable[] = "is_available";
|
||||
|
||||
constexpr char subscription[] = "subscription";
|
||||
constexpr char endDate[] = "end_date";
|
||||
}
|
||||
|
||||
namespace serviceType
|
||||
@@ -51,23 +54,23 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= static_cast<int>(rowCount()))
|
||||
return QVariant();
|
||||
|
||||
QJsonObject service = m_services.at(index.row()).toObject();
|
||||
QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject();
|
||||
auto serviceType = service.value(configKey::serviceType).toString();
|
||||
auto apiServiceData = m_services.at(index.row());
|
||||
auto serviceType = apiServiceData.type;
|
||||
auto isServiceAvailable = apiServiceData.isServiceAvailable;
|
||||
|
||||
switch (role) {
|
||||
case NameRole: {
|
||||
return serviceInfo.value(configKey::name).toString();
|
||||
return apiServiceData.serviceInfo.name;
|
||||
}
|
||||
case CardDescriptionRole: {
|
||||
auto speed = serviceInfo.value(configKey::speed).toString();
|
||||
auto speed = apiServiceData.serviceInfo.speed;
|
||||
if (serviceType == serviceType::amneziaPremium) {
|
||||
return tr("Classic VPN for comfortable work, downloading large files and watching videos. "
|
||||
"Works for any sites. Speed up to %1 MBit/s")
|
||||
.arg(speed);
|
||||
} else if (serviceType == serviceType::amneziaFree){
|
||||
QString description = tr("VPN to access blocked sites in regions with high levels of Internet censorship. ");
|
||||
if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) {
|
||||
if (isServiceAvailable) {
|
||||
description += tr("<p><a style=\"color: #EB5757;\">Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again.</a>");
|
||||
}
|
||||
return description;
|
||||
@@ -83,25 +86,24 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
case IsServiceAvailableRole: {
|
||||
if (serviceType == serviceType::amneziaFree) {
|
||||
if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) {
|
||||
if (isServiceAvailable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case SpeedRole: {
|
||||
auto speed = serviceInfo.value(configKey::speed).toString();
|
||||
return tr("%1 MBit/s").arg(speed);
|
||||
return tr("%1 MBit/s").arg(apiServiceData.serviceInfo.speed);
|
||||
}
|
||||
case WorkPeriodRole: {
|
||||
auto timelimit = serviceInfo.value(configKey::timelimit).toString();
|
||||
if (timelimit == "0") {
|
||||
case TimeLimitRole: {
|
||||
auto timeLimit = apiServiceData.serviceInfo.timeLimit;
|
||||
if (timeLimit == "0") {
|
||||
return "";
|
||||
}
|
||||
return tr("%1 days").arg(timelimit);
|
||||
return tr("%1 days").arg(timeLimit);
|
||||
}
|
||||
case RegionRole: {
|
||||
return serviceInfo.value(configKey::region).toString();
|
||||
return apiServiceData.serviceInfo.region;
|
||||
}
|
||||
case FeaturesRole: {
|
||||
if (serviceType == serviceType::amneziaPremium) {
|
||||
@@ -113,12 +115,15 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
}
|
||||
case PriceRole: {
|
||||
auto price = serviceInfo.value(configKey::price).toString();
|
||||
auto price = apiServiceData.serviceInfo.price;
|
||||
if (price == "free") {
|
||||
return tr("Free");
|
||||
}
|
||||
return tr("%1 $/month").arg(price);
|
||||
}
|
||||
case EndDateRole: {
|
||||
return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy");
|
||||
}
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
@@ -128,15 +133,18 @@ void ApiServicesModel::updateModel(const QJsonObject &data)
|
||||
{
|
||||
beginResetModel();
|
||||
|
||||
m_countryCode = data.value(configKey::userCountryCode).toString();
|
||||
m_services = data.value(configKey::services).toArray();
|
||||
if (m_services.isEmpty()) {
|
||||
QJsonObject service;
|
||||
service.insert(configKey::serviceInfo, data.value(configKey::serviceInfo));
|
||||
service.insert(configKey::serviceType, data.value(configKey::serviceType));
|
||||
m_services.clear();
|
||||
|
||||
m_services.push_back(service);
|
||||
m_countryCode = data.value(configKey::userCountryCode).toString();
|
||||
auto services = data.value(configKey::services).toArray();
|
||||
|
||||
if (services.isEmpty()) {
|
||||
m_services.push_back(getApiServicesData(data));
|
||||
m_selectedServiceIndex = 0;
|
||||
} else {
|
||||
for (const auto &service : services) {
|
||||
m_services.push_back(getApiServicesData(service.toObject()));
|
||||
}
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
@@ -149,32 +157,32 @@ void ApiServicesModel::setServiceIndex(const int index)
|
||||
|
||||
QJsonObject ApiServicesModel::getSelectedServiceInfo()
|
||||
{
|
||||
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
|
||||
return service.value(configKey::serviceInfo).toObject();
|
||||
auto service = m_services.at(m_selectedServiceIndex);
|
||||
return service.serviceInfo.object;
|
||||
}
|
||||
|
||||
QString ApiServicesModel::getSelectedServiceType()
|
||||
{
|
||||
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
|
||||
return service.value(configKey::serviceType).toString();
|
||||
auto service = m_services.at(m_selectedServiceIndex);
|
||||
return service.type;
|
||||
}
|
||||
|
||||
QString ApiServicesModel::getSelectedServiceProtocol()
|
||||
{
|
||||
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
|
||||
return service.value(configKey::serviceProtocol).toString();
|
||||
auto service = m_services.at(m_selectedServiceIndex);
|
||||
return service.protocol;
|
||||
}
|
||||
|
||||
QString ApiServicesModel::getSelectedServiceName()
|
||||
{
|
||||
auto modelIndex = index(m_selectedServiceIndex, 0);
|
||||
return data(modelIndex, ApiServicesModel::Roles::NameRole).toString();
|
||||
auto service = m_services.at(m_selectedServiceIndex);
|
||||
return service.serviceInfo.name;
|
||||
}
|
||||
|
||||
QJsonArray ApiServicesModel::getSelectedServiceCountries()
|
||||
{
|
||||
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
|
||||
return service.value(configKey::availableCountries).toArray();
|
||||
auto service = m_services.at(m_selectedServiceIndex);
|
||||
return service.availableCountries;
|
||||
}
|
||||
|
||||
QString ApiServicesModel::getCountryCode()
|
||||
@@ -184,8 +192,8 @@ QString ApiServicesModel::getCountryCode()
|
||||
|
||||
QString ApiServicesModel::getStoreEndpoint()
|
||||
{
|
||||
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
|
||||
return service.value(configKey::storeEndpoint).toString();
|
||||
auto service = m_services.at(m_selectedServiceIndex);
|
||||
return service.storeEndpoint;
|
||||
}
|
||||
|
||||
QVariant ApiServicesModel::getSelectedServiceData(const QString roleString)
|
||||
@@ -209,10 +217,46 @@ QHash<int, QByteArray> ApiServicesModel::roleNames() const
|
||||
roles[ServiceDescriptionRole] = "serviceDescription";
|
||||
roles[IsServiceAvailableRole] = "isServiceAvailable";
|
||||
roles[SpeedRole] = "speed";
|
||||
roles[WorkPeriodRole] = "workPeriod";
|
||||
roles[TimeLimitRole] = "timeLimit";
|
||||
roles[RegionRole] = "region";
|
||||
roles[FeaturesRole] = "features";
|
||||
roles[PriceRole] = "price";
|
||||
roles[EndDateRole] = "endDate";
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
ApiServicesModel::ApiServicesData ApiServicesModel::getApiServicesData(const QJsonObject &data)
|
||||
{
|
||||
auto serviceInfo = data.value(configKey::serviceInfo).toObject();
|
||||
auto serviceType = data.value(configKey::serviceType).toString();
|
||||
auto serviceProtocol = data.value(configKey::serviceProtocol).toString();
|
||||
auto availableCountries = data.value(configKey::availableCountries).toArray();
|
||||
|
||||
auto subscriptionObject = data.value(configKey::subscription).toObject();
|
||||
|
||||
ApiServicesData serviceData;
|
||||
serviceData.serviceInfo.name = serviceInfo.value(configKey::name).toString();
|
||||
serviceData.serviceInfo.price = serviceInfo.value(configKey::price).toString();
|
||||
serviceData.serviceInfo.region = serviceInfo.value(configKey::region).toString();
|
||||
serviceData.serviceInfo.speed = serviceInfo.value(configKey::speed).toString();
|
||||
serviceData.serviceInfo.timeLimit = serviceInfo.value(configKey::timelimit).toString();
|
||||
|
||||
serviceData.type = serviceType;
|
||||
serviceData.protocol = serviceProtocol;
|
||||
|
||||
serviceData.storeEndpoint = serviceInfo.value(configKey::storeEndpoint).toString();
|
||||
|
||||
if (serviceInfo.value(configKey::isAvailable).isBool()) {
|
||||
serviceData.isServiceAvailable = data.value(configKey::isAvailable).toBool();
|
||||
} else {
|
||||
serviceData.isServiceAvailable = true;
|
||||
}
|
||||
|
||||
serviceData.serviceInfo.object = serviceInfo;
|
||||
serviceData.availableCountries = availableCountries;
|
||||
|
||||
serviceData.subscription.endDate = subscriptionObject.value(configKey::endDate).toString();
|
||||
|
||||
return serviceData;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
class ApiServicesModel : public QAbstractListModel
|
||||
{
|
||||
@@ -15,10 +16,11 @@ public:
|
||||
ServiceDescriptionRole,
|
||||
IsServiceAvailableRole,
|
||||
SpeedRole,
|
||||
WorkPeriodRole,
|
||||
TimeLimitRole,
|
||||
RegionRole,
|
||||
FeaturesRole,
|
||||
PriceRole
|
||||
PriceRole,
|
||||
EndDateRole
|
||||
};
|
||||
|
||||
explicit ApiServicesModel(QObject *parent = nullptr);
|
||||
@@ -48,8 +50,40 @@ protected:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
struct ServiceInfo
|
||||
{
|
||||
QString name;
|
||||
QString speed;
|
||||
QString timeLimit;
|
||||
QString region;
|
||||
QString price;
|
||||
|
||||
QJsonObject object;
|
||||
};
|
||||
|
||||
struct Subscription
|
||||
{
|
||||
QString endDate;
|
||||
};
|
||||
|
||||
struct ApiServicesData
|
||||
{
|
||||
bool isServiceAvailable;
|
||||
|
||||
QString type;
|
||||
QString protocol;
|
||||
QString storeEndpoint;
|
||||
|
||||
ServiceInfo serviceInfo;
|
||||
Subscription subscription;
|
||||
|
||||
QJsonArray availableCountries;
|
||||
};
|
||||
|
||||
ApiServicesData getApiServicesData(const QJsonObject &data);
|
||||
|
||||
QString m_countryCode;
|
||||
QJsonArray m_services;
|
||||
QVector<ApiServicesData> m_services;
|
||||
|
||||
int m_selectedServiceIndex;
|
||||
};
|
||||
|
||||
@@ -77,6 +77,7 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co
|
||||
{
|
||||
beginResetModel();
|
||||
m_clientsTable = QJsonArray();
|
||||
endResetModel();
|
||||
|
||||
ErrorCode error = ErrorCode::NoError;
|
||||
|
||||
@@ -90,10 +91,10 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co
|
||||
const QByteArray clientsTableString = serverController->getTextFileFromContainer(container, credentials, clientsTableFile, error);
|
||||
if (error != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to get the clientsTable file from the server";
|
||||
endResetModel();
|
||||
return error;
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
m_clientsTable = QJsonDocument::fromJson(clientsTableString).array();
|
||||
|
||||
if (m_clientsTable.isEmpty()) {
|
||||
@@ -105,6 +106,8 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co
|
||||
error = getOpenVpnClients(container, credentials, serverController, count);
|
||||
} else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
|
||||
error = getWireGuardClients(container, credentials, serverController, count);
|
||||
} else if (container == DockerContainer::Xray) {
|
||||
error = getXrayClients(container, credentials, serverController, count);
|
||||
}
|
||||
if (error != ErrorCode::NoError) {
|
||||
endResetModel();
|
||||
@@ -238,6 +241,68 @@ ErrorCode ClientManagementModel::getWireGuardClients(const DockerContainer conta
|
||||
}
|
||||
return error;
|
||||
}
|
||||
ErrorCode ClientManagementModel::getXrayClients(const DockerContainer container, const ServerCredentials& credentials,
|
||||
const QSharedPointer<ServerController> &serverController, int &count)
|
||||
{
|
||||
ErrorCode error = ErrorCode::NoError;
|
||||
|
||||
const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath;
|
||||
const QString configString = serverController->getTextFileFromContainer(container, credentials, serverConfigPath, error);
|
||||
if (error != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to get the xray server config file from the server";
|
||||
return error;
|
||||
}
|
||||
|
||||
QJsonDocument serverConfig = QJsonDocument::fromJson(configString.toUtf8());
|
||||
if (serverConfig.isNull()) {
|
||||
logger.error() << "Failed to parse xray server config JSON";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
if (!serverConfig.object().contains("inbounds") || serverConfig.object()["inbounds"].toArray().isEmpty()) {
|
||||
logger.error() << "Invalid xray server config structure";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
const QJsonObject inbound = serverConfig.object()["inbounds"].toArray()[0].toObject();
|
||||
if (!inbound.contains("settings")) {
|
||||
logger.error() << "Missing settings in xray inbound config";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
const QJsonObject settings = inbound["settings"].toObject();
|
||||
if (!settings.contains("clients")) {
|
||||
logger.error() << "Missing clients in xray settings config";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
const QJsonArray clients = settings["clients"].toArray();
|
||||
for (const auto &clientValue : clients) {
|
||||
const QJsonObject clientObj = clientValue.toObject();
|
||||
if (!clientObj.contains("id")) {
|
||||
logger.error() << "Missing id in xray client config";
|
||||
continue;
|
||||
}
|
||||
QString clientId = clientObj["id"].toString();
|
||||
|
||||
QString xrayDefaultUuid = serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, error);
|
||||
xrayDefaultUuid.replace("\n", "");
|
||||
|
||||
if (!isClientExists(clientId) && clientId != xrayDefaultUuid) {
|
||||
QJsonObject client;
|
||||
client[configKey::clientId] = clientId;
|
||||
|
||||
QJsonObject userData;
|
||||
userData[configKey::clientName] = QString("Client %1").arg(count);
|
||||
client[configKey::userData] = userData;
|
||||
|
||||
m_clientsTable.push_back(client);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
ErrorCode ClientManagementModel::wgShow(const DockerContainer container, const ServerCredentials &credentials,
|
||||
const QSharedPointer<ServerController> &serverController, std::vector<WgShowData> &data)
|
||||
@@ -325,17 +390,67 @@ ErrorCode ClientManagementModel::appendClient(const DockerContainer container, c
|
||||
const QSharedPointer<ServerController> &serverController)
|
||||
{
|
||||
Proto protocol;
|
||||
if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) {
|
||||
protocol = Proto::OpenVpn;
|
||||
} else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
|
||||
protocol = ContainerProps::defaultProtocol(container);
|
||||
} else {
|
||||
return ErrorCode::NoError;
|
||||
switch (container) {
|
||||
case DockerContainer::ShadowSocks:
|
||||
case DockerContainer::Cloak:
|
||||
protocol = Proto::OpenVpn;
|
||||
break;
|
||||
case DockerContainer::OpenVpn:
|
||||
case DockerContainer::WireGuard:
|
||||
case DockerContainer::Awg:
|
||||
case DockerContainer::Xray:
|
||||
protocol = ContainerProps::defaultProtocol(container);
|
||||
break;
|
||||
default:
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig);
|
||||
return appendClient(protocolConfig, clientName, container, credentials, serverController);
|
||||
}
|
||||
|
||||
return appendClient(protocolConfig.value(config_key::clientId).toString(), clientName, container, credentials, serverController);
|
||||
ErrorCode ClientManagementModel::appendClient(QJsonObject &protocolConfig, const QString &clientName, const DockerContainer container,
|
||||
const ServerCredentials &credentials, const QSharedPointer<ServerController> &serverController)
|
||||
{
|
||||
QString clientId;
|
||||
if (container == DockerContainer::Xray) {
|
||||
if (!protocolConfig.contains("outbounds")) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
QJsonArray outbounds = protocolConfig.value("outbounds").toArray();
|
||||
if (outbounds.isEmpty()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
QJsonObject outbound = outbounds[0].toObject();
|
||||
if (!outbound.contains("settings")) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
QJsonObject settings = outbound["settings"].toObject();
|
||||
if (!settings.contains("vnext")) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
QJsonArray vnext = settings["vnext"].toArray();
|
||||
if (vnext.isEmpty()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
QJsonObject vnextObj = vnext[0].toObject();
|
||||
if (!vnextObj.contains("users")) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
QJsonArray users = vnextObj["users"].toArray();
|
||||
if (users.isEmpty()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
QJsonObject user = users[0].toObject();
|
||||
if (!user.contains("id")) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
clientId = user["id"].toString();
|
||||
} else {
|
||||
clientId = protocolConfig.value(config_key::clientId).toString();
|
||||
}
|
||||
|
||||
return appendClient(clientId, clientName, container, credentials, serverController);
|
||||
}
|
||||
|
||||
ErrorCode ClientManagementModel::appendClient(const QString &clientId, const QString &clientName, const DockerContainer container,
|
||||
@@ -421,10 +536,27 @@ ErrorCode ClientManagementModel::revokeClient(const int row, const DockerContain
|
||||
auto client = m_clientsTable.at(row).toObject();
|
||||
QString clientId = client.value(configKey::clientId).toString();
|
||||
|
||||
if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) {
|
||||
errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController);
|
||||
} else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
|
||||
errorCode = revokeWireGuard(row, container, credentials, serverController);
|
||||
switch(container)
|
||||
{
|
||||
case DockerContainer::OpenVpn:
|
||||
case DockerContainer::ShadowSocks:
|
||||
case DockerContainer::Cloak: {
|
||||
errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController);
|
||||
break;
|
||||
}
|
||||
case DockerContainer::WireGuard:
|
||||
case DockerContainer::Awg: {
|
||||
errorCode = revokeWireGuard(row, container, credentials, serverController);
|
||||
break;
|
||||
}
|
||||
case DockerContainer::Xray: {
|
||||
errorCode = revokeXray(row, container, credentials, serverController);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
logger.error() << "Internal error: received unexpected container type";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
@@ -462,19 +594,69 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig
|
||||
}
|
||||
|
||||
Proto protocol;
|
||||
if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) {
|
||||
protocol = Proto::OpenVpn;
|
||||
} else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
|
||||
protocol = ContainerProps::defaultProtocol(container);
|
||||
} else {
|
||||
return ErrorCode::NoError;
|
||||
|
||||
switch(container)
|
||||
{
|
||||
case DockerContainer::ShadowSocks:
|
||||
case DockerContainer::Cloak: {
|
||||
protocol = Proto::OpenVpn;
|
||||
break;
|
||||
}
|
||||
case DockerContainer::OpenVpn:
|
||||
case DockerContainer::WireGuard:
|
||||
case DockerContainer::Awg:
|
||||
case DockerContainer::Xray: {
|
||||
protocol = ContainerProps::defaultProtocol(container);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
logger.error() << "Internal error: received unexpected container type";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
}
|
||||
|
||||
auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig);
|
||||
|
||||
QString clientId;
|
||||
if (container == DockerContainer::Xray) {
|
||||
if (!protocolConfig.contains("outbounds")) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
QJsonArray outbounds = protocolConfig.value("outbounds").toArray();
|
||||
if (outbounds.isEmpty()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
QJsonObject outbound = outbounds[0].toObject();
|
||||
if (!outbound.contains("settings")) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
QJsonObject settings = outbound["settings"].toObject();
|
||||
if (!settings.contains("vnext")) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
QJsonArray vnext = settings["vnext"].toArray();
|
||||
if (vnext.isEmpty()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
QJsonObject vnextObj = vnext[0].toObject();
|
||||
if (!vnextObj.contains("users")) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
QJsonArray users = vnextObj["users"].toArray();
|
||||
if (users.isEmpty()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
QJsonObject user = users[0].toObject();
|
||||
if (!user.contains("id")) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
clientId = user["id"].toString();
|
||||
} else {
|
||||
clientId = protocolConfig.value(config_key::clientId).toString();
|
||||
}
|
||||
|
||||
int row;
|
||||
bool clientExists = false;
|
||||
QString clientId = protocolConfig.value(config_key::clientId).toString();
|
||||
for (row = 0; row < rowCount(); row++) {
|
||||
auto client = m_clientsTable.at(row).toObject();
|
||||
if (clientId == client.value(configKey::clientId).toString()) {
|
||||
@@ -486,11 +668,28 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) {
|
||||
switch (container)
|
||||
{
|
||||
case DockerContainer::OpenVpn:
|
||||
case DockerContainer::ShadowSocks:
|
||||
case DockerContainer::Cloak: {
|
||||
errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController);
|
||||
} else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
|
||||
errorCode = revokeWireGuard(row, container, credentials, serverController);
|
||||
break;
|
||||
}
|
||||
case DockerContainer::WireGuard:
|
||||
case DockerContainer::Awg: {
|
||||
errorCode = revokeWireGuard(row, container, credentials, serverController);
|
||||
break;
|
||||
}
|
||||
case DockerContainer::Xray: {
|
||||
errorCode = revokeXray(row, container, credentials, serverController);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
logger.error() << "Internal error: received unexpected container type";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
@@ -593,6 +792,117 @@ ErrorCode ClientManagementModel::revokeWireGuard(const int row, const DockerCont
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode ClientManagementModel::revokeXray(const int row,
|
||||
const DockerContainer container,
|
||||
const ServerCredentials &credentials,
|
||||
const QSharedPointer<ServerController> &serverController)
|
||||
{
|
||||
ErrorCode error = ErrorCode::NoError;
|
||||
|
||||
// Get server config
|
||||
const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath;
|
||||
const QString configString = serverController->getTextFileFromContainer(container, credentials, serverConfigPath, error);
|
||||
if (error != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to get the xray server config file";
|
||||
return error;
|
||||
}
|
||||
|
||||
QJsonDocument serverConfig = QJsonDocument::fromJson(configString.toUtf8());
|
||||
if (serverConfig.isNull()) {
|
||||
logger.error() << "Failed to parse xray server config JSON";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
// Get client ID to remove
|
||||
auto client = m_clientsTable.at(row).toObject();
|
||||
QString clientId = client.value(configKey::clientId).toString();
|
||||
|
||||
// Remove client from server config
|
||||
QJsonObject configObj = serverConfig.object();
|
||||
if (!configObj.contains("inbounds")) {
|
||||
logger.error() << "Missing inbounds in xray config";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QJsonArray inbounds = configObj["inbounds"].toArray();
|
||||
if (inbounds.isEmpty()) {
|
||||
logger.error() << "Empty inbounds array in xray config";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QJsonObject inbound = inbounds[0].toObject();
|
||||
if (!inbound.contains("settings")) {
|
||||
logger.error() << "Missing settings in xray inbound config";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QJsonObject settings = inbound["settings"].toObject();
|
||||
if (!settings.contains("clients")) {
|
||||
logger.error() << "Missing clients in xray settings";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
QJsonArray clients = settings["clients"].toArray();
|
||||
if (clients.isEmpty()) {
|
||||
logger.error() << "Empty clients array in xray config";
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
for (int i = 0; i < clients.size(); ++i) {
|
||||
QJsonObject clientObj = clients[i].toObject();
|
||||
if (clientObj.contains("id") && clientObj["id"].toString() == clientId) {
|
||||
clients.removeAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update server config
|
||||
settings["clients"] = clients;
|
||||
inbound["settings"] = settings;
|
||||
inbounds[0] = inbound;
|
||||
configObj["inbounds"] = inbounds;
|
||||
|
||||
// Upload updated config
|
||||
error = serverController->uploadTextFileToContainer(
|
||||
container,
|
||||
credentials,
|
||||
QJsonDocument(configObj).toJson(),
|
||||
serverConfigPath
|
||||
);
|
||||
if (error != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to upload updated xray config";
|
||||
return error;
|
||||
}
|
||||
|
||||
// Remove from local table
|
||||
beginRemoveRows(QModelIndex(), row, row);
|
||||
m_clientsTable.removeAt(row);
|
||||
endRemoveRows();
|
||||
|
||||
// Update clients table file on server
|
||||
const QByteArray clientsTableString = QJsonDocument(m_clientsTable).toJson();
|
||||
QString clientsTableFile = QString("/opt/amnezia/%1/clientsTable")
|
||||
.arg(ContainerProps::containerTypeToString(container));
|
||||
|
||||
error = serverController->uploadTextFileToContainer(container, credentials, clientsTableString, clientsTableFile);
|
||||
if (error != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to upload the clientsTable file";
|
||||
}
|
||||
|
||||
// Restart container
|
||||
QString restartScript = QString("sudo docker restart $CONTAINER_NAME");
|
||||
error = serverController->runScript(
|
||||
credentials,
|
||||
serverController->replaceVars(restartScript, serverController->genVarsForScript(credentials, container))
|
||||
);
|
||||
if (error != ErrorCode::NoError) {
|
||||
logger.error() << "Failed to restart xray container";
|
||||
return error;
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ClientManagementModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
@@ -601,5 +911,6 @@ QHash<int, QByteArray> ClientManagementModel::roleNames() const
|
||||
roles[LatestHandshakeRole] = "latestHandshake";
|
||||
roles[DataReceivedRole] = "dataReceived";
|
||||
roles[DataSentRole] = "dataSent";
|
||||
roles[AllowedIpsRole] = "allowedIps";
|
||||
return roles;
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,8 @@ public slots:
|
||||
const QSharedPointer<ServerController> &serverController);
|
||||
ErrorCode appendClient(const DockerContainer container, const ServerCredentials &credentials, const QJsonObject &containerConfig,
|
||||
const QString &clientName, const QSharedPointer<ServerController> &serverController);
|
||||
ErrorCode appendClient(QJsonObject &protocolConfig, const QString &clientName,const DockerContainer container,
|
||||
const ServerCredentials &credentials, const QSharedPointer<ServerController> &serverController);
|
||||
ErrorCode appendClient(const QString &clientId, const QString &clientName, const DockerContainer container,
|
||||
const ServerCredentials &credentials, const QSharedPointer<ServerController> &serverController);
|
||||
ErrorCode renameClient(const int row, const QString &userName, const DockerContainer container, const ServerCredentials &credentials,
|
||||
@@ -64,11 +66,15 @@ private:
|
||||
const QSharedPointer<ServerController> &serverController);
|
||||
ErrorCode revokeWireGuard(const int row, const DockerContainer container, const ServerCredentials &credentials,
|
||||
const QSharedPointer<ServerController> &serverController);
|
||||
ErrorCode revokeXray(const int row, const DockerContainer container, const ServerCredentials &credentials,
|
||||
const QSharedPointer<ServerController> &serverController);
|
||||
|
||||
ErrorCode getOpenVpnClients(const DockerContainer container, const ServerCredentials &credentials,
|
||||
const QSharedPointer<ServerController> &serverController, int &count);
|
||||
ErrorCode getWireGuardClients(const DockerContainer container, const ServerCredentials &credentials,
|
||||
const QSharedPointer<ServerController> &serverController, int &count);
|
||||
ErrorCode getXrayClients(const DockerContainer container, const ServerCredentials& credentials,
|
||||
const QSharedPointer<ServerController> &serverController, int &count);
|
||||
|
||||
ErrorCode wgShow(const DockerContainer container, const ServerCredentials &credentials,
|
||||
const QSharedPointer<ServerController> &serverController, std::vector<WgShowData> &data);
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace
|
||||
constexpr char serviceProtocol[] = "service_protocol";
|
||||
|
||||
constexpr char publicKeyInfo[] = "public_key";
|
||||
constexpr char endDate[] = "end_date";
|
||||
constexpr char expiresAt[] = "expires_at";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ ServersModel::ServersModel(std::shared_ptr<Settings> settings, QObject *parent)
|
||||
emit ServersModel::defaultServerNameChanged();
|
||||
updateDefaultServerContainersModel();
|
||||
});
|
||||
|
||||
connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged);
|
||||
connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged);
|
||||
}
|
||||
|
||||
int ServersModel::rowCount(const QModelIndex &parent) const
|
||||
@@ -79,6 +82,12 @@ bool ServersModel::setData(const QModelIndex &index, const QVariant &value, int
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ServersModel::setData(const int index, const QVariant &value, int role)
|
||||
{
|
||||
QModelIndex modelIndex = this->index(index);
|
||||
return setData(modelIndex, value, role);
|
||||
}
|
||||
|
||||
QVariant ServersModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= static_cast<int>(m_servers.size())) {
|
||||
@@ -679,6 +688,18 @@ QVariant ServersModel::getProcessedServerData(const QString roleString)
|
||||
return {};
|
||||
}
|
||||
|
||||
bool ServersModel::setProcessedServerData(const QString &roleString, const QVariant &value)
|
||||
{
|
||||
const auto roles = roleNames();
|
||||
for (auto it = roles.begin(); it != roles.end(); it++) {
|
||||
if (QString(it.value()) == roleString) {
|
||||
return setData(m_processedServerIndex, value, it.key());
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling()
|
||||
{
|
||||
auto server = m_servers.at(m_defaultServerIndex).toObject();
|
||||
@@ -718,9 +739,9 @@ bool ServersModel::isApiKeyExpired(const int serverIndex)
|
||||
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
|
||||
|
||||
auto publicKeyInfo = apiConfig.value(configKey::publicKeyInfo).toObject();
|
||||
const QString endDate = publicKeyInfo.value(configKey::endDate).toString();
|
||||
if (endDate.isEmpty()) {
|
||||
publicKeyInfo.insert(configKey::endDate, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate));
|
||||
const QString expiresAt = publicKeyInfo.value(configKey::expiresAt).toString();
|
||||
if (expiresAt.isEmpty()) {
|
||||
publicKeyInfo.insert(configKey::expiresAt, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate));
|
||||
apiConfig.insert(configKey::publicKeyInfo, publicKeyInfo);
|
||||
serverConfig.insert(configKey::apiConfig, apiConfig);
|
||||
editServer(serverConfig, serverIndex);
|
||||
@@ -728,8 +749,8 @@ bool ServersModel::isApiKeyExpired(const int serverIndex)
|
||||
return false;
|
||||
}
|
||||
|
||||
auto endDateDateTime = QDateTime::fromString(endDate, Qt::ISODate).toUTC();
|
||||
if (endDateDateTime < QDateTime::currentDateTimeUtc()) {
|
||||
auto expiresAtDateTime = QDateTime::fromString(expiresAt, Qt::ISODate).toUTC();
|
||||
if (expiresAtDateTime < QDateTime::currentDateTimeUtc()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -771,5 +792,5 @@ const QString ServersModel::getDefaultServerImagePathCollapsed()
|
||||
if (countryCode.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return QString("qrc:/countriesFlags/images/flagKit/%1.svg").arg(countryCode);
|
||||
return QString("qrc:/countriesFlags/images/flagKit/%1.svg").arg(countryCode.toUpper());
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ public:
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
|
||||
bool setData(const int index, const QVariant &value, int role = Qt::EditRole);
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QVariant data(const int index, int role = Qt::DisplayRole) const;
|
||||
|
||||
@@ -115,6 +116,7 @@ public slots:
|
||||
QVariant getDefaultServerData(const QString roleString);
|
||||
|
||||
QVariant getProcessedServerData(const QString roleString);
|
||||
bool setProcessedServerData(const QString &roleString, const QVariant &value);
|
||||
|
||||
bool isDefaultServerDefaultContainerHasSplitTunneling();
|
||||
|
||||
@@ -127,6 +129,9 @@ protected:
|
||||
|
||||
signals:
|
||||
void processedServerIndexChanged(const int index);
|
||||
// emitted when the processed server index or processed server data is changed
|
||||
void processedServerChanged();
|
||||
|
||||
void defaultServerIndexChanged(const int index);
|
||||
void defaultServerNameChanged();
|
||||
void defaultServerDescriptionChanged();
|
||||
|
||||
@@ -16,6 +16,32 @@ Button {
|
||||
property string connectedButtonColor: AmneziaStyle.color.goldenApricot
|
||||
property bool buttonActiveFocus: activeFocus && (Qt.platform.os !== "android" || SettingsController.isOnTv())
|
||||
|
||||
property bool isFocusable: true
|
||||
|
||||
Keys.onTabPressed: {
|
||||
FocusController.nextKeyTabItem()
|
||||
}
|
||||
|
||||
Keys.onBacktabPressed: {
|
||||
FocusController.previousKeyTabItem()
|
||||
}
|
||||
|
||||
Keys.onUpPressed: {
|
||||
FocusController.nextKeyUpItem()
|
||||
}
|
||||
|
||||
Keys.onDownPressed: {
|
||||
FocusController.nextKeyDownItem()
|
||||
}
|
||||
|
||||
Keys.onLeftPressed: {
|
||||
FocusController.nextKeyLeftItem()
|
||||
}
|
||||
|
||||
Keys.onRightPressed: {
|
||||
FocusController.nextKeyRightItem()
|
||||
}
|
||||
|
||||
implicitWidth: 190
|
||||
implicitHeight: 190
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ DrawerType2 {
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
expandedContent: ColumnLayout {
|
||||
expandedStateContent: ColumnLayout {
|
||||
id: content
|
||||
|
||||
anchors.top: parent.top
|
||||
@@ -26,14 +26,6 @@ DrawerType2 {
|
||||
root.expandedHeight = content.implicitHeight + 32
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
enabled: !GC.isMobile()
|
||||
function onOpened() {
|
||||
focusItem.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Header2Type {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
@@ -44,11 +36,6 @@ DrawerType2 {
|
||||
headerText: qsTr("Add new connection")
|
||||
}
|
||||
|
||||
Item {
|
||||
id: focusItem
|
||||
KeyNavigation.tab: ip.rightButton
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
id: ip
|
||||
Layout.fillWidth: true
|
||||
@@ -59,10 +46,8 @@ DrawerType2 {
|
||||
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageSetupWizardCredentials)
|
||||
root.close()
|
||||
root.closeTriggered()
|
||||
}
|
||||
|
||||
KeyNavigation.tab: qrCode.rightButton
|
||||
}
|
||||
|
||||
DividerType {}
|
||||
@@ -76,10 +61,8 @@ DrawerType2 {
|
||||
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageSetupWizardConfigSource)
|
||||
root.close()
|
||||
root.closeTriggered()
|
||||
}
|
||||
|
||||
KeyNavigation.tab: focusItem
|
||||
}
|
||||
|
||||
DividerType {}
|
||||
|
||||
@@ -17,55 +17,15 @@ ListView {
|
||||
property var rootWidth
|
||||
property var selectedText
|
||||
|
||||
property bool a: true
|
||||
|
||||
width: rootWidth
|
||||
height: menuContent.contentItem.height
|
||||
height: contentItem.height
|
||||
|
||||
clip: true
|
||||
interactive: false
|
||||
snapMode: ListView.SnapToItem
|
||||
|
||||
property FlickableType parentFlickable
|
||||
property var lastItemTabClicked
|
||||
ScrollBar.vertical: ScrollBarType {}
|
||||
|
||||
property int currentFocusIndex: 0
|
||||
|
||||
activeFocusOnTab: true
|
||||
onActiveFocusChanged: {
|
||||
if (activeFocus) {
|
||||
this.currentFocusIndex = 0
|
||||
this.itemAtIndex(currentFocusIndex).forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onTabPressed: {
|
||||
if (currentFocusIndex < this.count - 1) {
|
||||
currentFocusIndex += 1
|
||||
this.itemAtIndex(currentFocusIndex).forceActiveFocus()
|
||||
} else {
|
||||
currentFocusIndex = 0
|
||||
if (lastItemTabClicked && typeof lastItemTabClicked === "function") {
|
||||
lastItemTabClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
currentFocusIndex = 0
|
||||
focusItem.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: focusItem
|
||||
}
|
||||
|
||||
onCurrentFocusIndexChanged: {
|
||||
if (parentFlickable) {
|
||||
parentFlickable.ensureVisible(this.itemAtIndex(currentFocusIndex))
|
||||
}
|
||||
}
|
||||
property bool isFocusable: true
|
||||
|
||||
ButtonGroup {
|
||||
id: containersRadioButtonGroup
|
||||
@@ -75,12 +35,6 @@ ListView {
|
||||
implicitWidth: rootWidth
|
||||
implicitHeight: content.implicitHeight
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if (activeFocus) {
|
||||
containerRadioButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
|
||||
@@ -111,13 +65,13 @@ ListView {
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
containersDropDown.close()
|
||||
containersDropDown.closeTriggered()
|
||||
ServersModel.setDefaultContainer(ServersModel.defaultIndex, proxyDefaultServerContainersModel.mapToSource(index))
|
||||
} else {
|
||||
ContainersModel.setProcessedContainerIndex(proxyDefaultServerContainersModel.mapToSource(index))
|
||||
InstallController.setShouldCreateServer(false)
|
||||
PageController.goToPage(PageEnum.PageSetupWizardProtocolSettings)
|
||||
containersDropDown.close()
|
||||
containersDropDown.closeTriggered()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ DrawerType2 {
|
||||
anchors.fill: parent
|
||||
expandedHeight: parent.height * 0.9
|
||||
|
||||
expandedContent: ColumnLayout {
|
||||
expandedStateContent: ColumnLayout {
|
||||
id: content
|
||||
|
||||
anchors.top: parent.top
|
||||
@@ -24,14 +24,6 @@ DrawerType2 {
|
||||
anchors.right: parent.right
|
||||
spacing: 0
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
enabled: !GC.isMobile()
|
||||
function onOpened() {
|
||||
focusItem.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Header2Type {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
@@ -43,11 +35,6 @@ DrawerType2 {
|
||||
descriptionText: qsTr("Allows you to connect to some sites or applications through a VPN connection and bypass others")
|
||||
}
|
||||
|
||||
Item {
|
||||
id: focusItem
|
||||
KeyNavigation.tab: splitTunnelingSwitch.visible ? splitTunnelingSwitch : siteBasedSplitTunnelingSwitch.rightButton
|
||||
}
|
||||
|
||||
LabelWithButtonType {
|
||||
id: splitTunnelingSwitch
|
||||
Layout.fillWidth: true
|
||||
@@ -59,11 +46,9 @@ DrawerType2 {
|
||||
descriptionText: qsTr("Enabled \nCan't be disabled for current server")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
KeyNavigation.tab: siteBasedSplitTunnelingSwitch.visible ? siteBasedSplitTunnelingSwitch.rightButton : focusItem
|
||||
|
||||
clickedFunction: function() {
|
||||
// PageController.goToPage(PageEnum.PageSettingsSplitTunneling)
|
||||
// root.close()
|
||||
PageController.goToPage(PageEnum.PageSettingsSplitTunneling)
|
||||
root.closeTriggered()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,13 +65,9 @@ DrawerType2 {
|
||||
descriptionText: enabled && SitesModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
KeyNavigation.tab: appSplitTunnelingSwitch.visible ?
|
||||
appSplitTunnelingSwitch.rightButton :
|
||||
focusItem
|
||||
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageSettingsSplitTunneling)
|
||||
root.close()
|
||||
root.closeTriggered()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,11 +84,9 @@ DrawerType2 {
|
||||
descriptionText: AppSplitTunnelingModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled")
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
KeyNavigation.tab: focusItem
|
||||
|
||||
clickedFunction: function() {
|
||||
PageController.goToPage(PageEnum.PageSettingsAppSplitTunneling)
|
||||
root.close()
|
||||
root.closeTriggered()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ DrawerType2 {
|
||||
id: installedAppsModel
|
||||
}
|
||||
|
||||
expandedContent: Item {
|
||||
expandedStateContent: Item {
|
||||
id: container
|
||||
|
||||
implicitHeight: expandedHeight
|
||||
@@ -43,7 +43,7 @@ DrawerType2 {
|
||||
BackButtonType {
|
||||
backButtonImage: "qrc:/images/controls/arrow-left.svg"
|
||||
backButtonFunction: function() {
|
||||
root.close()
|
||||
root.closeTriggered()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@ DrawerType2 {
|
||||
clip: true
|
||||
interactive: true
|
||||
|
||||
property bool isFocusable: true
|
||||
|
||||
model: SortFilterProxyModel {
|
||||
id: proxyInstalledAppsModel
|
||||
sourceModel: installedAppsModel
|
||||
@@ -79,10 +81,7 @@ DrawerType2 {
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
id: scrollBar
|
||||
policy: ScrollBar.AlwaysOn
|
||||
}
|
||||
ScrollBar.vertical: ScrollBarType {}
|
||||
|
||||
ButtonGroup {
|
||||
id: buttonGroup
|
||||
@@ -155,7 +154,7 @@ DrawerType2 {
|
||||
PageController.showBusyIndicator(true)
|
||||
AppSplitTunnelingController.addApps(installedAppsModel.getSelectedAppsInfo())
|
||||
PageController.showBusyIndicator(false)
|
||||
root.close()
|
||||
root.closeTriggered()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ DrawerType2 {
|
||||
property var yesButtonFunction
|
||||
property var noButtonFunction
|
||||
|
||||
expandedContent: ColumnLayout {
|
||||
expandedStateContent: ColumnLayout {
|
||||
id: content
|
||||
|
||||
anchors.top: parent.top
|
||||
@@ -33,14 +33,6 @@ DrawerType2 {
|
||||
root.expandedHeight = content.implicitHeight + 32
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
enabled: !GC.isMobile()
|
||||
function onOpened() {
|
||||
focusItem.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Header2TextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
@@ -59,11 +51,6 @@ DrawerType2 {
|
||||
text: descriptionText
|
||||
}
|
||||
|
||||
Item {
|
||||
id: focusItem
|
||||
KeyNavigation.tab: yesButton
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: yesButton
|
||||
Layout.fillWidth: true
|
||||
@@ -78,8 +65,6 @@ DrawerType2 {
|
||||
yesButtonFunction()
|
||||
}
|
||||
}
|
||||
|
||||
KeyNavigation.tab: noButton
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
@@ -102,8 +87,6 @@ DrawerType2 {
|
||||
noButtonFunction()
|
||||
}
|
||||
}
|
||||
|
||||
KeyNavigation.tab: focusItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import "../Config"
|
||||
DrawerType2 {
|
||||
id: root
|
||||
|
||||
expandedContent: Item {
|
||||
expandedStateContent: Item {
|
||||
id: container
|
||||
|
||||
implicitHeight: root.height * 0.9
|
||||
@@ -20,19 +20,6 @@ DrawerType2 {
|
||||
root.expandedHeight = container.implicitHeight
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
enabled: !GC.isMobile()
|
||||
function onOpened() {
|
||||
focusItem.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: focusItem
|
||||
KeyNavigation.tab: backButton
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: backButtonLayout
|
||||
|
||||
@@ -43,167 +30,148 @@ DrawerType2 {
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
backButtonImage: "qrc:/images/controls/arrow-left.svg"
|
||||
backButtonFunction: function() { root.close() }
|
||||
KeyNavigation.tab: listView
|
||||
backButtonFunction: function() { root.closeTriggered() }
|
||||
}
|
||||
|
||||
Header2Type {
|
||||
id: header
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
headerText: qsTr("Choose language")
|
||||
}
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
ListView {
|
||||
id: listView
|
||||
|
||||
anchors.top: backButtonLayout.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
contentHeight: content.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
property bool isFocusable: true
|
||||
property int selectedIndex: LanguageModel.currentLanguageIndex
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
reuseItems: true
|
||||
|
||||
Header2Type {
|
||||
id: header
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
ScrollBar.vertical: ScrollBarType {}
|
||||
|
||||
headerText: qsTr("Choose language")
|
||||
}
|
||||
model: LanguageModel
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
ButtonGroup {
|
||||
id: buttonGroup
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
height: listView.contentItem.height
|
||||
delegate: Item {
|
||||
implicitWidth: root.width
|
||||
implicitHeight: delegateContent.implicitHeight
|
||||
|
||||
clip: true
|
||||
interactive: false
|
||||
ColumnLayout {
|
||||
id: delegateContent
|
||||
|
||||
model: LanguageModel
|
||||
currentIndex: LanguageModel.currentLanguageIndex
|
||||
anchors.fill: parent
|
||||
|
||||
ButtonGroup {
|
||||
id: buttonGroup
|
||||
}
|
||||
RadioButton {
|
||||
id: radioButton
|
||||
|
||||
property int currentFocusIndex: 0
|
||||
implicitWidth: parent.width
|
||||
implicitHeight: radioButtonContent.implicitHeight
|
||||
|
||||
activeFocusOnTab: true
|
||||
onActiveFocusChanged: {
|
||||
if (activeFocus) {
|
||||
this.currentFocusIndex = 0
|
||||
this.itemAtIndex(currentFocusIndex).forceActiveFocus()
|
||||
}
|
||||
}
|
||||
hoverEnabled: true
|
||||
|
||||
Keys.onTabPressed: {
|
||||
if (currentFocusIndex < this.count - 1) {
|
||||
currentFocusIndex += 1
|
||||
this.itemAtIndex(currentFocusIndex).forceActiveFocus()
|
||||
} else {
|
||||
listViewFocusItem.forceActiveFocus()
|
||||
focusItem.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
property bool isFocusable: true
|
||||
|
||||
Item {
|
||||
id: listViewFocusItem
|
||||
Keys.onTabPressed: {
|
||||
root.forceActiveFocus()
|
||||
FocusController.nextKeyTabItem()
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
listViewFocusItem.forceActiveFocus()
|
||||
focusItem.forceActiveFocus()
|
||||
Keys.onBacktabPressed: {
|
||||
FocusController.previousKeyTabItem()
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
implicitWidth: root.width
|
||||
implicitHeight: delegateContent.implicitHeight
|
||||
Keys.onUpPressed: {
|
||||
FocusController.nextKeyUpItem()
|
||||
}
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if (activeFocus) {
|
||||
radioButton.forceActiveFocus()
|
||||
Keys.onDownPressed: {
|
||||
FocusController.nextKeyDownItem()
|
||||
}
|
||||
|
||||
Keys.onLeftPressed: {
|
||||
FocusController.nextKeyLeftItem()
|
||||
}
|
||||
|
||||
Keys.onRightPressed: {
|
||||
FocusController.nextKeyRightItem()
|
||||
}
|
||||
|
||||
indicator: Rectangle {
|
||||
width: parent.width - 1
|
||||
height: parent.height
|
||||
color: radioButton.hovered ? AmneziaStyle.color.slateGray : AmneziaStyle.color.onyxBlack
|
||||
border.color: radioButton.focus ? AmneziaStyle.color.paleGray : AmneziaStyle.color.transparent
|
||||
border.width: radioButton.focus ? 1 : 0
|
||||
|
||||
Behavior on color {
|
||||
PropertyAnimation { duration: 200 }
|
||||
}
|
||||
Behavior on border.color {
|
||||
PropertyAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: delegateContent
|
||||
|
||||
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 {
|
||||
width: parent.width - 1
|
||||
height: parent.height
|
||||
color: radioButton.hovered ? AmneziaStyle.color.slateGray : AmneziaStyle.color.onyxBlack
|
||||
border.color: radioButton.focus ? AmneziaStyle.color.paleGray : AmneziaStyle.color.transparent
|
||||
border.width: radioButton.focus ? 1 : 0
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 20
|
||||
Layout.bottomMargin: 20
|
||||
|
||||
Behavior on color {
|
||||
PropertyAnimation { duration: 200 }
|
||||
}
|
||||
Behavior on border.color {
|
||||
PropertyAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
text: languageName
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: radioButtonContent
|
||||
anchors.fill: parent
|
||||
Image {
|
||||
source: "qrc:/images/controls/check.svg"
|
||||
visible: radioButton.checked
|
||||
|
||||
anchors.rightMargin: 16
|
||||
anchors.leftMargin: 16
|
||||
width: 24
|
||||
height: 24
|
||||
|
||||
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
|
||||
|
||||
onClicked: {
|
||||
listView.currentIndex = index
|
||||
LanguageModel.changeLanguage(languageIndex)
|
||||
root.close()
|
||||
}
|
||||
Layout.rightMargin: 8
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: radioButton.clicked()
|
||||
Keys.onReturnPressed: radioButton.clicked()
|
||||
ButtonGroup.group: buttonGroup
|
||||
checked: listView.selectedIndex === index
|
||||
|
||||
onClicked: {
|
||||
listView.selectedIndex = index
|
||||
LanguageModel.changeLanguage(languageIndex)
|
||||
root.closeTriggered()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: radioButton.clicked()
|
||||
Keys.onReturnPressed: radioButton.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
126
client/ui/qml/Components/ServersListView.qml
Normal file
126
client/ui/qml/Components/ServersListView.qml
Normal file
@@ -0,0 +1,126 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
import PageEnum 1.0
|
||||
import ProtocolEnum 1.0
|
||||
import ContainerProps 1.0
|
||||
import ContainersModelFilters 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
|
||||
ListView {
|
||||
id: root
|
||||
|
||||
property int selectedIndex: ServersModel.defaultIndex
|
||||
|
||||
anchors.top: serversMenuHeader.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: 16
|
||||
|
||||
model: ServersModel
|
||||
|
||||
ScrollBar.vertical: ScrollBarType {}
|
||||
|
||||
property bool isFocusable: true
|
||||
|
||||
Connections {
|
||||
target: ServersModel
|
||||
function onDefaultServerIndexChanged(serverIndex) {
|
||||
root.selectedIndex = serverIndex
|
||||
}
|
||||
}
|
||||
|
||||
clip: true
|
||||
reuseItems: true
|
||||
|
||||
delegate: Item {
|
||||
id: menuContentDelegate
|
||||
objectName: "menuContentDelegate"
|
||||
|
||||
property variant delegateData: model
|
||||
property VerticalRadioButton serverRadioButtonProperty: serverRadioButton
|
||||
|
||||
implicitWidth: root.width
|
||||
implicitHeight: serverRadioButtonContent.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: serverRadioButtonContent
|
||||
objectName: "serverRadioButtonContent"
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 16
|
||||
anchors.leftMargin: 16
|
||||
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
objectName: "serverRadioButtonRowLayout"
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
VerticalRadioButton {
|
||||
id: serverRadioButton
|
||||
objectName: "serverRadioButton"
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: name
|
||||
descriptionText: serverDescription
|
||||
|
||||
checked: index === root.selectedIndex
|
||||
checkable: !ConnectionController.isConnected
|
||||
|
||||
ButtonGroup.group: serversRadioButtonGroup
|
||||
|
||||
onClicked: {
|
||||
if (ConnectionController.isConnected) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change server while there is an active connection"))
|
||||
return
|
||||
}
|
||||
|
||||
root.selectedIndex = index
|
||||
|
||||
ServersModel.defaultIndex = index
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: serverRadioButton.clicked()
|
||||
Keys.onReturnPressed: serverRadioButton.clicked()
|
||||
}
|
||||
|
||||
ImageButtonType {
|
||||
id: serverInfoButton
|
||||
objectName: "serverInfoButton"
|
||||
|
||||
image: "qrc:/images/controls/settings.svg"
|
||||
imageColor: AmneziaStyle.color.paleGray
|
||||
|
||||
implicitWidth: 56
|
||||
implicitHeight: 56
|
||||
|
||||
z: 1
|
||||
|
||||
onClicked: function() {
|
||||
ServersModel.processedIndex = index
|
||||
PageController.goToPage(PageEnum.PageSettingsServerInfo)
|
||||
drawer.closeTriggered()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 0
|
||||
Layout.rightMargin: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,47 +20,14 @@ ListView {
|
||||
height: root.contentItem.height
|
||||
|
||||
clip: true
|
||||
interactive: false
|
||||
reuseItems: true
|
||||
|
||||
activeFocusOnTab: true
|
||||
Keys.onTabPressed: {
|
||||
if (currentIndex < this.count - 1) {
|
||||
this.incrementCurrentIndex()
|
||||
} else {
|
||||
currentIndex = 0
|
||||
lastItemTabClickedSignal()
|
||||
}
|
||||
}
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
if (visible) {
|
||||
if (fl.contentHeight > fl.height) {
|
||||
var item = this.currentItem
|
||||
if (item.y < fl.height) {
|
||||
fl.contentY = item.y
|
||||
} else if (item.y + item.height > fl.contentY + fl.height) {
|
||||
fl.contentY = item.y + item.height - fl.height
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
this.currentIndex = 0
|
||||
}
|
||||
}
|
||||
property bool isFocusable: false
|
||||
|
||||
delegate: Item {
|
||||
implicitWidth: root.width
|
||||
implicitHeight: delegateContent.implicitHeight
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if (activeFocus) {
|
||||
containerRadioButton.rightButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: delegateContent
|
||||
|
||||
|
||||
@@ -36,17 +36,9 @@ DrawerType2 {
|
||||
configFileName = "amnezia_config"
|
||||
}
|
||||
|
||||
expandedContent: Item {
|
||||
expandedStateContent: Item {
|
||||
implicitHeight: root.expandedHeight
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
enabled: !GC.isMobile()
|
||||
function onOpened() {
|
||||
header.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Header2Type {
|
||||
id: header
|
||||
anchors.top: parent.top
|
||||
@@ -57,24 +49,27 @@ DrawerType2 {
|
||||
anchors.rightMargin: 16
|
||||
|
||||
headerText: root.headerText
|
||||
|
||||
KeyNavigation.tab: shareButton
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
ListView {
|
||||
id: listView
|
||||
|
||||
anchors.top: header.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
contentHeight: content.height + 32
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
property bool isFocusable: true
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
ScrollBar.vertical: ScrollBarType {}
|
||||
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
model: 1
|
||||
|
||||
clip: true
|
||||
reuseItems: true
|
||||
|
||||
header: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
visible: root.contentVisible
|
||||
|
||||
@@ -82,11 +77,11 @@ DrawerType2 {
|
||||
id: shareButton
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: qsTr("Share")
|
||||
imageSource: "qrc:/images/controls/share-2.svg"
|
||||
|
||||
KeyNavigation.tab: copyConfigTextButton
|
||||
leftImageSource: "qrc:/images/controls/share-2.svg"
|
||||
|
||||
clickedFunc: function() {
|
||||
var fileName = ""
|
||||
@@ -111,6 +106,8 @@ DrawerType2 {
|
||||
id: copyConfigTextButton
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
defaultColor: AmneziaStyle.color.transparent
|
||||
hoveredColor: AmneziaStyle.color.translucentWhite
|
||||
@@ -120,18 +117,18 @@ DrawerType2 {
|
||||
borderWidth: 1
|
||||
|
||||
text: qsTr("Copy")
|
||||
imageSource: "qrc:/images/controls/copy.svg"
|
||||
leftImageSource: "qrc:/images/controls/copy.svg"
|
||||
|
||||
Keys.onReturnPressed: { copyConfigTextButton.clicked() }
|
||||
Keys.onEnterPressed: { copyConfigTextButton.clicked() }
|
||||
|
||||
KeyNavigation.tab: copyNativeConfigStringButton.visible ? copyNativeConfigStringButton : showSettingsButton
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: copyNativeConfigStringButton
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
visible: false
|
||||
|
||||
@@ -143,7 +140,7 @@ DrawerType2 {
|
||||
borderWidth: 1
|
||||
|
||||
text: qsTr("Copy config string")
|
||||
imageSource: "qrc:/images/controls/copy.svg"
|
||||
leftImageSource: "qrc:/images/controls/copy.svg"
|
||||
|
||||
KeyNavigation.tab: showSettingsButton
|
||||
}
|
||||
@@ -153,6 +150,8 @@ DrawerType2 {
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
defaultColor: AmneziaStyle.color.transparent
|
||||
hoveredColor: AmneziaStyle.color.translucentWhite
|
||||
@@ -164,10 +163,8 @@ DrawerType2 {
|
||||
text: qsTr("Show connection settings")
|
||||
|
||||
clickedFunc: function() {
|
||||
configContentDrawer.open()
|
||||
configContentDrawer.openTriggered()
|
||||
}
|
||||
|
||||
KeyNavigation.tab: header
|
||||
}
|
||||
|
||||
DrawerType2 {
|
||||
@@ -178,30 +175,11 @@ DrawerType2 {
|
||||
anchors.fill: parent
|
||||
expandedHeight: parent.height * 0.9
|
||||
|
||||
onClosed: {
|
||||
if (!GC.isMobile()) {
|
||||
header.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
expandedContent: Item {
|
||||
expandedStateContent: Item {
|
||||
id: configContentContainer
|
||||
|
||||
implicitHeight: configContentDrawer.expandedHeight
|
||||
|
||||
Connections {
|
||||
target: configContentDrawer
|
||||
enabled: !GC.isMobile()
|
||||
function onOpened() {
|
||||
focusItem.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: focusItem
|
||||
KeyNavigation.tab: backButton
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: copyNativeConfigStringButton
|
||||
function onClicked() {
|
||||
@@ -231,9 +209,7 @@ DrawerType2 {
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 16
|
||||
|
||||
backButtonFunction: function() { configContentDrawer.close() }
|
||||
|
||||
KeyNavigation.tab: focusItem
|
||||
backButtonFunction: function() { configContentDrawer.closeTriggered() }
|
||||
}
|
||||
|
||||
FlickableType {
|
||||
@@ -302,6 +278,10 @@ DrawerType2 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
Rectangle {
|
||||
id: qrCodeContainer
|
||||
@@ -309,6 +289,8 @@ DrawerType2 {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width
|
||||
Layout.topMargin: 20
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
visible: ExportController.qrCodesCount > 0
|
||||
|
||||
@@ -320,6 +302,32 @@ DrawerType2 {
|
||||
|
||||
source: ExportController.qrCodesCount ? ExportController.qrCodes[0] : ""
|
||||
|
||||
property bool isFocusable: true
|
||||
|
||||
Keys.onTabPressed: {
|
||||
FocusController.nextKeyTabItem()
|
||||
}
|
||||
|
||||
Keys.onBacktabPressed: {
|
||||
FocusController.previousKeyTabItem()
|
||||
}
|
||||
|
||||
Keys.onUpPressed: {
|
||||
FocusController.nextKeyUpItem()
|
||||
}
|
||||
|
||||
Keys.onDownPressed: {
|
||||
FocusController.nextKeyDownItem()
|
||||
}
|
||||
|
||||
Keys.onLeftPressed: {
|
||||
FocusController.nextKeyLeftItem()
|
||||
}
|
||||
|
||||
Keys.onRightPressed: {
|
||||
FocusController.nextKeyRightItem()
|
||||
}
|
||||
|
||||
Timer {
|
||||
property int index: 0
|
||||
interval: 1000
|
||||
@@ -346,6 +354,8 @@ DrawerType2 {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 32
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
visible: ExportController.qrCodesCount > 0
|
||||
|
||||
|
||||
@@ -39,8 +39,6 @@ Rectangle {
|
||||
implicitWidth: (rootWidth - 32) / 2
|
||||
text: "UDP"
|
||||
|
||||
KeyNavigation.tab: tcpButton
|
||||
|
||||
onClicked: {
|
||||
root.currentIndex = 0
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import Qt5Compat.GraphicalEffects
|
||||
|
||||
import Style 1.0
|
||||
|
||||
Item {
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
property string backButtonImage: "qrc:/images/controls/arrow-left.svg"
|
||||
@@ -15,12 +15,6 @@ Item {
|
||||
|
||||
visible: backButtonImage !== ""
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if (activeFocus) {
|
||||
backButton.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: content
|
||||
|
||||
|
||||
@@ -22,9 +22,10 @@ Button {
|
||||
property int borderWidth: 0
|
||||
property int borderFocusedWidth: 1
|
||||
|
||||
property string imageSource
|
||||
property string leftImageSource
|
||||
property string rightImageSource
|
||||
property string leftImageColor: textColor
|
||||
property bool changeLeftImageSize: true
|
||||
|
||||
property bool squareLeftSide: false
|
||||
|
||||
@@ -34,10 +35,35 @@ Button {
|
||||
|
||||
property alias buttonTextLabel: buttonText
|
||||
|
||||
property bool isFocusable: true
|
||||
|
||||
Keys.onTabPressed: {
|
||||
FocusController.nextKeyTabItem()
|
||||
}
|
||||
|
||||
Keys.onBacktabPressed: {
|
||||
FocusController.previousKeyTabItem()
|
||||
}
|
||||
|
||||
Keys.onUpPressed: {
|
||||
FocusController.nextKeyUpItem()
|
||||
}
|
||||
|
||||
Keys.onDownPressed: {
|
||||
FocusController.nextKeyDownItem()
|
||||
}
|
||||
|
||||
Keys.onLeftPressed: {
|
||||
FocusController.nextKeyLeftItem()
|
||||
}
|
||||
|
||||
Keys.onRightPressed: {
|
||||
FocusController.nextKeyRightItem()
|
||||
}
|
||||
|
||||
implicitHeight: 56
|
||||
|
||||
hoverEnabled: true
|
||||
focusPolicy: Qt.TabFocus
|
||||
|
||||
onFocusChanged: {
|
||||
if (root.activeFocus) {
|
||||
@@ -127,24 +153,29 @@ Button {
|
||||
anchors.centerIn: parent
|
||||
|
||||
Image {
|
||||
Layout.preferredHeight: 20
|
||||
Layout.preferredWidth: 20
|
||||
|
||||
source: root.imageSource
|
||||
visible: root.imageSource === "" ? false : true
|
||||
id: leftImage
|
||||
source: root.leftImageSource
|
||||
visible: root.leftImageSource === "" ? false : true
|
||||
|
||||
layer {
|
||||
enabled: true
|
||||
enabled: leftImageColor !== "" ? true : false
|
||||
effect: ColorOverlay {
|
||||
color: leftImageColor
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.changeLeftImageSize) {
|
||||
leftImage.Layout.preferredHeight = 20
|
||||
leftImage.Layout.preferredWidth = 20
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ButtonTextType {
|
||||
id: buttonText
|
||||
|
||||
color: textColor
|
||||
color: root.textColor
|
||||
text: root.text
|
||||
visible: root.text === "" ? false : true
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Popup {
|
||||
visible: false
|
||||
|
||||
Overlay.modal: Rectangle {
|
||||
color: Qt.rgba(14/255, 14/255, 17/255, 0.8)
|
||||
color: AmneziaStyle.color.translucentMidnightBlack
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
|
||||
@@ -19,9 +19,10 @@ RadioButton {
|
||||
|
||||
property string textColor: AmneziaStyle.color.midnightBlack
|
||||
|
||||
property string pressedBorderColor: Qt.rgba(251/255, 178/255, 106/255, 0.3)
|
||||
property string pressedBorderColor: AmneziaStyle.color.softGoldenApricot
|
||||
property string selectedBorderColor: AmneziaStyle.color.goldenApricot
|
||||
property string defaultBodredColor: AmneziaStyle.color.transparent
|
||||
property string focusBorderColor: AmneziaStyle.color.paleGray
|
||||
property int borderWidth: 0
|
||||
|
||||
implicitWidth: content.implicitWidth
|
||||
@@ -29,6 +30,32 @@ RadioButton {
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
property bool isFocusable: true
|
||||
|
||||
Keys.onTabPressed: {
|
||||
FocusController.nextKeyTabItem()
|
||||
}
|
||||
|
||||
Keys.onBacktabPressed: {
|
||||
FocusController.previousKeyTabItem()
|
||||
}
|
||||
|
||||
Keys.onUpPressed: {
|
||||
FocusController.nextKeyUpItem()
|
||||
}
|
||||
|
||||
Keys.onDownPressed: {
|
||||
FocusController.nextKeyDownItem()
|
||||
}
|
||||
|
||||
Keys.onLeftPressed: {
|
||||
FocusController.nextKeyLeftItem()
|
||||
}
|
||||
|
||||
Keys.onRightPressed: {
|
||||
FocusController.nextKeyRightItem()
|
||||
}
|
||||
|
||||
indicator: Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 16
|
||||
@@ -52,6 +79,8 @@ RadioButton {
|
||||
return pressedBorderColor
|
||||
} else if (root.checked) {
|
||||
return selectedBorderColor
|
||||
} else if (root.activeFocus) {
|
||||
return focusBorderColor
|
||||
}
|
||||
}
|
||||
return defaultBodredColor
|
||||
@@ -59,7 +88,7 @@ RadioButton {
|
||||
|
||||
border.width: {
|
||||
if (root.enabled) {
|
||||
if(root.checked) {
|
||||
if(root.checked || root.activeFocus) {
|
||||
return 1
|
||||
}
|
||||
return root.pressed ? 1 : 0
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user