Compare commits

..

134 Commits

Author SHA1 Message Date
Cyril Anisimov
25a24bf38c focusControl => FocusControl 2024-12-30 18:10:25 +01:00
Cyril Anisimov
5e492e8392 remove debug output from focusControl 2024-12-30 18:05:31 +01:00
Cyril Anisimov
b25d911328 remove debug output from ListViewFocusController 2024-12-30 18:04:05 +01:00
Cyril Anisimov
caf29fb982 remove debug output from FocusController 2024-12-30 18:03:26 +01:00
Cyril Anisimov
32ef1eae84 fix PageDevMenu 2024-12-30 00:19:11 +01:00
Cyril Anisimov
b0fdbe6c4f fix PageSettingsAppSplitTunneling 2024-12-29 22:11:35 +01:00
Cyril Anisimov
dccfcde6f6 fix PageProtocolCloakSettings 2024-12-29 22:04:00 +01:00
Cyril Anisimov
26ba520022 fix ListViewWithLabelsType 2024-12-29 22:01:41 +01:00
Cyril Anisimov
0f98fc955f remove comments and clean up 2024-12-29 21:19:56 +01:00
Cyril Anisimov
2c1f3eaf3a refactor PageSetupWizardProtocols 2024-12-29 20:26:22 +01:00
Cyril Anisimov
867ad8ee3c fix focus movement on dynamic delegates in listView 2024-12-29 00:34:20 +01:00
Cyril Anisimov
f8f4d75cb7 Merge commit 'b88ab8e432fbd2ab5002856cd83ef57181a6d7fa' into improve_navigation_cpp 2024-12-23 09:36:55 +01:00
albexk
b88ab8e432 fix(build): fix aqtinstall (#1312) 2024-12-23 08:27:09 +07:00
Cyril Anisimov
13e680f6e1 fix PageSetupWizardApiServicesList 2024-12-22 16:38:07 +01:00
Cyril Anisimov
8767f487b8 fix ScrollBarType 2024-12-22 16:28:13 +01:00
Cyril Anisimov
0cdd2ff826 fix CMakeLists.txt 2024-12-20 15:35:40 +01:00
Cyril Anisimov
c973b8868b rename focusControl to qmlUtils 2024-12-20 15:30:20 +01:00
Cyril Anisimov
53aa14a2ee fix back button on SelectLanguageDrawer 2024-12-20 02:26:05 +01:00
Cyril Anisimov
b29b5962d2 I have nothing is visible only on start screen 2024-12-20 02:17:50 +01:00
Cyril Anisimov
441a2f6df6 Restore from backup is visible only on start screen 2024-12-20 02:15:48 +01:00
Cyril Anisimov
fa64c8d68d fix focus movement in ServersListView 2024-12-20 02:05:28 +01:00
Nethius
48f6cf904e chore/minor UI fixes (#1308)
* chore: corrected the translation error

* bugfix: fixed basic button left iamge color
2024-12-19 14:36:20 +07:00
Cyril Anisimov
ae31bafeb9 remove arrow from Close application item 2024-12-19 00:22:59 +01:00
Cyril Anisimov
14d67503a8 add printSectionName method to listViewController 2024-12-19 00:00:36 +01:00
Cyril Anisimov
1029142b47 refactor focusConroller according to focusControl 2024-12-18 23:59:14 +01:00
Cyril Anisimov
7670d9dace refactor listViewFocusController acoording to focusControl 2024-12-18 23:57:08 +01:00
Cyril Anisimov
2266702a0b add focusControl with utility functions for
focus control
2024-12-18 23:47:52 +01:00
Cyril Anisimov
11d20974f0 format FocusController 2024-12-16 22:12:15 +01:00
Cyril Anisimov
a57e94e4f2 format ListViewFocusController 2024-12-16 22:12:00 +01:00
Cyril Anisimov
9620a24d6a remove manual focus control on PageShare 2024-12-15 23:23:15 +01:00
Cyril Anisimov
62be292833 Fix list view focus moving on PageShare 2024-12-15 23:14:52 +01:00
Cyril Anisimov
10e91b06f4 PageShare readonly properties 2024-12-15 23:12:59 +01:00
Cyril Anisimov
3a62f1f20b fix hide/unhide password 2024-12-14 23:44:15 +01:00
Cyril Anisimov
3021066180 refactor SwitcherType 2024-12-14 19:34:24 +01:00
Cyril Anisimov
378db1a9e5 make unchangable properties readonly 2024-12-14 19:33:54 +01:00
Cyril Anisimov
906cf7b939 fix split tunneling settings 2024-12-14 19:32:46 +01:00
Cyril Anisimov
8bc31b60a1 fix checked item in lists 2024-12-14 19:29:35 +01:00
Cyril Anisimov
149c9d12d3 Merge commit '367789bda28f33a11a72d61b222453c5b502299e' into improve_navigation_cpp 2024-12-14 18:38:41 +01:00
Cyril Anisimov
b33b97802e update logging settings page 2024-12-14 18:01:34 +01:00
KsZnak
367789bda2 Update README_RU.md (#1300)
* Update README_RU.md
2024-12-14 19:29:33 +07:00
Cyril Anisimov
d18f50a3e0 Update select language list 2024-12-13 22:27:21 +01:00
Cyril Anisimov
ef2ffce47a Fix selection in language change list 2024-12-13 22:14:27 +01:00
Cyril Anisimov
53f41408b8 Scrollbar is on if the content is larger than a screen 2024-12-13 22:09:37 +01:00
Cyril Anisimov
d06924c59d feature/xray user management (#972)
* feature: implement client management functionality for Xray

---------

Co-authored-by: aiamnezia <ai@amnezia.org>
Co-authored-by: vladimir.kuznetsov <nethiuswork@gmail.com>
2024-12-10 09:17:16 +07:00
albexk
5b9ba8c027 fix: get rid of the assign function call 2024-12-09 20:56:58 +03:00
Nethius
2db99715b1 feature: added subscription expiration date for premium v2 (#1261)
* feature: added subscription expiration date for premium v2

* feature: added a check for the presence of the “services” field in the response body of the getServicesList() function

* feature: added prohibition to change location when connection is active

* bugfix: renamed public_key->end_date to public_key->expires_at according to the changes on the backend
2024-12-09 13:32:49 +07:00
Cyril Anisimov
882c288d92 make minor fixes and clean up 2024-12-09 01:53:48 +01:00
Cyril Anisimov
ea39447708 fix focus behavior on new page/popup 2024-12-09 01:35:10 +01:00
Cyril Anisimov
3cc70790bd add setDelegateIndex method to listViewFocusController 2024-12-09 01:32:11 +01:00
Cyril Anisimov
f331c11f51 refactor credentials setup page to handle the focus navigation 2024-12-09 01:30:11 +01:00
Cyril Anisimov
39cbe6de28 update initial config page 2024-12-09 01:28:57 +01:00
pokamest
9688a8e52d Merge pull request #1292 from amnezia-vpn/Add-english-option
Update README.md
2024-12-08 10:40:20 +01:00
pokamest
b2c429f74d Merge pull request #1291 from amnezia-vpn/readme_ru_update
Update README_RU.md
2024-12-08 10:39:01 +01:00
Nethius
6ea6ab1bd9 chore: added clang-format config files (#1293) 2024-12-08 12:14:22 +07:00
KsZnak
c5aa070bf4 Update README_RU.md 2024-12-08 05:49:26 +02:00
KsZnak
d67201ede9 Update README.md 2024-12-08 05:34:18 +02:00
pokamest
4323fb2063 Merge pull request #1290 from amnezia-vpn/Readme_ru
Add files via upload
2024-12-07 15:36:56 +01:00
pokamest
6d5452b8ee Merge pull request #1288 from amnezia-vpn/Readme-ru
Update README_RU.md
2024-12-07 15:36:08 +01:00
KsZnak
569d63ef0f Add files via upload 2024-12-07 15:53:40 +02:00
Cyril Anisimov
87d00116e1 add ScrollBarType 2024-12-06 22:59:12 +01:00
KsZnak
ea910ba300 Update README_RU.md 2024-12-06 22:15:01 +02:00
Pokamest Nikak
1c1e74d06f ru readme 2024-12-06 12:40:04 +00:00
Nethius
5dc16c06f1 chore: increased the api request timeout (#1276) 2024-12-03 12:47:33 +07:00
Cyril Anisimov
f992252c6e Merge commit into improve_navigation_cpp 2024-12-02 11:10:30 +01:00
Nethius
4efaf20a1c chore: fix deploy workflow (#1280) 2024-12-02 14:46:20 +07:00
Pokamest Nikak
9d96b1cd13 Update Readme 2024-11-29 22:10:35 +00:00
Anton Sosnin
1d721ffb9a SteamDeck/OS installation fix (#1270) 2024-11-27 09:55:23 +07:00
Nethius
2130131a9d bugfix: added scroll on page with services list (#1262)
* added scroll on page with services list

* fixed margins on PageSetupWizardApiServicesList
2024-11-26 11:41:17 +07:00
Aftershock669
e0b091b474 Update readme (#1267) 2024-11-25 23:51:46 +07:00
albexk
c9205afbd4 chore: bump version code 2024-11-25 14:08:25 +03:00
albexk
09b1f322ba fix: hide UI elements that use file saving 2024-11-25 13:56:14 +03:00
albexk
4d2d7e4071 fix: add the touch emulation method for Android TV 2024-11-25 12:33:34 +03:00
albexk
c72d76aec7 Merge branch 'improve_navigation_cpp' into feature/android-tv 2024-11-25 12:20:31 +03:00
albexk
50bd364f29 Merge branch 'dev' into feature/android-tv
# Conflicts:
#	CMakeLists.txt
2024-11-25 12:19:52 +03:00
albexk
3eeab2f2e7 fix: null uri processing 2024-11-25 12:17:22 +03:00
albexk
2987e03a86 fix: add the touch emulation method for Android TV 2024-11-25 12:17:22 +03:00
albexk
392cacaabc refactor: make TvFilePicker activity more sustainable 2024-11-25 12:17:22 +03:00
albexk
51092e91cf fix: change the banner format for TV 2024-11-25 12:17:22 +03:00
albexk
d02a6dfb22 fix: add a workaround to open files on Android TV due to lack of SAF 2024-11-25 12:17:22 +03:00
Nethius
4e1862a802 chore: minor fixes (#1235) 2024-11-25 12:15:36 +03:00
albexk
53e766f6d3 Fix connection check for AWG/WG 2024-11-25 12:15:36 +03:00
albexk
341d6f569b fix(android): add CHANGE_NETWORK_STATE permission for all Android versions 2024-11-25 12:15:36 +03:00
albexk
a5abab8caf fix: add separate method for reading files to fix file reading on Android TV 2024-11-25 12:15:36 +03:00
albexk
7df050371e Fix importing files on TVs 2024-11-25 12:15:36 +03:00
albexk
65870ad5c9 Add mandatory requirement for android.software.leanback. 2024-11-25 12:15:36 +03:00
albexk
940806a6fa Bump version 2024-11-25 12:15:36 +03:00
Cyril Anisimov
0620b4536a update popup 2024-11-21 19:11:57 +01:00
Cyril Anisimov
d956be901d refactor page logging settings for focus navigation 2024-11-21 19:11:57 +01:00
Cyril Anisimov
942805cca2 fix focus move on qr code on share page 2024-11-21 19:11:57 +01:00
Cyril Anisimov
ed6fc27e52 fix focus navigation in dynamic delegate of list view 2024-11-21 19:11:57 +01:00
Cyril Anisimov
416421cba7 fix crash on return with esc key 2024-11-21 19:11:57 +01:00
Cyril Anisimov
45b8235a81 update config page for scrolling with tab 2024-11-21 19:11:57 +01:00
Cyril Anisimov
a92f706524 fix focus on notification 2024-11-21 19:11:57 +01:00
Cyril Anisimov
5e9202f6ac refactor focusController to make the logic more
straightforward
2024-11-21 19:11:57 +01:00
Cyril Anisimov
dc6c1cdf49 refactor listViewFocusController 2024-11-21 19:11:57 +01:00
Cyril Anisimov
f020bdb6e8 fix drawer open close usage 2024-11-21 19:11:57 +01:00
Cyril Anisimov
88958c042f fix share details drawer 2024-11-21 19:11:57 +01:00
Cyril Anisimov
e4d21dc4d7 fix spawn signals on switch 2024-11-21 19:11:57 +01:00
Cyril Anisimov
2e896ed34f fix server list back button handler 2024-11-21 19:11:57 +01:00
Cyril Anisimov
163399816f minor fixes 2024-11-21 19:11:57 +01:00
Cyril Anisimov
3d7209ee7a fix focus move on list views with header and-or footer 2024-11-21 19:11:57 +01:00
Cyril Anisimov
42645a98f8 refactor about page for correct focus move 2024-11-21 19:11:57 +01:00
Cyril Anisimov
7c3d08d80e fix page share default server selection 2024-11-21 19:11:57 +01:00
Cyril Anisimov
c77e01fb37 fix server rename 2024-11-21 19:11:57 +01:00
Cyril Anisimov
b6c59b08a1 clean up page share 2024-11-21 19:11:56 +01:00
Cyril Anisimov
766e1c92fc fix focus on users on page share 2024-11-21 19:11:56 +01:00
Cyril Anisimov
21755cbd54 fix protocol settings focus move 2024-11-21 19:11:56 +01:00
Cyril Anisimov
dac45a9f7f fix crashes after ListView navigation 2024-11-21 19:11:56 +01:00
Cyril Anisimov
db5d289edc fix back navigation on default focus item 2024-11-21 19:11:56 +01:00
Cyril Anisimov
c962211068 fix PageSetupWizardProtocolSettings focus move 2024-11-21 19:11:56 +01:00
Cyril Anisimov
9cfa4c1389 fix drawer layout and focus move 2024-11-21 19:11:56 +01:00
Cyril Anisimov
2c9fa10b8b fix drawer radio buttons selection 2024-11-21 19:11:56 +01:00
Cyril Anisimov
626b9e1e76 fix reverse focus move on listView 2024-11-21 19:11:56 +01:00
Cyril Anisimov
ada3f9a7fa remove useless slots, logs, Drawer open and close 2024-11-21 19:11:56 +01:00
Cyril Anisimov
063851445a remove useless key navigation 2024-11-21 19:11:56 +01:00
Cyril Anisimov
852e90e317 update CardType for using with focus navigation 2024-11-21 19:11:56 +01:00
Cyril Anisimov
89ac585e07 fix ListView navigation 2024-11-21 19:11:56 +01:00
Cyril Anisimov
f3df9eb5f5 add ListViewFocusController 2024-11-21 19:11:56 +01:00
Cyril Anisimov
75f189e256 update pages 2024-11-21 19:11:56 +01:00
Cyril Anisimov
3c655d0051 update transitions 2024-11-21 19:11:56 +01:00
Cyril Anisimov
b4f4ec4ac9 add default focus item 2024-11-21 19:11:56 +01:00
Cyril Anisimov
f189f7b6af add reverse focus change to FocusController 2024-11-21 19:11:56 +01:00
Cyril Anisimov
78a4caa47e fixed language selector 2024-11-21 19:11:56 +01:00
Cyril Anisimov
01e31b4b4d add focus navigation to qml 2024-11-21 19:11:56 +01:00
Cyril Anisimov
cecee3769e add more key handlers 2024-11-21 19:11:55 +01:00
Cyril Anisimov
02bbcd3a31 add focusController class 2024-11-21 19:11:55 +01:00
Nethius
8547de82ea bump xcode-version for macos build (#1249) 2024-11-14 10:58:04 +07:00
Nethius
aa871bd1c9 feature: added country selection on home page drawer (#1215) 2024-11-12 13:22:34 +07:00
albexk
23806e1def chore: bump version to 4.8.2.4 (#1240) 2024-11-08 15:22:16 +07:00
Nethius
31867993ce chore: minor fixes (#1235) 2024-11-06 12:57:39 +07:00
pokamest
7b7a922d92 Merge pull request #1226 from amnezia-vpn/fix/android-awg-connection
Fix connection check for AWG/WG
2024-11-05 12:25:49 +01:00
pokamest
09bd958d8d Merge pull request #1231 from amnezia-vpn/fix/android-network-state
Add CHANGE_NETWORK_STATE permission for all Android versions
2024-11-05 12:25:11 +01:00
albexk
576e2226fe fix(android): add CHANGE_NETWORK_STATE permission for all Android versions 2024-11-03 16:11:23 +03:00
albexk
1533270e4e Fix connection check for AWG/WG 2024-11-02 00:54:24 +03:00
144 changed files with 5276 additions and 4035 deletions

39
.clang-format Normal file
View 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
View 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

View File

@@ -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

View File

@@ -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")

View File

@@ -1,24 +1,29 @@
# Amnezia VPN # Amnezia VPN
## _The best client for self-hosted VPN_
### _The best client for self-hosted VPN_
[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev) [![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev)
[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/amnezia-vpn/amnezia-client) [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](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)
![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)
<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> [![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](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
View File

@@ -0,0 +1,181 @@
# Amnezia VPN
### _Лучший клиент для создания VPN на собственном сервере_
[![Build Status](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/amnezia-vpn/amnezia-client/actions/workflows/deploy.yml?query=branch:dev)
[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](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 на вашем сервере.
[![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](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) за поддержку нашего проекта.

View File

@@ -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

View File

@@ -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());

View File

@@ -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;

View File

@@ -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"

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -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>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_banner_background">#1E1E1F</color>
</resources>

View File

@@ -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>

View File

@@ -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) {

View 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()
}
}
}

View File

@@ -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)

View File

@@ -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);

View File

@@ -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

View File

@@ -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));

View File

@@ -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;
} }

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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>

View 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

View File

@@ -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"/>

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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();

View 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;
}
}

View 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

View File

@@ -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)

View File

@@ -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) {

View 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;
}

View 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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);
} }

View File

@@ -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));

View File

@@ -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
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;
}; };

View File

@@ -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;
} }

View File

@@ -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);

View File

@@ -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;

View File

@@ -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();

View File

@@ -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

View File

@@ -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 {}

View File

@@ -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()
} }
} }

View File

@@ -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()
} }
} }

View File

@@ -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()
} }
} }
} }

View File

@@ -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
} }
} }
} }

View File

@@ -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()
} }
} }
} }

View 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
}
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()
}
}
} }

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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 }
} }

View File

@@ -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

View File

@@ -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
} }
} }

View File

@@ -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
} }
} }

View File

@@ -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
} }
} }

View File

@@ -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()

View 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
}

View File

@@ -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
} }
} }

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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();
} // }
} }
} }

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)
} }
} }

View File

@@ -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

View File

@@ -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
}
} }
} }
} }

View File

@@ -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()

View File

@@ -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)
} }
} }
} }

View File

@@ -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()

View File

@@ -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()

View File

@@ -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.")

View File

@@ -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()

View File

@@ -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?")

View File

@@ -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()

View File

@@ -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()

View File

@@ -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