mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-03 09:02:07 +03:00
Compare commits
134 Commits
checking-s
...
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 |
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
|
||||||
23
.github/workflows/deploy.yml
vendored
23
.github/workflows/deploy.yml
vendored
@@ -217,7 +217,11 @@ jobs:
|
|||||||
export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/ios/bin"
|
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 QT_MACOS_ROOT_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos"
|
||||||
export PATH=$PATH:~/go/bin
|
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:
|
env:
|
||||||
IOS_TRUST_CERT_BASE64: ${{ secrets.IOS_TRUST_CERT_BASE64 }}
|
IOS_TRUST_CERT_BASE64: ${{ secrets.IOS_TRUST_CERT_BASE64 }}
|
||||||
IOS_SIGNING_CERT_BASE64: ${{ secrets.IOS_SIGNING_CERT_BASE64 }}
|
IOS_SIGNING_CERT_BASE64: ${{ secrets.IOS_SIGNING_CERT_BASE64 }}
|
||||||
@@ -256,7 +260,7 @@ jobs:
|
|||||||
- name: 'Setup xcode'
|
- name: 'Setup xcode'
|
||||||
uses: maxim-lobanov/setup-xcode@v1
|
uses: maxim-lobanov/setup-xcode@v1
|
||||||
with:
|
with:
|
||||||
xcode-version: '14.3.1'
|
xcode-version: '15.4.0'
|
||||||
|
|
||||||
- name: 'Install Qt'
|
- name: 'Install Qt'
|
||||||
uses: jurplel/install-qt-action@v3
|
uses: jurplel/install-qt-action@v3
|
||||||
@@ -331,7 +335,8 @@ jobs:
|
|||||||
arch: 'linux_gcc_64'
|
arch: 'linux_gcc_64'
|
||||||
modules: ${{ env.QT_MODULES }}
|
modules: ${{ env.QT_MODULES }}
|
||||||
dir: ${{ runner.temp }}
|
dir: ${{ runner.temp }}
|
||||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
py7zrversion: '==0.22.*'
|
||||||
|
extra: '--base ${{ env.QT_MIRROR }}'
|
||||||
|
|
||||||
- name: 'Install android_x86_64 Qt'
|
- name: 'Install android_x86_64 Qt'
|
||||||
uses: jurplel/install-qt-action@v4
|
uses: jurplel/install-qt-action@v4
|
||||||
@@ -342,7 +347,8 @@ jobs:
|
|||||||
arch: 'android_x86_64'
|
arch: 'android_x86_64'
|
||||||
modules: ${{ env.QT_MODULES }}
|
modules: ${{ env.QT_MODULES }}
|
||||||
dir: ${{ runner.temp }}
|
dir: ${{ runner.temp }}
|
||||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
py7zrversion: '==0.22.*'
|
||||||
|
extra: '--base ${{ env.QT_MIRROR }}'
|
||||||
|
|
||||||
- name: 'Install android_x86 Qt'
|
- name: 'Install android_x86 Qt'
|
||||||
uses: jurplel/install-qt-action@v4
|
uses: jurplel/install-qt-action@v4
|
||||||
@@ -353,7 +359,8 @@ jobs:
|
|||||||
arch: 'android_x86'
|
arch: 'android_x86'
|
||||||
modules: ${{ env.QT_MODULES }}
|
modules: ${{ env.QT_MODULES }}
|
||||||
dir: ${{ runner.temp }}
|
dir: ${{ runner.temp }}
|
||||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
py7zrversion: '==0.22.*'
|
||||||
|
extra: '--base ${{ env.QT_MIRROR }}'
|
||||||
|
|
||||||
- name: 'Install android_armv7 Qt'
|
- name: 'Install android_armv7 Qt'
|
||||||
uses: jurplel/install-qt-action@v4
|
uses: jurplel/install-qt-action@v4
|
||||||
@@ -364,7 +371,8 @@ jobs:
|
|||||||
arch: 'android_armv7'
|
arch: 'android_armv7'
|
||||||
modules: ${{ env.QT_MODULES }}
|
modules: ${{ env.QT_MODULES }}
|
||||||
dir: ${{ runner.temp }}
|
dir: ${{ runner.temp }}
|
||||||
extra: '--external 7z --base ${{ env.QT_MIRROR }}'
|
py7zrversion: '==0.22.*'
|
||||||
|
extra: '--base ${{ env.QT_MIRROR }}'
|
||||||
|
|
||||||
- name: 'Install android_arm64_v8a Qt'
|
- name: 'Install android_arm64_v8a Qt'
|
||||||
uses: jurplel/install-qt-action@v4
|
uses: jurplel/install-qt-action@v4
|
||||||
@@ -375,7 +383,8 @@ jobs:
|
|||||||
arch: 'android_arm64_v8a'
|
arch: 'android_arm64_v8a'
|
||||||
modules: ${{ env.QT_MODULES }}
|
modules: ${{ env.QT_MODULES }}
|
||||||
dir: ${{ runner.temp }}
|
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'
|
- name: 'Grant execute permission for qt-cmake'
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
|||||||
|
|
||||||
set(PROJECT AmneziaVPN)
|
set(PROJECT AmneziaVPN)
|
||||||
|
|
||||||
project(${PROJECT} VERSION 4.8.2.3
|
project(${PROJECT} VERSION 4.8.3.0
|
||||||
DESCRIPTION "AmneziaVPN"
|
DESCRIPTION "AmneziaVPN"
|
||||||
HOMEPAGE_URL "https://amnezia.org/"
|
HOMEPAGE_URL "https://amnezia.org/"
|
||||||
)
|
)
|
||||||
@@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
|
|||||||
set(RELEASE_DATE "${CURRENT_DATE}")
|
set(RELEASE_DATE "${CURRENT_DATE}")
|
||||||
|
|
||||||
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||||
set(APP_ANDROID_VERSION_CODE 2069)
|
set(APP_ANDROID_VERSION_CODE 2072)
|
||||||
|
|
||||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||||
set(MZ_PLATFORM_NAME "linux")
|
set(MZ_PLATFORM_NAME "linux")
|
||||||
|
|||||||
30
README.md
30
README.md
@@ -1,24 +1,29 @@
|
|||||||
# Amnezia VPN
|
# 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://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev)
|
||||||
[](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client)
|
[](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://amnezia.org/downloads"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/download.png" width="150" style="max-width: 100%;"></a>
|
[](https://amnezia.org)
|
||||||
<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>
|
|
||||||
|
|
||||||
[Alternative download link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org/downloads)
|
### [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)
|
||||||
|
|
||||||
|
> [!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)
|
[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>
|
<a href="https://www.testiny.io"><img src="https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/testiny.png" height="28px"></a>
|
||||||
|
|
||||||
@@ -33,7 +38,8 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep
|
|||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
- [https://amnezia.org](https://amnezia.org) - project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org)
|
- [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://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_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)
|
- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi)
|
||||||
@@ -182,8 +188,8 @@ Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn
|
|||||||
Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p <br>
|
Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p <br>
|
||||||
USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4 <br>
|
USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4 <br>
|
||||||
USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d <br>
|
USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d <br>
|
||||||
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
|
XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3 <br>
|
||||||
|
TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
This project is tested with BrowserStack.
|
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) за поддержку нашего проекта.
|
||||||
@@ -146,6 +146,7 @@ set(HEADERS ${HEADERS}
|
|||||||
${CMAKE_CURRENT_LIST_DIR}/core/serialization/transfer.h
|
${CMAKE_CURRENT_LIST_DIR}/core/serialization/transfer.h
|
||||||
${CMAKE_CURRENT_LIST_DIR}/core/enums/apiEnums.h
|
${CMAKE_CURRENT_LIST_DIR}/core/enums/apiEnums.h
|
||||||
${CMAKE_CURRENT_LIST_DIR}/../common/logger/logger.h
|
${CMAKE_CURRENT_LIST_DIR}/../common/logger/logger.h
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/utils/qmlUtils.h
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mozilla headres
|
# Mozilla headres
|
||||||
@@ -197,6 +198,7 @@ set(SOURCES ${SOURCES}
|
|||||||
${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess.cpp
|
${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess_new.cpp
|
${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess_new.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/../common/logger/logger.cpp
|
${CMAKE_CURRENT_LIST_DIR}/../common/logger/logger.cpp
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/utils/qmlUtils.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mozilla sources
|
# Mozilla sources
|
||||||
|
|||||||
@@ -404,6 +404,9 @@ void AmneziaApplication::initControllers()
|
|||||||
m_pageController.reset(new PageController(m_serversModel, m_settings));
|
m_pageController.reset(new PageController(m_serversModel, m_settings));
|
||||||
m_engine->rootContext()->setContextProperty("PageController", m_pageController.get());
|
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_installController.reset(new InstallController(m_serversModel, m_containersModel, m_protocolsModel, m_clientManagementModel,
|
||||||
m_apiServicesModel, m_settings));
|
m_apiServicesModel, m_settings));
|
||||||
m_engine->rootContext()->setContextProperty("InstallController", m_installController.get());
|
m_engine->rootContext()->setContextProperty("InstallController", m_installController.get());
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include "ui/controllers/exportController.h"
|
#include "ui/controllers/exportController.h"
|
||||||
#include "ui/controllers/importController.h"
|
#include "ui/controllers/importController.h"
|
||||||
#include "ui/controllers/installController.h"
|
#include "ui/controllers/installController.h"
|
||||||
|
#include "ui/controllers/focusController.h"
|
||||||
#include "ui/controllers/pageController.h"
|
#include "ui/controllers/pageController.h"
|
||||||
#include "ui/controllers/settingsController.h"
|
#include "ui/controllers/settingsController.h"
|
||||||
#include "ui/controllers/sitesController.h"
|
#include "ui/controllers/sitesController.h"
|
||||||
@@ -124,6 +125,7 @@ private:
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
QScopedPointer<ConnectionController> m_connectionController;
|
QScopedPointer<ConnectionController> m_connectionController;
|
||||||
|
QScopedPointer<FocusController> m_focusController;
|
||||||
QScopedPointer<PageController> m_pageController;
|
QScopedPointer<PageController> m_pageController;
|
||||||
QScopedPointer<InstallController> m_installController;
|
QScopedPointer<InstallController> m_installController;
|
||||||
QScopedPointer<ImportController> m_importController;
|
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.any" android:required="false" />
|
||||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||||
<!-- for TV -->
|
<!-- 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" />
|
<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
|
<!-- 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" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<!-- To request network state -->
|
<!-- 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.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
@@ -91,6 +91,13 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@style/Translucent" />
|
android:theme="@style/Translucent" />
|
||||||
|
|
||||||
|
<activity android:name=".TvFilePicker"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@style/Translucent" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ImportConfigActivity"
|
android:name=".ImportConfigActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
|
|||||||
@@ -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="notificationSettingsDialogTitle">Настройки уведомлений</string>
|
||||||
<string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string>
|
<string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string>
|
||||||
<string name="openNotificationSettings">Открыть настройки уведомлений</string>
|
<string name="openNotificationSettings">Открыть настройки уведомлений</string>
|
||||||
|
|
||||||
|
<string name="tvNoFileBrowser">Пожалуйста, установите приложение для просмотра файлов</string>
|
||||||
</resources>
|
</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="notificationSettingsDialogTitle">Notification settings</string>
|
||||||
<string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system 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="openNotificationSettings">Open notification settings</string>
|
||||||
|
|
||||||
|
<string name="tvNoFileBrowser">Please install a file management utility to browse files</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -4,6 +4,7 @@ import android.Manifest
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -12,6 +13,7 @@ import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
|
|||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -20,8 +22,13 @@ import android.os.IBinder
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.Message
|
import android.os.Message
|
||||||
import android.os.Messenger
|
import android.os.Messenger
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.provider.OpenableColumns
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.view.WindowManager.LayoutParams
|
import android.view.WindowManager.LayoutParams
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@@ -30,6 +37,7 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.LazyThreadSafetyMode.NONE
|
import kotlin.LazyThreadSafetyMode.NONE
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.text.RegexOption.IGNORE_CASE
|
import kotlin.text.RegexOption.IGNORE_CASE
|
||||||
import AppListProvider
|
import AppListProvider
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
@@ -71,6 +79,7 @@ class AmneziaActivity : QtActivity() {
|
|||||||
private var isInBoundState = false
|
private var isInBoundState = false
|
||||||
private var notificationStateReceiver: BroadcastReceiver? = null
|
private var notificationStateReceiver: BroadcastReceiver? = null
|
||||||
private lateinit var vpnServiceMessenger: IpcMessenger
|
private lateinit var vpnServiceMessenger: IpcMessenger
|
||||||
|
private var pfd: ParcelFileDescriptor? = null
|
||||||
|
|
||||||
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
|
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
|
||||||
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
|
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
|
||||||
@@ -514,21 +523,25 @@ class AmneziaActivity : QtActivity() {
|
|||||||
type = "text/*"
|
type = "text/*"
|
||||||
putExtra(Intent.EXTRA_TITLE, fileName)
|
putExtra(Intent.EXTRA_TITLE, fileName)
|
||||||
}.also {
|
}.also {
|
||||||
startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler(
|
try {
|
||||||
onSuccess = {
|
startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler(
|
||||||
it?.data?.let { uri ->
|
onSuccess = {
|
||||||
Log.v(TAG, "Save file to $uri")
|
it?.data?.let { uri ->
|
||||||
try {
|
Log.v(TAG, "Save file to $uri")
|
||||||
contentResolver.openOutputStream(uri)?.use { os ->
|
try {
|
||||||
os.bufferedWriter().use { it.write(data) }
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -537,35 +550,46 @@ class AmneziaActivity : QtActivity() {
|
|||||||
fun openFile(filter: String?) {
|
fun openFile(filter: String?) {
|
||||||
Log.v(TAG, "Open file with filter: $filter")
|
Log.v(TAG, "Open file with filter: $filter")
|
||||||
mainScope.launch {
|
mainScope.launch {
|
||||||
val mimeTypes = if (!filter.isNullOrEmpty()) {
|
val intent = if (!isOnTv()) {
|
||||||
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
|
val mimeTypes = if (!filter.isNullOrEmpty()) {
|
||||||
val mime = MimeTypeMap.getSingleton()
|
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
|
||||||
extensionRegex.findAll(filter).map {
|
val mime = MimeTypeMap.getSingleton()
|
||||||
it.groups[1]?.value?.let { mime.getMimeTypeFromExtension(it) } ?: "*/*"
|
extensionRegex.findAll(filter).map {
|
||||||
}.toSet()
|
it.groups[1]?.value?.let { mime.getMimeTypeFromExtension(it) } ?: "*/*"
|
||||||
} else emptySet()
|
}.toSet()
|
||||||
|
} else emptySet()
|
||||||
|
|
||||||
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
Log.v(TAG, "File mimyType filter: $mimeTypes")
|
Log.v(TAG, "File mimyType filter: $mimeTypes")
|
||||||
if ("*/*" in mimeTypes) {
|
if ("*/*" in mimeTypes) {
|
||||||
type = "*/*"
|
type = "*/*"
|
||||||
} else {
|
} else {
|
||||||
when (mimeTypes.size) {
|
when (mimeTypes.size) {
|
||||||
1 -> type = mimeTypes.first()
|
1 -> type = mimeTypes.first()
|
||||||
|
|
||||||
in 2..Int.MAX_VALUE -> {
|
in 2..Int.MAX_VALUE -> {
|
||||||
type = "*/*"
|
type = "*/*"
|
||||||
putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
|
putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> type = "*/*"
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> type = "*/*"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.also {
|
} else {
|
||||||
startActivityForResult(it, OPEN_FILE_ACTION_CODE, ActivityResultHandler(
|
Intent(this@AmneziaActivity, TvFilePicker::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
startActivityForResult(intent, OPEN_FILE_ACTION_CODE, ActivityResultHandler(
|
||||||
onAny = {
|
onAny = {
|
||||||
val uri = it?.data?.toString() ?: ""
|
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")
|
Log.v(TAG, "Open file: $uri")
|
||||||
mainScope.launch {
|
mainScope.launch {
|
||||||
qtInitialized.await()
|
qtInitialized.await()
|
||||||
@@ -573,10 +597,68 @@ class AmneziaActivity : QtActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
|
} 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")
|
@Suppress("unused")
|
||||||
@SuppressLint("UnsupportedChromeOsCameraSystemFeature")
|
@SuppressLint("UnsupportedChromeOsCameraSystemFeature")
|
||||||
fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
|
fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
|
||||||
@@ -721,6 +803,50 @@ 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
|
// 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
|
// also disable right-click, as it causes the application to crash
|
||||||
private var lastButtonState = 0
|
private var lastButtonState = 0
|
||||||
@@ -770,6 +896,7 @@ class AmneziaActivity : QtActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
||||||
|
Log.v(TAG, "dispatchTouch: $ev")
|
||||||
if (ev != null && ev.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
|
if (ev != null && ev.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
|
||||||
return handleMouseEvent(ev) { super.dispatchTouchEvent(it) }
|
return handleMouseEvent(ev) { super.dispatchTouchEvent(it) }
|
||||||
}
|
}
|
||||||
@@ -784,6 +911,13 @@ class AmneziaActivity : QtActivity() {
|
|||||||
/**
|
/**
|
||||||
* Utils methods
|
* Utils methods
|
||||||
*/
|
*/
|
||||||
|
private fun <T> blockingCall(
|
||||||
|
context: CoroutineContext = Dispatchers.Main.immediate,
|
||||||
|
block: suspend () -> T
|
||||||
|
) = runBlocking {
|
||||||
|
mainScope.async(context) { block() }.await()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private fun actionCodeToString(actionCode: Int): String =
|
private fun actionCodeToString(actionCode: Int): String =
|
||||||
when (actionCode) {
|
when (actionCode) {
|
||||||
|
|||||||
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
package org.amnezia.vpn.protocol.wireguard
|
package org.amnezia.vpn.protocol.wireguard
|
||||||
|
|
||||||
import android.net.VpnService.Builder
|
import android.net.VpnService.Builder
|
||||||
import java.io.IOException
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import java.util.Locale
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.launch
|
||||||
import org.amnezia.awg.GoBackend
|
import org.amnezia.awg.GoBackend
|
||||||
import org.amnezia.vpn.protocol.Protocol
|
import org.amnezia.vpn.protocol.Protocol
|
||||||
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
|
import org.amnezia.vpn.protocol.ProtocolState.CONNECTED
|
||||||
@@ -27,6 +28,8 @@ open class Wireguard : Protocol() {
|
|||||||
|
|
||||||
private var tunnelHandle: Int = -1
|
private var tunnelHandle: Int = -1
|
||||||
protected open val ifName: String = "amn0"
|
protected open val ifName: String = "amn0"
|
||||||
|
private lateinit var scope: CoroutineScope
|
||||||
|
private var statusJob: Job? = null
|
||||||
|
|
||||||
override val statistics: Statistics
|
override val statistics: Statistics
|
||||||
get() {
|
get() {
|
||||||
@@ -49,46 +52,17 @@ open class Wireguard : Protocol() {
|
|||||||
|
|
||||||
override fun internalInit() {
|
override fun internalInit() {
|
||||||
if (!isInitialized) loadSharedLibrary(context, "wg-go")
|
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) {
|
override suspend fun startVpn(config: JSONObject, vpnBuilder: Builder, protect: (Int) -> Boolean) {
|
||||||
val wireguardConfig = parseConfig(config)
|
val wireguardConfig = parseConfig(config)
|
||||||
val startTime = System.currentTimeMillis()
|
|
||||||
start(wireguardConfig, vpnBuilder, protect)
|
start(wireguardConfig, vpnBuilder, protect)
|
||||||
waitForConnection(startTime)
|
|
||||||
state.value = CONNECTED
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun waitForConnection(startTime: Long) {
|
|
||||||
Log.d(TAG, "Waiting for connection")
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val time = String.format(Locale.ROOT,"%.3f", startTime / 1000.0)
|
|
||||||
try {
|
|
||||||
delay(1000)
|
|
||||||
var log = getLogcat(time)
|
|
||||||
Log.v(TAG, "First waiting log: $log")
|
|
||||||
// check that there is a connection log,
|
|
||||||
// to avoid infinite connection
|
|
||||||
if (!log.contains("Attaching to interface")) {
|
|
||||||
Log.w(TAG, "Logs do not contain a connection log")
|
|
||||||
return@withContext
|
|
||||||
}
|
|
||||||
while (!log.contains("Received handshake response")) {
|
|
||||||
delay(1000)
|
|
||||||
log = getLogcat(time)
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e(TAG, "Failed to get logcat: $e")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getLogcat(time: String): String =
|
|
||||||
ProcessBuilder("logcat", "--buffer=main", "--format=raw", "*:S AmneziaWG/awg0", "-t", time)
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
.start()
|
|
||||||
.inputStream.reader().readText()
|
|
||||||
|
|
||||||
protected open fun parseConfig(config: JSONObject): WireguardConfig {
|
protected open fun parseConfig(config: JSONObject): WireguardConfig {
|
||||||
val configData = config.getJSONObject("wireguard_config_data")
|
val configData = config.getJSONObject("wireguard_config_data")
|
||||||
return WireguardConfig.build {
|
return WireguardConfig.build {
|
||||||
@@ -178,6 +152,43 @@ open class Wireguard : Protocol() {
|
|||||||
tunnelHandle = -1
|
tunnelHandle = -1
|
||||||
throw VpnStartException("Protect VPN interface: permission not granted or revoked")
|
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() {
|
override fun stopVpn() {
|
||||||
@@ -185,6 +196,8 @@ open class Wireguard : Protocol() {
|
|||||||
Log.w(TAG, "Tunnel already down")
|
Log.w(TAG, "Tunnel already down")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
statusJob?.cancel()
|
||||||
|
statusJob = null
|
||||||
val handleToClose = tunnelHandle
|
val handleToClose = tunnelHandle
|
||||||
tunnelHandle = -1
|
tunnelHandle = -1
|
||||||
GoBackend.awgTurnOff(handleToClose)
|
GoBackend.awgTurnOff(handleToClose)
|
||||||
|
|||||||
@@ -3,38 +3,169 @@
|
|||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QUuid>
|
||||||
|
#include "logger.h"
|
||||||
|
|
||||||
#include "containers/containers_defs.h"
|
#include "containers/containers_defs.h"
|
||||||
#include "core/controllers/serverController.h"
|
#include "core/controllers/serverController.h"
|
||||||
#include "core/scripts_registry.h"
|
#include "core/scripts_registry.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
Logger logger("XrayConfigurator");
|
||||||
|
}
|
||||||
|
|
||||||
XrayConfigurator::XrayConfigurator(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController, QObject *parent)
|
XrayConfigurator::XrayConfigurator(std::shared_ptr<Settings> settings, const QSharedPointer<ServerController> &serverController, QObject *parent)
|
||||||
: ConfiguratorBase(settings, serverController, parent)
|
: ConfiguratorBase(settings, serverController, parent)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
|
QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container,
|
||||||
ErrorCode &errorCode)
|
const QJsonObject &containerConfig, ErrorCode &errorCode)
|
||||||
{
|
{
|
||||||
QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container),
|
// Generate new UUID for client
|
||||||
m_serverController->genVarsForScript(credentials, container, containerConfig));
|
QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
|
|
||||||
QString xrayPublicKey =
|
// Get current server config
|
||||||
m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode);
|
QString currentConfig = m_serverController->getTextFileFromContainer(
|
||||||
xrayPublicKey.replace("\n", "");
|
container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode);
|
||||||
|
|
||||||
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", "");
|
|
||||||
|
|
||||||
if (errorCode != ErrorCode::NoError) {
|
if (errorCode != ErrorCode::NoError) {
|
||||||
|
logger.error() << "Failed to get server config file";
|
||||||
return "";
|
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_PUBLIC_KEY", xrayPublicKey);
|
||||||
config.replace("$XRAY_SHORT_ID", xrayShortId);
|
config.replace("$XRAY_SHORT_ID", xrayShortId);
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ public:
|
|||||||
|
|
||||||
QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
|
QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
|
||||||
ErrorCode &errorCode);
|
ErrorCode &errorCode);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig,
|
||||||
|
ErrorCode &errorCode);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // XRAY_CONFIGURATOR_H
|
#endif // XRAY_CONFIGURATOR_H
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ namespace
|
|||||||
constexpr char authData[] = "auth_data";
|
constexpr char authData[] = "auth_data";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const int requestTimeoutMsecs = 12 * 1000; // 12 secs
|
||||||
|
|
||||||
ErrorCode checkErrors(const QList<QSslError> &sslErrors, QNetworkReply *reply)
|
ErrorCode checkErrors(const QList<QSslError> &sslErrors, QNetworkReply *reply)
|
||||||
{
|
{
|
||||||
if (!sslErrors.empty()) {
|
if (!sslErrors.empty()) {
|
||||||
@@ -177,7 +179,7 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle
|
|||||||
QStringList ApiController::getProxyUrls()
|
QStringList ApiController::getProxyUrls()
|
||||||
{
|
{
|
||||||
QNetworkRequest request;
|
QNetworkRequest request;
|
||||||
request.setTransferTimeout(7000);
|
request.setTransferTimeout(requestTimeoutMsecs);
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
|
||||||
QEventLoop wait;
|
QEventLoop wait;
|
||||||
@@ -280,7 +282,7 @@ void ApiController::updateServerConfigFromApi(const QString &installationUuid, c
|
|||||||
|
|
||||||
if (serverConfig.value(config_key::configVersion).toInt()) {
|
if (serverConfig.value(config_key::configVersion).toInt()) {
|
||||||
QNetworkRequest request;
|
QNetworkRequest request;
|
||||||
request.setTransferTimeout(7000);
|
request.setTransferTimeout(requestTimeoutMsecs);
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
request.setRawHeader("Authorization", "Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8());
|
request.setRawHeader("Authorization", "Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8());
|
||||||
QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString();
|
QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString();
|
||||||
@@ -336,7 +338,7 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody)
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
QNetworkRequest request;
|
QNetworkRequest request;
|
||||||
request.setTransferTimeout(7000);
|
request.setTransferTimeout(requestTimeoutMsecs);
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
|
||||||
request.setUrl(QString("%1v1/services").arg(m_gatewayEndpoint));
|
request.setUrl(QString("%1v1/services").arg(m_gatewayEndpoint));
|
||||||
@@ -377,6 +379,13 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody)
|
|||||||
|
|
||||||
auto errorCode = checkErrors(sslErrors, reply);
|
auto errorCode = checkErrors(sslErrors, reply);
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
|
|
||||||
|
if (errorCode == ErrorCode::NoError) {
|
||||||
|
if (!responseBody.contains("services")) {
|
||||||
|
return ErrorCode::ApiServicesMissingError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return errorCode;
|
return errorCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,7 +399,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
QNetworkRequest request;
|
QNetworkRequest request;
|
||||||
request.setTransferTimeout(7000);
|
request.setTransferTimeout(requestTimeoutMsecs);
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
|
||||||
request.setUrl(QString("%1v1/config").arg(m_gatewayEndpoint));
|
request.setUrl(QString("%1v1/config").arg(m_gatewayEndpoint));
|
||||||
|
|||||||
@@ -770,12 +770,6 @@ ErrorCode ServerController::isUserInSudo(const ServerCredentials &credentials, D
|
|||||||
|
|
||||||
if (!stdOut.contains("sudo"))
|
if (!stdOut.contains("sudo"))
|
||||||
return ErrorCode::ServerUserNotInSudo;
|
return ErrorCode::ServerUserNotInSudo;
|
||||||
if (stdOut.contains("command not found"))
|
|
||||||
return ErrorCode::SudoPackageIsNotPreinstalled;
|
|
||||||
if (stdOut.contains("sudoers"))
|
|
||||||
return ErrorCode::ServerUserNotListedInSudoers;
|
|
||||||
if (stdOut.contains("password is required"))
|
|
||||||
return ErrorCode::ServerUserPasswordRequired;
|
|
||||||
|
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,9 +56,6 @@ namespace amnezia
|
|||||||
ServerCancelInstallation = 204,
|
ServerCancelInstallation = 204,
|
||||||
ServerUserNotInSudo = 205,
|
ServerUserNotInSudo = 205,
|
||||||
ServerPacketManagerError = 206,
|
ServerPacketManagerError = 206,
|
||||||
SudoPackageIsNotPreinstalled = 207,
|
|
||||||
ServerUserNotListedInSudoers = 208,
|
|
||||||
ServerUserPasswordRequired = 209,
|
|
||||||
|
|
||||||
// Ssh connection errors
|
// Ssh connection errors
|
||||||
SshRequestDeniedError = 300,
|
SshRequestDeniedError = 300,
|
||||||
@@ -112,6 +109,7 @@ namespace amnezia
|
|||||||
ApiConfigSslError = 1104,
|
ApiConfigSslError = 1104,
|
||||||
ApiMissingAgwPublicKey = 1105,
|
ApiMissingAgwPublicKey = 1105,
|
||||||
ApiConfigDecryptionError = 1106,
|
ApiConfigDecryptionError = 1106,
|
||||||
|
ApiServicesMissingError = 1107,
|
||||||
|
|
||||||
// QFile errors
|
// QFile errors
|
||||||
OpenError = 1200,
|
OpenError = 1200,
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ QString errorString(ErrorCode code) {
|
|||||||
case(ErrorCode::ServerCancelInstallation): errorMessage = QObject::tr("Installation canceled by user"); break;
|
case(ErrorCode::ServerCancelInstallation): errorMessage = QObject::tr("Installation canceled by user"); break;
|
||||||
case(ErrorCode::ServerUserNotInSudo): errorMessage = QObject::tr("The user does not have permission to use sudo"); break;
|
case(ErrorCode::ServerUserNotInSudo): errorMessage = QObject::tr("The user does not have permission to use sudo"); break;
|
||||||
case(ErrorCode::ServerPacketManagerError): errorMessage = QObject::tr("Server error: Packet manager error"); break;
|
case(ErrorCode::ServerPacketManagerError): errorMessage = QObject::tr("Server error: Packet manager error"); break;
|
||||||
case(ErrorCode::SudoPackageIsNotPreinstalled): errorMessage = QObject::tr("The sudo package is not pre-installed"); break;
|
|
||||||
case(ErrorCode::ServerUserNotListedInSudoers): errorMessage = QObject::tr("The user is not listed in sudoers"); break;
|
|
||||||
case(ErrorCode::ServerUserPasswordRequired): errorMessage = QObject::tr("The user's password is required"); break;
|
|
||||||
|
|
||||||
// Libssh errors
|
// Libssh errors
|
||||||
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
|
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;
|
||||||
@@ -66,7 +63,8 @@ QString errorString(ErrorCode code) {
|
|||||||
case (ErrorCode::ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); 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::ApiMissingAgwPublicKey): errorMessage = QObject::tr("Missing AGW public key"); break;
|
||||||
case (ErrorCode::ApiConfigDecryptionError): errorMessage = QObject::tr("Failed to decrypt response payload"); 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
|
// QFile errors
|
||||||
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
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;
|
case(ErrorCode::ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break;
|
||||||
|
|||||||
@@ -163,9 +163,7 @@ QString AndroidController::openFile(const QString &filter)
|
|||||||
QString fileName;
|
QString fileName;
|
||||||
connect(this, &AndroidController::fileOpened, this,
|
connect(this, &AndroidController::fileOpened, this,
|
||||||
[&fileName, &wait](const QString &uri) {
|
[&fileName, &wait](const QString &uri) {
|
||||||
qDebug() << "Android event: file opened; uri:" << uri;
|
fileName = uri;
|
||||||
fileName = QQmlFile::urlToLocalFileOrQrc(uri);
|
|
||||||
qDebug() << "Android opened filename:" << fileName;
|
|
||||||
wait.quit();
|
wait.quit();
|
||||||
},
|
},
|
||||||
static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::SingleShotConnection));
|
static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::SingleShotConnection));
|
||||||
@@ -175,6 +173,25 @@ QString AndroidController::openFile(const QString &filter)
|
|||||||
return fileName;
|
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()
|
bool AndroidController::isCameraPresent()
|
||||||
{
|
{
|
||||||
return callActivityMethod<jboolean>("isCameraPresent", "()Z");
|
return callActivityMethod<jboolean>("isCameraPresent", "()Z");
|
||||||
@@ -287,6 +304,11 @@ bool AndroidController::requestAuthentication()
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AndroidController::sendTouch(float x, float y)
|
||||||
|
{
|
||||||
|
callActivityMethod("sendTouch", "(FF)V", x, y);
|
||||||
|
}
|
||||||
|
|
||||||
// Moving log processing to the Android side
|
// Moving log processing to the Android side
|
||||||
jclass AndroidController::log;
|
jclass AndroidController::log;
|
||||||
jmethodID AndroidController::logDebug;
|
jmethodID AndroidController::logDebug;
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ public:
|
|||||||
void resetLastServer(int serverIndex);
|
void resetLastServer(int serverIndex);
|
||||||
void saveFile(const QString &fileName, const QString &data);
|
void saveFile(const QString &fileName, const QString &data);
|
||||||
QString openFile(const QString &filter);
|
QString openFile(const QString &filter);
|
||||||
|
int getFd(const QString &fileName);
|
||||||
|
void closeFd();
|
||||||
|
QString getFileName(const QString &uri);
|
||||||
bool isCameraPresent();
|
bool isCameraPresent();
|
||||||
bool isOnTv();
|
bool isOnTv();
|
||||||
void startQrReaderActivity();
|
void startQrReaderActivity();
|
||||||
@@ -48,6 +51,7 @@ public:
|
|||||||
bool isNotificationPermissionGranted();
|
bool isNotificationPermissionGranted();
|
||||||
void requestNotificationPermission();
|
void requestNotificationPermission();
|
||||||
bool requestAuthentication();
|
bool requestAuthentication();
|
||||||
|
void sendTouch(float x, float y);
|
||||||
|
|
||||||
static bool initLogging();
|
static bool initLogging();
|
||||||
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
|
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
|
||||||
|
|||||||
@@ -1,225 +1,227 @@
|
|||||||
<RCC>
|
<RCC>
|
||||||
<qresource prefix="/">
|
<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/active.png</file>
|
||||||
<file>images/tray/default.png</file>
|
<file>images/tray/default.png</file>
|
||||||
<file>images/tray/error.png</file>
|
<file>images/tray/error.png</file>
|
||||||
<file>images/AmneziaVPN.png</file>
|
<file>server_scripts/awg/configure_container.sh</file>
|
||||||
<file>server_scripts/remove_container.sh</file>
|
<file>server_scripts/awg/Dockerfile</file>
|
||||||
<file>server_scripts/setup_host_firewall.sh</file>
|
<file>server_scripts/awg/run_container.sh</file>
|
||||||
<file>server_scripts/openvpn_cloak/Dockerfile</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/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/start.sh</file>
|
||||||
<file>server_scripts/openvpn_cloak/template.ovpn</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/configure_container.sh</file>
|
||||||
<file>server_scripts/openvpn_shadowsocks/Dockerfile</file>
|
<file>server_scripts/openvpn_shadowsocks/Dockerfile</file>
|
||||||
<file>server_scripts/openvpn_shadowsocks/run_container.sh</file>
|
<file>server_scripts/openvpn_shadowsocks/run_container.sh</file>
|
||||||
<file>server_scripts/openvpn_shadowsocks/start.sh</file>
|
<file>server_scripts/openvpn_shadowsocks/start.sh</file>
|
||||||
<file>server_scripts/openvpn_shadowsocks/template.ovpn</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/configure_container.sh</file>
|
||||||
<file>server_scripts/wireguard/Dockerfile</file>
|
<file>server_scripts/wireguard/Dockerfile</file>
|
||||||
<file>server_scripts/wireguard/run_container.sh</file>
|
<file>server_scripts/wireguard/run_container.sh</file>
|
||||||
<file>server_scripts/wireguard/start.sh</file>
|
<file>server_scripts/wireguard/start.sh</file>
|
||||||
<file>server_scripts/wireguard/template.conf</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/configure_container.sh</file>
|
||||||
<file>server_scripts/xray/Dockerfile</file>
|
<file>server_scripts/xray/Dockerfile</file>
|
||||||
<file>server_scripts/xray/run_container.sh</file>
|
<file>server_scripts/xray/run_container.sh</file>
|
||||||
<file>server_scripts/xray/start.sh</file>
|
<file>server_scripts/xray/start.sh</file>
|
||||||
<file>server_scripts/xray/template.json</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>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>ui/qml/Components/InstalledAppsDrawer.qml</file>
|
||||||
<file>images/controls/alert-circle.svg</file>
|
<file>ui/qml/Components/QuestionDrawer.qml</file>
|
||||||
<file>images/controls/file-check-2.svg</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>ui/qml/Controls2/WarningType.qml</file>
|
||||||
<file>fonts/pt-root-ui_vf.ttf</file>
|
<file>ui/qml/Filters/ContainersModelFilters.qml</file>
|
||||||
<file>ui/qml/Modules/Style/qmldir</file>
|
<file>ui/qml/main2.qml</file>
|
||||||
<file>ui/qml/Modules/Style/AmneziaStyle.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>ui/qml/Pages2/PageServiceSocksProxySettings.qml</file>
|
||||||
<file>server_scripts/socks5_proxy/run_container.sh</file>
|
<file>ui/qml/Pages2/PageServiceTorWebsiteSettings.qml</file>
|
||||||
<file>server_scripts/socks5_proxy/Dockerfile</file>
|
<file>ui/qml/Pages2/PageSettings.qml</file>
|
||||||
<file>server_scripts/socks5_proxy/configure_container.sh</file>
|
<file>ui/qml/Pages2/PageSettingsAbout.qml</file>
|
||||||
<file>server_scripts/socks5_proxy/start.sh</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/PageProtocolAwgClientSettings.qml</file>
|
||||||
<file>ui/qml/Pages2/PageProtocolWireGuardClientSettings.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/Pages2/PageSetupWizardApiServiceInfo.qml</file>
|
||||||
<file>ui/qml/Controls2/CardWithIconsType.qml</file>
|
<file>ui/qml/Pages2/PageSetupWizardApiServicesList.qml</file>
|
||||||
<file>images/controls/tag.svg</file>
|
<file>ui/qml/Pages2/PageSetupWizardConfigSource.qml</file>
|
||||||
<file>images/controls/history.svg</file>
|
<file>ui/qml/Pages2/PageSetupWizardCredentials.qml</file>
|
||||||
<file>images/controls/gauge.svg</file>
|
<file>ui/qml/Pages2/PageSetupWizardEasy.qml</file>
|
||||||
<file>images/controls/map-pin.svg</file>
|
<file>ui/qml/Pages2/PageSetupWizardInstalling.qml</file>
|
||||||
<file>ui/qml/Controls2/LabelWithImageType.qml</file>
|
<file>ui/qml/Pages2/PageSetupWizardProtocols.qml</file>
|
||||||
<file>images/controls/info.svg</file>
|
<file>ui/qml/Pages2/PageSetupWizardProtocolSettings.qml</file>
|
||||||
<file>ui/qml/Controls2/TextAreaWithFooterType.qml</file>
|
<file>ui/qml/Pages2/PageSetupWizardQrReader.qml</file>
|
||||||
<file>images/controls/scan-line.svg</file>
|
<file>ui/qml/Pages2/PageSetupWizardStart.qml</file>
|
||||||
<file>images/controls/folder-search-2.svg</file>
|
<file>ui/qml/Pages2/PageSetupWizardTextKey.qml</file>
|
||||||
<file>ui/qml/Pages2/PageSettingsApiServerInfo.qml</file>
|
<file>ui/qml/Pages2/PageSetupWizardViewConfig.qml</file>
|
||||||
<file>images/controls/bug.svg</file>
|
<file>ui/qml/Pages2/PageShare.qml</file>
|
||||||
<file>ui/qml/Pages2/PageDevMenu.qml</file>
|
<file>ui/qml/Pages2/PageShareFullAccess.qml</file>
|
||||||
<file>images/controls/refresh-cw.svg</file>
|
<file>ui/qml/Pages2/PageStart.qml</file>
|
||||||
<file>ui/qml/Pages2/PageSettingsApiLanguageList.qml</file>
|
|
||||||
<file>images/controls/archive-restore.svg</file>
|
|
||||||
<file>images/controls/help-circle.svg</file>
|
|
||||||
</qresource>
|
</qresource>
|
||||||
<qresource prefix="/countriesFlags">
|
<qresource prefix="/countriesFlags">
|
||||||
<file>images/flagKit/ZW.svg</file>
|
<file>images/flagKit/ZW.svg</file>
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
echo $LC_MESSAGES | grep -qE "en_US.UTF-8|C.UTF-8" || export LC_MESSAGES=C.UTF-8;\
|
|
||||||
CUR_USER=$(whoami);\
|
CUR_USER=$(whoami);\
|
||||||
groups $CUR_USER | grep sudo && sudo -nu $CUR_USER sudo -n uname > /dev/null
|
groups $CUR_USER
|
||||||
@@ -2679,7 +2679,7 @@ and will not be shared or disclosed to the Amnezia or any third parties</source>
|
|||||||
<message>
|
<message>
|
||||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="154"/>
|
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="154"/>
|
||||||
<source>Where to get connection data, step-by-step instructions for buying a VPS</source>
|
<source>Where to get connection data, step-by-step instructions for buying a VPS</source>
|
||||||
<translation>Где взять данные для подключения, пошаговые инстуркции по покупке VPS</translation>
|
<translation>Где взять данные для подключения, пошаговые инструкции по покупке VPS</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="170"/>
|
<location filename="../ui/qml/Pages2/PageSetupWizardCredentials.qml" line="170"/>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ void ConnectionController::openConnection()
|
|||||||
&& !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
|
&& !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) {
|
||||||
emit updateApiConfigFromGateway();
|
emit updateApiConfigFromGateway();
|
||||||
} else if (configVersion && m_serversModel->isApiKeyExpired(serverIndex)) {
|
} 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) {
|
if (configVersion == ApiConfigSources::Telegram) {
|
||||||
emit updateApiConfigFromTelegram();
|
emit updateApiConfigFromTelegram();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -121,9 +121,8 @@ ErrorCode ExportController::generateNativeConfig(const DockerContainer container
|
|||||||
|
|
||||||
jsonNativeConfig = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object();
|
jsonNativeConfig = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object();
|
||||||
|
|
||||||
if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg) {
|
if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg || protocol == Proto::Xray) {
|
||||||
auto clientId = jsonNativeConfig.value(config_key::clientId).toString();
|
errorCode = m_clientManagementModel->appendClient(jsonNativeConfig, clientName, container, credentials, serverController);
|
||||||
errorCode = m_clientManagementModel->appendClient(clientId, clientName, container, credentials, serverController);
|
|
||||||
}
|
}
|
||||||
return errorCode;
|
return errorCode;
|
||||||
}
|
}
|
||||||
@@ -248,10 +247,10 @@ void ExportController::generateCloakConfig()
|
|||||||
emit exportConfigChanged();
|
emit exportConfigChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ExportController::generateXrayConfig()
|
void ExportController::generateXrayConfig(const QString &clientName)
|
||||||
{
|
{
|
||||||
QJsonObject nativeConfig;
|
QJsonObject nativeConfig;
|
||||||
ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, "", Proto::Xray, nativeConfig);
|
ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, clientName, Proto::Xray, nativeConfig);
|
||||||
if (errorCode) {
|
if (errorCode) {
|
||||||
emit exportErrorOccurred(errorCode);
|
emit exportErrorOccurred(errorCode);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public slots:
|
|||||||
void generateAwgConfig(const QString &clientName);
|
void generateAwgConfig(const QString &clientName);
|
||||||
void generateShadowSocksConfig();
|
void generateShadowSocksConfig();
|
||||||
void generateCloakConfig();
|
void generateCloakConfig();
|
||||||
void generateXrayConfig();
|
void generateXrayConfig(const QString &clientName);
|
||||||
|
|
||||||
QString getConfig();
|
QString getConfig();
|
||||||
QString getNativeConfigString();
|
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/errorstrings.h"
|
||||||
#include "core/serialization/serialization.h"
|
#include "core/serialization/serialization.h"
|
||||||
|
#include "systemController.h"
|
||||||
#include "utilities.h"
|
#include "utilities.h"
|
||||||
|
|
||||||
#ifdef Q_OS_ANDROID
|
#ifdef Q_OS_ANDROID
|
||||||
@@ -76,17 +77,18 @@ ImportController::ImportController(const QSharedPointer<ServersModel> &serversMo
|
|||||||
|
|
||||||
bool ImportController::extractConfigFromFile(const QString &fileName)
|
bool ImportController::extractConfigFromFile(const QString &fileName)
|
||||||
{
|
{
|
||||||
QFile file(fileName);
|
QString data;
|
||||||
|
if (!SystemController::readFile(fileName, data)) {
|
||||||
if (file.open(QIODevice::ReadOnly)) {
|
emit importErrorOccurred(ErrorCode::ImportOpenConfigError, false);
|
||||||
QString data = file.readAll();
|
return false;
|
||||||
|
|
||||||
m_configFileName = QFileInfo(file.fileName()).fileName();
|
|
||||||
return extractConfigFromData(data);
|
|
||||||
}
|
}
|
||||||
|
m_configFileName = QFileInfo(QFile(fileName).fileName()).fileName();
|
||||||
emit importErrorOccurred(ErrorCode::ImportOpenConfigError, false);
|
#ifdef Q_OS_ANDROID
|
||||||
return false;
|
if (m_configFileName.isEmpty()) {
|
||||||
|
m_configFileName = AndroidController::instance()->getFileName(fileName);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return extractConfigFromData(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ImportController::extractConfigFromData(QString data)
|
bool ImportController::extractConfigFromData(QString data)
|
||||||
|
|||||||
@@ -848,7 +848,6 @@ bool InstallController::updateServiceFromApi(const int serverIndex, const QStrin
|
|||||||
|
|
||||||
newServerConfig.insert(configKey::apiConfig, newApiConfig);
|
newServerConfig.insert(configKey::apiConfig, newApiConfig);
|
||||||
newServerConfig.insert(configKey::authData, authData);
|
newServerConfig.insert(configKey::authData, authData);
|
||||||
newServerConfig.insert(config_key::crc, serverConfig.value(config_key::crc));
|
|
||||||
m_serversModel->editServer(newServerConfig, serverIndex);
|
m_serversModel->editServer(newServerConfig, serverIndex);
|
||||||
|
|
||||||
if (reloadServiceConfig) {
|
if (reloadServiceConfig) {
|
||||||
|
|||||||
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: {
|
case Qt::Key_Escape: {
|
||||||
if (m_drawerDepth) {
|
if (m_drawerDepth) {
|
||||||
emit closeTopDrawer();
|
emit closeTopDrawer();
|
||||||
setDrawerDepth(getDrawerDepth() - 1);
|
decrementDrawerDepth();
|
||||||
} else {
|
} else {
|
||||||
emit escapePressed();
|
emit escapePressed();
|
||||||
}
|
}
|
||||||
@@ -142,11 +142,25 @@ void PageController::setDrawerDepth(const int depth)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int PageController::getDrawerDepth()
|
int PageController::getDrawerDepth() const
|
||||||
{
|
{
|
||||||
return m_drawerDepth;
|
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)
|
void PageController::onShowErrorMessage(ErrorCode errorCode)
|
||||||
{
|
{
|
||||||
const auto fullErrorMessage = errorString(errorCode);
|
const auto fullErrorMessage = errorString(errorCode);
|
||||||
|
|||||||
@@ -100,7 +100,9 @@ public slots:
|
|||||||
void closeApplication();
|
void closeApplication();
|
||||||
|
|
||||||
void setDrawerDepth(const int depth);
|
void setDrawerDepth(const int depth);
|
||||||
int getDrawerDepth();
|
int getDrawerDepth() const;
|
||||||
|
int incrementDrawerDepth();
|
||||||
|
int decrementDrawerDepth();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onShowErrorMessage(amnezia::ErrorCode errorCode);
|
void onShowErrorMessage(amnezia::ErrorCode errorCode);
|
||||||
@@ -135,9 +137,6 @@ signals:
|
|||||||
void escapePressed();
|
void escapePressed();
|
||||||
void closeTopDrawer();
|
void closeTopDrawer();
|
||||||
|
|
||||||
void forceTabBarActiveFocus();
|
|
||||||
void forceStackActiveFocus();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QSharedPointer<ServersModel> m_serversModel;
|
QSharedPointer<ServersModel> m_serversModel;
|
||||||
|
|
||||||
|
|||||||
@@ -131,12 +131,8 @@ void SettingsController::backupAppConfig(const QString &fileName)
|
|||||||
|
|
||||||
void SettingsController::restoreAppConfig(const QString &fileName)
|
void SettingsController::restoreAppConfig(const QString &fileName)
|
||||||
{
|
{
|
||||||
QFile file(fileName);
|
QByteArray data;
|
||||||
|
SystemController::readFile(fileName, data);
|
||||||
file.open(QIODevice::ReadOnly);
|
|
||||||
|
|
||||||
QByteArray data = file.readAll();
|
|
||||||
|
|
||||||
restoreAppConfigFromData(data);
|
restoreAppConfigFromData(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,14 +82,12 @@ void SitesController::removeSite(int index)
|
|||||||
|
|
||||||
void SitesController::importSites(const QString &fileName, bool replaceExisting)
|
void SitesController::importSites(const QString &fileName, bool replaceExisting)
|
||||||
{
|
{
|
||||||
QFile file(fileName);
|
QByteArray jsonData;
|
||||||
|
if (!SystemController::readFile(fileName, jsonData)) {
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
|
||||||
emit errorOccurred(tr("Can't open file: %1").arg(fileName));
|
emit errorOccurred(tr("Can't open file: %1").arg(fileName));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray jsonData = file.readAll();
|
|
||||||
QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonData);
|
QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonData);
|
||||||
if (jsonDocument.isNull()) {
|
if (jsonDocument.isNull()) {
|
||||||
emit errorOccurred(tr("Failed to parse JSON data from file: %1").arg(fileName));
|
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
|
#if defined Q_OS_ANDROID
|
||||||
AndroidController::instance()->saveFile(fileName, data);
|
AndroidController::instance()->saveFile(fileName, data);
|
||||||
@@ -62,6 +62,31 @@ void SystemController::saveFile(QString fileName, const QString &data)
|
|||||||
#endif
|
#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,
|
QString SystemController::getFileName(const QString &acceptLabel, const QString &nameFilter,
|
||||||
const QString &selectedFile, const bool isSaveMode, const QString &defaultSuffix)
|
const QString &selectedFile, const bool isSaveMode, const QString &defaultSuffix)
|
||||||
{
|
{
|
||||||
@@ -134,3 +159,10 @@ bool SystemController::isAuthenticated()
|
|||||||
return true;
|
return true;
|
||||||
#endif
|
#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:
|
public:
|
||||||
explicit SystemController(const std::shared_ptr<Settings> &setting, QObject *parent = nullptr);
|
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:
|
public slots:
|
||||||
QString getFileName(const QString &acceptLabel, const QString &nameFilter, const QString &selectedFile = "",
|
QString getFileName(const QString &acceptLabel, const QString &nameFilter, const QString &selectedFile = "",
|
||||||
@@ -20,6 +22,8 @@ public slots:
|
|||||||
void setQmlRoot(QObject *qmlRoot);
|
void setQmlRoot(QObject *qmlRoot);
|
||||||
|
|
||||||
bool isAuthenticated();
|
bool isAuthenticated();
|
||||||
|
void sendTouch(float x, float y);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void fileDialogClosed(const bool isAccepted);
|
void fileDialogClosed(const bool isAccepted);
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ namespace
|
|||||||
constexpr char storeEndpoint[] = "store_endpoint";
|
constexpr char storeEndpoint[] = "store_endpoint";
|
||||||
|
|
||||||
constexpr char isAvailable[] = "is_available";
|
constexpr char isAvailable[] = "is_available";
|
||||||
|
|
||||||
|
constexpr char subscription[] = "subscription";
|
||||||
|
constexpr char endDate[] = "end_date";
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace serviceType
|
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()))
|
if (!index.isValid() || index.row() < 0 || index.row() >= static_cast<int>(rowCount()))
|
||||||
return QVariant();
|
return QVariant();
|
||||||
|
|
||||||
QJsonObject service = m_services.at(index.row()).toObject();
|
auto apiServiceData = m_services.at(index.row());
|
||||||
QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject();
|
auto serviceType = apiServiceData.type;
|
||||||
auto serviceType = service.value(configKey::serviceType).toString();
|
auto isServiceAvailable = apiServiceData.isServiceAvailable;
|
||||||
|
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case NameRole: {
|
case NameRole: {
|
||||||
return serviceInfo.value(configKey::name).toString();
|
return apiServiceData.serviceInfo.name;
|
||||||
}
|
}
|
||||||
case CardDescriptionRole: {
|
case CardDescriptionRole: {
|
||||||
auto speed = serviceInfo.value(configKey::speed).toString();
|
auto speed = apiServiceData.serviceInfo.speed;
|
||||||
if (serviceType == serviceType::amneziaPremium) {
|
if (serviceType == serviceType::amneziaPremium) {
|
||||||
return tr("Classic VPN for comfortable work, downloading large files and watching videos. "
|
return tr("Classic VPN for comfortable work, downloading large files and watching videos. "
|
||||||
"Works for any sites. Speed up to %1 MBit/s")
|
"Works for any sites. Speed up to %1 MBit/s")
|
||||||
.arg(speed);
|
.arg(speed);
|
||||||
} else if (serviceType == serviceType::amneziaFree){
|
} else if (serviceType == serviceType::amneziaFree){
|
||||||
QString description = tr("VPN to access blocked sites in regions with high levels of Internet censorship. ");
|
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>");
|
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;
|
return description;
|
||||||
@@ -83,25 +86,24 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
|
|||||||
}
|
}
|
||||||
case IsServiceAvailableRole: {
|
case IsServiceAvailableRole: {
|
||||||
if (serviceType == serviceType::amneziaFree) {
|
if (serviceType == serviceType::amneziaFree) {
|
||||||
if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) {
|
if (isServiceAvailable) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
case SpeedRole: {
|
case SpeedRole: {
|
||||||
auto speed = serviceInfo.value(configKey::speed).toString();
|
return tr("%1 MBit/s").arg(apiServiceData.serviceInfo.speed);
|
||||||
return tr("%1 MBit/s").arg(speed);
|
|
||||||
}
|
}
|
||||||
case WorkPeriodRole: {
|
case TimeLimitRole: {
|
||||||
auto timelimit = serviceInfo.value(configKey::timelimit).toString();
|
auto timeLimit = apiServiceData.serviceInfo.timeLimit;
|
||||||
if (timelimit == "0") {
|
if (timeLimit == "0") {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return tr("%1 days").arg(timelimit);
|
return tr("%1 days").arg(timeLimit);
|
||||||
}
|
}
|
||||||
case RegionRole: {
|
case RegionRole: {
|
||||||
return serviceInfo.value(configKey::region).toString();
|
return apiServiceData.serviceInfo.region;
|
||||||
}
|
}
|
||||||
case FeaturesRole: {
|
case FeaturesRole: {
|
||||||
if (serviceType == serviceType::amneziaPremium) {
|
if (serviceType == serviceType::amneziaPremium) {
|
||||||
@@ -113,12 +115,15 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case PriceRole: {
|
case PriceRole: {
|
||||||
auto price = serviceInfo.value(configKey::price).toString();
|
auto price = apiServiceData.serviceInfo.price;
|
||||||
if (price == "free") {
|
if (price == "free") {
|
||||||
return tr("Free");
|
return tr("Free");
|
||||||
}
|
}
|
||||||
return tr("%1 $/month").arg(price);
|
return tr("%1 $/month").arg(price);
|
||||||
}
|
}
|
||||||
|
case EndDateRole: {
|
||||||
|
return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return QVariant();
|
return QVariant();
|
||||||
@@ -128,15 +133,18 @@ void ApiServicesModel::updateModel(const QJsonObject &data)
|
|||||||
{
|
{
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
|
|
||||||
m_countryCode = data.value(configKey::userCountryCode).toString();
|
m_services.clear();
|
||||||
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.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;
|
m_selectedServiceIndex = 0;
|
||||||
|
} else {
|
||||||
|
for (const auto &service : services) {
|
||||||
|
m_services.push_back(getApiServicesData(service.toObject()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
endResetModel();
|
endResetModel();
|
||||||
@@ -149,32 +157,32 @@ void ApiServicesModel::setServiceIndex(const int index)
|
|||||||
|
|
||||||
QJsonObject ApiServicesModel::getSelectedServiceInfo()
|
QJsonObject ApiServicesModel::getSelectedServiceInfo()
|
||||||
{
|
{
|
||||||
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
|
auto service = m_services.at(m_selectedServiceIndex);
|
||||||
return service.value(configKey::serviceInfo).toObject();
|
return service.serviceInfo.object;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ApiServicesModel::getSelectedServiceType()
|
QString ApiServicesModel::getSelectedServiceType()
|
||||||
{
|
{
|
||||||
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
|
auto service = m_services.at(m_selectedServiceIndex);
|
||||||
return service.value(configKey::serviceType).toString();
|
return service.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ApiServicesModel::getSelectedServiceProtocol()
|
QString ApiServicesModel::getSelectedServiceProtocol()
|
||||||
{
|
{
|
||||||
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
|
auto service = m_services.at(m_selectedServiceIndex);
|
||||||
return service.value(configKey::serviceProtocol).toString();
|
return service.protocol;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ApiServicesModel::getSelectedServiceName()
|
QString ApiServicesModel::getSelectedServiceName()
|
||||||
{
|
{
|
||||||
auto modelIndex = index(m_selectedServiceIndex, 0);
|
auto service = m_services.at(m_selectedServiceIndex);
|
||||||
return data(modelIndex, ApiServicesModel::Roles::NameRole).toString();
|
return service.serviceInfo.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonArray ApiServicesModel::getSelectedServiceCountries()
|
QJsonArray ApiServicesModel::getSelectedServiceCountries()
|
||||||
{
|
{
|
||||||
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
|
auto service = m_services.at(m_selectedServiceIndex);
|
||||||
return service.value(configKey::availableCountries).toArray();
|
return service.availableCountries;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ApiServicesModel::getCountryCode()
|
QString ApiServicesModel::getCountryCode()
|
||||||
@@ -184,8 +192,8 @@ QString ApiServicesModel::getCountryCode()
|
|||||||
|
|
||||||
QString ApiServicesModel::getStoreEndpoint()
|
QString ApiServicesModel::getStoreEndpoint()
|
||||||
{
|
{
|
||||||
QJsonObject service = m_services.at(m_selectedServiceIndex).toObject();
|
auto service = m_services.at(m_selectedServiceIndex);
|
||||||
return service.value(configKey::storeEndpoint).toString();
|
return service.storeEndpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant ApiServicesModel::getSelectedServiceData(const QString roleString)
|
QVariant ApiServicesModel::getSelectedServiceData(const QString roleString)
|
||||||
@@ -209,10 +217,46 @@ QHash<int, QByteArray> ApiServicesModel::roleNames() const
|
|||||||
roles[ServiceDescriptionRole] = "serviceDescription";
|
roles[ServiceDescriptionRole] = "serviceDescription";
|
||||||
roles[IsServiceAvailableRole] = "isServiceAvailable";
|
roles[IsServiceAvailableRole] = "isServiceAvailable";
|
||||||
roles[SpeedRole] = "speed";
|
roles[SpeedRole] = "speed";
|
||||||
roles[WorkPeriodRole] = "workPeriod";
|
roles[TimeLimitRole] = "timeLimit";
|
||||||
roles[RegionRole] = "region";
|
roles[RegionRole] = "region";
|
||||||
roles[FeaturesRole] = "features";
|
roles[FeaturesRole] = "features";
|
||||||
roles[PriceRole] = "price";
|
roles[PriceRole] = "price";
|
||||||
|
roles[EndDateRole] = "endDate";
|
||||||
|
|
||||||
return roles;
|
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 <QAbstractListModel>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
class ApiServicesModel : public QAbstractListModel
|
class ApiServicesModel : public QAbstractListModel
|
||||||
{
|
{
|
||||||
@@ -15,10 +16,11 @@ public:
|
|||||||
ServiceDescriptionRole,
|
ServiceDescriptionRole,
|
||||||
IsServiceAvailableRole,
|
IsServiceAvailableRole,
|
||||||
SpeedRole,
|
SpeedRole,
|
||||||
WorkPeriodRole,
|
TimeLimitRole,
|
||||||
RegionRole,
|
RegionRole,
|
||||||
FeaturesRole,
|
FeaturesRole,
|
||||||
PriceRole
|
PriceRole,
|
||||||
|
EndDateRole
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit ApiServicesModel(QObject *parent = nullptr);
|
explicit ApiServicesModel(QObject *parent = nullptr);
|
||||||
@@ -48,8 +50,40 @@ protected:
|
|||||||
QHash<int, QByteArray> roleNames() const override;
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
private:
|
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;
|
QString m_countryCode;
|
||||||
QJsonArray m_services;
|
QVector<ApiServicesData> m_services;
|
||||||
|
|
||||||
int m_selectedServiceIndex;
|
int m_selectedServiceIndex;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co
|
|||||||
error = getOpenVpnClients(container, credentials, serverController, count);
|
error = getOpenVpnClients(container, credentials, serverController, count);
|
||||||
} else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
|
} else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
|
||||||
error = getWireGuardClients(container, credentials, serverController, count);
|
error = getWireGuardClients(container, credentials, serverController, count);
|
||||||
|
} else if (container == DockerContainer::Xray) {
|
||||||
|
error = getXrayClients(container, credentials, serverController, count);
|
||||||
}
|
}
|
||||||
if (error != ErrorCode::NoError) {
|
if (error != ErrorCode::NoError) {
|
||||||
endResetModel();
|
endResetModel();
|
||||||
@@ -239,6 +241,68 @@ ErrorCode ClientManagementModel::getWireGuardClients(const DockerContainer conta
|
|||||||
}
|
}
|
||||||
return error;
|
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,
|
ErrorCode ClientManagementModel::wgShow(const DockerContainer container, const ServerCredentials &credentials,
|
||||||
const QSharedPointer<ServerController> &serverController, std::vector<WgShowData> &data)
|
const QSharedPointer<ServerController> &serverController, std::vector<WgShowData> &data)
|
||||||
@@ -326,17 +390,67 @@ ErrorCode ClientManagementModel::appendClient(const DockerContainer container, c
|
|||||||
const QSharedPointer<ServerController> &serverController)
|
const QSharedPointer<ServerController> &serverController)
|
||||||
{
|
{
|
||||||
Proto protocol;
|
Proto protocol;
|
||||||
if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) {
|
switch (container) {
|
||||||
protocol = Proto::OpenVpn;
|
case DockerContainer::ShadowSocks:
|
||||||
} else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
|
case DockerContainer::Cloak:
|
||||||
protocol = ContainerProps::defaultProtocol(container);
|
protocol = Proto::OpenVpn;
|
||||||
} else {
|
break;
|
||||||
return ErrorCode::NoError;
|
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);
|
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,
|
ErrorCode ClientManagementModel::appendClient(const QString &clientId, const QString &clientName, const DockerContainer container,
|
||||||
@@ -422,10 +536,27 @@ ErrorCode ClientManagementModel::revokeClient(const int row, const DockerContain
|
|||||||
auto client = m_clientsTable.at(row).toObject();
|
auto client = m_clientsTable.at(row).toObject();
|
||||||
QString clientId = client.value(configKey::clientId).toString();
|
QString clientId = client.value(configKey::clientId).toString();
|
||||||
|
|
||||||
if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) {
|
switch(container)
|
||||||
errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController);
|
{
|
||||||
} else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
|
case DockerContainer::OpenVpn:
|
||||||
errorCode = revokeWireGuard(row, container, credentials, serverController);
|
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) {
|
if (errorCode == ErrorCode::NoError) {
|
||||||
@@ -463,19 +594,69 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig
|
|||||||
}
|
}
|
||||||
|
|
||||||
Proto protocol;
|
Proto protocol;
|
||||||
if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) {
|
|
||||||
protocol = Proto::OpenVpn;
|
switch(container)
|
||||||
} else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
|
{
|
||||||
protocol = ContainerProps::defaultProtocol(container);
|
case DockerContainer::ShadowSocks:
|
||||||
} else {
|
case DockerContainer::Cloak: {
|
||||||
return ErrorCode::NoError;
|
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);
|
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;
|
int row;
|
||||||
bool clientExists = false;
|
bool clientExists = false;
|
||||||
QString clientId = protocolConfig.value(config_key::clientId).toString();
|
|
||||||
for (row = 0; row < rowCount(); row++) {
|
for (row = 0; row < rowCount(); row++) {
|
||||||
auto client = m_clientsTable.at(row).toObject();
|
auto client = m_clientsTable.at(row).toObject();
|
||||||
if (clientId == client.value(configKey::clientId).toString()) {
|
if (clientId == client.value(configKey::clientId).toString()) {
|
||||||
@@ -487,11 +668,28 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig
|
|||||||
return errorCode;
|
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);
|
errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController);
|
||||||
} else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) {
|
break;
|
||||||
errorCode = revokeWireGuard(row, container, credentials, serverController);
|
|
||||||
}
|
}
|
||||||
|
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;
|
return errorCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,6 +792,117 @@ ErrorCode ClientManagementModel::revokeWireGuard(const int row, const DockerCont
|
|||||||
return ErrorCode::NoError;
|
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> ClientManagementModel::roleNames() const
|
||||||
{
|
{
|
||||||
QHash<int, QByteArray> roles;
|
QHash<int, QByteArray> roles;
|
||||||
@@ -604,4 +913,4 @@ QHash<int, QByteArray> ClientManagementModel::roleNames() const
|
|||||||
roles[DataSentRole] = "dataSent";
|
roles[DataSentRole] = "dataSent";
|
||||||
roles[AllowedIpsRole] = "allowedIps";
|
roles[AllowedIpsRole] = "allowedIps";
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
@@ -40,6 +40,8 @@ public slots:
|
|||||||
const QSharedPointer<ServerController> &serverController);
|
const QSharedPointer<ServerController> &serverController);
|
||||||
ErrorCode appendClient(const DockerContainer container, const ServerCredentials &credentials, const QJsonObject &containerConfig,
|
ErrorCode appendClient(const DockerContainer container, const ServerCredentials &credentials, const QJsonObject &containerConfig,
|
||||||
const QString &clientName, const QSharedPointer<ServerController> &serverController);
|
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,
|
ErrorCode appendClient(const QString &clientId, const QString &clientName, const DockerContainer container,
|
||||||
const ServerCredentials &credentials, const QSharedPointer<ServerController> &serverController);
|
const ServerCredentials &credentials, const QSharedPointer<ServerController> &serverController);
|
||||||
ErrorCode renameClient(const int row, const QString &userName, const DockerContainer container, const ServerCredentials &credentials,
|
ErrorCode renameClient(const int row, const QString &userName, const DockerContainer container, const ServerCredentials &credentials,
|
||||||
@@ -64,11 +66,15 @@ private:
|
|||||||
const QSharedPointer<ServerController> &serverController);
|
const QSharedPointer<ServerController> &serverController);
|
||||||
ErrorCode revokeWireGuard(const int row, const DockerContainer container, const ServerCredentials &credentials,
|
ErrorCode revokeWireGuard(const int row, const DockerContainer container, const ServerCredentials &credentials,
|
||||||
const QSharedPointer<ServerController> &serverController);
|
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,
|
ErrorCode getOpenVpnClients(const DockerContainer container, const ServerCredentials &credentials,
|
||||||
const QSharedPointer<ServerController> &serverController, int &count);
|
const QSharedPointer<ServerController> &serverController, int &count);
|
||||||
ErrorCode getWireGuardClients(const DockerContainer container, const ServerCredentials &credentials,
|
ErrorCode getWireGuardClients(const DockerContainer container, const ServerCredentials &credentials,
|
||||||
const QSharedPointer<ServerController> &serverController, int &count);
|
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,
|
ErrorCode wgShow(const DockerContainer container, const ServerCredentials &credentials,
|
||||||
const QSharedPointer<ServerController> &serverController, std::vector<WgShowData> &data);
|
const QSharedPointer<ServerController> &serverController, std::vector<WgShowData> &data);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ namespace
|
|||||||
constexpr char serviceProtocol[] = "service_protocol";
|
constexpr char serviceProtocol[] = "service_protocol";
|
||||||
|
|
||||||
constexpr char publicKeyInfo[] = "public_key";
|
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();
|
emit ServersModel::defaultServerNameChanged();
|
||||||
updateDefaultServerContainersModel();
|
updateDefaultServerContainersModel();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged);
|
||||||
|
connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
int ServersModel::rowCount(const QModelIndex &parent) const
|
int ServersModel::rowCount(const QModelIndex &parent) const
|
||||||
@@ -79,6 +82,12 @@ bool ServersModel::setData(const QModelIndex &index, const QVariant &value, int
|
|||||||
return true;
|
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
|
QVariant ServersModel::data(const QModelIndex &index, int role) const
|
||||||
{
|
{
|
||||||
if (!index.isValid() || index.row() < 0 || index.row() >= static_cast<int>(m_servers.size())) {
|
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 {};
|
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()
|
bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling()
|
||||||
{
|
{
|
||||||
auto server = m_servers.at(m_defaultServerIndex).toObject();
|
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 apiConfig = serverConfig.value(configKey::apiConfig).toObject();
|
||||||
|
|
||||||
auto publicKeyInfo = apiConfig.value(configKey::publicKeyInfo).toObject();
|
auto publicKeyInfo = apiConfig.value(configKey::publicKeyInfo).toObject();
|
||||||
const QString endDate = publicKeyInfo.value(configKey::endDate).toString();
|
const QString expiresAt = publicKeyInfo.value(configKey::expiresAt).toString();
|
||||||
if (endDate.isEmpty()) {
|
if (expiresAt.isEmpty()) {
|
||||||
publicKeyInfo.insert(configKey::endDate, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate));
|
publicKeyInfo.insert(configKey::expiresAt, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate));
|
||||||
apiConfig.insert(configKey::publicKeyInfo, publicKeyInfo);
|
apiConfig.insert(configKey::publicKeyInfo, publicKeyInfo);
|
||||||
serverConfig.insert(configKey::apiConfig, apiConfig);
|
serverConfig.insert(configKey::apiConfig, apiConfig);
|
||||||
editServer(serverConfig, serverIndex);
|
editServer(serverConfig, serverIndex);
|
||||||
@@ -728,8 +749,8 @@ bool ServersModel::isApiKeyExpired(const int serverIndex)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto endDateDateTime = QDateTime::fromString(endDate, Qt::ISODate).toUTC();
|
auto expiresAtDateTime = QDateTime::fromString(expiresAt, Qt::ISODate).toUTC();
|
||||||
if (endDateDateTime < QDateTime::currentDateTimeUtc()) {
|
if (expiresAtDateTime < QDateTime::currentDateTimeUtc()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ public:
|
|||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
|
||||||
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) 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 QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
QVariant data(const int index, int role = Qt::DisplayRole) const;
|
QVariant data(const int index, int role = Qt::DisplayRole) const;
|
||||||
|
|
||||||
@@ -115,6 +116,7 @@ public slots:
|
|||||||
QVariant getDefaultServerData(const QString roleString);
|
QVariant getDefaultServerData(const QString roleString);
|
||||||
|
|
||||||
QVariant getProcessedServerData(const QString roleString);
|
QVariant getProcessedServerData(const QString roleString);
|
||||||
|
bool setProcessedServerData(const QString &roleString, const QVariant &value);
|
||||||
|
|
||||||
bool isDefaultServerDefaultContainerHasSplitTunneling();
|
bool isDefaultServerDefaultContainerHasSplitTunneling();
|
||||||
|
|
||||||
@@ -127,6 +129,9 @@ protected:
|
|||||||
|
|
||||||
signals:
|
signals:
|
||||||
void processedServerIndexChanged(const int index);
|
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 defaultServerIndexChanged(const int index);
|
||||||
void defaultServerNameChanged();
|
void defaultServerNameChanged();
|
||||||
void defaultServerDescriptionChanged();
|
void defaultServerDescriptionChanged();
|
||||||
|
|||||||
@@ -16,6 +16,32 @@ Button {
|
|||||||
property string connectedButtonColor: AmneziaStyle.color.goldenApricot
|
property string connectedButtonColor: AmneziaStyle.color.goldenApricot
|
||||||
property bool buttonActiveFocus: activeFocus && (Qt.platform.os !== "android" || SettingsController.isOnTv())
|
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
|
implicitWidth: 190
|
||||||
implicitHeight: 190
|
implicitHeight: 190
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ DrawerType2 {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height
|
height: parent.height
|
||||||
|
|
||||||
expandedContent: ColumnLayout {
|
expandedStateContent: ColumnLayout {
|
||||||
id: content
|
id: content
|
||||||
|
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
@@ -26,14 +26,6 @@ DrawerType2 {
|
|||||||
root.expandedHeight = content.implicitHeight + 32
|
root.expandedHeight = content.implicitHeight + 32
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
enabled: !GC.isMobile()
|
|
||||||
function onOpened() {
|
|
||||||
focusItem.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Header2Type {
|
Header2Type {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 24
|
Layout.topMargin: 24
|
||||||
@@ -44,11 +36,6 @@ DrawerType2 {
|
|||||||
headerText: qsTr("Add new connection")
|
headerText: qsTr("Add new connection")
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
|
||||||
id: focusItem
|
|
||||||
KeyNavigation.tab: ip.rightButton
|
|
||||||
}
|
|
||||||
|
|
||||||
LabelWithButtonType {
|
LabelWithButtonType {
|
||||||
id: ip
|
id: ip
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
@@ -59,10 +46,8 @@ DrawerType2 {
|
|||||||
|
|
||||||
clickedFunction: function() {
|
clickedFunction: function() {
|
||||||
PageController.goToPage(PageEnum.PageSetupWizardCredentials)
|
PageController.goToPage(PageEnum.PageSetupWizardCredentials)
|
||||||
root.close()
|
root.closeTriggered()
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyNavigation.tab: qrCode.rightButton
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DividerType {}
|
DividerType {}
|
||||||
@@ -76,10 +61,8 @@ DrawerType2 {
|
|||||||
|
|
||||||
clickedFunction: function() {
|
clickedFunction: function() {
|
||||||
PageController.goToPage(PageEnum.PageSetupWizardConfigSource)
|
PageController.goToPage(PageEnum.PageSetupWizardConfigSource)
|
||||||
root.close()
|
root.closeTriggered()
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyNavigation.tab: focusItem
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DividerType {}
|
DividerType {}
|
||||||
|
|||||||
@@ -17,55 +17,15 @@ ListView {
|
|||||||
property var rootWidth
|
property var rootWidth
|
||||||
property var selectedText
|
property var selectedText
|
||||||
|
|
||||||
property bool a: true
|
|
||||||
|
|
||||||
width: rootWidth
|
width: rootWidth
|
||||||
height: menuContent.contentItem.height
|
height: contentItem.height
|
||||||
|
|
||||||
clip: true
|
clip: true
|
||||||
interactive: false
|
snapMode: ListView.SnapToItem
|
||||||
|
|
||||||
property FlickableType parentFlickable
|
ScrollBar.vertical: ScrollBarType {}
|
||||||
property var lastItemTabClicked
|
|
||||||
|
|
||||||
property int currentFocusIndex: 0
|
property bool isFocusable: true
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ButtonGroup {
|
ButtonGroup {
|
||||||
id: containersRadioButtonGroup
|
id: containersRadioButtonGroup
|
||||||
@@ -75,12 +35,6 @@ ListView {
|
|||||||
implicitWidth: rootWidth
|
implicitWidth: rootWidth
|
||||||
implicitHeight: content.implicitHeight
|
implicitHeight: content.implicitHeight
|
||||||
|
|
||||||
onActiveFocusChanged: {
|
|
||||||
if (activeFocus) {
|
|
||||||
containerRadioButton.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: content
|
id: content
|
||||||
|
|
||||||
@@ -111,13 +65,13 @@ ListView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (checked) {
|
if (checked) {
|
||||||
containersDropDown.close()
|
containersDropDown.closeTriggered()
|
||||||
ServersModel.setDefaultContainer(ServersModel.defaultIndex, proxyDefaultServerContainersModel.mapToSource(index))
|
ServersModel.setDefaultContainer(ServersModel.defaultIndex, proxyDefaultServerContainersModel.mapToSource(index))
|
||||||
} else {
|
} else {
|
||||||
ContainersModel.setProcessedContainerIndex(proxyDefaultServerContainersModel.mapToSource(index))
|
ContainersModel.setProcessedContainerIndex(proxyDefaultServerContainersModel.mapToSource(index))
|
||||||
InstallController.setShouldCreateServer(false)
|
InstallController.setShouldCreateServer(false)
|
||||||
PageController.goToPage(PageEnum.PageSetupWizardProtocolSettings)
|
PageController.goToPage(PageEnum.PageSetupWizardProtocolSettings)
|
||||||
containersDropDown.close()
|
containersDropDown.closeTriggered()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ DrawerType2 {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
expandedHeight: parent.height * 0.9
|
expandedHeight: parent.height * 0.9
|
||||||
|
|
||||||
expandedContent: ColumnLayout {
|
expandedStateContent: ColumnLayout {
|
||||||
id: content
|
id: content
|
||||||
|
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
@@ -24,14 +24,6 @@ DrawerType2 {
|
|||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
enabled: !GC.isMobile()
|
|
||||||
function onOpened() {
|
|
||||||
focusItem.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Header2Type {
|
Header2Type {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 24
|
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")
|
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 {
|
LabelWithButtonType {
|
||||||
id: splitTunnelingSwitch
|
id: splitTunnelingSwitch
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
@@ -59,11 +46,9 @@ DrawerType2 {
|
|||||||
descriptionText: qsTr("Enabled \nCan't be disabled for current server")
|
descriptionText: qsTr("Enabled \nCan't be disabled for current server")
|
||||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||||
|
|
||||||
KeyNavigation.tab: siteBasedSplitTunnelingSwitch.visible ? siteBasedSplitTunnelingSwitch.rightButton : focusItem
|
|
||||||
|
|
||||||
clickedFunction: function() {
|
clickedFunction: function() {
|
||||||
// PageController.goToPage(PageEnum.PageSettingsSplitTunneling)
|
PageController.goToPage(PageEnum.PageSettingsSplitTunneling)
|
||||||
// root.close()
|
root.closeTriggered()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,13 +65,9 @@ DrawerType2 {
|
|||||||
descriptionText: enabled && SitesModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled")
|
descriptionText: enabled && SitesModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled")
|
||||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||||
|
|
||||||
KeyNavigation.tab: appSplitTunnelingSwitch.visible ?
|
|
||||||
appSplitTunnelingSwitch.rightButton :
|
|
||||||
focusItem
|
|
||||||
|
|
||||||
clickedFunction: function() {
|
clickedFunction: function() {
|
||||||
PageController.goToPage(PageEnum.PageSettingsSplitTunneling)
|
PageController.goToPage(PageEnum.PageSettingsSplitTunneling)
|
||||||
root.close()
|
root.closeTriggered()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,11 +84,9 @@ DrawerType2 {
|
|||||||
descriptionText: AppSplitTunnelingModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled")
|
descriptionText: AppSplitTunnelingModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled")
|
||||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||||
|
|
||||||
KeyNavigation.tab: focusItem
|
|
||||||
|
|
||||||
clickedFunction: function() {
|
clickedFunction: function() {
|
||||||
PageController.goToPage(PageEnum.PageSettingsAppSplitTunneling)
|
PageController.goToPage(PageEnum.PageSettingsAppSplitTunneling)
|
||||||
root.close()
|
root.closeTriggered()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ DrawerType2 {
|
|||||||
id: installedAppsModel
|
id: installedAppsModel
|
||||||
}
|
}
|
||||||
|
|
||||||
expandedContent: Item {
|
expandedStateContent: Item {
|
||||||
id: container
|
id: container
|
||||||
|
|
||||||
implicitHeight: expandedHeight
|
implicitHeight: expandedHeight
|
||||||
@@ -43,7 +43,7 @@ DrawerType2 {
|
|||||||
BackButtonType {
|
BackButtonType {
|
||||||
backButtonImage: "qrc:/images/controls/arrow-left.svg"
|
backButtonImage: "qrc:/images/controls/arrow-left.svg"
|
||||||
backButtonFunction: function() {
|
backButtonFunction: function() {
|
||||||
root.close()
|
root.closeTriggered()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +69,8 @@ DrawerType2 {
|
|||||||
clip: true
|
clip: true
|
||||||
interactive: true
|
interactive: true
|
||||||
|
|
||||||
|
property bool isFocusable: true
|
||||||
|
|
||||||
model: SortFilterProxyModel {
|
model: SortFilterProxyModel {
|
||||||
id: proxyInstalledAppsModel
|
id: proxyInstalledAppsModel
|
||||||
sourceModel: installedAppsModel
|
sourceModel: installedAppsModel
|
||||||
@@ -79,10 +81,7 @@ DrawerType2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
ScrollBar.vertical: ScrollBarType {}
|
||||||
id: scrollBar
|
|
||||||
policy: ScrollBar.AlwaysOn
|
|
||||||
}
|
|
||||||
|
|
||||||
ButtonGroup {
|
ButtonGroup {
|
||||||
id: buttonGroup
|
id: buttonGroup
|
||||||
@@ -155,7 +154,7 @@ DrawerType2 {
|
|||||||
PageController.showBusyIndicator(true)
|
PageController.showBusyIndicator(true)
|
||||||
AppSplitTunnelingController.addApps(installedAppsModel.getSelectedAppsInfo())
|
AppSplitTunnelingController.addApps(installedAppsModel.getSelectedAppsInfo())
|
||||||
PageController.showBusyIndicator(false)
|
PageController.showBusyIndicator(false)
|
||||||
root.close()
|
root.closeTriggered()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ DrawerType2 {
|
|||||||
property var yesButtonFunction
|
property var yesButtonFunction
|
||||||
property var noButtonFunction
|
property var noButtonFunction
|
||||||
|
|
||||||
expandedContent: ColumnLayout {
|
expandedStateContent: ColumnLayout {
|
||||||
id: content
|
id: content
|
||||||
|
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
@@ -33,14 +33,6 @@ DrawerType2 {
|
|||||||
root.expandedHeight = content.implicitHeight + 32
|
root.expandedHeight = content.implicitHeight + 32
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
enabled: !GC.isMobile()
|
|
||||||
function onOpened() {
|
|
||||||
focusItem.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Header2TextType {
|
Header2TextType {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 16
|
Layout.topMargin: 16
|
||||||
@@ -59,11 +51,6 @@ DrawerType2 {
|
|||||||
text: descriptionText
|
text: descriptionText
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
|
||||||
id: focusItem
|
|
||||||
KeyNavigation.tab: yesButton
|
|
||||||
}
|
|
||||||
|
|
||||||
BasicButtonType {
|
BasicButtonType {
|
||||||
id: yesButton
|
id: yesButton
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
@@ -78,8 +65,6 @@ DrawerType2 {
|
|||||||
yesButtonFunction()
|
yesButtonFunction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyNavigation.tab: noButton
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BasicButtonType {
|
BasicButtonType {
|
||||||
@@ -102,8 +87,6 @@ DrawerType2 {
|
|||||||
noButtonFunction()
|
noButtonFunction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyNavigation.tab: focusItem
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import "../Config"
|
|||||||
DrawerType2 {
|
DrawerType2 {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
expandedContent: Item {
|
expandedStateContent: Item {
|
||||||
id: container
|
id: container
|
||||||
|
|
||||||
implicitHeight: root.height * 0.9
|
implicitHeight: root.height * 0.9
|
||||||
@@ -20,19 +20,6 @@ DrawerType2 {
|
|||||||
root.expandedHeight = container.implicitHeight
|
root.expandedHeight = container.implicitHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
enabled: !GC.isMobile()
|
|
||||||
function onOpened() {
|
|
||||||
focusItem.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: focusItem
|
|
||||||
KeyNavigation.tab: backButton
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: backButtonLayout
|
id: backButtonLayout
|
||||||
|
|
||||||
@@ -43,167 +30,148 @@ DrawerType2 {
|
|||||||
|
|
||||||
BackButtonType {
|
BackButtonType {
|
||||||
id: backButton
|
id: backButton
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
backButtonImage: "qrc:/images/controls/arrow-left.svg"
|
backButtonImage: "qrc:/images/controls/arrow-left.svg"
|
||||||
backButtonFunction: function() { root.close() }
|
backButtonFunction: function() { root.closeTriggered() }
|
||||||
KeyNavigation.tab: listView
|
}
|
||||||
|
|
||||||
|
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.top: backButtonLayout.bottom
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
contentHeight: content.implicitHeight
|
|
||||||
|
|
||||||
ColumnLayout {
|
property bool isFocusable: true
|
||||||
id: content
|
property int selectedIndex: LanguageModel.currentLanguageIndex
|
||||||
|
|
||||||
anchors.fill: parent
|
clip: true
|
||||||
|
reuseItems: true
|
||||||
|
|
||||||
Header2Type {
|
ScrollBar.vertical: ScrollBarType {}
|
||||||
id: header
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
Layout.rightMargin: 16
|
|
||||||
Layout.leftMargin: 16
|
|
||||||
|
|
||||||
headerText: qsTr("Choose language")
|
model: LanguageModel
|
||||||
}
|
|
||||||
|
|
||||||
ListView {
|
ButtonGroup {
|
||||||
id: listView
|
id: buttonGroup
|
||||||
|
}
|
||||||
|
|
||||||
Layout.fillWidth: true
|
delegate: Item {
|
||||||
height: listView.contentItem.height
|
implicitWidth: root.width
|
||||||
|
implicitHeight: delegateContent.implicitHeight
|
||||||
|
|
||||||
clip: true
|
ColumnLayout {
|
||||||
interactive: false
|
id: delegateContent
|
||||||
|
|
||||||
model: LanguageModel
|
anchors.fill: parent
|
||||||
currentIndex: LanguageModel.currentLanguageIndex
|
|
||||||
|
|
||||||
ButtonGroup {
|
RadioButton {
|
||||||
id: buttonGroup
|
id: radioButton
|
||||||
}
|
|
||||||
|
|
||||||
property int currentFocusIndex: 0
|
implicitWidth: parent.width
|
||||||
|
implicitHeight: radioButtonContent.implicitHeight
|
||||||
|
|
||||||
activeFocusOnTab: true
|
hoverEnabled: true
|
||||||
onActiveFocusChanged: {
|
|
||||||
if (activeFocus) {
|
|
||||||
this.currentFocusIndex = 0
|
|
||||||
this.itemAtIndex(currentFocusIndex).forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onTabPressed: {
|
property bool isFocusable: true
|
||||||
if (currentFocusIndex < this.count - 1) {
|
|
||||||
currentFocusIndex += 1
|
|
||||||
this.itemAtIndex(currentFocusIndex).forceActiveFocus()
|
|
||||||
} else {
|
|
||||||
listViewFocusItem.forceActiveFocus()
|
|
||||||
focusItem.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: listViewFocusItem
|
|
||||||
Keys.onTabPressed: {
|
Keys.onTabPressed: {
|
||||||
root.forceActiveFocus()
|
FocusController.nextKeyTabItem()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
Keys.onBacktabPressed: {
|
||||||
if (visible) {
|
FocusController.previousKeyTabItem()
|
||||||
listViewFocusItem.forceActiveFocus()
|
|
||||||
focusItem.forceActiveFocus()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Item {
|
Keys.onUpPressed: {
|
||||||
implicitWidth: root.width
|
FocusController.nextKeyUpItem()
|
||||||
implicitHeight: delegateContent.implicitHeight
|
}
|
||||||
|
|
||||||
onActiveFocusChanged: {
|
Keys.onDownPressed: {
|
||||||
if (activeFocus) {
|
FocusController.nextKeyDownItem()
|
||||||
radioButton.forceActiveFocus()
|
}
|
||||||
|
|
||||||
|
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 {
|
RowLayout {
|
||||||
id: delegateContent
|
id: radioButtonContent
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
RadioButton {
|
anchors.rightMargin: 16
|
||||||
id: radioButton
|
anchors.leftMargin: 16
|
||||||
|
|
||||||
implicitWidth: parent.width
|
spacing: 0
|
||||||
implicitHeight: radioButtonContent.implicitHeight
|
|
||||||
|
|
||||||
hoverEnabled: true
|
z: 1
|
||||||
|
|
||||||
indicator: Rectangle {
|
ParagraphTextType {
|
||||||
width: parent.width - 1
|
Layout.fillWidth: true
|
||||||
height: parent.height
|
Layout.topMargin: 20
|
||||||
color: radioButton.hovered ? AmneziaStyle.color.slateGray : AmneziaStyle.color.onyxBlack
|
Layout.bottomMargin: 20
|
||||||
border.color: radioButton.focus ? AmneziaStyle.color.paleGray : AmneziaStyle.color.transparent
|
|
||||||
border.width: radioButton.focus ? 1 : 0
|
|
||||||
|
|
||||||
Behavior on color {
|
text: languageName
|
||||||
PropertyAnimation { duration: 200 }
|
}
|
||||||
}
|
|
||||||
Behavior on border.color {
|
|
||||||
PropertyAnimation { duration: 200 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
Image {
|
||||||
id: radioButtonContent
|
source: "qrc:/images/controls/check.svg"
|
||||||
anchors.fill: parent
|
visible: radioButton.checked
|
||||||
|
|
||||||
anchors.rightMargin: 16
|
width: 24
|
||||||
anchors.leftMargin: 16
|
height: 24
|
||||||
|
|
||||||
spacing: 0
|
Layout.rightMargin: 8
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onEnterPressed: radioButton.clicked()
|
ButtonGroup.group: buttonGroup
|
||||||
Keys.onReturnPressed: radioButton.clicked()
|
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
|
height: root.contentItem.height
|
||||||
|
|
||||||
clip: true
|
clip: true
|
||||||
interactive: false
|
reuseItems: true
|
||||||
|
|
||||||
activeFocusOnTab: true
|
property bool isFocusable: false
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
implicitWidth: root.width
|
implicitWidth: root.width
|
||||||
implicitHeight: delegateContent.implicitHeight
|
implicitHeight: delegateContent.implicitHeight
|
||||||
|
|
||||||
onActiveFocusChanged: {
|
|
||||||
if (activeFocus) {
|
|
||||||
containerRadioButton.rightButton.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: delegateContent
|
id: delegateContent
|
||||||
|
|
||||||
|
|||||||
@@ -36,17 +36,9 @@ DrawerType2 {
|
|||||||
configFileName = "amnezia_config"
|
configFileName = "amnezia_config"
|
||||||
}
|
}
|
||||||
|
|
||||||
expandedContent: Item {
|
expandedStateContent: Item {
|
||||||
implicitHeight: root.expandedHeight
|
implicitHeight: root.expandedHeight
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
enabled: !GC.isMobile()
|
|
||||||
function onOpened() {
|
|
||||||
header.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Header2Type {
|
Header2Type {
|
||||||
id: header
|
id: header
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
@@ -57,24 +49,27 @@ DrawerType2 {
|
|||||||
anchors.rightMargin: 16
|
anchors.rightMargin: 16
|
||||||
|
|
||||||
headerText: root.headerText
|
headerText: root.headerText
|
||||||
|
|
||||||
KeyNavigation.tab: shareButton
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FlickableType {
|
ListView {
|
||||||
|
id: listView
|
||||||
|
|
||||||
anchors.top: header.bottom
|
anchors.top: header.bottom
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
contentHeight: content.height + 32
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
|
||||||
ColumnLayout {
|
property bool isFocusable: true
|
||||||
id: content
|
|
||||||
|
|
||||||
anchors.top: parent.top
|
ScrollBar.vertical: ScrollBarType {}
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
|
|
||||||
anchors.leftMargin: 16
|
model: 1
|
||||||
anchors.rightMargin: 16
|
|
||||||
|
clip: true
|
||||||
|
reuseItems: true
|
||||||
|
|
||||||
|
header: ColumnLayout {
|
||||||
|
width: listView.width
|
||||||
|
|
||||||
visible: root.contentVisible
|
visible: root.contentVisible
|
||||||
|
|
||||||
@@ -82,11 +77,11 @@ DrawerType2 {
|
|||||||
id: shareButton
|
id: shareButton
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 16
|
Layout.topMargin: 16
|
||||||
|
Layout.leftMargin: 16
|
||||||
|
Layout.rightMargin: 16
|
||||||
|
|
||||||
text: qsTr("Share")
|
text: qsTr("Share")
|
||||||
imageSource: "qrc:/images/controls/share-2.svg"
|
leftImageSource: "qrc:/images/controls/share-2.svg"
|
||||||
|
|
||||||
KeyNavigation.tab: copyConfigTextButton
|
|
||||||
|
|
||||||
clickedFunc: function() {
|
clickedFunc: function() {
|
||||||
var fileName = ""
|
var fileName = ""
|
||||||
@@ -111,6 +106,8 @@ DrawerType2 {
|
|||||||
id: copyConfigTextButton
|
id: copyConfigTextButton
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 8
|
Layout.topMargin: 8
|
||||||
|
Layout.leftMargin: 16
|
||||||
|
Layout.rightMargin: 16
|
||||||
|
|
||||||
defaultColor: AmneziaStyle.color.transparent
|
defaultColor: AmneziaStyle.color.transparent
|
||||||
hoveredColor: AmneziaStyle.color.translucentWhite
|
hoveredColor: AmneziaStyle.color.translucentWhite
|
||||||
@@ -120,18 +117,18 @@ DrawerType2 {
|
|||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
|
|
||||||
text: qsTr("Copy")
|
text: qsTr("Copy")
|
||||||
imageSource: "qrc:/images/controls/copy.svg"
|
leftImageSource: "qrc:/images/controls/copy.svg"
|
||||||
|
|
||||||
Keys.onReturnPressed: { copyConfigTextButton.clicked() }
|
Keys.onReturnPressed: { copyConfigTextButton.clicked() }
|
||||||
Keys.onEnterPressed: { copyConfigTextButton.clicked() }
|
Keys.onEnterPressed: { copyConfigTextButton.clicked() }
|
||||||
|
|
||||||
KeyNavigation.tab: copyNativeConfigStringButton.visible ? copyNativeConfigStringButton : showSettingsButton
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BasicButtonType {
|
BasicButtonType {
|
||||||
id: copyNativeConfigStringButton
|
id: copyNativeConfigStringButton
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 8
|
Layout.topMargin: 8
|
||||||
|
Layout.leftMargin: 16
|
||||||
|
Layout.rightMargin: 16
|
||||||
|
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
@@ -143,7 +140,7 @@ DrawerType2 {
|
|||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
|
|
||||||
text: qsTr("Copy config string")
|
text: qsTr("Copy config string")
|
||||||
imageSource: "qrc:/images/controls/copy.svg"
|
leftImageSource: "qrc:/images/controls/copy.svg"
|
||||||
|
|
||||||
KeyNavigation.tab: showSettingsButton
|
KeyNavigation.tab: showSettingsButton
|
||||||
}
|
}
|
||||||
@@ -153,6 +150,8 @@ DrawerType2 {
|
|||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 24
|
Layout.topMargin: 24
|
||||||
|
Layout.leftMargin: 16
|
||||||
|
Layout.rightMargin: 16
|
||||||
|
|
||||||
defaultColor: AmneziaStyle.color.transparent
|
defaultColor: AmneziaStyle.color.transparent
|
||||||
hoveredColor: AmneziaStyle.color.translucentWhite
|
hoveredColor: AmneziaStyle.color.translucentWhite
|
||||||
@@ -164,10 +163,8 @@ DrawerType2 {
|
|||||||
text: qsTr("Show connection settings")
|
text: qsTr("Show connection settings")
|
||||||
|
|
||||||
clickedFunc: function() {
|
clickedFunc: function() {
|
||||||
configContentDrawer.open()
|
configContentDrawer.openTriggered()
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyNavigation.tab: header
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawerType2 {
|
DrawerType2 {
|
||||||
@@ -178,30 +175,11 @@ DrawerType2 {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
expandedHeight: parent.height * 0.9
|
expandedHeight: parent.height * 0.9
|
||||||
|
|
||||||
onClosed: {
|
expandedStateContent: Item {
|
||||||
if (!GC.isMobile()) {
|
|
||||||
header.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expandedContent: Item {
|
|
||||||
id: configContentContainer
|
id: configContentContainer
|
||||||
|
|
||||||
implicitHeight: configContentDrawer.expandedHeight
|
implicitHeight: configContentDrawer.expandedHeight
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: configContentDrawer
|
|
||||||
enabled: !GC.isMobile()
|
|
||||||
function onOpened() {
|
|
||||||
focusItem.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: focusItem
|
|
||||||
KeyNavigation.tab: backButton
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: copyNativeConfigStringButton
|
target: copyNativeConfigStringButton
|
||||||
function onClicked() {
|
function onClicked() {
|
||||||
@@ -231,9 +209,7 @@ DrawerType2 {
|
|||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.topMargin: 16
|
anchors.topMargin: 16
|
||||||
|
|
||||||
backButtonFunction: function() { configContentDrawer.close() }
|
backButtonFunction: function() { configContentDrawer.closeTriggered() }
|
||||||
|
|
||||||
KeyNavigation.tab: focusItem
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FlickableType {
|
FlickableType {
|
||||||
@@ -302,6 +278,10 @@ DrawerType2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: ColumnLayout {
|
||||||
|
width: listView.width
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: qrCodeContainer
|
id: qrCodeContainer
|
||||||
@@ -309,6 +289,8 @@ DrawerType2 {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: width
|
Layout.preferredHeight: width
|
||||||
Layout.topMargin: 20
|
Layout.topMargin: 20
|
||||||
|
Layout.leftMargin: 16
|
||||||
|
Layout.rightMargin: 16
|
||||||
|
|
||||||
visible: ExportController.qrCodesCount > 0
|
visible: ExportController.qrCodesCount > 0
|
||||||
|
|
||||||
@@ -320,6 +302,32 @@ DrawerType2 {
|
|||||||
|
|
||||||
source: ExportController.qrCodesCount ? ExportController.qrCodes[0] : ""
|
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 {
|
Timer {
|
||||||
property int index: 0
|
property int index: 0
|
||||||
interval: 1000
|
interval: 1000
|
||||||
@@ -346,6 +354,8 @@ DrawerType2 {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 24
|
Layout.topMargin: 24
|
||||||
Layout.bottomMargin: 32
|
Layout.bottomMargin: 32
|
||||||
|
Layout.leftMargin: 16
|
||||||
|
Layout.rightMargin: 16
|
||||||
|
|
||||||
visible: ExportController.qrCodesCount > 0
|
visible: ExportController.qrCodesCount > 0
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,6 @@ Rectangle {
|
|||||||
implicitWidth: (rootWidth - 32) / 2
|
implicitWidth: (rootWidth - 32) / 2
|
||||||
text: "UDP"
|
text: "UDP"
|
||||||
|
|
||||||
KeyNavigation.tab: tcpButton
|
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.currentIndex = 0
|
root.currentIndex = 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Qt5Compat.GraphicalEffects
|
|||||||
|
|
||||||
import Style 1.0
|
import Style 1.0
|
||||||
|
|
||||||
Item {
|
FocusScope {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property string backButtonImage: "qrc:/images/controls/arrow-left.svg"
|
property string backButtonImage: "qrc:/images/controls/arrow-left.svg"
|
||||||
@@ -15,12 +15,6 @@ Item {
|
|||||||
|
|
||||||
visible: backButtonImage !== ""
|
visible: backButtonImage !== ""
|
||||||
|
|
||||||
onActiveFocusChanged: {
|
|
||||||
if (activeFocus) {
|
|
||||||
backButton.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: content
|
id: content
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ Button {
|
|||||||
property int borderWidth: 0
|
property int borderWidth: 0
|
||||||
property int borderFocusedWidth: 1
|
property int borderFocusedWidth: 1
|
||||||
|
|
||||||
property string imageSource
|
property string leftImageSource
|
||||||
property string rightImageSource
|
property string rightImageSource
|
||||||
property string leftImageColor: textColor
|
property string leftImageColor: textColor
|
||||||
|
property bool changeLeftImageSize: true
|
||||||
|
|
||||||
property bool squareLeftSide: false
|
property bool squareLeftSide: false
|
||||||
|
|
||||||
@@ -34,10 +35,35 @@ Button {
|
|||||||
|
|
||||||
property alias buttonTextLabel: buttonText
|
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
|
implicitHeight: 56
|
||||||
|
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
focusPolicy: Qt.TabFocus
|
|
||||||
|
|
||||||
onFocusChanged: {
|
onFocusChanged: {
|
||||||
if (root.activeFocus) {
|
if (root.activeFocus) {
|
||||||
@@ -127,24 +153,29 @@ Button {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
Layout.preferredHeight: 20
|
id: leftImage
|
||||||
Layout.preferredWidth: 20
|
source: root.leftImageSource
|
||||||
|
visible: root.leftImageSource === "" ? false : true
|
||||||
source: root.imageSource
|
|
||||||
visible: root.imageSource === "" ? false : true
|
|
||||||
|
|
||||||
layer {
|
layer {
|
||||||
enabled: true
|
enabled: leftImageColor !== "" ? true : false
|
||||||
effect: ColorOverlay {
|
effect: ColorOverlay {
|
||||||
color: leftImageColor
|
color: leftImageColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (root.changeLeftImageSize) {
|
||||||
|
leftImage.Layout.preferredHeight = 20
|
||||||
|
leftImage.Layout.preferredWidth = 20
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ButtonTextType {
|
ButtonTextType {
|
||||||
id: buttonText
|
id: buttonText
|
||||||
|
|
||||||
color: textColor
|
color: root.textColor
|
||||||
text: root.text
|
text: root.text
|
||||||
visible: root.text === "" ? false : true
|
visible: root.text === "" ? false : true
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Popup {
|
|||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
Overlay.modal: Rectangle {
|
Overlay.modal: Rectangle {
|
||||||
color: Qt.rgba(14/255, 14/255, 17/255, 0.8)
|
color: AmneziaStyle.color.translucentMidnightBlack
|
||||||
}
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ RadioButton {
|
|||||||
|
|
||||||
property string textColor: AmneziaStyle.color.midnightBlack
|
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 selectedBorderColor: AmneziaStyle.color.goldenApricot
|
||||||
property string defaultBodredColor: AmneziaStyle.color.transparent
|
property string defaultBodredColor: AmneziaStyle.color.transparent
|
||||||
|
property string focusBorderColor: AmneziaStyle.color.paleGray
|
||||||
property int borderWidth: 0
|
property int borderWidth: 0
|
||||||
|
|
||||||
implicitWidth: content.implicitWidth
|
implicitWidth: content.implicitWidth
|
||||||
@@ -29,6 +30,32 @@ RadioButton {
|
|||||||
|
|
||||||
hoverEnabled: true
|
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 {
|
indicator: Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: 16
|
radius: 16
|
||||||
@@ -52,6 +79,8 @@ RadioButton {
|
|||||||
return pressedBorderColor
|
return pressedBorderColor
|
||||||
} else if (root.checked) {
|
} else if (root.checked) {
|
||||||
return selectedBorderColor
|
return selectedBorderColor
|
||||||
|
} else if (root.activeFocus) {
|
||||||
|
return focusBorderColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return defaultBodredColor
|
return defaultBodredColor
|
||||||
@@ -59,7 +88,7 @@ RadioButton {
|
|||||||
|
|
||||||
border.width: {
|
border.width: {
|
||||||
if (root.enabled) {
|
if (root.enabled) {
|
||||||
if(root.checked) {
|
if(root.checked || root.activeFocus) {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
return root.pressed ? 1 : 0
|
return root.pressed ? 1 : 0
|
||||||
|
|||||||
@@ -25,10 +25,15 @@ Button {
|
|||||||
|
|
||||||
property real textOpacity: 1.0
|
property real textOpacity: 1.0
|
||||||
|
|
||||||
|
property alias focusItem: rightImage
|
||||||
|
|
||||||
|
property FlickableType parentFlickable
|
||||||
|
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
id: backgroundRect
|
id: backgroundRect
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: 16
|
radius: 16
|
||||||
|
|
||||||
@@ -39,13 +44,31 @@ Button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureVisible(item) {
|
||||||
|
if (item.activeFocus) {
|
||||||
|
if (root.parentFlickable) {
|
||||||
|
root.parentFlickable.ensureVisible(root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocusChanged: {
|
||||||
|
ensureVisible(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
focusItem.onFocusChanged: {
|
||||||
|
root.ensureVisible(focusItem)
|
||||||
|
}
|
||||||
|
|
||||||
contentItem: Item {
|
contentItem: Item {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
|
|
||||||
implicitHeight: content.implicitHeight
|
implicitHeight: content.implicitHeight
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: content
|
id: content
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
@@ -61,6 +84,7 @@ Button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
|
||||||
ListItemTitleType {
|
ListItemTitleType {
|
||||||
text: root.headerText
|
text: root.headerText
|
||||||
visible: text !== ""
|
visible: text !== ""
|
||||||
@@ -123,6 +147,7 @@ Button {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: rightImageBackground
|
id: rightImageBackground
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: 12
|
radius: 12
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
@@ -131,10 +156,9 @@ Button {
|
|||||||
PropertyAnimation { duration: 200 }
|
PropertyAnimation { duration: 200 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (clickedFunction && typeof clickedFunction === "function") {
|
root.clicked()
|
||||||
clickedFunction()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,6 +169,7 @@ Button {
|
|||||||
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
|
enabled: root.enabled
|
||||||
|
|
||||||
onEntered: {
|
onEntered: {
|
||||||
backgroundRect.color = root.hoveredColor
|
backgroundRect.color = root.hoveredColor
|
||||||
|
|||||||
@@ -9,17 +9,14 @@ import "TextTypes"
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property string drawerExpanded: "expanded"
|
readonly property string drawerExpandedStateName: "expanded"
|
||||||
readonly property string drawerCollapsed: "collapsed"
|
readonly property string drawerCollapsedStateName: "collapsed"
|
||||||
|
|
||||||
readonly property bool isOpened: drawerContent.state === root.drawerExpanded || (drawerContent.state === root.drawerCollapsed && dragArea.drag.active === true)
|
readonly property bool isOpened: isExpandedStateActive() || (isCollapsedStateActive && (dragArea.drag.active === true))
|
||||||
readonly property bool isClosed: drawerContent.state === root.drawerCollapsed && dragArea.drag.active === false
|
readonly property bool isClosed: isCollapsedStateActive() && (dragArea.drag.active === false)
|
||||||
|
|
||||||
readonly property bool isExpanded: drawerContent.state === root.drawerExpanded
|
property Component collapsedStateContent
|
||||||
readonly property bool isCollapsed: drawerContent.state === root.drawerCollapsed
|
property Component expandedStateContent
|
||||||
|
|
||||||
property Component collapsedContent
|
|
||||||
property Component expandedContent
|
|
||||||
|
|
||||||
property string defaultColor: AmneziaStyle.color.onyxBlack
|
property string defaultColor: AmneziaStyle.color.onyxBlack
|
||||||
property string borderColor: AmneziaStyle.color.slateGray
|
property string borderColor: AmneziaStyle.color.slateGray
|
||||||
@@ -29,29 +26,41 @@ Item {
|
|||||||
|
|
||||||
property int depthIndex: 0
|
property int depthIndex: 0
|
||||||
|
|
||||||
signal entered
|
signal cursorEntered
|
||||||
signal exited
|
signal cursorExited
|
||||||
signal pressed(bool pressed, bool entered)
|
signal pressed(bool pressed, bool entered)
|
||||||
|
|
||||||
signal aboutToHide
|
signal aboutToHide
|
||||||
signal aboutToShow
|
signal aboutToShow
|
||||||
signal close
|
signal closeTriggered
|
||||||
signal open
|
signal openTriggered
|
||||||
signal closed
|
signal closed
|
||||||
signal opened
|
signal opened
|
||||||
|
|
||||||
|
function isExpandedStateActive() {
|
||||||
|
return isStateActive(drawerExpandedStateName)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCollapsedStateActive() {
|
||||||
|
return isStateActive(drawerCollapsedStateName)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStateActive(stateName) {
|
||||||
|
return drawerContent.state === stateName
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: PageController
|
target: PageController
|
||||||
|
|
||||||
function onCloseTopDrawer() {
|
function onCloseTopDrawer() {
|
||||||
if (depthIndex === PageController.getDrawerDepth()) {
|
if (depthIndex === PageController.getDrawerDepth()) {
|
||||||
if (isCollapsed) {
|
if (isCollapsedStateActive()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
aboutToHide()
|
aboutToHide()
|
||||||
|
|
||||||
drawerContent.state = root.drawerCollapsed
|
drawerContent.state = root.drawerCollapsedStateName
|
||||||
depthIndex = 0
|
depthIndex = 0
|
||||||
closed()
|
closed()
|
||||||
}
|
}
|
||||||
@@ -61,30 +70,52 @@ Item {
|
|||||||
Connections {
|
Connections {
|
||||||
target: root
|
target: root
|
||||||
|
|
||||||
function onClose() {
|
function onCloseTriggered() {
|
||||||
if (isCollapsed) {
|
if (isCollapsedStateActive()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
aboutToHide()
|
aboutToHide()
|
||||||
|
|
||||||
drawerContent.state = root.drawerCollapsed
|
|
||||||
depthIndex = 0
|
|
||||||
PageController.setDrawerDepth(PageController.getDrawerDepth() - 1)
|
|
||||||
closed()
|
closed()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onOpen() {
|
function onClosed() {
|
||||||
if (isExpanded) {
|
drawerContent.state = root.drawerCollapsedStateName
|
||||||
|
|
||||||
|
if (root.isCollapsedStateActive()) {
|
||||||
|
var initialPageNavigationBarColor = PageController.getInitialPageNavigationBarColor()
|
||||||
|
if (initialPageNavigationBarColor !== 0xFF1C1D21) {
|
||||||
|
PageController.updateNavigationBarColor(initialPageNavigationBarColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depthIndex = 0
|
||||||
|
PageController.decrementDrawerDepth()
|
||||||
|
FocusController.dropRootObject(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOpenTriggered() {
|
||||||
|
if (root.isExpandedStateActive()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
aboutToShow()
|
root.aboutToShow()
|
||||||
|
|
||||||
drawerContent.state = root.drawerExpanded
|
root.opened()
|
||||||
depthIndex = PageController.getDrawerDepth() + 1
|
}
|
||||||
PageController.setDrawerDepth(depthIndex)
|
|
||||||
opened()
|
function onOpened() {
|
||||||
|
drawerContent.state = root.drawerExpandedStateName
|
||||||
|
|
||||||
|
if (isExpandedStateActive()) {
|
||||||
|
if (PageController.getInitialPageNavigationBarColor() !== 0xFF1C1D21) {
|
||||||
|
PageController.updateNavigationBarColor(0xFF1C1D21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depthIndex = PageController.incrementDrawerDepth()
|
||||||
|
FocusController.pushRootObject(root)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +123,7 @@ Item {
|
|||||||
id: background
|
id: background
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: root.isCollapsed ? AmneziaStyle.color.transparent : Qt.rgba(14/255, 14/255, 17/255, 0.8)
|
color: root.isCollapsed ? AmneziaStyle.color.transparent : AmneziaStyle.color.translucentMidnightBlack
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
PropertyAnimation { duration: 200 }
|
PropertyAnimation { duration: 200 }
|
||||||
@@ -102,18 +133,17 @@ Item {
|
|||||||
MouseArea {
|
MouseArea {
|
||||||
id: emptyArea
|
id: emptyArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
enabled: root.isExpanded
|
|
||||||
visible: enabled
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.close()
|
root.closeTriggered()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: dragArea
|
id: dragArea
|
||||||
|
objectName: "dragArea"
|
||||||
|
|
||||||
anchors.fill: drawerContentBackground
|
anchors.fill: drawerContentBackground
|
||||||
cursorShape: root.isCollapsed ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
|
|
||||||
enabled: drawerContent.implicitHeight > 0
|
enabled: drawerContent.implicitHeight > 0
|
||||||
@@ -125,35 +155,36 @@ Item {
|
|||||||
|
|
||||||
/** If drag area is released at any point other than min or max y, transition to the other state */
|
/** If drag area is released at any point other than min or max y, transition to the other state */
|
||||||
onReleased: {
|
onReleased: {
|
||||||
if (root.isCollapsed && drawerContent.y < dragArea.drag.maximumY) {
|
if (isCollapsedStateActive() && drawerContent.y < dragArea.drag.maximumY) {
|
||||||
root.open()
|
root.openTriggered()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (root.isExpanded && drawerContent.y > dragArea.drag.minimumY) {
|
if (isExpandedStateActive() && drawerContent.y > dragArea.drag.minimumY) {
|
||||||
root.close()
|
root.closeTriggered()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onEntered: {
|
onEntered: {
|
||||||
root.entered()
|
root.cursorEntered()
|
||||||
}
|
}
|
||||||
onExited: {
|
onExited: {
|
||||||
root.exited()
|
root.cursorExited()
|
||||||
}
|
}
|
||||||
onPressedChanged: {
|
onPressedChanged: {
|
||||||
root.pressed(pressed, entered)
|
root.pressed(pressed, entered)
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (root.isCollapsed) {
|
if (isCollapsedStateActive()) {
|
||||||
root.open()
|
root.openTriggered()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: drawerContentBackground
|
id: drawerContentBackground
|
||||||
|
objectName: "drawerContentBackground"
|
||||||
|
|
||||||
anchors { left: drawerContent.left; right: drawerContent.right; top: drawerContent.top }
|
anchors { left: drawerContent.left; right: drawerContent.right; top: drawerContent.top }
|
||||||
height: root.height
|
height: root.height
|
||||||
@@ -174,53 +205,80 @@ Item {
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: drawerContent
|
id: drawerContent
|
||||||
|
objectName: "drawerContent"
|
||||||
|
|
||||||
Drag.active: dragArea.drag.active
|
Drag.active: dragArea.drag.active
|
||||||
anchors.right: root.right
|
anchors.right: root.right
|
||||||
anchors.left: root.left
|
anchors.left: root.left
|
||||||
y: root.height - drawerContent.height
|
|
||||||
state: root.drawerCollapsed
|
|
||||||
|
|
||||||
implicitHeight: root.isCollapsed ? collapsedHeight : expandedHeight
|
state: root.drawerCollapsedStateName
|
||||||
|
|
||||||
onStateChanged: {
|
|
||||||
if (root.isCollapsed) {
|
|
||||||
var initialPageNavigationBarColor = PageController.getInitialPageNavigationBarColor()
|
|
||||||
if (initialPageNavigationBarColor !== 0xFF1C1D21) {
|
|
||||||
PageController.updateNavigationBarColor(initialPageNavigationBarColor)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (root.isExpanded) {
|
|
||||||
if (PageController.getInitialPageNavigationBarColor() !== 0xFF1C1D21) {
|
|
||||||
PageController.updateNavigationBarColor(0xFF1C1D21)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
states: [
|
states: [
|
||||||
State {
|
State {
|
||||||
name: root.drawerCollapsed
|
name: root.drawerCollapsedStateName
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: drawerContent
|
target: drawerContent
|
||||||
|
implicitHeight: collapsedHeight
|
||||||
y: root.height - root.collapsedHeight
|
y: root.height - root.collapsedHeight
|
||||||
}
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: background
|
||||||
|
color: AmneziaStyle.color.transparent
|
||||||
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: dragArea
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: emptyArea
|
||||||
|
enabled: false
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: collapsedLoader
|
||||||
|
// visible: true
|
||||||
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: expandedLoader
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
}
|
||||||
},
|
},
|
||||||
State {
|
State {
|
||||||
name: root.drawerExpanded
|
name: root.drawerExpandedStateName
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
target: drawerContent
|
target: drawerContent
|
||||||
|
implicitHeight: expandedHeight
|
||||||
y: dragArea.drag.minimumY
|
y: dragArea.drag.minimumY
|
||||||
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: background
|
||||||
|
color: Qt.rgba(14/255, 14/255, 17/255, 0.8)
|
||||||
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: dragArea
|
||||||
|
cursorShape: Qt.ArrowCursor
|
||||||
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: emptyArea
|
||||||
|
enabled: true
|
||||||
|
visible: true
|
||||||
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: collapsedLoader
|
||||||
|
// visible: false
|
||||||
|
}
|
||||||
|
PropertyChanges {
|
||||||
|
target: expandedLoader
|
||||||
|
visible: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
transitions: [
|
transitions: [
|
||||||
Transition {
|
Transition {
|
||||||
from: root.drawerCollapsed
|
from: root.drawerCollapsedStateName
|
||||||
to: root.drawerExpanded
|
to: root.drawerExpandedStateName
|
||||||
PropertyAnimation {
|
PropertyAnimation {
|
||||||
target: drawerContent
|
target: drawerContent
|
||||||
properties: "y"
|
properties: "y"
|
||||||
@@ -228,8 +286,8 @@ Item {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
Transition {
|
Transition {
|
||||||
from: root.drawerExpanded
|
from: root.drawerExpandedStateName
|
||||||
to: root.drawerCollapsed
|
to: root.drawerCollapsedStateName
|
||||||
PropertyAnimation {
|
PropertyAnimation {
|
||||||
target: drawerContent
|
target: drawerContent
|
||||||
properties: "y"
|
properties: "y"
|
||||||
@@ -241,7 +299,7 @@ Item {
|
|||||||
Loader {
|
Loader {
|
||||||
id: collapsedLoader
|
id: collapsedLoader
|
||||||
|
|
||||||
sourceComponent: root.collapsedContent
|
sourceComponent: root.collapsedStateContent
|
||||||
|
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -250,8 +308,7 @@ Item {
|
|||||||
Loader {
|
Loader {
|
||||||
id: expandedLoader
|
id: expandedLoader
|
||||||
|
|
||||||
visible: root.isExpanded
|
sourceComponent: root.expandedStateContent
|
||||||
sourceComponent: root.expandedContent
|
|
||||||
|
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
@@ -45,33 +45,56 @@ Item {
|
|||||||
property Item drawerParent
|
property Item drawerParent
|
||||||
property Component listView
|
property Component listView
|
||||||
|
|
||||||
signal open
|
signal openTriggered
|
||||||
signal close
|
signal closeTriggered
|
||||||
|
|
||||||
function popupClosedFunc() {
|
readonly property bool isFocusable: true
|
||||||
if (!GC.isMobile()) {
|
|
||||||
this.forceActiveFocus()
|
Keys.onTabPressed: {
|
||||||
}
|
FocusController.nextKeyTabItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
property var parentFlickable
|
Keys.onBacktabPressed: {
|
||||||
onFocusChanged: {
|
FocusController.previousKeyTabItem()
|
||||||
if (root.activeFocus) {
|
}
|
||||||
if (root.parentFlickable) {
|
|
||||||
root.parentFlickable.ensureVisible(root)
|
Keys.onUpPressed: {
|
||||||
}
|
FocusController.nextKeyUpItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onDownPressed: {
|
||||||
|
FocusController.nextKeyDownItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onLeftPressed: {
|
||||||
|
FocusController.nextKeyLeftItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onRightPressed: {
|
||||||
|
FocusController.nextKeyRightItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
implicitWidth: rootButtonContent.implicitWidth
|
implicitWidth: rootButtonContent.implicitWidth
|
||||||
implicitHeight: rootButtonContent.implicitHeight
|
implicitHeight: rootButtonContent.implicitHeight
|
||||||
|
|
||||||
onOpen: {
|
onOpenTriggered: {
|
||||||
menu.open()
|
menu.openTriggered()
|
||||||
}
|
}
|
||||||
|
|
||||||
onClose: {
|
onCloseTriggered: {
|
||||||
menu.close()
|
menu.closeTriggered()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onEnterPressed: {
|
||||||
|
if (menu.isClosed) {
|
||||||
|
menu.openTriggered()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onReturnPressed: {
|
||||||
|
if (menu.isClosed) {
|
||||||
|
menu.openTriggered()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -173,7 +196,7 @@ Item {
|
|||||||
if (rootButtonClickedFunction && typeof rootButtonClickedFunction === "function") {
|
if (rootButtonClickedFunction && typeof rootButtonClickedFunction === "function") {
|
||||||
rootButtonClickedFunction()
|
rootButtonClickedFunction()
|
||||||
} else {
|
} else {
|
||||||
menu.open()
|
menu.openTriggered()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,27 +209,10 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
expandedHeight: drawerParent.height * drawerHeight
|
expandedHeight: drawerParent.height * drawerHeight
|
||||||
|
|
||||||
onClosed: {
|
expandedStateContent: Item {
|
||||||
root.popupClosedFunc()
|
|
||||||
}
|
|
||||||
|
|
||||||
expandedContent: Item {
|
|
||||||
id: container
|
id: container
|
||||||
implicitHeight: menu.expandedHeight
|
implicitHeight: menu.expandedHeight
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: menu
|
|
||||||
enabled: !GC.isMobile()
|
|
||||||
function onOpened() {
|
|
||||||
focusItem.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: focusItem
|
|
||||||
KeyNavigation.tab: backButton
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: header
|
id: header
|
||||||
|
|
||||||
@@ -218,61 +224,35 @@ Item {
|
|||||||
BackButtonType {
|
BackButtonType {
|
||||||
id: backButton
|
id: backButton
|
||||||
backButtonImage: root.headerBackButtonImage
|
backButtonImage: root.headerBackButtonImage
|
||||||
backButtonFunction: function() { menu.close() }
|
backButtonFunction: function() { menu.closeTriggered() }
|
||||||
KeyNavigation.tab: listViewLoader.item
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FlickableType {
|
Column {
|
||||||
id: flickable
|
id: col
|
||||||
anchors.top: header.bottom
|
anchors.top: header.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
anchors.topMargin: 16
|
anchors.topMargin: 16
|
||||||
contentHeight: col.implicitHeight
|
|
||||||
|
|
||||||
Column {
|
spacing: 16
|
||||||
id: col
|
|
||||||
anchors.top: parent.top
|
Header2Type {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
|
anchors.leftMargin: 16
|
||||||
|
anchors.rightMargin: 16
|
||||||
|
|
||||||
spacing: 16
|
headerText: root.headerText
|
||||||
|
|
||||||
Header2Type {
|
width: parent.width
|
||||||
anchors.left: parent.left
|
}
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.leftMargin: 16
|
|
||||||
anchors.rightMargin: 16
|
|
||||||
|
|
||||||
headerText: root.headerText
|
Loader {
|
||||||
|
id: listViewLoader
|
||||||
width: parent.width
|
sourceComponent: root.listView
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: listViewLoader
|
|
||||||
sourceComponent: root.listView
|
|
||||||
|
|
||||||
onLoaded: {
|
|
||||||
listViewLoader.item.parentFlickable = flickable
|
|
||||||
listViewLoader.item.lastItemTabClicked = function() {
|
|
||||||
focusItem.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onEnterPressed: {
|
|
||||||
if (menu.isClosed) {
|
|
||||||
menu.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onReturnPressed: {
|
|
||||||
if (menu.isClosed) {
|
|
||||||
menu.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ Flickable {
|
|||||||
|
|
||||||
function ensureVisible(item) {
|
function ensureVisible(item) {
|
||||||
if (item.y < fl.contentY) {
|
if (item.y < fl.contentY) {
|
||||||
fl.contentY = item.y
|
fl.contentY = item.y - 40 // 40 is a top margin
|
||||||
} else if (item.y + item.height > fl.contentY + fl.height) {
|
} else if (item.y + item.height > fl.contentY + fl.height) {
|
||||||
fl.contentY = item.y + item.height - fl.height + 40 // 40 is a bottom margin
|
fl.contentY = item.y + item.height - fl.height + 40 // 40 is a bottom margin
|
||||||
}
|
}
|
||||||
|
fl.returnToBounds()
|
||||||
}
|
}
|
||||||
|
|
||||||
clip: true
|
clip: true
|
||||||
@@ -24,7 +25,7 @@ Flickable {
|
|||||||
Keys.onUpPressed: scrollBar.decrease()
|
Keys.onUpPressed: scrollBar.decrease()
|
||||||
Keys.onDownPressed: scrollBar.increase()
|
Keys.onDownPressed: scrollBar.increase()
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
ScrollBar.vertical: ScrollBarType {
|
||||||
id: scrollBar
|
id: scrollBar
|
||||||
policy: fl.height >= fl.contentHeight ? ScrollBar.AlwaysOff : ScrollBar.AlwaysOn
|
policy: fl.height >= fl.contentHeight ? ScrollBar.AlwaysOff : ScrollBar.AlwaysOn
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ Item {
|
|||||||
|
|
||||||
property string descriptionText
|
property string descriptionText
|
||||||
|
|
||||||
focus: true
|
|
||||||
|
|
||||||
implicitWidth: content.implicitWidth
|
implicitWidth: content.implicitWidth
|
||||||
implicitHeight: content.implicitHeight
|
implicitHeight: content.implicitHeight
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,32 @@ RadioButton {
|
|||||||
implicitWidth: content.implicitWidth
|
implicitWidth: content.implicitWidth
|
||||||
implicitHeight: content.implicitHeight
|
implicitHeight: content.implicitHeight
|
||||||
|
|
||||||
|
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 {
|
indicator: Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: 16
|
radius: 16
|
||||||
|
|||||||
@@ -24,22 +24,39 @@ Button {
|
|||||||
property int borderFocusedWidth: 1
|
property int borderFocusedWidth: 1
|
||||||
|
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
focus: true
|
|
||||||
focusPolicy: Qt.TabFocus
|
|
||||||
|
|
||||||
icon.source: image
|
icon.source: image
|
||||||
icon.color: root.enabled ? imageColor : disableImageColor
|
icon.color: root.enabled ? imageColor : disableImageColor
|
||||||
|
|
||||||
property Flickable parentFlickable
|
property bool isFocusable: true
|
||||||
|
|
||||||
onFocusChanged: {
|
Keys.onTabPressed: {
|
||||||
if (root.activeFocus) {
|
FocusController.nextKeyTabItem()
|
||||||
if (root.parentFlickable) {
|
|
||||||
root.parentFlickable.ensureVisible(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onBacktabPressed: {
|
||||||
|
FocusController.previousKeyTabItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onUpPressed: {
|
||||||
|
FocusController.nextKeyUpItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onDownPressed: {
|
||||||
|
FocusController.nextKeyDownItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onLeftPressed: {
|
||||||
|
FocusController.nextKeyLeftItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onRightPressed: {
|
||||||
|
FocusController.nextKeyRightItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onEnterPressed: root.clicked()
|
||||||
|
Keys.onReturnPressed: root.clicked()
|
||||||
|
|
||||||
Behavior on icon.color {
|
Behavior on icon.color {
|
||||||
PropertyAnimation { duration: 200 }
|
PropertyAnimation { duration: 200 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,32 @@ Item {
|
|||||||
property bool descriptionOnTop: false
|
property bool descriptionOnTop: false
|
||||||
property bool hideDescription: true
|
property bool hideDescription: true
|
||||||
|
|
||||||
|
property bool isFocusable: !(eyeImage.visible || rightImage.visible) // TODO: this component already has focusable items
|
||||||
|
|
||||||
|
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: content.implicitWidth + content.anchors.topMargin + content.anchors.bottomMargin
|
implicitWidth: content.implicitWidth + content.anchors.topMargin + content.anchors.bottomMargin
|
||||||
implicitHeight: content.implicitHeight + content.anchors.leftMargin + content.anchors.rightMargin
|
implicitHeight: content.implicitHeight + content.anchors.leftMargin + content.anchors.rightMargin
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ ListView {
|
|||||||
|
|
||||||
property bool dividerVisible: false
|
property bool dividerVisible: false
|
||||||
|
|
||||||
currentIndex: 0
|
property int selectedIndex: 0
|
||||||
|
|
||||||
width: rootWidth
|
width: rootWidth
|
||||||
height: menuContent.contentItem.height
|
height: menuContent.contentItem.height
|
||||||
@@ -45,7 +45,7 @@ ListView {
|
|||||||
rightImageSource: imageSource
|
rightImageSource: imageSource
|
||||||
|
|
||||||
clickedFunction: function() {
|
clickedFunction: function() {
|
||||||
menuContent.currentIndex = index
|
menuContent.selectedIndex = index
|
||||||
menuContent.selectedText = name
|
menuContent.selectedText = name
|
||||||
if (menuContent.clickedFunction && typeof menuContent.clickedFunction === "function") {
|
if (menuContent.clickedFunction && typeof menuContent.clickedFunction === "function") {
|
||||||
menuContent.clickedFunction()
|
menuContent.clickedFunction()
|
||||||
@@ -62,7 +62,7 @@ ListView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (menuContent.currentIndex === index) {
|
if (menuContent.selectedIndex === index) {
|
||||||
menuContent.selectedText = name
|
menuContent.selectedText = name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,155 +20,132 @@ ListView {
|
|||||||
|
|
||||||
property var clickedFunction
|
property var clickedFunction
|
||||||
|
|
||||||
currentIndex: 0
|
property int selectedIndex: 0
|
||||||
|
|
||||||
width: rootWidth
|
width: rootWidth
|
||||||
height: root.contentItem.height
|
height: root.contentItem.height
|
||||||
|
|
||||||
clip: true
|
clip: true
|
||||||
interactive: false
|
reuseItems: true
|
||||||
|
|
||||||
property FlickableType parentFlickable
|
property bool isFocusable: true
|
||||||
property var lastItemTabClicked
|
|
||||||
|
|
||||||
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
|
|
||||||
} else {
|
|
||||||
currentFocusIndex = 0
|
|
||||||
}
|
|
||||||
this.itemAtIndex(currentFocusIndex).forceActiveFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: focusItem
|
|
||||||
Keys.onTabPressed: {
|
|
||||||
root.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible) {
|
|
||||||
focusItem.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onCurrentFocusIndexChanged: {
|
|
||||||
if (parentFlickable) {
|
|
||||||
parentFlickable.ensureVisible(this.itemAtIndex(currentFocusIndex))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ButtonGroup {
|
ButtonGroup {
|
||||||
id: buttonGroup
|
id: buttonGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerCurrentItem() {
|
function triggerCurrentItem() {
|
||||||
var item = root.itemAtIndex(currentIndex)
|
var item = root.itemAtIndex(selectedIndex)
|
||||||
var radioButton = item.children[0].children[0]
|
item.selectable.clicked()
|
||||||
radioButton.clicked()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
delegate: Item {
|
delegate: ColumnLayout {
|
||||||
|
id: content
|
||||||
|
|
||||||
|
property alias selectable: radioButton
|
||||||
|
|
||||||
implicitWidth: rootWidth
|
implicitWidth: rootWidth
|
||||||
implicitHeight: content.implicitHeight
|
|
||||||
|
|
||||||
onActiveFocusChanged: {
|
RadioButton {
|
||||||
if (activeFocus) {
|
id: radioButton
|
||||||
radioButton.forceActiveFocus()
|
|
||||||
|
implicitWidth: parent.width
|
||||||
|
implicitHeight: radioButtonContent.implicitHeight
|
||||||
|
|
||||||
|
hoverEnabled: true
|
||||||
|
|
||||||
|
property bool isFocusable: true
|
||||||
|
|
||||||
|
Keys.onTabPressed: {
|
||||||
|
FocusController.nextKeyTabItem()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
Keys.onBacktabPressed: {
|
||||||
id: content
|
FocusController.previousKeyTabItem()
|
||||||
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
Keys.onUpPressed: {
|
||||||
|
FocusController.nextKeyUpItem()
|
||||||
|
}
|
||||||
|
|
||||||
RadioButton {
|
Keys.onDownPressed: {
|
||||||
id: radioButton
|
FocusController.nextKeyDownItem()
|
||||||
|
}
|
||||||
|
|
||||||
implicitWidth: parent.width
|
Keys.onLeftPressed: {
|
||||||
implicitHeight: radioButtonContent.implicitHeight
|
FocusController.nextKeyLeftItem()
|
||||||
|
}
|
||||||
|
|
||||||
hoverEnabled: true
|
Keys.onRightPressed: {
|
||||||
|
FocusController.nextKeyRightItem()
|
||||||
|
}
|
||||||
|
|
||||||
indicator: Rectangle {
|
indicator: Rectangle {
|
||||||
width: parent.width - 1
|
width: parent.width - 1
|
||||||
height: parent.height
|
height: parent.height
|
||||||
color: radioButton.hovered ? AmneziaStyle.color.slateGray : AmneziaStyle.color.onyxBlack
|
color: radioButton.hovered ? AmneziaStyle.color.slateGray : AmneziaStyle.color.onyxBlack
|
||||||
border.color: radioButton.focus ? AmneziaStyle.color.paleGray : AmneziaStyle.color.transparent
|
border.color: radioButton.focus ? AmneziaStyle.color.paleGray : AmneziaStyle.color.transparent
|
||||||
border.width: radioButton.focus ? 1 : 0
|
border.width: radioButton.focus ? 1 : 0
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
PropertyAnimation { duration: 200 }
|
PropertyAnimation { duration: 200 }
|
||||||
}
|
}
|
||||||
Behavior on border.color {
|
Behavior on border.color {
|
||||||
PropertyAnimation { duration: 200 }
|
PropertyAnimation { duration: 200 }
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
MouseArea {
|
||||||
id: radioButtonContent
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
anchors.rightMargin: 16
|
RowLayout {
|
||||||
anchors.leftMargin: 16
|
id: radioButtonContent
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
z: 1
|
anchors.rightMargin: 16
|
||||||
|
anchors.leftMargin: 16
|
||||||
|
|
||||||
ParagraphTextType {
|
z: 1
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 20
|
|
||||||
Layout.bottomMargin: 20
|
|
||||||
|
|
||||||
text: name
|
ParagraphTextType {
|
||||||
maximumLineCount: root.textMaximumLineCount
|
Layout.fillWidth: true
|
||||||
elide: root.textElide
|
Layout.topMargin: 20
|
||||||
|
Layout.bottomMargin: 20
|
||||||
|
|
||||||
}
|
text: name
|
||||||
|
maximumLineCount: root.textMaximumLineCount
|
||||||
|
elide: root.textElide
|
||||||
|
|
||||||
Image {
|
|
||||||
source: imageSource
|
|
||||||
visible: radioButton.checked
|
|
||||||
|
|
||||||
width: 24
|
|
||||||
height: 24
|
|
||||||
|
|
||||||
Layout.rightMargin: 8
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ButtonGroup.group: buttonGroup
|
Image {
|
||||||
checked: root.currentIndex === index
|
source: imageSource
|
||||||
|
visible: radioButton.checked
|
||||||
|
|
||||||
onClicked: {
|
width: 24
|
||||||
root.currentIndex = index
|
height: 24
|
||||||
root.selectedText = name
|
|
||||||
if (clickedFunction && typeof clickedFunction === "function") {
|
Layout.rightMargin: 8
|
||||||
clickedFunction()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ButtonGroup.group: buttonGroup
|
||||||
|
checked: root.selectedIndex === index
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.selectedIndex = index
|
||||||
|
root.selectedText = name
|
||||||
|
if (clickedFunction && typeof clickedFunction === "function") {
|
||||||
|
clickedFunction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (root.currentIndex === index) {
|
if (root.selectedIndex === index) {
|
||||||
root.selectedText = name
|
root.selectedText = name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,53 +9,22 @@ Item {
|
|||||||
|
|
||||||
property StackView stackView: StackView.view
|
property StackView stackView: StackView.view
|
||||||
|
|
||||||
property var defaultActiveFocusItem: null
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (visible && !GC.isMobile()) {
|
if (visible) {
|
||||||
timer.start()
|
timer.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function lastItemTabClicked(focusItem) {
|
|
||||||
if (GC.isMobile()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (focusItem) {
|
|
||||||
focusItem.forceActiveFocus()
|
|
||||||
PageController.forceTabBarActiveFocus()
|
|
||||||
} else {
|
|
||||||
if (defaultActiveFocusItem) {
|
|
||||||
defaultActiveFocusItem.forceActiveFocus()
|
|
||||||
}
|
|
||||||
PageController.forceTabBarActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MouseArea {
|
|
||||||
// id: globalMouseArea
|
|
||||||
// z: 99
|
|
||||||
// anchors.fill: parent
|
|
||||||
|
|
||||||
// enabled: true
|
|
||||||
|
|
||||||
// onPressed: function(mouse) {
|
|
||||||
// forceActiveFocus()
|
|
||||||
// mouse.accepted = false
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Set a timer to set focus after a short delay
|
// Set a timer to set focus after a short delay
|
||||||
Timer {
|
Timer {
|
||||||
id: timer
|
id: timer
|
||||||
interval: 100 // Milliseconds
|
interval: 200 // Milliseconds
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (defaultActiveFocusItem) {
|
console.debug(">>> PageType timer triggered")
|
||||||
defaultActiveFocusItem.forceActiveFocus()
|
FocusController.resetRootObject()
|
||||||
}
|
FocusController.setFocusOnDefaultItem()
|
||||||
}
|
}
|
||||||
repeat: false // Stop the timer after one trigger
|
repeat: false // Stop the timer after one trigger
|
||||||
running: !GC.isMobile() // Start the timer
|
running: true // Start the timer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import QtQuick.Layouts
|
|||||||
import Style 1.0
|
import Style 1.0
|
||||||
|
|
||||||
import "TextTypes"
|
import "TextTypes"
|
||||||
|
import "../Config"
|
||||||
|
|
||||||
Popup {
|
Popup {
|
||||||
id: root
|
id: root
|
||||||
@@ -24,15 +25,15 @@ Popup {
|
|||||||
|
|
||||||
Overlay.modal: Rectangle {
|
Overlay.modal: Rectangle {
|
||||||
visible: root.closeButtonVisible
|
visible: root.closeButtonVisible
|
||||||
color: Qt.rgba(14/255, 14/255, 17/255, 0.8)
|
color: AmneziaStyle.color.translucentMidnightBlack
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpened: {
|
onOpened: {
|
||||||
focusItem.forceActiveFocus()
|
timer.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
onClosed: {
|
onClosed: {
|
||||||
PageController.forceStackActiveFocus()
|
FocusController.dropRootObject(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
@@ -42,6 +43,17 @@ Popup {
|
|||||||
radius: 4
|
radius: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: timer
|
||||||
|
interval: 200 // Milliseconds
|
||||||
|
onTriggered: {
|
||||||
|
FocusController.pushRootObject(root)
|
||||||
|
FocusController.setFocusItem(closeButton)
|
||||||
|
}
|
||||||
|
repeat: false // Stop the timer after one trigger
|
||||||
|
running: true // Start the timer
|
||||||
|
}
|
||||||
|
|
||||||
contentItem: Item {
|
contentItem: Item {
|
||||||
implicitWidth: content.implicitWidth
|
implicitWidth: content.implicitWidth
|
||||||
implicitHeight: content.implicitHeight
|
implicitHeight: content.implicitHeight
|
||||||
@@ -72,11 +84,6 @@ Popup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
|
||||||
id: focusItem
|
|
||||||
KeyNavigation.tab: closeButton
|
|
||||||
}
|
|
||||||
|
|
||||||
BasicButtonType {
|
BasicButtonType {
|
||||||
id: closeButton
|
id: closeButton
|
||||||
visible: closeButtonVisible
|
visible: closeButtonVisible
|
||||||
@@ -92,7 +99,6 @@ Popup {
|
|||||||
borderWidth: 0
|
borderWidth: 0
|
||||||
|
|
||||||
text: qsTr("Close")
|
text: qsTr("Close")
|
||||||
KeyNavigation.tab: focusItem
|
|
||||||
|
|
||||||
clickedFunc: function() {
|
clickedFunc: function() {
|
||||||
root.close()
|
root.close()
|
||||||
|
|||||||
11
client/ui/qml/Controls2/ScrollBarType.qml
Normal file
11
client/ui/qml/Controls2/ScrollBarType.qml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
|
||||||
|
import "./"
|
||||||
|
import "../Controls2"
|
||||||
|
|
||||||
|
ScrollBar {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
policy: parent.height >= parent.contentHeight ? ScrollBar.AlwaysOff : ScrollBar.AlwaysOn
|
||||||
|
}
|
||||||
@@ -35,10 +35,37 @@ Switch {
|
|||||||
property string hoveredIndicatorBackgroundColor: AmneziaStyle.color.translucentWhite
|
property string hoveredIndicatorBackgroundColor: AmneziaStyle.color.translucentWhite
|
||||||
property string defaultIndicatorBackgroundColor: AmneziaStyle.color.transparent
|
property string defaultIndicatorBackgroundColor: AmneziaStyle.color.transparent
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
hoverEnabled: enabled ? true : false
|
hoverEnabled: enabled ? true : false
|
||||||
focusPolicy: Qt.TabFocus
|
focusPolicy: Qt.TabFocus
|
||||||
|
|
||||||
property FlickableType parentFlickable: null
|
property FlickableType parentFlickable: null
|
||||||
|
|
||||||
onFocusChanged: {
|
onFocusChanged: {
|
||||||
if (root.activeFocus) {
|
if (root.activeFocus) {
|
||||||
if (root.parentFlickable) {
|
if (root.parentFlickable) {
|
||||||
@@ -131,13 +158,15 @@ Switch {
|
|||||||
enabled: false
|
enabled: false
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onEnterPressed: {
|
Keys.onEnterPressed: event => handleSwitch(event)
|
||||||
root.checked = !root.checked
|
Keys.onReturnPressed: event => handleSwitch(event)
|
||||||
root.checkedChanged()
|
Keys.onSpacePressed: event => handleSwitch(event)
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onReturnPressed: {
|
function handleSwitch(event) {
|
||||||
root.checked = !root.checked
|
if (!event.isAutoRepeat) {
|
||||||
root.checkedChanged()
|
root.checked = !root.checked
|
||||||
|
root.checkedChanged()
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,35 @@ TabButton {
|
|||||||
|
|
||||||
property bool isSelected: false
|
property bool isSelected: false
|
||||||
|
|
||||||
|
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: 48
|
implicitHeight: 48
|
||||||
|
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
focusPolicy: Qt.TabFocus
|
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
id: background
|
id: background
|
||||||
|
|||||||
@@ -14,13 +14,38 @@ TabButton {
|
|||||||
|
|
||||||
property bool isSelected: false
|
property bool isSelected: false
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
property string borderFocusedColor: AmneziaStyle.color.paleGray
|
property string borderFocusedColor: AmneziaStyle.color.paleGray
|
||||||
property int borderFocusedWidth: 1
|
property int borderFocusedWidth: 1
|
||||||
|
|
||||||
property var clickedFunc
|
property var clickedFunc
|
||||||
|
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
focusPolicy: Qt.TabFocus
|
|
||||||
|
|
||||||
icon.source: image
|
icon.source: image
|
||||||
icon.color: isSelected ? selectedColor : defaultColor
|
icon.color: isSelected ? selectedColor : defaultColor
|
||||||
@@ -41,7 +66,7 @@ TabButton {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
enabled: false
|
enabled: false
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onEnterPressed: {
|
Keys.onEnterPressed: {
|
||||||
if (root.clickedFunc && typeof root.clickedFunc === "function") {
|
if (root.clickedFunc && typeof root.clickedFunc === "function") {
|
||||||
root.clickedFunc()
|
root.clickedFunc()
|
||||||
|
|||||||
@@ -78,9 +78,6 @@ Rectangle {
|
|||||||
placeholderText: root.placeholderText
|
placeholderText: root.placeholderText
|
||||||
text: root.text
|
text: root.text
|
||||||
|
|
||||||
|
|
||||||
KeyNavigation.tab: firstButton
|
|
||||||
|
|
||||||
onCursorVisibleChanged: {
|
onCursorVisibleChanged: {
|
||||||
if (textArea.cursorVisible) {
|
if (textArea.cursorVisible) {
|
||||||
fl.interactive = true
|
fl.interactive = true
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ Item {
|
|||||||
implicitHeight: content.implicitHeight
|
implicitHeight: content.implicitHeight
|
||||||
|
|
||||||
property FlickableType parentFlickable
|
property FlickableType parentFlickable
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: textField
|
target: textField
|
||||||
function onFocusChanged() {
|
function onFocusChanged() {
|
||||||
@@ -84,7 +85,16 @@ Item {
|
|||||||
|
|
||||||
TextField {
|
TextField {
|
||||||
id: textField
|
id: textField
|
||||||
activeFocusOnTab: false
|
|
||||||
|
property bool isFocusable: true
|
||||||
|
|
||||||
|
Keys.onTabPressed: {
|
||||||
|
FocusController.nextKeyTabItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onBacktabPressed: {
|
||||||
|
FocusController.previousKeyTabItem()
|
||||||
|
}
|
||||||
|
|
||||||
enabled: root.textFieldEditable
|
enabled: root.textFieldEditable
|
||||||
color: root.enabled ? root.textFieldTextColor : root.textFieldTextDisabledColor
|
color: root.enabled ? root.textFieldTextColor : root.textFieldTextDisabledColor
|
||||||
@@ -183,7 +193,7 @@ Item {
|
|||||||
|
|
||||||
focusPolicy: Qt.NoFocus
|
focusPolicy: Qt.NoFocus
|
||||||
text: root.buttonText
|
text: root.buttonText
|
||||||
imageSource: root.buttonImageSource
|
leftImageSource: root.buttonImageSource
|
||||||
|
|
||||||
anchors.top: content.top
|
anchors.top: content.top
|
||||||
anchors.bottom: content.bottom
|
anchors.bottom: content.bottom
|
||||||
@@ -209,9 +219,9 @@ Item {
|
|||||||
clickedFunc()
|
clickedFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (KeyNavigation.tab) {
|
// if (KeyNavigation.tab) {
|
||||||
KeyNavigation.tab.forceActiveFocus();
|
// KeyNavigation.tab.forceActiveFocus();
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onReturnPressed: {
|
Keys.onReturnPressed: {
|
||||||
@@ -219,8 +229,8 @@ Item {
|
|||||||
clickedFunc()
|
clickedFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (KeyNavigation.tab) {
|
// if (KeyNavigation.tab) {
|
||||||
KeyNavigation.tab.forceActiveFocus();
|
// KeyNavigation.tab.forceActiveFocus();
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Popup {
|
|||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
Overlay.modal: Rectangle {
|
Overlay.modal: Rectangle {
|
||||||
color: Qt.rgba(14/255, 14/255, 17/255, 0.8)
|
color: AmneziaStyle.color.translucentMidnightBlack
|
||||||
}
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
|
|||||||
@@ -28,8 +28,33 @@ RadioButton {
|
|||||||
property string imageSource
|
property string imageSource
|
||||||
property bool showImage
|
property bool showImage
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
focusPolicy: Qt.TabFocus
|
|
||||||
|
|
||||||
indicator: Rectangle {
|
indicator: Rectangle {
|
||||||
id: background
|
id: background
|
||||||
|
|||||||
@@ -22,5 +22,9 @@ QtObject {
|
|||||||
readonly property color sheerWhite: Qt.rgba(1, 1, 1, 0.12)
|
readonly property color sheerWhite: Qt.rgba(1, 1, 1, 0.12)
|
||||||
readonly property color translucentWhite: Qt.rgba(1, 1, 1, 0.08)
|
readonly property color translucentWhite: Qt.rgba(1, 1, 1, 0.08)
|
||||||
readonly property color barelyTranslucentWhite: Qt.rgba(1, 1, 1, 0.05)
|
readonly property color barelyTranslucentWhite: Qt.rgba(1, 1, 1, 0.05)
|
||||||
|
readonly property color translucentMidnightBlack: Qt.rgba(14/255, 14/255, 17/255, 0.8)
|
||||||
|
readonly property color softGoldenApricot: Qt.rgba(251/255, 178/255, 106/255, 0.3)
|
||||||
|
readonly property color mistyGray: Qt.rgba(215/255, 216/255, 219/255, 0.8)
|
||||||
|
readonly property color cloudyGray: Qt.rgba(215/255, 216/255, 219/255, 0.65)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,40 +16,28 @@ import "../Components"
|
|||||||
PageType {
|
PageType {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
defaultActiveFocusItem: focusItem
|
BackButtonType {
|
||||||
|
id: backButton
|
||||||
Item {
|
|
||||||
id: focusItem
|
|
||||||
KeyNavigation.tab: backButton
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: backButtonLayout
|
|
||||||
|
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
|
|
||||||
anchors.topMargin: 20
|
anchors.topMargin: 20
|
||||||
|
|
||||||
BackButtonType {
|
|
||||||
id: backButton
|
|
||||||
// KeyNavigation.tab: removeButton
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FlickableType {
|
ListView {
|
||||||
id: fl
|
id: listView
|
||||||
anchors.top: backButtonLayout.bottom
|
anchors.top: backButton.bottom
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
contentHeight: content.implicitHeight
|
anchors.right: parent.right
|
||||||
|
anchors.left: parent.left
|
||||||
|
|
||||||
ColumnLayout {
|
property bool isFocusable: true
|
||||||
id: content
|
|
||||||
|
|
||||||
anchors.top: parent.top
|
ScrollBar.vertical: ScrollBarType {}
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
header: ColumnLayout {
|
||||||
|
width: listView.width
|
||||||
|
|
||||||
HeaderType {
|
HeaderType {
|
||||||
id: header
|
id: header
|
||||||
@@ -60,7 +48,14 @@ PageType {
|
|||||||
|
|
||||||
headerText: "Dev menu"
|
headerText: "Dev menu"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
model: 1
|
||||||
|
clip: true
|
||||||
|
spacing: 16
|
||||||
|
|
||||||
|
delegate: ColumnLayout {
|
||||||
|
width: listView.width
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
TextFieldWithHeaderType {
|
||||||
id: passwordTextField
|
id: passwordTextField
|
||||||
@@ -69,7 +64,6 @@ PageType {
|
|||||||
Layout.topMargin: 16
|
Layout.topMargin: 16
|
||||||
Layout.rightMargin: 16
|
Layout.rightMargin: 16
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
parentFlickable: fl
|
|
||||||
|
|
||||||
headerText: qsTr("Gateway endpoint")
|
headerText: qsTr("Gateway endpoint")
|
||||||
textFieldText: SettingsController.gatewayEndpoint
|
textFieldText: SettingsController.gatewayEndpoint
|
||||||
@@ -86,17 +80,19 @@ PageType {
|
|||||||
SettingsController.gatewayEndpoint = textFieldText
|
SettingsController.gatewayEndpoint = textFieldText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeyNavigation.tab: saveButton
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer: ColumnLayout {
|
||||||
|
width: listView.width
|
||||||
|
|
||||||
SwitcherType {
|
SwitcherType {
|
||||||
id: switcher
|
id: switcher
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 24
|
||||||
Layout.rightMargin: 16
|
Layout.rightMargin: 16
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
Layout.topMargin: 16
|
|
||||||
|
|
||||||
text: qsTr("Dev gateway environment")
|
text: qsTr("Dev gateway environment")
|
||||||
checked: SettingsController.isDevGatewayEnv
|
checked: SettingsController.isDevGatewayEnv
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ import "../Components"
|
|||||||
PageType {
|
PageType {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
defaultActiveFocusItem: focusItem
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
objectName: "pageControllerConnections"
|
||||||
|
|
||||||
target: PageController
|
target: PageController
|
||||||
|
|
||||||
function onRestorePageHomeState(isContainerInstalled) {
|
function onRestorePageHomeState(isContainerInstalled) {
|
||||||
drawer.open()
|
drawer.openTriggered()
|
||||||
if (isContainerInstalled) {
|
if (isContainerInstalled) {
|
||||||
containersDropDown.rootButtonClickedFunction()
|
containersDropDown.rootButtonClickedFunction()
|
||||||
}
|
}
|
||||||
@@ -33,23 +33,22 @@ PageType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
objectName: "homeColumnItem"
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.bottomMargin: drawer.collapsedHeight
|
anchors.bottomMargin: drawer.collapsedHeight
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
objectName: "homeColumnLayout"
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.topMargin: 34
|
anchors.topMargin: 34
|
||||||
anchors.bottomMargin: 34
|
anchors.bottomMargin: 34
|
||||||
|
|
||||||
Item {
|
|
||||||
id: focusItem
|
|
||||||
KeyNavigation.tab: loggingButton.visible ?
|
|
||||||
loggingButton :
|
|
||||||
connectButton
|
|
||||||
}
|
|
||||||
|
|
||||||
BasicButtonType {
|
BasicButtonType {
|
||||||
id: loggingButton
|
id: loggingButton
|
||||||
|
objectName: "loggingButton"
|
||||||
|
|
||||||
property bool isLoggingEnabled: SettingsController.isLoggingEnabled
|
property bool isLoggingEnabled: SettingsController.isLoggingEnabled
|
||||||
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
@@ -69,8 +68,6 @@ PageType {
|
|||||||
Keys.onEnterPressed: loggingButton.clicked()
|
Keys.onEnterPressed: loggingButton.clicked()
|
||||||
Keys.onReturnPressed: loggingButton.clicked()
|
Keys.onReturnPressed: loggingButton.clicked()
|
||||||
|
|
||||||
KeyNavigation.tab: connectButton
|
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
PageController.goToPage(PageEnum.PageSettingsLogging)
|
PageController.goToPage(PageEnum.PageSettingsLogging)
|
||||||
}
|
}
|
||||||
@@ -78,13 +75,15 @@ PageType {
|
|||||||
|
|
||||||
ConnectButton {
|
ConnectButton {
|
||||||
id: connectButton
|
id: connectButton
|
||||||
|
objectName: "connectButton"
|
||||||
|
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.alignment: Qt.AlignCenter
|
Layout.alignment: Qt.AlignCenter
|
||||||
KeyNavigation.tab: splitTunnelingButton
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BasicButtonType {
|
BasicButtonType {
|
||||||
id: splitTunnelingButton
|
id: splitTunnelingButton
|
||||||
|
objectName: "splitTunnelingButton"
|
||||||
|
|
||||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
|
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
|
||||||
Layout.bottomMargin: 34
|
Layout.bottomMargin: 34
|
||||||
@@ -98,7 +97,6 @@ PageType {
|
|||||||
pressedColor: AmneziaStyle.color.sheerWhite
|
pressedColor: AmneziaStyle.color.sheerWhite
|
||||||
disabledColor: AmneziaStyle.color.mutedGray
|
disabledColor: AmneziaStyle.color.mutedGray
|
||||||
textColor: AmneziaStyle.color.mutedGray
|
textColor: AmneziaStyle.color.mutedGray
|
||||||
leftImageColor: AmneziaStyle.color.transparent
|
|
||||||
borderWidth: 0
|
borderWidth: 0
|
||||||
|
|
||||||
buttonTextLabel.lineHeight: 20
|
buttonTextLabel.lineHeight: 20
|
||||||
@@ -110,62 +108,48 @@ PageType {
|
|||||||
|
|
||||||
text: isSplitTunnelingEnabled ? qsTr("Split tunneling enabled") : qsTr("Split tunneling disabled")
|
text: isSplitTunnelingEnabled ? qsTr("Split tunneling enabled") : qsTr("Split tunneling disabled")
|
||||||
|
|
||||||
imageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : ""
|
leftImageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : ""
|
||||||
|
leftImageColor: ""
|
||||||
rightImageSource: "qrc:/images/controls/chevron-down.svg"
|
rightImageSource: "qrc:/images/controls/chevron-down.svg"
|
||||||
|
|
||||||
Keys.onEnterPressed: splitTunnelingButton.clicked()
|
Keys.onEnterPressed: splitTunnelingButton.clicked()
|
||||||
Keys.onReturnPressed: splitTunnelingButton.clicked()
|
Keys.onReturnPressed: splitTunnelingButton.clicked()
|
||||||
|
|
||||||
KeyNavigation.tab: drawer
|
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
homeSplitTunnelingDrawer.open()
|
homeSplitTunnelingDrawer.openTriggered()
|
||||||
}
|
}
|
||||||
|
|
||||||
HomeSplitTunnelingDrawer {
|
HomeSplitTunnelingDrawer {
|
||||||
id: homeSplitTunnelingDrawer
|
id: homeSplitTunnelingDrawer
|
||||||
parent: root
|
objectName: "homeSplitTunnelingDrawer"
|
||||||
|
|
||||||
onClosed: {
|
parent: root
|
||||||
if (!GC.isMobile()) {
|
|
||||||
focusItem.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
DrawerType2 {
|
DrawerType2 {
|
||||||
id: drawer
|
id: drawer
|
||||||
|
objectName: "drawerProtocol"
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
onClosed: {
|
collapsedStateContent: Item {
|
||||||
if (!GC.isMobile()) {
|
objectName: "ProtocolDrawerCollapsedContent"
|
||||||
focusItem.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
collapsedContent: Item {
|
|
||||||
implicitHeight: Qt.platform.os !== "ios" ? root.height * 0.9 : screen.height * 0.77
|
implicitHeight: Qt.platform.os !== "ios" ? root.height * 0.9 : screen.height * 0.77
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
drawer.expandedHeight = implicitHeight
|
drawer.expandedHeight = implicitHeight
|
||||||
}
|
}
|
||||||
Connections {
|
|
||||||
target: drawer
|
|
||||||
enabled: !GC.isMobile()
|
|
||||||
function onActiveFocusChanged() {
|
|
||||||
if (drawer.activeFocus && !drawer.isOpened) {
|
|
||||||
collapsedButtonChevron.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: collapsed
|
id: collapsed
|
||||||
|
objectName: "collapsedColumnLayout"
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
drawer.collapsedHeight = collapsed.implicitHeight
|
drawer.collapsedHeight = collapsed.implicitHeight
|
||||||
@@ -180,6 +164,8 @@ PageType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
objectName: "rowLayout"
|
||||||
|
|
||||||
Layout.topMargin: 14
|
Layout.topMargin: 14
|
||||||
Layout.leftMargin: 24
|
Layout.leftMargin: 24
|
||||||
Layout.rightMargin: 24
|
Layout.rightMargin: 24
|
||||||
@@ -188,9 +174,11 @@ PageType {
|
|||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
objectName: "drawerConnections"
|
||||||
|
|
||||||
target: drawer
|
target: drawer
|
||||||
function onEntered() {
|
function onCursorEntered() {
|
||||||
if (drawer.isCollapsed) {
|
if (drawer.isCollapsedStateActive) {
|
||||||
collapsedButtonChevron.backgroundColor = collapsedButtonChevron.hoveredColor
|
collapsedButtonChevron.backgroundColor = collapsedButtonChevron.hoveredColor
|
||||||
collapsedButtonHeader.opacity = 0.8
|
collapsedButtonHeader.opacity = 0.8
|
||||||
} else {
|
} else {
|
||||||
@@ -198,8 +186,8 @@ PageType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onExited() {
|
function onCursorExited() {
|
||||||
if (drawer.isCollapsed) {
|
if (drawer.isCollapsedStateActive) {
|
||||||
collapsedButtonChevron.backgroundColor = collapsedButtonChevron.defaultColor
|
collapsedButtonChevron.backgroundColor = collapsedButtonChevron.defaultColor
|
||||||
collapsedButtonHeader.opacity = 1
|
collapsedButtonHeader.opacity = 1
|
||||||
} else {
|
} else {
|
||||||
@@ -208,7 +196,7 @@ PageType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onPressed(pressed, entered) {
|
function onPressed(pressed, entered) {
|
||||||
if (drawer.isCollapsed) {
|
if (drawer.isCollapsedStateActive) {
|
||||||
collapsedButtonChevron.backgroundColor = pressed ? collapsedButtonChevron.pressedColor : entered ? collapsedButtonChevron.hoveredColor : collapsedButtonChevron.defaultColor
|
collapsedButtonChevron.backgroundColor = pressed ? collapsedButtonChevron.pressedColor : entered ? collapsedButtonChevron.hoveredColor : collapsedButtonChevron.defaultColor
|
||||||
collapsedButtonHeader.opacity = 0.7
|
collapsedButtonHeader.opacity = 0.7
|
||||||
} else {
|
} else {
|
||||||
@@ -219,6 +207,8 @@ PageType {
|
|||||||
|
|
||||||
Header1TextType {
|
Header1TextType {
|
||||||
id: collapsedButtonHeader
|
id: collapsedButtonHeader
|
||||||
|
objectName: "collapsedButtonHeader"
|
||||||
|
|
||||||
Layout.maximumWidth: drawer.width - 48 - 18 - 12
|
Layout.maximumWidth: drawer.width - 48 - 18 - 12
|
||||||
|
|
||||||
maximumLineCount: 2
|
maximumLineCount: 2
|
||||||
@@ -227,8 +217,6 @@ PageType {
|
|||||||
text: ServersModel.defaultServerName
|
text: ServersModel.defaultServerName
|
||||||
horizontalAlignment: Qt.AlignHCenter
|
horizontalAlignment: Qt.AlignHCenter
|
||||||
|
|
||||||
KeyNavigation.tab: tabBar
|
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
PropertyAnimation { duration: 200 }
|
PropertyAnimation { duration: 200 }
|
||||||
}
|
}
|
||||||
@@ -236,10 +224,11 @@ PageType {
|
|||||||
|
|
||||||
ImageButtonType {
|
ImageButtonType {
|
||||||
id: collapsedButtonChevron
|
id: collapsedButtonChevron
|
||||||
|
objectName: "collapsedButtonChevron"
|
||||||
|
|
||||||
Layout.leftMargin: 8
|
Layout.leftMargin: 8
|
||||||
|
|
||||||
visible: drawer.isCollapsed
|
visible: drawer.isCollapsedStateActive()
|
||||||
|
|
||||||
hoverEnabled: false
|
hoverEnabled: false
|
||||||
image: "qrc:/images/controls/chevron-down.svg"
|
image: "qrc:/images/controls/chevron-down.svg"
|
||||||
@@ -254,47 +243,58 @@ PageType {
|
|||||||
|
|
||||||
Keys.onEnterPressed: collapsedButtonChevron.clicked()
|
Keys.onEnterPressed: collapsedButtonChevron.clicked()
|
||||||
Keys.onReturnPressed: collapsedButtonChevron.clicked()
|
Keys.onReturnPressed: collapsedButtonChevron.clicked()
|
||||||
Keys.onTabPressed: lastItemTabClicked()
|
|
||||||
|
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (drawer.isCollapsed) {
|
if (drawer.isCollapsedStateActive()) {
|
||||||
drawer.open()
|
drawer.openTriggered()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
objectName: "rowLayoutLabel"
|
||||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||||
Layout.bottomMargin: drawer.isCollapsed ? 44 : ServersModel.isDefaultServerFromApi ? 89 : 44
|
Layout.topMargin: 8
|
||||||
|
Layout.bottomMargin: drawer.isCollapsed ? 44 : ServersModel.isDefaultServerFromApi ? 61 : 16
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Image {
|
BasicButtonType {
|
||||||
Layout.rightMargin: 8
|
enabled: (ServersModel.defaultServerImagePathCollapsed !== "") && drawer.isCollapsed
|
||||||
visible: source !== ""
|
hoverEnabled: enabled
|
||||||
source: ServersModel.defaultServerImagePathCollapsed
|
|
||||||
}
|
implicitHeight: 36
|
||||||
|
|
||||||
|
leftPadding: 16
|
||||||
|
rightPadding: 16
|
||||||
|
|
||||||
|
defaultColor: AmneziaStyle.color.transparent
|
||||||
|
hoveredColor: AmneziaStyle.color.translucentWhite
|
||||||
|
pressedColor: AmneziaStyle.color.sheerWhite
|
||||||
|
disabledColor: AmneziaStyle.color.transparent
|
||||||
|
textColor: AmneziaStyle.color.mutedGray
|
||||||
|
|
||||||
|
buttonTextLabel.lineHeight: 16
|
||||||
|
buttonTextLabel.font.pixelSize: 13
|
||||||
|
buttonTextLabel.font.weight: 400
|
||||||
|
|
||||||
LabelTextType {
|
|
||||||
id: collapsedServerMenuDescription
|
|
||||||
text: drawer.isCollapsed ? ServersModel.defaultServerDescriptionCollapsed : ServersModel.defaultServerDescriptionExpanded
|
text: drawer.isCollapsed ? ServersModel.defaultServerDescriptionCollapsed : ServersModel.defaultServerDescriptionExpanded
|
||||||
}
|
leftImageSource: ServersModel.defaultServerImagePathCollapsed
|
||||||
}
|
changeLeftImageSize: false
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
rightImageSource: hoverEnabled ? "qrc:/images/controls/chevron-down.svg" : ""
|
||||||
target: drawer
|
|
||||||
enabled: !GC.isMobile()
|
onClicked: {
|
||||||
function onIsCollapsedChanged() {
|
ServersModel.processedIndex = ServersModel.defaultIndex
|
||||||
if (!drawer.isCollapsed) {
|
PageController.goToPage(PageEnum.PageSettingsServerInfo)
|
||||||
focusItem1.forceActiveFocus()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: serversMenuHeader
|
id: serversMenuHeader
|
||||||
|
objectName: "serversMenuHeader"
|
||||||
|
|
||||||
anchors.top: collapsed.bottom
|
anchors.top: collapsed.bottom
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
@@ -306,45 +306,42 @@ PageType {
|
|||||||
|
|
||||||
visible: !ServersModel.isDefaultServerFromApi
|
visible: !ServersModel.isDefaultServerFromApi
|
||||||
|
|
||||||
Item {
|
|
||||||
id: focusItem1
|
|
||||||
KeyNavigation.tab: containersDropDown
|
|
||||||
}
|
|
||||||
|
|
||||||
DropDownType {
|
DropDownType {
|
||||||
id: containersDropDown
|
id: containersDropDown
|
||||||
|
objectName: "containersDropDown"
|
||||||
|
|
||||||
rootButtonImageColor: AmneziaStyle.color.midnightBlack
|
rootButtonImageColor: AmneziaStyle.color.midnightBlack
|
||||||
rootButtonBackgroundColor: AmneziaStyle.color.paleGray
|
rootButtonBackgroundColor: AmneziaStyle.color.paleGray
|
||||||
rootButtonBackgroundHoveredColor: Qt.rgba(215, 216, 219, 0.8)
|
rootButtonBackgroundHoveredColor: AmneziaStyle.color.mistyGray
|
||||||
rootButtonBackgroundPressedColor: Qt.rgba(215, 216, 219, 0.65)
|
rootButtonBackgroundPressedColor: AmneziaStyle.color.cloudyGray
|
||||||
rootButtonHoveredBorderColor: AmneziaStyle.color.transparent
|
rootButtonHoveredBorderColor: AmneziaStyle.color.transparent
|
||||||
rootButtonDefaultBorderColor: AmneziaStyle.color.transparent
|
rootButtonDefaultBorderColor: AmneziaStyle.color.transparent
|
||||||
rootButtonTextTopMargin: 8
|
rootButtonTextTopMargin: 8
|
||||||
rootButtonTextBottomMargin: 8
|
rootButtonTextBottomMargin: 8
|
||||||
|
|
||||||
|
enabled: drawer.isOpened
|
||||||
|
|
||||||
text: ServersModel.defaultServerDefaultContainerName
|
text: ServersModel.defaultServerDefaultContainerName
|
||||||
textColor: AmneziaStyle.color.midnightBlack
|
textColor: AmneziaStyle.color.midnightBlack
|
||||||
headerText: qsTr("VPN protocol")
|
headerText: qsTr("VPN protocol")
|
||||||
headerBackButtonImage: "qrc:/images/controls/arrow-left.svg"
|
headerBackButtonImage: "qrc:/images/controls/arrow-left.svg"
|
||||||
|
|
||||||
rootButtonClickedFunction: function() {
|
rootButtonClickedFunction: function() {
|
||||||
containersDropDown.open()
|
containersDropDown.openTriggered()
|
||||||
}
|
}
|
||||||
|
|
||||||
drawerParent: root
|
drawerParent: root
|
||||||
KeyNavigation.tab: serversMenuContent
|
|
||||||
|
|
||||||
listView: HomeContainersListView {
|
listView: HomeContainersListView {
|
||||||
id: containersListView
|
id: containersListView
|
||||||
|
objectName: "containersListView"
|
||||||
|
|
||||||
rootWidth: root.width
|
rootWidth: root.width
|
||||||
onVisibleChanged: {
|
height: 500 // TODO: make calculated
|
||||||
if (containersDropDown.visible && !GC.isMobile()) {
|
|
||||||
focusItem1.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
objectName: "rowLayoutConnections"
|
||||||
|
|
||||||
target: ServersModel
|
target: ServersModel
|
||||||
|
|
||||||
function onDefaultServerIndexChanged() {
|
function onDefaultServerIndexChanged() {
|
||||||
@@ -386,167 +383,21 @@ PageType {
|
|||||||
|
|
||||||
ButtonGroup {
|
ButtonGroup {
|
||||||
id: serversRadioButtonGroup
|
id: serversRadioButtonGroup
|
||||||
|
objectName: "serversRadioButtonGroup"
|
||||||
}
|
}
|
||||||
|
|
||||||
ListView {
|
ServersListView {
|
||||||
id: serversMenuContent
|
id: serversMenuContent
|
||||||
|
objectName: "serversMenuContent"
|
||||||
|
|
||||||
anchors.top: serversMenuHeader.bottom
|
isFocusable: false
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.topMargin: 16
|
|
||||||
|
|
||||||
model: ServersModel
|
|
||||||
currentIndex: ServersModel.defaultIndex
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
id: scrollBar
|
|
||||||
policy: serversMenuContent.height >= serversMenuContent.contentHeight ? ScrollBar.AlwaysOff : ScrollBar.AlwaysOn
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
activeFocusOnTab: true
|
|
||||||
focus: true
|
|
||||||
|
|
||||||
property int focusItemIndex: 0
|
|
||||||
onActiveFocusChanged: {
|
|
||||||
if (activeFocus) {
|
|
||||||
serversMenuContent.focusItemIndex = 0
|
|
||||||
serversMenuContent.itemAtIndex(focusItemIndex).forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onFocusItemIndexChanged: {
|
|
||||||
const focusedElement = serversMenuContent.itemAtIndex(focusItemIndex)
|
|
||||||
if (focusedElement) {
|
|
||||||
if (focusedElement.y + focusedElement.height > serversMenuContent.height) {
|
|
||||||
serversMenuContent.contentY = focusedElement.y + focusedElement.height - serversMenuContent.height
|
|
||||||
} else {
|
|
||||||
serversMenuContent.contentY = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onUpPressed: scrollBar.decrease()
|
|
||||||
Keys.onDownPressed: scrollBar.increase()
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: drawer
|
target: drawer
|
||||||
enabled: !GC.isMobile()
|
|
||||||
function onIsCollapsedChanged() {
|
|
||||||
if (drawer.isCollapsed) {
|
|
||||||
const item = serversMenuContent.itemAtIndex(serversMenuContent.focusItemIndex)
|
|
||||||
if (item) { item.serverRadioButtonProperty.focus = false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
// this item shouldn't be focused when drawer is closed
|
||||||
target: ServersModel
|
function onIsOpenedChanged() {
|
||||||
function onDefaultServerIndexChanged(serverIndex) {
|
serversMenuContent.isFocusable = drawer.isOpened
|
||||||
serversMenuContent.currentIndex = serverIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
id: menuContentDelegate
|
|
||||||
|
|
||||||
property variant delegateData: model
|
|
||||||
property VerticalRadioButton serverRadioButtonProperty: serverRadioButton
|
|
||||||
|
|
||||||
implicitWidth: serversMenuContent.width
|
|
||||||
implicitHeight: serverRadioButtonContent.implicitHeight
|
|
||||||
|
|
||||||
onActiveFocusChanged: {
|
|
||||||
if (activeFocus) {
|
|
||||||
serverRadioButton.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: serverRadioButtonContent
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.rightMargin: 16
|
|
||||||
anchors.leftMargin: 16
|
|
||||||
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
VerticalRadioButton {
|
|
||||||
id: serverRadioButton
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
text: name
|
|
||||||
descriptionText: serverDescription
|
|
||||||
|
|
||||||
checked: index === serversMenuContent.currentIndex
|
|
||||||
checkable: !ConnectionController.isConnected
|
|
||||||
|
|
||||||
ButtonGroup.group: serversRadioButtonGroup
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
if (ConnectionController.isConnected) {
|
|
||||||
PageController.showNotificationMessage(qsTr("Unable change server while there is an active connection"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
serversMenuContent.currentIndex = index
|
|
||||||
|
|
||||||
ServersModel.defaultIndex = index
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: serverRadioButton
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onTabPressed: serverInfoButton.forceActiveFocus()
|
|
||||||
Keys.onEnterPressed: serverRadioButton.clicked()
|
|
||||||
Keys.onReturnPressed: serverRadioButton.clicked()
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageButtonType {
|
|
||||||
id: serverInfoButton
|
|
||||||
image: "qrc:/images/controls/settings.svg"
|
|
||||||
imageColor: AmneziaStyle.color.paleGray
|
|
||||||
|
|
||||||
implicitWidth: 56
|
|
||||||
implicitHeight: 56
|
|
||||||
|
|
||||||
z: 1
|
|
||||||
|
|
||||||
Keys.onTabPressed: {
|
|
||||||
if (serversMenuContent.focusItemIndex < serversMenuContent.count - 1) {
|
|
||||||
serversMenuContent.focusItemIndex++
|
|
||||||
serversMenuContent.itemAtIndex(serversMenuContent.focusItemIndex).forceActiveFocus()
|
|
||||||
} else {
|
|
||||||
focusItem1.forceActiveFocus()
|
|
||||||
serversMenuContent.contentY = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Keys.onEnterPressed: serverInfoButton.clicked()
|
|
||||||
Keys.onReturnPressed: serverInfoButton.clicked()
|
|
||||||
|
|
||||||
onClicked: function() {
|
|
||||||
ServersModel.processedIndex = index
|
|
||||||
PageController.goToPage(PageEnum.PageSettingsServerInfo)
|
|
||||||
drawer.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DividerType {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.leftMargin: 0
|
|
||||||
Layout.rightMargin: 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,18 +16,6 @@ import "../Components"
|
|||||||
PageType {
|
PageType {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
defaultActiveFocusItem: listview.currentItem.mtuTextField.textField
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: focusItem
|
|
||||||
onFocusChanged: {
|
|
||||||
if (activeFocus) {
|
|
||||||
fl.ensureVisible(focusItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyNavigation.tab: backButton
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: backButtonLayout
|
id: backButtonLayout
|
||||||
|
|
||||||
@@ -39,229 +27,235 @@ PageType {
|
|||||||
|
|
||||||
BackButtonType {
|
BackButtonType {
|
||||||
id: backButton
|
id: backButton
|
||||||
KeyNavigation.tab: listview.currentItem.mtuTextField.textField
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FlickableType {
|
ListView {
|
||||||
id: fl
|
id: listview
|
||||||
|
|
||||||
anchors.top: backButtonLayout.bottom
|
anchors.top: backButtonLayout.bottom
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: saveButton.top
|
||||||
contentHeight: content.implicitHeight + saveButton.implicitHeight + saveButton.anchors.bottomMargin + saveButton.anchors.topMargin
|
|
||||||
|
|
||||||
Column {
|
width: parent.width
|
||||||
id: content
|
|
||||||
|
|
||||||
anchors.top: parent.top
|
clip: true
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
|
|
||||||
ListView {
|
property bool isFocusable: true
|
||||||
id: listview
|
|
||||||
|
|
||||||
width: parent.width
|
Keys.onTabPressed: {
|
||||||
height: listview.contentItem.height
|
FocusController.nextKeyTabItem()
|
||||||
|
}
|
||||||
|
|
||||||
clip: true
|
Keys.onBacktabPressed: {
|
||||||
interactive: false
|
FocusController.previousKeyTabItem()
|
||||||
|
}
|
||||||
|
|
||||||
model: AwgConfigModel
|
Keys.onUpPressed: {
|
||||||
|
FocusController.nextKeyUpItem()
|
||||||
|
}
|
||||||
|
|
||||||
delegate: Item {
|
Keys.onDownPressed: {
|
||||||
id: delegateItem
|
FocusController.nextKeyDownItem()
|
||||||
implicitWidth: listview.width
|
}
|
||||||
implicitHeight: col.implicitHeight
|
|
||||||
|
|
||||||
property alias mtuTextField: mtuTextField
|
Keys.onLeftPressed: {
|
||||||
property bool isSaveButtonEnabled: mtuTextField.errorText === "" &&
|
FocusController.nextKeyLeftItem()
|
||||||
junkPacketMaxSizeTextField.errorText === "" &&
|
}
|
||||||
junkPacketMinSizeTextField.errorText === "" &&
|
|
||||||
junkPacketCountTextField.errorText === ""
|
|
||||||
|
|
||||||
ColumnLayout {
|
Keys.onRightPressed: {
|
||||||
id: col
|
FocusController.nextKeyRightItem()
|
||||||
|
}
|
||||||
|
|
||||||
anchors.top: parent.top
|
model: AwgConfigModel
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
|
|
||||||
anchors.leftMargin: 16
|
delegate: Item {
|
||||||
anchors.rightMargin: 16
|
id: delegateItem
|
||||||
|
implicitWidth: listview.width
|
||||||
|
implicitHeight: col.implicitHeight
|
||||||
|
|
||||||
spacing: 0
|
property alias mtuTextField: mtuTextField
|
||||||
|
property bool isSaveButtonEnabled: mtuTextField.errorText === "" &&
|
||||||
|
junkPacketMaxSizeTextField.errorText === "" &&
|
||||||
|
junkPacketMinSizeTextField.errorText === "" &&
|
||||||
|
junkPacketCountTextField.errorText === ""
|
||||||
|
|
||||||
HeaderType {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
id: col
|
||||||
|
|
||||||
headerText: qsTr("AmneziaWG settings")
|
anchors.top: parent.top
|
||||||
}
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
anchors.leftMargin: 16
|
||||||
id: mtuTextField
|
anchors.rightMargin: 16
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 40
|
|
||||||
|
|
||||||
headerText: qsTr("MTU")
|
spacing: 0
|
||||||
textFieldText: clientMtu
|
|
||||||
textField.validator: IntValidator { bottom: 576; top: 65535 }
|
|
||||||
|
|
||||||
textField.onEditingFinished: {
|
HeaderType {
|
||||||
if (textFieldText !== clientMtu) {
|
Layout.fillWidth: true
|
||||||
clientMtu = textFieldText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkEmptyText: true
|
|
||||||
KeyNavigation.tab: junkPacketCountTextField.textField
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
headerText: qsTr("AmneziaWG settings")
|
||||||
id: junkPacketCountTextField
|
}
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
|
|
||||||
headerText: "Jc - Junk packet count"
|
TextFieldWithHeaderType {
|
||||||
textFieldText: clientJunkPacketCount
|
id: mtuTextField
|
||||||
textField.validator: IntValidator { bottom: 0 }
|
Layout.fillWidth: true
|
||||||
parentFlickable: fl
|
Layout.topMargin: 40
|
||||||
|
|
||||||
textField.onEditingFinished: {
|
headerText: qsTr("MTU")
|
||||||
if (textFieldText !== clientJunkPacketCount) {
|
textFieldText: clientMtu
|
||||||
clientJunkPacketCount = textFieldText
|
textField.validator: IntValidator { bottom: 576; top: 65535 }
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkEmptyText: true
|
textField.onEditingFinished: {
|
||||||
|
if (textFieldText !== clientMtu) {
|
||||||
KeyNavigation.tab: junkPacketMinSizeTextField.textField
|
clientMtu = textFieldText
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
|
||||||
id: junkPacketMinSizeTextField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
|
|
||||||
headerText: "Jmin - Junk packet minimum size"
|
|
||||||
textFieldText: clientJunkPacketMinSize
|
|
||||||
textField.validator: IntValidator { bottom: 0 }
|
|
||||||
parentFlickable: fl
|
|
||||||
|
|
||||||
textField.onEditingFinished: {
|
|
||||||
if (textFieldText !== clientJunkPacketMinSize) {
|
|
||||||
clientJunkPacketMinSize = textFieldText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkEmptyText: true
|
|
||||||
|
|
||||||
KeyNavigation.tab: junkPacketMaxSizeTextField.textField
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
|
||||||
id: junkPacketMaxSizeTextField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
|
|
||||||
headerText: "Jmax - Junk packet maximum size"
|
|
||||||
textFieldText: clientJunkPacketMaxSize
|
|
||||||
textField.validator: IntValidator { bottom: 0 }
|
|
||||||
parentFlickable: fl
|
|
||||||
|
|
||||||
textField.onEditingFinished: {
|
|
||||||
if (textFieldText !== clientJunkPacketMaxSize) {
|
|
||||||
clientJunkPacketMaxSize = textFieldText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkEmptyText: true
|
|
||||||
|
|
||||||
Keys.onTabPressed: saveButton.forceActiveFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
Header2TextType {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
|
|
||||||
text: qsTr("Server settings")
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
|
||||||
id: portTextField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 8
|
|
||||||
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
headerText: qsTr("Port")
|
|
||||||
textFieldText: port
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
|
||||||
id: initPacketJunkSizeTextField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
headerText: "S1 - Init packet junk size"
|
|
||||||
textFieldText: serverInitPacketJunkSize
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
|
||||||
id: responsePacketJunkSizeTextField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
headerText: "S2 - Response packet junk size"
|
|
||||||
textFieldText: serverResponsePacketJunkSize
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
|
||||||
id: initPacketMagicHeaderTextField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
headerText: "H1 - Init packet magic header"
|
|
||||||
textFieldText: serverInitPacketMagicHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
|
||||||
id: responsePacketMagicHeaderTextField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
headerText: "H2 - Response packet magic header"
|
|
||||||
textFieldText: serverResponsePacketMagicHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
|
||||||
id: underloadPacketMagicHeaderTextField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
parentFlickable: fl
|
|
||||||
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
headerText: "H3 - Underload packet magic header"
|
|
||||||
textFieldText: serverUnderloadPacketMagicHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
|
||||||
id: transportPacketMagicHeaderTextField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
headerText: "H4 - Transport packet magic header"
|
|
||||||
textFieldText: serverTransportPacketMagicHeader
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
checkEmptyText: true
|
||||||
|
KeyNavigation.tab: junkPacketCountTextField.textField
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: junkPacketCountTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
headerText: "Jc - Junk packet count"
|
||||||
|
textFieldText: clientJunkPacketCount
|
||||||
|
textField.validator: IntValidator { bottom: 0 }
|
||||||
|
|
||||||
|
textField.onEditingFinished: {
|
||||||
|
if (textFieldText !== clientJunkPacketCount) {
|
||||||
|
clientJunkPacketCount = textFieldText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEmptyText: true
|
||||||
|
|
||||||
|
KeyNavigation.tab: junkPacketMinSizeTextField.textField
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: junkPacketMinSizeTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
headerText: "Jmin - Junk packet minimum size"
|
||||||
|
textFieldText: clientJunkPacketMinSize
|
||||||
|
textField.validator: IntValidator { bottom: 0 }
|
||||||
|
|
||||||
|
textField.onEditingFinished: {
|
||||||
|
if (textFieldText !== clientJunkPacketMinSize) {
|
||||||
|
clientJunkPacketMinSize = textFieldText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEmptyText: true
|
||||||
|
|
||||||
|
KeyNavigation.tab: junkPacketMaxSizeTextField.textField
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: junkPacketMaxSizeTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
headerText: "Jmax - Junk packet maximum size"
|
||||||
|
textFieldText: clientJunkPacketMaxSize
|
||||||
|
textField.validator: IntValidator { bottom: 0 }
|
||||||
|
|
||||||
|
textField.onEditingFinished: {
|
||||||
|
if (textFieldText !== clientJunkPacketMaxSize) {
|
||||||
|
clientJunkPacketMaxSize = textFieldText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEmptyText: true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Header2TextType {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
text: qsTr("Server settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: portTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 8
|
||||||
|
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
headerText: qsTr("Port")
|
||||||
|
textFieldText: port
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: initPacketJunkSizeTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
headerText: "S1 - Init packet junk size"
|
||||||
|
textFieldText: serverInitPacketJunkSize
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: responsePacketJunkSizeTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
headerText: "S2 - Response packet junk size"
|
||||||
|
textFieldText: serverResponsePacketJunkSize
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: initPacketMagicHeaderTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
headerText: "H1 - Init packet magic header"
|
||||||
|
textFieldText: serverInitPacketMagicHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: responsePacketMagicHeaderTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
headerText: "H2 - Response packet magic header"
|
||||||
|
textFieldText: serverResponsePacketMagicHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: underloadPacketMagicHeaderTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
headerText: "H3 - Underload packet magic header"
|
||||||
|
textFieldText: serverUnderloadPacketMagicHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: transportPacketMagicHeaderTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
headerText: "H4 - Transport packet magic header"
|
||||||
|
textFieldText: serverTransportPacketMagicHeader
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,7 +277,11 @@ PageType {
|
|||||||
|
|
||||||
text: qsTr("Save")
|
text: qsTr("Save")
|
||||||
|
|
||||||
Keys.onTabPressed: lastItemTabClicked(focusItem)
|
onActiveFocusChanged: {
|
||||||
|
if(activeFocus) {
|
||||||
|
listview.positionViewAtEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
clickedFunc: function() {
|
clickedFunc: function() {
|
||||||
forceActiveFocus()
|
forceActiveFocus()
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import QtQuick
|
|||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import QtCore
|
||||||
|
|
||||||
import SortFilterProxyModel 0.2
|
import SortFilterProxyModel 0.2
|
||||||
|
|
||||||
import PageEnum 1.0
|
import PageEnum 1.0
|
||||||
@@ -17,18 +19,6 @@ import "../Components"
|
|||||||
PageType {
|
PageType {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
defaultActiveFocusItem: listview.currentItem.portTextField.textField
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: focusItem
|
|
||||||
onFocusChanged: {
|
|
||||||
if (activeFocus) {
|
|
||||||
fl.ensureVisible(focusItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyNavigation.tab: backButton
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: backButtonLayout
|
id: backButtonLayout
|
||||||
|
|
||||||
@@ -40,341 +30,357 @@ PageType {
|
|||||||
|
|
||||||
BackButtonType {
|
BackButtonType {
|
||||||
id: backButton
|
id: backButton
|
||||||
KeyNavigation.tab: listview.currentItem.portTextField.textField
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FlickableType {
|
ListView {
|
||||||
id: fl
|
id: listview
|
||||||
|
|
||||||
anchors.top: backButtonLayout.bottom
|
anchors.top: backButtonLayout.bottom
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
contentHeight: content.implicitHeight
|
|
||||||
|
|
||||||
Column {
|
width: parent.width
|
||||||
id: content
|
|
||||||
|
|
||||||
anchors.top: parent.top
|
property bool isFocusable: true
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
|
|
||||||
ListView {
|
Keys.onTabPressed: {
|
||||||
id: listview
|
FocusController.nextKeyTabItem()
|
||||||
|
}
|
||||||
|
|
||||||
width: parent.width
|
Keys.onBacktabPressed: {
|
||||||
height: listview.contentItem.height
|
FocusController.previousKeyTabItem()
|
||||||
|
}
|
||||||
|
|
||||||
clip: true
|
Keys.onUpPressed: {
|
||||||
interactive: false
|
FocusController.nextKeyUpItem()
|
||||||
|
}
|
||||||
|
|
||||||
model: AwgConfigModel
|
Keys.onDownPressed: {
|
||||||
|
FocusController.nextKeyDownItem()
|
||||||
|
}
|
||||||
|
|
||||||
delegate: Item {
|
Keys.onLeftPressed: {
|
||||||
id: delegateItem
|
FocusController.nextKeyLeftItem()
|
||||||
implicitWidth: listview.width
|
}
|
||||||
implicitHeight: col.implicitHeight
|
|
||||||
|
|
||||||
property alias portTextField: portTextField
|
Keys.onRightPressed: {
|
||||||
property bool isEnabled: ServersModel.isProcessedServerHasWriteAccess()
|
FocusController.nextKeyRightItem()
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
clip: true
|
||||||
id: col
|
|
||||||
|
|
||||||
anchors.top: parent.top
|
model: AwgConfigModel
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
|
|
||||||
anchors.leftMargin: 16
|
delegate: Item {
|
||||||
anchors.rightMargin: 16
|
id: delegateItem
|
||||||
|
implicitWidth: listview.width
|
||||||
|
implicitHeight: col.implicitHeight
|
||||||
|
|
||||||
spacing: 0
|
property alias portTextField: portTextField
|
||||||
|
property bool isEnabled: ServersModel.isProcessedServerHasWriteAccess()
|
||||||
|
|
||||||
HeaderType {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
id: col
|
||||||
|
|
||||||
headerText: qsTr("AmneziaWG settings")
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
|
||||||
|
anchors.leftMargin: 16
|
||||||
|
anchors.rightMargin: 16
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
HeaderType {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
headerText: qsTr("AmneziaWG settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: portTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 40
|
||||||
|
|
||||||
|
enabled: delegateItem.isEnabled
|
||||||
|
|
||||||
|
headerText: qsTr("Port")
|
||||||
|
textFieldText: port
|
||||||
|
textField.maximumLength: 5
|
||||||
|
textField.validator: IntValidator { bottom: 1; top: 65535 }
|
||||||
|
|
||||||
|
textField.onEditingFinished: {
|
||||||
|
if (textFieldText !== port) {
|
||||||
|
port = textFieldText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEmptyText: true
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: mtuTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
headerText: qsTr("MTU")
|
||||||
|
textFieldText: mtu
|
||||||
|
textField.validator: IntValidator { bottom: 576; top: 65535 }
|
||||||
|
|
||||||
|
textField.onEditingFinished: {
|
||||||
|
if (textFieldText === "") {
|
||||||
|
textFieldText = "0"
|
||||||
|
}
|
||||||
|
if (textFieldText !== mtu) {
|
||||||
|
mtu = textFieldText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkEmptyText: true
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: junkPacketCountTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
headerText: qsTr("Jc - Junk packet count")
|
||||||
|
textFieldText: serverJunkPacketCount
|
||||||
|
textField.validator: IntValidator { bottom: 0 }
|
||||||
|
|
||||||
|
textField.onEditingFinished: {
|
||||||
|
if (textFieldText === "") {
|
||||||
|
textFieldText = "0"
|
||||||
}
|
}
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
if (textFieldText !== serverJunkPacketCount) {
|
||||||
id: portTextField
|
serverJunkPacketCount = textFieldText
|
||||||
Layout.fillWidth: true
|
}
|
||||||
Layout.topMargin: 40
|
}
|
||||||
|
|
||||||
enabled: delegateItem.isEnabled
|
checkEmptyText: true
|
||||||
|
}
|
||||||
|
|
||||||
headerText: qsTr("Port")
|
TextFieldWithHeaderType {
|
||||||
textFieldText: port
|
id: junkPacketMinSizeTextField
|
||||||
textField.maximumLength: 5
|
Layout.fillWidth: true
|
||||||
textField.validator: IntValidator { bottom: 1; top: 65535 }
|
Layout.topMargin: 16
|
||||||
parentFlickable: fl
|
|
||||||
|
|
||||||
textField.onEditingFinished: {
|
headerText: qsTr("Jmin - Junk packet minimum size")
|
||||||
if (textFieldText !== port) {
|
textFieldText: serverJunkPacketMinSize
|
||||||
port = textFieldText
|
textField.validator: IntValidator { bottom: 0 }
|
||||||
}
|
|
||||||
|
textField.onEditingFinished: {
|
||||||
|
if (textFieldText !== serverJunkPacketMinSize) {
|
||||||
|
serverJunkPacketMinSize = textFieldText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEmptyText: true
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: junkPacketMaxSizeTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
headerText: qsTr("Jmax - Junk packet maximum size")
|
||||||
|
textFieldText: serverJunkPacketMaxSize
|
||||||
|
textField.validator: IntValidator { bottom: 0 }
|
||||||
|
|
||||||
|
textField.onEditingFinished: {
|
||||||
|
if (textFieldText !== serverJunkPacketMaxSize) {
|
||||||
|
serverJunkPacketMaxSize = textFieldText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEmptyText: true
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: initPacketJunkSizeTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
headerText: qsTr("S1 - Init packet junk size")
|
||||||
|
textFieldText: serverInitPacketJunkSize
|
||||||
|
textField.validator: IntValidator { bottom: 0 }
|
||||||
|
|
||||||
|
textField.onEditingFinished: {
|
||||||
|
if (textFieldText !== serverInitPacketJunkSize) {
|
||||||
|
serverInitPacketJunkSize = textFieldText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEmptyText: true
|
||||||
|
|
||||||
|
onActiveFocusChanged: {
|
||||||
|
if(activeFocus) {
|
||||||
|
listview.positionViewAtEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: responsePacketJunkSizeTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
headerText: qsTr("S2 - Response packet junk size")
|
||||||
|
textFieldText: serverResponsePacketJunkSize
|
||||||
|
textField.validator: IntValidator { bottom: 0 }
|
||||||
|
|
||||||
|
textField.onEditingFinished: {
|
||||||
|
if (textFieldText !== serverResponsePacketJunkSize) {
|
||||||
|
serverResponsePacketJunkSize = textFieldText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEmptyText: true
|
||||||
|
|
||||||
|
onActiveFocusChanged: {
|
||||||
|
if(activeFocus) {
|
||||||
|
listview.positionViewAtEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: initPacketMagicHeaderTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
headerText: qsTr("H1 - Init packet magic header")
|
||||||
|
textFieldText: serverInitPacketMagicHeader
|
||||||
|
textField.validator: IntValidator { bottom: 0 }
|
||||||
|
|
||||||
|
textField.onEditingFinished: {
|
||||||
|
if (textFieldText !== serverInitPacketMagicHeader) {
|
||||||
|
serverInitPacketMagicHeader = textFieldText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEmptyText: true
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: responsePacketMagicHeaderTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
headerText: qsTr("H2 - Response packet magic header")
|
||||||
|
textFieldText: serverResponsePacketMagicHeader
|
||||||
|
textField.validator: IntValidator { bottom: 0 }
|
||||||
|
|
||||||
|
textField.onEditingFinished: {
|
||||||
|
if (textFieldText !== serverResponsePacketMagicHeader) {
|
||||||
|
serverResponsePacketMagicHeader = textFieldText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEmptyText: true
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: transportPacketMagicHeaderTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
headerText: qsTr("H4 - Transport packet magic header")
|
||||||
|
textFieldText: serverTransportPacketMagicHeader
|
||||||
|
textField.validator: IntValidator { bottom: 0 }
|
||||||
|
|
||||||
|
textField.onEditingFinished: {
|
||||||
|
if (textFieldText !== serverTransportPacketMagicHeader) {
|
||||||
|
serverTransportPacketMagicHeader = textFieldText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEmptyText: true
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: underloadPacketMagicHeaderTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
headerText: qsTr("H3 - Underload packet magic header")
|
||||||
|
textFieldText: serverUnderloadPacketMagicHeader
|
||||||
|
textField.validator: IntValidator { bottom: 0 }
|
||||||
|
|
||||||
|
textField.onEditingFinished: {
|
||||||
|
if (textFieldText !== serverUnderloadPacketMagicHeader) {
|
||||||
|
serverUnderloadPacketMagicHeader = textFieldText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEmptyText: true
|
||||||
|
}
|
||||||
|
|
||||||
|
BasicButtonType {
|
||||||
|
id: saveRestartButton
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 24
|
||||||
|
Layout.bottomMargin: 24
|
||||||
|
|
||||||
|
enabled: underloadPacketMagicHeaderTextField.errorText === "" &&
|
||||||
|
transportPacketMagicHeaderTextField.errorText === "" &&
|
||||||
|
responsePacketMagicHeaderTextField.errorText === "" &&
|
||||||
|
initPacketMagicHeaderTextField.errorText === "" &&
|
||||||
|
responsePacketJunkSizeTextField.errorText === "" &&
|
||||||
|
initPacketJunkSizeTextField.errorText === "" &&
|
||||||
|
junkPacketMaxSizeTextField.errorText === "" &&
|
||||||
|
junkPacketMinSizeTextField.errorText === "" &&
|
||||||
|
junkPacketCountTextField.errorText === "" &&
|
||||||
|
portTextField.errorText === ""
|
||||||
|
|
||||||
|
text: qsTr("Save")
|
||||||
|
|
||||||
|
onActiveFocusChanged: {
|
||||||
|
if(activeFocus) {
|
||||||
|
listview.positionViewAtEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clickedFunc: function() {
|
||||||
|
forceActiveFocus()
|
||||||
|
|
||||||
|
if (delegateItem.isEnabled) {
|
||||||
|
if (AwgConfigModel.isHeadersEqual(underloadPacketMagicHeaderTextField.textField.text,
|
||||||
|
transportPacketMagicHeaderTextField.textField.text,
|
||||||
|
responsePacketMagicHeaderTextField.textField.text,
|
||||||
|
initPacketMagicHeaderTextField.textField.text)) {
|
||||||
|
PageController.showErrorMessage(qsTr("The values of the H1-H4 fields must be unique"))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
checkEmptyText: true
|
if (AwgConfigModel.isPacketSizeEqual(parseInt(initPacketJunkSizeTextField.textField.text),
|
||||||
|
parseInt(responsePacketJunkSizeTextField.textField.text))) {
|
||||||
KeyNavigation.tab: junkPacketCountTextField.textField
|
PageController.showErrorMessage(qsTr("The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92)"))
|
||||||
}
|
return
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
|
||||||
id: junkPacketCountTextField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
|
|
||||||
headerText: qsTr("Jc - Junk packet count")
|
|
||||||
textFieldText: serverJunkPacketCount
|
|
||||||
textField.validator: IntValidator { bottom: 0 }
|
|
||||||
parentFlickable: fl
|
|
||||||
|
|
||||||
textField.onEditingFinished: {
|
|
||||||
if (textFieldText === "") {
|
|
||||||
textFieldText = "0"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textFieldText !== serverJunkPacketCount) {
|
|
||||||
serverJunkPacketCount = textFieldText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkEmptyText: true
|
|
||||||
|
|
||||||
KeyNavigation.tab: junkPacketMinSizeTextField.textField
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
|
||||||
id: junkPacketMinSizeTextField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
|
|
||||||
headerText: qsTr("Jmin - Junk packet minimum size")
|
|
||||||
textFieldText: serverJunkPacketMinSize
|
|
||||||
textField.validator: IntValidator { bottom: 0 }
|
|
||||||
parentFlickable: fl
|
|
||||||
|
|
||||||
textField.onEditingFinished: {
|
|
||||||
if (textFieldText !== serverJunkPacketMinSize) {
|
|
||||||
serverJunkPacketMinSize = textFieldText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkEmptyText: true
|
|
||||||
|
|
||||||
KeyNavigation.tab: junkPacketMaxSizeTextField.textField
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
|
||||||
id: junkPacketMaxSizeTextField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
|
|
||||||
headerText: qsTr("Jmax - Junk packet maximum size")
|
|
||||||
textFieldText: serverJunkPacketMaxSize
|
|
||||||
textField.validator: IntValidator { bottom: 0 }
|
|
||||||
parentFlickable: fl
|
|
||||||
|
|
||||||
textField.onEditingFinished: {
|
|
||||||
if (textFieldText !== serverJunkPacketMaxSize) {
|
|
||||||
serverJunkPacketMaxSize = textFieldText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkEmptyText: true
|
|
||||||
|
|
||||||
KeyNavigation.tab: initPacketJunkSizeTextField.textField
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
|
||||||
id: initPacketJunkSizeTextField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
|
|
||||||
headerText: qsTr("S1 - Init packet junk size")
|
|
||||||
textFieldText: serverInitPacketJunkSize
|
|
||||||
textField.validator: IntValidator { bottom: 0 }
|
|
||||||
parentFlickable: fl
|
|
||||||
|
|
||||||
textField.onEditingFinished: {
|
|
||||||
if (textFieldText !== serverInitPacketJunkSize) {
|
|
||||||
serverInitPacketJunkSize = textFieldText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkEmptyText: true
|
|
||||||
|
|
||||||
KeyNavigation.tab: responsePacketJunkSizeTextField.textField
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
|
||||||
id: responsePacketJunkSizeTextField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
|
|
||||||
headerText: qsTr("S2 - Response packet junk size")
|
|
||||||
textFieldText: serverResponsePacketJunkSize
|
|
||||||
textField.validator: IntValidator { bottom: 0 }
|
|
||||||
parentFlickable: fl
|
|
||||||
|
|
||||||
textField.onEditingFinished: {
|
|
||||||
if (textFieldText !== serverResponsePacketJunkSize) {
|
|
||||||
serverResponsePacketJunkSize = textFieldText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkEmptyText: true
|
|
||||||
|
|
||||||
KeyNavigation.tab: initPacketMagicHeaderTextField.textField
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
|
||||||
id: initPacketMagicHeaderTextField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
|
|
||||||
headerText: qsTr("H1 - Init packet magic header")
|
|
||||||
textFieldText: serverInitPacketMagicHeader
|
|
||||||
textField.validator: IntValidator { bottom: 0 }
|
|
||||||
parentFlickable: fl
|
|
||||||
|
|
||||||
textField.onEditingFinished: {
|
|
||||||
if (textFieldText !== serverInitPacketMagicHeader) {
|
|
||||||
serverInitPacketMagicHeader = textFieldText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkEmptyText: true
|
|
||||||
|
|
||||||
KeyNavigation.tab: responsePacketMagicHeaderTextField.textField
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
|
||||||
id: responsePacketMagicHeaderTextField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
|
|
||||||
headerText: qsTr("H2 - Response packet magic header")
|
|
||||||
textFieldText: serverResponsePacketMagicHeader
|
|
||||||
textField.validator: IntValidator { bottom: 0 }
|
|
||||||
parentFlickable: fl
|
|
||||||
|
|
||||||
textField.onEditingFinished: {
|
|
||||||
if (textFieldText !== serverResponsePacketMagicHeader) {
|
|
||||||
serverResponsePacketMagicHeader = textFieldText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkEmptyText: true
|
|
||||||
|
|
||||||
KeyNavigation.tab: transportPacketMagicHeaderTextField.textField
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
|
||||||
id: transportPacketMagicHeaderTextField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
|
|
||||||
headerText: qsTr("H4 - Transport packet magic header")
|
|
||||||
textFieldText: serverTransportPacketMagicHeader
|
|
||||||
textField.validator: IntValidator { bottom: 0 }
|
|
||||||
parentFlickable: fl
|
|
||||||
|
|
||||||
textField.onEditingFinished: {
|
|
||||||
if (textFieldText !== serverTransportPacketMagicHeader) {
|
|
||||||
serverTransportPacketMagicHeader = textFieldText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkEmptyText: true
|
|
||||||
|
|
||||||
KeyNavigation.tab: underloadPacketMagicHeaderTextField.textField
|
|
||||||
}
|
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
|
||||||
id: underloadPacketMagicHeaderTextField
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 16
|
|
||||||
parentFlickable: fl
|
|
||||||
|
|
||||||
headerText: qsTr("H3 - Underload packet magic header")
|
|
||||||
textFieldText: serverUnderloadPacketMagicHeader
|
|
||||||
textField.validator: IntValidator { bottom: 0 }
|
|
||||||
|
|
||||||
textField.onEditingFinished: {
|
|
||||||
if (textFieldText !== serverUnderloadPacketMagicHeader) {
|
|
||||||
serverUnderloadPacketMagicHeader = textFieldText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkEmptyText: true
|
|
||||||
|
|
||||||
KeyNavigation.tab: saveRestartButton
|
|
||||||
}
|
|
||||||
|
|
||||||
BasicButtonType {
|
|
||||||
id: saveRestartButton
|
|
||||||
parentFlickable: fl
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 24
|
|
||||||
Layout.bottomMargin: 24
|
|
||||||
|
|
||||||
enabled: underloadPacketMagicHeaderTextField.errorText === "" &&
|
|
||||||
transportPacketMagicHeaderTextField.errorText === "" &&
|
|
||||||
responsePacketMagicHeaderTextField.errorText === "" &&
|
|
||||||
initPacketMagicHeaderTextField.errorText === "" &&
|
|
||||||
responsePacketJunkSizeTextField.errorText === "" &&
|
|
||||||
initPacketJunkSizeTextField.errorText === "" &&
|
|
||||||
junkPacketMaxSizeTextField.errorText === "" &&
|
|
||||||
junkPacketMinSizeTextField.errorText === "" &&
|
|
||||||
junkPacketCountTextField.errorText === "" &&
|
|
||||||
portTextField.errorText === ""
|
|
||||||
|
|
||||||
text: qsTr("Save")
|
|
||||||
|
|
||||||
Keys.onTabPressed: lastItemTabClicked(focusItem)
|
|
||||||
|
|
||||||
clickedFunc: function() {
|
|
||||||
forceActiveFocus()
|
|
||||||
|
|
||||||
if (delegateItem.isEnabled) {
|
|
||||||
if (AwgConfigModel.isHeadersEqual(underloadPacketMagicHeaderTextField.textField.text,
|
|
||||||
transportPacketMagicHeaderTextField.textField.text,
|
|
||||||
responsePacketMagicHeaderTextField.textField.text,
|
|
||||||
initPacketMagicHeaderTextField.textField.text)) {
|
|
||||||
PageController.showErrorMessage(qsTr("The values of the H1-H4 fields must be unique"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AwgConfigModel.isPacketSizeEqual(parseInt(initPacketJunkSizeTextField.textField.text),
|
|
||||||
parseInt(responsePacketJunkSizeTextField.textField.text))) {
|
|
||||||
PageController.showErrorMessage(qsTr("The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92)"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var headerText = qsTr("Save settings?")
|
|
||||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
|
||||||
var yesButtonText = qsTr("Continue")
|
|
||||||
var noButtonText = qsTr("Cancel")
|
|
||||||
|
|
||||||
var yesButtonFunction = function() {
|
|
||||||
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) {
|
|
||||||
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
PageController.goToPage(PageEnum.PageSetupWizardInstalling);
|
|
||||||
InstallController.updateContainer(AwgConfigModel.getConfig())
|
|
||||||
}
|
|
||||||
var noButtonFunction = function() {
|
|
||||||
if (!GC.isMobile()) {
|
|
||||||
saveRestartButton.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var headerText = qsTr("Save settings?")
|
||||||
|
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||||
|
var yesButtonText = qsTr("Continue")
|
||||||
|
var noButtonText = qsTr("Cancel")
|
||||||
|
|
||||||
|
var yesButtonFunction = function() {
|
||||||
|
if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) {
|
||||||
|
PageController.showNotificationMessage(qsTr("Unable change settings while there is an active connection"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
PageController.goToPage(PageEnum.PageSetupWizardInstalling);
|
||||||
|
InstallController.updateContainer(AwgConfigModel.getConfig())
|
||||||
|
}
|
||||||
|
var noButtonFunction = function() {
|
||||||
|
if (!GC.isMobile()) {
|
||||||
|
saveRestartButton.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,13 +16,6 @@ import "../Components"
|
|||||||
PageType {
|
PageType {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
defaultActiveFocusItem: listview.currentItem.trafficFromField.textField
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: focusItem
|
|
||||||
KeyNavigation.tab: backButton
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: backButtonLayout
|
id: backButtonLayout
|
||||||
|
|
||||||
@@ -34,7 +27,6 @@ PageType {
|
|||||||
|
|
||||||
BackButtonType {
|
BackButtonType {
|
||||||
id: backButton
|
id: backButton
|
||||||
KeyNavigation.tab: listview.currentItem.trafficFromField.textField
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,11 +48,13 @@ PageType {
|
|||||||
ListView {
|
ListView {
|
||||||
id: listview
|
id: listview
|
||||||
|
|
||||||
|
property int selectedIndex: 0
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: listview.contentItem.height
|
height: listview.contentItem.height
|
||||||
|
|
||||||
clip: true
|
clip: true
|
||||||
interactive: false
|
reuseItems: true
|
||||||
|
|
||||||
model: CloakConfigModel
|
model: CloakConfigModel
|
||||||
|
|
||||||
@@ -110,8 +104,6 @@ PageType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyNavigation.tab: portTextField.textField
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TextFieldWithHeaderType {
|
TextFieldWithHeaderType {
|
||||||
@@ -130,8 +122,6 @@ PageType {
|
|||||||
port = textFieldText
|
port = textFieldText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyNavigation.tab: cipherDropDown
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DropDownType {
|
DropDownType {
|
||||||
@@ -143,7 +133,6 @@ PageType {
|
|||||||
headerText: qsTr("Cipher")
|
headerText: qsTr("Cipher")
|
||||||
|
|
||||||
drawerParent: root
|
drawerParent: root
|
||||||
KeyNavigation.tab: saveRestartButton
|
|
||||||
|
|
||||||
listView: ListViewWithRadioButtonType {
|
listView: ListViewWithRadioButtonType {
|
||||||
id: cipherListView
|
id: cipherListView
|
||||||
@@ -161,7 +150,7 @@ PageType {
|
|||||||
clickedFunction: function() {
|
clickedFunction: function() {
|
||||||
cipherDropDown.text = selectedText
|
cipherDropDown.text = selectedText
|
||||||
cipher = cipherDropDown.text
|
cipher = cipherDropDown.text
|
||||||
cipherDropDown.close()
|
cipherDropDown.closeTriggered()
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
@@ -169,7 +158,7 @@ PageType {
|
|||||||
|
|
||||||
for (var i = 0; i < cipherListView.model.count; i++) {
|
for (var i = 0; i < cipherListView.model.count; i++) {
|
||||||
if (cipherListView.model.get(i).name === cipherDropDown.text) {
|
if (cipherListView.model.get(i).name === cipherDropDown.text) {
|
||||||
currentIndex = i
|
selectedIndex = i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,7 +173,6 @@ PageType {
|
|||||||
Layout.bottomMargin: 24
|
Layout.bottomMargin: 24
|
||||||
|
|
||||||
text: qsTr("Save")
|
text: qsTr("Save")
|
||||||
Keys.onTabPressed: lastItemTabClicked(focusItem)
|
|
||||||
|
|
||||||
clickedFunc: function() {
|
clickedFunc: function() {
|
||||||
forceActiveFocus()
|
forceActiveFocus()
|
||||||
|
|||||||
@@ -17,18 +17,6 @@ import "../Components"
|
|||||||
PageType {
|
PageType {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
defaultActiveFocusItem: listview.currentItem.vpnAddressSubnetTextField.textField
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: focusItem
|
|
||||||
KeyNavigation.tab: backButton
|
|
||||||
onActiveFocusChanged: {
|
|
||||||
if (activeFocus) {
|
|
||||||
fl.ensureVisible(focusItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: backButtonLayout
|
id: backButtonLayout
|
||||||
|
|
||||||
@@ -40,7 +28,6 @@ PageType {
|
|||||||
|
|
||||||
BackButtonType {
|
BackButtonType {
|
||||||
id: backButton
|
id: backButton
|
||||||
KeyNavigation.tab: listview.currentItem.vpnAddressSubnetTextField.textField
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +91,6 @@ PageType {
|
|||||||
textFieldText: subnetAddress
|
textFieldText: subnetAddress
|
||||||
|
|
||||||
parentFlickable: fl
|
parentFlickable: fl
|
||||||
KeyNavigation.tab: transportProtoSelector
|
|
||||||
|
|
||||||
textField.onEditingFinished: {
|
textField.onEditingFinished: {
|
||||||
if (textFieldText !== subnetAddress) {
|
if (textFieldText !== subnetAddress) {
|
||||||
@@ -132,8 +118,6 @@ PageType {
|
|||||||
return transportProto === "tcp" ? 1 : 0
|
return transportProto === "tcp" ? 1 : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyNavigation.tab: portTextField.enabled ? portTextField.textField : autoNegotiateEncryprionSwitcher
|
|
||||||
|
|
||||||
onCurrentIndexChanged: {
|
onCurrentIndexChanged: {
|
||||||
if (transportProto === "tcp" && currentIndex === 0) {
|
if (transportProto === "tcp" && currentIndex === 0) {
|
||||||
transportProto = "udp"
|
transportProto = "udp"
|
||||||
@@ -162,8 +146,6 @@ PageType {
|
|||||||
port = textFieldText
|
port = textFieldText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyNavigation.tab: autoNegotiateEncryprionSwitcher
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SwitcherType {
|
SwitcherType {
|
||||||
@@ -181,10 +163,6 @@ PageType {
|
|||||||
autoNegotiateEncryprion = checked
|
autoNegotiateEncryprion = checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyNavigation.tab: hashDropDown.enabled ?
|
|
||||||
hashDropDown :
|
|
||||||
tlsAuthCheckBox
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DropDownType {
|
DropDownType {
|
||||||
@@ -199,9 +177,6 @@ PageType {
|
|||||||
|
|
||||||
drawerParent: root
|
drawerParent: root
|
||||||
parentFlickable: fl
|
parentFlickable: fl
|
||||||
KeyNavigation.tab: cipherDropDown.enabled ?
|
|
||||||
cipherDropDown :
|
|
||||||
tlsAuthCheckBox
|
|
||||||
|
|
||||||
listView: ListViewWithRadioButtonType {
|
listView: ListViewWithRadioButtonType {
|
||||||
id: hashListView
|
id: hashListView
|
||||||
@@ -224,7 +199,7 @@ PageType {
|
|||||||
clickedFunction: function() {
|
clickedFunction: function() {
|
||||||
hashDropDown.text = selectedText
|
hashDropDown.text = selectedText
|
||||||
hash = hashDropDown.text
|
hash = hashDropDown.text
|
||||||
hashDropDown.close()
|
hashDropDown.closeTriggered()
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
@@ -252,8 +227,6 @@ PageType {
|
|||||||
drawerParent: root
|
drawerParent: root
|
||||||
parentFlickable: fl
|
parentFlickable: fl
|
||||||
|
|
||||||
KeyNavigation.tab: tlsAuthCheckBox
|
|
||||||
|
|
||||||
listView: ListViewWithRadioButtonType {
|
listView: ListViewWithRadioButtonType {
|
||||||
id: cipherListView
|
id: cipherListView
|
||||||
|
|
||||||
@@ -275,7 +248,7 @@ PageType {
|
|||||||
clickedFunction: function() {
|
clickedFunction: function() {
|
||||||
cipherDropDown.text = selectedText
|
cipherDropDown.text = selectedText
|
||||||
cipher = cipherDropDown.text
|
cipher = cipherDropDown.text
|
||||||
cipherDropDown.close()
|
cipherDropDown.closeTriggered()
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
@@ -320,8 +293,6 @@ PageType {
|
|||||||
text: qsTr("TLS auth")
|
text: qsTr("TLS auth")
|
||||||
checked: tlsAuth
|
checked: tlsAuth
|
||||||
|
|
||||||
KeyNavigation.tab: blockDnsCheckBox
|
|
||||||
|
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
if (checked !== tlsAuth) {
|
if (checked !== tlsAuth) {
|
||||||
console.log("tlsAuth changed to: " + checked)
|
console.log("tlsAuth changed to: " + checked)
|
||||||
@@ -339,8 +310,6 @@ PageType {
|
|||||||
text: qsTr("Block DNS requests outside of VPN")
|
text: qsTr("Block DNS requests outside of VPN")
|
||||||
checked: blockDns
|
checked: blockDns
|
||||||
|
|
||||||
KeyNavigation.tab: additionalClientCommandsSwitcher
|
|
||||||
|
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
if (checked !== blockDns) {
|
if (checked !== blockDns) {
|
||||||
blockDns = checked
|
blockDns = checked
|
||||||
@@ -355,9 +324,6 @@ PageType {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 32
|
Layout.topMargin: 32
|
||||||
parentFlickable: fl
|
parentFlickable: fl
|
||||||
KeyNavigation.tab: additionalClientCommandsTextArea.visible ?
|
|
||||||
additionalClientCommandsTextArea.textArea :
|
|
||||||
additionalServerCommandsSwitcher
|
|
||||||
|
|
||||||
checked: additionalClientCommands !== ""
|
checked: additionalClientCommands !== ""
|
||||||
|
|
||||||
@@ -376,7 +342,7 @@ PageType {
|
|||||||
Layout.topMargin: 16
|
Layout.topMargin: 16
|
||||||
|
|
||||||
visible: additionalClientCommandsSwitcher.checked
|
visible: additionalClientCommandsSwitcher.checked
|
||||||
KeyNavigation.tab: additionalServerCommandsSwitcher
|
|
||||||
parentFlickable: fl
|
parentFlickable: fl
|
||||||
|
|
||||||
textAreaText: additionalClientCommands
|
textAreaText: additionalClientCommands
|
||||||
@@ -394,9 +360,6 @@ PageType {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 16
|
Layout.topMargin: 16
|
||||||
parentFlickable: fl
|
parentFlickable: fl
|
||||||
KeyNavigation.tab: additionalServerCommandsTextArea.visible ?
|
|
||||||
additionalServerCommandsTextArea.textArea :
|
|
||||||
saveRestartButton
|
|
||||||
|
|
||||||
checked: additionalServerCommands !== ""
|
checked: additionalServerCommands !== ""
|
||||||
|
|
||||||
@@ -419,7 +382,6 @@ PageType {
|
|||||||
textAreaText: additionalServerCommands
|
textAreaText: additionalServerCommands
|
||||||
placeholderText: qsTr("Commands:")
|
placeholderText: qsTr("Commands:")
|
||||||
parentFlickable: fl
|
parentFlickable: fl
|
||||||
KeyNavigation.tab: saveRestartButton
|
|
||||||
textArea.onEditingFinished: {
|
textArea.onEditingFinished: {
|
||||||
if (additionalServerCommands !== textAreaText) {
|
if (additionalServerCommands !== textAreaText) {
|
||||||
additionalServerCommands = textAreaText
|
additionalServerCommands = textAreaText
|
||||||
@@ -436,7 +398,6 @@ PageType {
|
|||||||
|
|
||||||
text: qsTr("Save")
|
text: qsTr("Save")
|
||||||
parentFlickable: fl
|
parentFlickable: fl
|
||||||
Keys.onTabPressed: lastItemTabClicked(focusItem)
|
|
||||||
|
|
||||||
clickedFunc: function() {
|
clickedFunc: function() {
|
||||||
forceActiveFocus()
|
forceActiveFocus()
|
||||||
|
|||||||
@@ -19,13 +19,6 @@ import "../Components"
|
|||||||
PageType {
|
PageType {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
defaultActiveFocusItem: focusItem
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: focusItem
|
|
||||||
KeyNavigation.tab: backButton
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: header
|
id: header
|
||||||
|
|
||||||
@@ -37,7 +30,6 @@ PageType {
|
|||||||
|
|
||||||
BackButtonType {
|
BackButtonType {
|
||||||
id: backButton
|
id: backButton
|
||||||
KeyNavigation.tab: listView
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HeaderType {
|
HeaderType {
|
||||||
@@ -75,13 +67,6 @@ PageType {
|
|||||||
activeFocusOnTab: true
|
activeFocusOnTab: true
|
||||||
focus: true
|
focus: true
|
||||||
|
|
||||||
onActiveFocusChanged: {
|
|
||||||
if (focus) {
|
|
||||||
listView.currentIndex = 0
|
|
||||||
listView.currentItem.focusItem.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
implicitWidth: parent.width
|
implicitWidth: parent.width
|
||||||
implicitHeight: delegateContent.implicitHeight
|
implicitHeight: delegateContent.implicitHeight
|
||||||
@@ -101,11 +86,9 @@ PageType {
|
|||||||
text: qsTr("Show connection options")
|
text: qsTr("Show connection options")
|
||||||
|
|
||||||
clickedFunction: function() {
|
clickedFunction: function() {
|
||||||
configContentDrawer.open()
|
configContentDrawer.openTriggered()
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyNavigation.tab: removeButton
|
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: button
|
anchors.fill: button
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
@@ -120,31 +103,12 @@ PageType {
|
|||||||
|
|
||||||
expandedHeight: root.height * 0.9
|
expandedHeight: root.height * 0.9
|
||||||
|
|
||||||
onClosed: {
|
|
||||||
if (!GC.isMobile()) {
|
|
||||||
defaultActiveFocusItem.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parent: root
|
parent: root
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
expandedContent: Item {
|
expandedStateContent: Item {
|
||||||
implicitHeight: configContentDrawer.expandedHeight
|
implicitHeight: configContentDrawer.expandedHeight
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: configContentDrawer
|
|
||||||
enabled: !GC.isMobile()
|
|
||||||
function onOpened() {
|
|
||||||
focusItem1.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: focusItem1
|
|
||||||
KeyNavigation.tab: backButton1
|
|
||||||
}
|
|
||||||
|
|
||||||
BackButtonType {
|
BackButtonType {
|
||||||
id: backButton1
|
id: backButton1
|
||||||
|
|
||||||
@@ -154,10 +118,8 @@ PageType {
|
|||||||
anchors.topMargin: 16
|
anchors.topMargin: 16
|
||||||
|
|
||||||
backButtonFunction: function() {
|
backButtonFunction: function() {
|
||||||
configContentDrawer.close()
|
configContentDrawer.closeTriggered()
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyNavigation.tab: focusItem1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FlickableType {
|
FlickableType {
|
||||||
@@ -226,7 +188,6 @@ PageType {
|
|||||||
text: qsTr("Remove ") + ContainersModel.getProcessedContainerName()
|
text: qsTr("Remove ") + ContainersModel.getProcessedContainerName()
|
||||||
textColor: AmneziaStyle.color.vibrantRed
|
textColor: AmneziaStyle.color.vibrantRed
|
||||||
|
|
||||||
Keys.onTabPressed: lastItemTabClicked(focusItem)
|
|
||||||
clickedFunction: function() {
|
clickedFunction: function() {
|
||||||
var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName())
|
var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName())
|
||||||
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.")
|
||||||
|
|||||||
@@ -16,15 +16,6 @@ import "../Components"
|
|||||||
PageType {
|
PageType {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
defaultActiveFocusItem: listview.currentItem.focusItemId.enabled ?
|
|
||||||
listview.currentItem.focusItemId.textField :
|
|
||||||
focusItem
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: focusItem
|
|
||||||
KeyNavigation.tab: backButton
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: backButtonLayout
|
id: backButtonLayout
|
||||||
|
|
||||||
@@ -36,9 +27,6 @@ PageType {
|
|||||||
|
|
||||||
BackButtonType {
|
BackButtonType {
|
||||||
id: backButton
|
id: backButton
|
||||||
KeyNavigation.tab: listview.currentItem.focusItemId.enabled ?
|
|
||||||
listview.currentItem.focusItemId.textField :
|
|
||||||
focusItem
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,8 +102,6 @@ PageType {
|
|||||||
port = textFieldText
|
port = textFieldText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyNavigation.tab: cipherDropDown
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DropDownType {
|
DropDownType {
|
||||||
@@ -129,9 +115,9 @@ PageType {
|
|||||||
headerText: qsTr("Cipher")
|
headerText: qsTr("Cipher")
|
||||||
|
|
||||||
drawerParent: root
|
drawerParent: root
|
||||||
KeyNavigation.tab: saveRestartButton
|
|
||||||
|
|
||||||
listView: ListViewWithRadioButtonType {
|
listView: ListViewWithRadioButtonType {
|
||||||
|
|
||||||
id: cipherListView
|
id: cipherListView
|
||||||
|
|
||||||
rootWidth: root.width
|
rootWidth: root.width
|
||||||
@@ -147,7 +133,7 @@ PageType {
|
|||||||
clickedFunction: function() {
|
clickedFunction: function() {
|
||||||
cipherDropDown.text = selectedText
|
cipherDropDown.text = selectedText
|
||||||
cipher = cipherDropDown.text
|
cipher = cipherDropDown.text
|
||||||
cipherDropDown.close()
|
cipherDropDown.closeTriggered()
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
@@ -172,7 +158,6 @@ PageType {
|
|||||||
enabled: isPortEditable | isCipherEditable
|
enabled: isPortEditable | isCipherEditable
|
||||||
|
|
||||||
text: qsTr("Save")
|
text: qsTr("Save")
|
||||||
Keys.onTabPressed: lastItemTabClicked(focusItem)
|
|
||||||
|
|
||||||
clickedFunc: function() {
|
clickedFunc: function() {
|
||||||
forceActiveFocus()
|
forceActiveFocus()
|
||||||
|
|||||||
@@ -150,8 +150,6 @@ PageType {
|
|||||||
|
|
||||||
text: qsTr("Save")
|
text: qsTr("Save")
|
||||||
|
|
||||||
Keys.onTabPressed: lastItemTabClicked(focusItem)
|
|
||||||
|
|
||||||
clickedFunc: function() {
|
clickedFunc: function() {
|
||||||
forceActiveFocus()
|
forceActiveFocus()
|
||||||
var headerText = qsTr("Save settings?")
|
var headerText = qsTr("Save settings?")
|
||||||
|
|||||||
@@ -16,13 +16,6 @@ import "../Components"
|
|||||||
PageType {
|
PageType {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
defaultActiveFocusItem: listview
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: focusItem
|
|
||||||
KeyNavigation.tab: backButton
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: backButtonLayout
|
id: backButtonLayout
|
||||||
|
|
||||||
@@ -34,7 +27,6 @@ PageType {
|
|||||||
|
|
||||||
BackButtonType {
|
BackButtonType {
|
||||||
id: backButton
|
id: backButton
|
||||||
KeyNavigation.tab: listview
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,13 +56,6 @@ PageType {
|
|||||||
|
|
||||||
model: WireGuardConfigModel
|
model: WireGuardConfigModel
|
||||||
|
|
||||||
activeFocusOnTab: true
|
|
||||||
onActiveFocusChanged: {
|
|
||||||
if (activeFocus) {
|
|
||||||
listview.itemAtIndex(0)?.focusItemId.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
id: delegateItem
|
id: delegateItem
|
||||||
|
|
||||||
@@ -109,8 +94,6 @@ PageType {
|
|||||||
textField.maximumLength: 5
|
textField.maximumLength: 5
|
||||||
textField.validator: IntValidator { bottom: 1; top: 65535 }
|
textField.validator: IntValidator { bottom: 1; top: 65535 }
|
||||||
|
|
||||||
KeyNavigation.tab: saveButton
|
|
||||||
|
|
||||||
textField.onEditingFinished: {
|
textField.onEditingFinished: {
|
||||||
if (textFieldText !== port) {
|
if (textFieldText !== port) {
|
||||||
port = textFieldText
|
port = textFieldText
|
||||||
@@ -120,6 +103,26 @@ PageType {
|
|||||||
checkEmptyText: true
|
checkEmptyText: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: mtuTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
|
||||||
|
headerText: qsTr("MTU")
|
||||||
|
textFieldText: mtu
|
||||||
|
textField.validator: IntValidator { bottom: 576; top: 65535 }
|
||||||
|
|
||||||
|
textField.onEditingFinished: {
|
||||||
|
if (textFieldText === "") {
|
||||||
|
textFieldText = "0"
|
||||||
|
}
|
||||||
|
if (textFieldText !== mtu) {
|
||||||
|
mtu = textFieldText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkEmptyText: true
|
||||||
|
}
|
||||||
|
|
||||||
BasicButtonType {
|
BasicButtonType {
|
||||||
id: saveButton
|
id: saveButton
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
@@ -130,8 +133,6 @@ PageType {
|
|||||||
|
|
||||||
text: qsTr("Save")
|
text: qsTr("Save")
|
||||||
|
|
||||||
Keys.onTabPressed: lastItemTabClicked(focusItem)
|
|
||||||
|
|
||||||
onClicked: function() {
|
onClicked: function() {
|
||||||
forceActiveFocus()
|
forceActiveFocus()
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,6 @@ import "../Components"
|
|||||||
PageType {
|
PageType {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
defaultActiveFocusItem: listview
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: focusItem
|
|
||||||
KeyNavigation.tab: backButton
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: backButtonLayout
|
id: backButtonLayout
|
||||||
|
|
||||||
@@ -35,7 +28,6 @@ PageType {
|
|||||||
|
|
||||||
BackButtonType {
|
BackButtonType {
|
||||||
id: backButton
|
id: backButton
|
||||||
KeyNavigation.tab: listview
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,13 +57,6 @@ PageType {
|
|||||||
|
|
||||||
model: XrayConfigModel
|
model: XrayConfigModel
|
||||||
|
|
||||||
activeFocusOnTab: true
|
|
||||||
onActiveFocusChanged: {
|
|
||||||
if (activeFocus) {
|
|
||||||
listview.itemAtIndex(0)?.focusItemId.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
property alias focusItemId: textFieldWithHeaderType.textField
|
property alias focusItemId: textFieldWithHeaderType.textField
|
||||||
|
|
||||||
@@ -103,8 +88,6 @@ PageType {
|
|||||||
headerText: qsTr("Disguised as traffic from")
|
headerText: qsTr("Disguised as traffic from")
|
||||||
textFieldText: site
|
textFieldText: site
|
||||||
|
|
||||||
KeyNavigation.tab: basicButton
|
|
||||||
|
|
||||||
textField.onEditingFinished: {
|
textField.onEditingFinished: {
|
||||||
if (textFieldText !== site) {
|
if (textFieldText !== site) {
|
||||||
var tmpText = textFieldText
|
var tmpText = textFieldText
|
||||||
@@ -128,8 +111,6 @@ PageType {
|
|||||||
|
|
||||||
text: qsTr("Save")
|
text: qsTr("Save")
|
||||||
|
|
||||||
Keys.onTabPressed: lastItemTabClicked(focusItem)
|
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
forceActiveFocus()
|
forceActiveFocus()
|
||||||
|
|
||||||
|
|||||||
@@ -16,13 +16,6 @@ import "../Components"
|
|||||||
PageType {
|
PageType {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
defaultActiveFocusItem: focusItem
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: focusItem
|
|
||||||
KeyNavigation.tab: backButton
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: backButtonLayout
|
id: backButtonLayout
|
||||||
|
|
||||||
@@ -34,7 +27,6 @@ PageType {
|
|||||||
|
|
||||||
BackButtonType {
|
BackButtonType {
|
||||||
id: backButton
|
id: backButton
|
||||||
KeyNavigation.tab: removeButton
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,8 +64,6 @@ PageType {
|
|||||||
text: qsTr("Remove ") + ContainersModel.getProcessedContainerName()
|
text: qsTr("Remove ") + ContainersModel.getProcessedContainerName()
|
||||||
textColor: AmneziaStyle.color.vibrantRed
|
textColor: AmneziaStyle.color.vibrantRed
|
||||||
|
|
||||||
Keys.onTabPressed: root.lastItemTabClicked()
|
|
||||||
|
|
||||||
clickedFunction: function() {
|
clickedFunction: function() {
|
||||||
var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName())
|
var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName())
|
||||||
var yesButtonText = qsTr("Continue")
|
var yesButtonText = qsTr("Continue")
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user