diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..5c459fd2 --- /dev/null +++ b/.clang-format @@ -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 diff --git a/.clang-format-ignore b/.clang-format-ignore new file mode 100644 index 00000000..4019357f --- /dev/null +++ b/.clang-format-ignore @@ -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 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 370c069d..1f270cd1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -217,7 +217,11 @@ jobs: export QT_BIN_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/ios/bin" export QT_MACOS_ROOT_DIR="${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/macos" export PATH=$PATH:~/go/bin - sh deploy/build_ios.sh + sh deploy/build_ios.sh | \ + sed -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DPROD_AGW_PUBLIC_KEY/d' | \ + sed -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/,/-Xcc/ { /-Xcc/!d; }' -e '/-Xcc -DDEV_AGW_PUBLIC_KEY/d' | \ + sed -e '/-DPROD_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DPROD_AGW_PUBLIC_KEY/d' | \ + sed -e '/-DDEV_AGW_PUBLIC_KEY/,/-D/ { /-D/!d; }' -e '/-DDEV_AGW_PUBLIC_KEY/d' env: IOS_TRUST_CERT_BASE64: ${{ secrets.IOS_TRUST_CERT_BASE64 }} IOS_SIGNING_CERT_BASE64: ${{ secrets.IOS_SIGNING_CERT_BASE64 }} @@ -256,7 +260,7 @@ jobs: - name: 'Setup xcode' uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '14.3.1' + xcode-version: '15.4.0' - name: 'Install Qt' uses: jurplel/install-qt-action@v3 @@ -331,7 +335,8 @@ jobs: arch: 'gcc_64' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Install android_x86_64 Qt' uses: jurplel/install-qt-action@v4 @@ -342,7 +347,8 @@ jobs: arch: 'android_x86_64' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Install android_x86 Qt' uses: jurplel/install-qt-action@v4 @@ -353,7 +359,8 @@ jobs: arch: 'android_x86' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Install android_armv7 Qt' uses: jurplel/install-qt-action@v4 @@ -364,7 +371,8 @@ jobs: arch: 'android_armv7' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Install android_arm64_v8a Qt' uses: jurplel/install-qt-action@v4 @@ -375,7 +383,8 @@ jobs: arch: 'android_arm64_v8a' modules: ${{ env.QT_MODULES }} dir: ${{ runner.temp }} - extra: '--external 7z --base ${{ env.QT_MIRROR }}' + py7zrversion: '==0.22.*' + extra: '--base ${{ env.QT_MIRROR }}' - name: 'Grant execute permission for qt-cmake' shell: bash diff --git a/CMakeLists.txt b/CMakeLists.txt index 45c923e0..1c670911 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR) set(PROJECT AmneziaVPN) -project(${PROJECT} VERSION 4.8.2.4 +project(${PROJECT} VERSION 4.8.3.0 DESCRIPTION "AmneziaVPN" HOMEPAGE_URL "https://amnezia.org/" ) @@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d") set(RELEASE_DATE "${CURRENT_DATE}") set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) -set(APP_ANDROID_VERSION_CODE 1071) +set(APP_ANDROID_VERSION_CODE 1073) if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(MZ_PLATFORM_NAME "linux") diff --git a/README.md b/README.md index eed800f5..8f887808 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,29 @@ # 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) [![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) -
+[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. - - - +[![Image](https://github.com/amnezia-vpn/amnezia-client/blob/dev/metadata/img-readme/uipic4.png)](https://amnezia.org) -[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). + + + [All releases](https://github.com/amnezia-vpn/amnezia-client/releases) -
+
@@ -33,7 +38,8 @@ Amnezia is an open-source VPN client, with a key feature that enables you to dep ## Links -- [https://amnezia.org](https://amnezia.org) - project website | [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://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) @@ -182,8 +188,8 @@ Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p
USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4
USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d
-XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3 - +XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
+TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns ## Acknowledgments This project is tested with BrowserStack. diff --git a/README_RU.md b/README_RU.md new file mode 100644 index 00000000..59518f4b --- /dev/null +++ b/README_RU.md @@ -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). + + + + +[Все релизы](https://github.com/amnezia-vpn/amnezia-client/releases) + +
+ + + +## Особенности + +- Простой в использовании — введите 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="/Qt//ios/bin" +export QT_MACOS_ROOT_DIR="/Qt//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 +``` +Замените и на ваши значения. + +Если появляется ошибка 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__Clang_-`. Для разработки Android-компонентов откройте сгенерированный проект в Android Studio, указав папку `build-amnezia-client-Android_Qt__Clang_-/client/android-build` в качестве корневой. +Изменения в сгенерированном проекте нужно вручную перенести в репозиторий. После этого можно коммитить изменения. +Если возникают проблемы со сборкой в QT Creator после работы в Android Studio, выполните команду `./gradlew clean` в корневой папке сгенерированного проекта (`/client/android-build/.`). + + +## Лицензия + +GPL v3.0 + +## Донаты + +Patreon: [https://www.patreon.com/amneziavpn](https://www.patreon.com/amneziavpn) + +Bitcoin: bc1q26eevjcg9j0wuyywd2e3uc9cs2w58lpkpjxq6p
+USDT BEP20: 0x6abD576765a826f87D1D95183438f9408C901bE4
+USDT TRC20: TELAitazF1MZGmiNjTcnxDjEiH5oe7LC9d
+XMR: 48spms39jt1L2L5vyw2RQW6CXD6odUd4jFu19GZcDyKKQV9U88wsJVjSbL4CfRys37jVMdoaWVPSvezCQPhHXUW5UKLqUp3
+TON: UQDpU1CyKRmg7L8mNScKk9FRc2SlESuI7N-Hby4nX-CcVmns + +## Благодарности + +Этот проект тестируется с помощью BrowserStack. +Мы выражаем благодарность [BrowserStack](https://www.browserstack.com) за поддержку нашего проекта. diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 05f9f17c..3ef92385 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -146,6 +146,7 @@ set(HEADERS ${HEADERS} ${CMAKE_CURRENT_LIST_DIR}/core/serialization/transfer.h ${CMAKE_CURRENT_LIST_DIR}/core/enums/apiEnums.h ${CMAKE_CURRENT_LIST_DIR}/../common/logger/logger.h + ${CMAKE_CURRENT_LIST_DIR}/utils/qmlUtils.h ) # Mozilla headres @@ -197,6 +198,7 @@ set(SOURCES ${SOURCES} ${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess.cpp ${CMAKE_CURRENT_LIST_DIR}/core/serialization/vmess_new.cpp ${CMAKE_CURRENT_LIST_DIR}/../common/logger/logger.cpp + ${CMAKE_CURRENT_LIST_DIR}/utils/qmlUtils.cpp ) # Mozilla sources diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index 4e25097d..aeed439b 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -404,6 +404,9 @@ void AmneziaApplication::initControllers() m_pageController.reset(new PageController(m_serversModel, m_settings)); m_engine->rootContext()->setContextProperty("PageController", m_pageController.get()); + m_focusController.reset(new FocusController(m_engine, this)); + m_engine->rootContext()->setContextProperty("FocusController", m_focusController.get()); + m_installController.reset(new InstallController(m_serversModel, m_containersModel, m_protocolsModel, m_clientManagementModel, m_apiServicesModel, m_settings)); m_engine->rootContext()->setContextProperty("InstallController", m_installController.get()); diff --git a/client/amnezia_application.h b/client/amnezia_application.h index 64566216..cfeac0d1 100644 --- a/client/amnezia_application.h +++ b/client/amnezia_application.h @@ -19,6 +19,7 @@ #include "ui/controllers/exportController.h" #include "ui/controllers/importController.h" #include "ui/controllers/installController.h" +#include "ui/controllers/focusController.h" #include "ui/controllers/pageController.h" #include "ui/controllers/settingsController.h" #include "ui/controllers/sitesController.h" @@ -124,6 +125,7 @@ private: #endif QScopedPointer m_connectionController; + QScopedPointer m_focusController; QScopedPointer m_pageController; QScopedPointer m_installController; QScopedPointer m_importController; diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml index 085ff5a0..314df0de 100644 --- a/client/android/AndroidManifest.xml +++ b/client/android/AndroidManifest.xml @@ -97,6 +97,13 @@ android:exported="false" android:theme="@style/Translucent" /> + + - - - - \ No newline at end of file diff --git a/client/android/res/mipmap-hdpi/ic_banner.png b/client/android/res/mipmap-hdpi/ic_banner.png new file mode 100644 index 00000000..a444777f Binary files /dev/null and b/client/android/res/mipmap-hdpi/ic_banner.png differ diff --git a/client/android/res/mipmap-mdpi/ic_banner.png b/client/android/res/mipmap-mdpi/ic_banner.png new file mode 100644 index 00000000..b9ad1db7 Binary files /dev/null and b/client/android/res/mipmap-mdpi/ic_banner.png differ diff --git a/client/android/res/mipmap-xhdpi/ic_banner_foreground.png b/client/android/res/mipmap-xhdpi/ic_banner_foreground.png deleted file mode 100644 index 1c21902e..00000000 Binary files a/client/android/res/mipmap-xhdpi/ic_banner_foreground.png and /dev/null differ diff --git a/client/android/res/values-ru/strings.xml b/client/android/res/values-ru/strings.xml index 8bdabfc0..5e35bba5 100644 --- a/client/android/res/values-ru/strings.xml +++ b/client/android/res/values-ru/strings.xml @@ -23,4 +23,6 @@ Настройки уведомлений Для показа уведомлений необходимо включить уведомления в системных настройках Открыть настройки уведомлений + + Пожалуйста, установите приложение для просмотра файлов \ No newline at end of file diff --git a/client/android/res/values/ic_banner_background.xml b/client/android/res/values/ic_banner_background.xml deleted file mode 100644 index fa6f91c7..00000000 --- a/client/android/res/values/ic_banner_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #1E1E1F - \ No newline at end of file diff --git a/client/android/res/values/strings.xml b/client/android/res/values/strings.xml index 5251403b..bf8d76d1 100644 --- a/client/android/res/values/strings.xml +++ b/client/android/res/values/strings.xml @@ -23,4 +23,6 @@ Notification settings To show notifications, you must enable notifications in the system settings Open notification settings + + Please install a file management utility to browse files \ No newline at end of file diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index 30bf09a3..ad958204 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -3,6 +3,7 @@ package org.amnezia.vpn import android.Manifest import android.annotation.SuppressLint import android.app.AlertDialog +import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Intent import android.content.Intent.EXTRA_MIME_TYPES @@ -10,6 +11,7 @@ import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY import android.content.ServiceConnection import android.content.pm.PackageManager import android.graphics.Bitmap +import android.net.Uri import android.net.VpnService import android.os.Build import android.os.Bundle @@ -18,7 +20,13 @@ import android.os.IBinder import android.os.Looper import android.os.Message import android.os.Messenger +import android.os.ParcelFileDescriptor +import android.os.SystemClock +import android.provider.OpenableColumns import android.provider.Settings +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup import android.view.WindowManager.LayoutParams import android.webkit.MimeTypeMap import android.widget.Toast @@ -27,6 +35,7 @@ import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import java.io.IOException import kotlin.LazyThreadSafetyMode.NONE +import kotlin.coroutines.CoroutineContext import kotlin.text.RegexOption.IGNORE_CASE import AppListProvider import kotlinx.coroutines.CompletableDeferred @@ -67,6 +76,7 @@ class AmneziaActivity : QtActivity() { private var isServiceConnected = false private var isInBoundState = false private lateinit var vpnServiceMessenger: IpcMessenger + private var pfd: ParcelFileDescriptor? = null private val actionResultHandlers = mutableMapOf() private val permissionRequestHandlers = mutableMapOf() @@ -487,21 +497,25 @@ class AmneziaActivity : QtActivity() { type = "text/*" putExtra(Intent.EXTRA_TITLE, fileName) }.also { - startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler( - onSuccess = { - it?.data?.let { uri -> - Log.v(TAG, "Save file to $uri") - try { - contentResolver.openOutputStream(uri)?.use { os -> - os.bufferedWriter().use { it.write(data) } + try { + startActivityForResult(it, CREATE_FILE_ACTION_CODE, ActivityResultHandler( + onSuccess = { + it?.data?.let { uri -> + Log.v(TAG, "Save file to $uri") + try { + contentResolver.openOutputStream(uri)?.use { os -> + os.bufferedWriter().use { it.write(data) } + } + } catch (e: IOException) { + Log.e(TAG, "Failed to save file $uri: $e") + // todo: send error to Qt } - } catch (e: IOException) { - Log.e(TAG, "Failed to save file $uri: $e") - // todo: send error to Qt } } - } - )) + )) + } catch (_: ActivityNotFoundException) { + Toast.makeText(this@AmneziaActivity, "Unsupported", Toast.LENGTH_LONG).show() + } } } } @@ -510,35 +524,46 @@ class AmneziaActivity : QtActivity() { fun openFile(filter: String?) { Log.v(TAG, "Open file with filter: $filter") mainScope.launch { - val mimeTypes = if (!filter.isNullOrEmpty()) { - val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE) - val mime = MimeTypeMap.getSingleton() - extensionRegex.findAll(filter).map { - it.groups[1]?.value?.let { mime.getMimeTypeFromExtension(it) } ?: "*/*" - }.toSet() - } else emptySet() + val intent = if (!isOnTv()) { + val mimeTypes = if (!filter.isNullOrEmpty()) { + val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE) + val mime = MimeTypeMap.getSingleton() + extensionRegex.findAll(filter).map { + it.groups[1]?.value?.let { mime.getMimeTypeFromExtension(it) } ?: "*/*" + }.toSet() + } else emptySet() - Intent(Intent.ACTION_OPEN_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - Log.v(TAG, "File mimyType filter: $mimeTypes") - if ("*/*" in mimeTypes) { - type = "*/*" - } else { - when (mimeTypes.size) { - 1 -> type = mimeTypes.first() + Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + Log.v(TAG, "File mimyType filter: $mimeTypes") + if ("*/*" in mimeTypes) { + type = "*/*" + } else { + when (mimeTypes.size) { + 1 -> type = mimeTypes.first() - in 2..Int.MAX_VALUE -> { - type = "*/*" - putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray()) + in 2..Int.MAX_VALUE -> { + type = "*/*" + putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray()) + } + + else -> type = "*/*" } - - else -> type = "*/*" } } - }.also { - startActivityForResult(it, OPEN_FILE_ACTION_CODE, ActivityResultHandler( + } else { + Intent(this@AmneziaActivity, TvFilePicker::class.java) + } + + try { + startActivityForResult(intent, OPEN_FILE_ACTION_CODE, ActivityResultHandler( onAny = { - val uri = it?.data?.toString() ?: "" + if (isOnTv() && it?.hasExtra("activityNotFound") == true) { + showNoFileBrowserAlertDialog() + } + val uri = it?.data?.apply { + grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION) + }?.toString() ?: "" Log.v(TAG, "Open file: $uri") mainScope.launch { qtInitialized.await() @@ -546,10 +571,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") @SuppressLint("UnsupportedChromeOsCameraSystemFeature") fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA) @@ -694,9 +777,60 @@ 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 + ) + /** * Utils methods */ + private fun blockingCall( + context: CoroutineContext = Dispatchers.Main.immediate, + block: suspend () -> T + ) = runBlocking { + mainScope.async(context) { block() }.await() + } + companion object { private fun actionCodeToString(actionCode: Int): String = when (actionCode) { diff --git a/client/android/src/org/amnezia/vpn/TvFilePicker.kt b/client/android/src/org/amnezia/vpn/TvFilePicker.kt new file mode 100644 index 00000000..1ac275eb --- /dev/null +++ b/client/android/src/org/amnezia/vpn/TvFilePicker.kt @@ -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() + } + } +} diff --git a/client/configurators/wireguard_configurator.cpp b/client/configurators/wireguard_configurator.cpp index 3f96e74c..1bca973d 100644 --- a/client/configurators/wireguard_configurator.cpp +++ b/client/configurators/wireguard_configurator.cpp @@ -120,7 +120,7 @@ WireguardConfigurator::ConnectionData WireguardConfigurator::prepareWireguardCon } } - QString subnetIp = containerConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress); + QString subnetIp = containerConfig.value(m_protocolName).toObject().value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress); { QStringList l = subnetIp.split(".", Qt::SkipEmptyParts); if (l.isEmpty()) { diff --git a/client/configurators/xray_configurator.cpp b/client/configurators/xray_configurator.cpp index 786da47c..514aa821 100644 --- a/client/configurators/xray_configurator.cpp +++ b/client/configurators/xray_configurator.cpp @@ -3,38 +3,169 @@ #include #include #include +#include +#include "logger.h" #include "containers/containers_defs.h" #include "core/controllers/serverController.h" #include "core/scripts_registry.h" +namespace { +Logger logger("XrayConfigurator"); +} + XrayConfigurator::XrayConfigurator(std::shared_ptr settings, const QSharedPointer &serverController, QObject *parent) : ConfiguratorBase(settings, serverController, parent) { } -QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, - ErrorCode &errorCode) +QString XrayConfigurator::prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, + const QJsonObject &containerConfig, ErrorCode &errorCode) { - QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), - m_serverController->genVarsForScript(credentials, container, containerConfig)); - - QString xrayPublicKey = - m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode); - xrayPublicKey.replace("\n", ""); - - QString xrayUuid = m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, errorCode); - xrayUuid.replace("\n", ""); - - QString xrayShortId = - m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode); - xrayShortId.replace("\n", ""); - + // Generate new UUID for client + QString clientId = QUuid::createUuid().toString(QUuid::WithoutBraces); + + // Get current server config + QString currentConfig = m_serverController->getTextFileFromContainer( + container, credentials, amnezia::protocols::xray::serverConfigPath, errorCode); + if (errorCode != ErrorCode::NoError) { + logger.error() << "Failed to get server config file"; return ""; } - config.replace("$XRAY_CLIENT_ID", xrayUuid); + // Parse current config as JSON + QJsonDocument doc = QJsonDocument::fromJson(currentConfig.toUtf8()); + if (doc.isNull() || !doc.isObject()) { + logger.error() << "Failed to parse server config JSON"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonObject serverConfig = doc.object(); + + // Validate server config structure + if (!serverConfig.contains("inbounds")) { + logger.error() << "Server config missing 'inbounds' field"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonArray inbounds = serverConfig["inbounds"].toArray(); + if (inbounds.isEmpty()) { + logger.error() << "Server config has empty 'inbounds' array"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonObject inbound = inbounds[0].toObject(); + if (!inbound.contains("settings")) { + logger.error() << "Inbound missing 'settings' field"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonObject settings = inbound["settings"].toObject(); + if (!settings.contains("clients")) { + logger.error() << "Settings missing 'clients' field"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QJsonArray clients = settings["clients"].toArray(); + + // Create configuration for new client + QJsonObject clientConfig { + {"id", clientId}, + {"flow", "xtls-rprx-vision"} + }; + + clients.append(clientConfig); + + // Update config + settings["clients"] = clients; + inbound["settings"] = settings; + inbounds[0] = inbound; + serverConfig["inbounds"] = inbounds; + + // Save updated config to server + QString updatedConfig = QJsonDocument(serverConfig).toJson(); + errorCode = m_serverController->uploadTextFileToContainer( + container, + credentials, + updatedConfig, + amnezia::protocols::xray::serverConfigPath, + libssh::ScpOverwriteMode::ScpOverwriteExisting + ); + if (errorCode != ErrorCode::NoError) { + logger.error() << "Failed to upload updated config"; + return ""; + } + + // Restart container + QString restartScript = QString("sudo docker restart $CONTAINER_NAME"); + errorCode = m_serverController->runScript( + credentials, + m_serverController->replaceVars(restartScript, m_serverController->genVarsForScript(credentials, container)) + ); + + if (errorCode != ErrorCode::NoError) { + logger.error() << "Failed to restart container"; + return ""; + } + + return clientId; +} + +QString XrayConfigurator::createConfig(const ServerCredentials &credentials, DockerContainer container, + const QJsonObject &containerConfig, ErrorCode &errorCode) +{ + // Get client ID from prepareServerConfig + QString xrayClientId = prepareServerConfig(credentials, container, containerConfig, errorCode); + if (errorCode != ErrorCode::NoError || xrayClientId.isEmpty()) { + logger.error() << "Failed to prepare server config"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QString config = m_serverController->replaceVars(amnezia::scriptData(ProtocolScriptType::xray_template, container), + m_serverController->genVarsForScript(credentials, container, containerConfig)); + + if (config.isEmpty()) { + logger.error() << "Failed to get config template"; + errorCode = ErrorCode::InternalError; + return ""; + } + + QString xrayPublicKey = + m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::PublicKeyPath, errorCode); + if (errorCode != ErrorCode::NoError || xrayPublicKey.isEmpty()) { + logger.error() << "Failed to get public key"; + errorCode = ErrorCode::InternalError; + return ""; + } + xrayPublicKey.replace("\n", ""); + + QString xrayShortId = + m_serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::shortidPath, errorCode); + if (errorCode != ErrorCode::NoError || xrayShortId.isEmpty()) { + logger.error() << "Failed to get short ID"; + errorCode = ErrorCode::InternalError; + return ""; + } + xrayShortId.replace("\n", ""); + + // Validate all required variables are present + if (!config.contains("$XRAY_CLIENT_ID") || !config.contains("$XRAY_PUBLIC_KEY") || !config.contains("$XRAY_SHORT_ID")) { + logger.error() << "Config template missing required variables:" + << "XRAY_CLIENT_ID:" << !config.contains("$XRAY_CLIENT_ID") + << "XRAY_PUBLIC_KEY:" << !config.contains("$XRAY_PUBLIC_KEY") + << "XRAY_SHORT_ID:" << !config.contains("$XRAY_SHORT_ID"); + errorCode = ErrorCode::InternalError; + return ""; + } + + config.replace("$XRAY_CLIENT_ID", xrayClientId); config.replace("$XRAY_PUBLIC_KEY", xrayPublicKey); config.replace("$XRAY_SHORT_ID", xrayShortId); diff --git a/client/configurators/xray_configurator.h b/client/configurators/xray_configurator.h index 2acfdf71..8ed4e775 100644 --- a/client/configurators/xray_configurator.h +++ b/client/configurators/xray_configurator.h @@ -14,6 +14,10 @@ public: QString createConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, ErrorCode &errorCode); + +private: + QString prepareServerConfig(const ServerCredentials &credentials, DockerContainer container, const QJsonObject &containerConfig, + ErrorCode &errorCode); }; #endif // XRAY_CONFIGURATOR_H diff --git a/client/core/controllers/apiController.cpp b/client/core/controllers/apiController.cpp index 75a3f93c..6562632a 100644 --- a/client/core/controllers/apiController.cpp +++ b/client/core/controllers/apiController.cpp @@ -50,6 +50,8 @@ namespace constexpr char authData[] = "auth_data"; } + const int requestTimeoutMsecs = 12 * 1000; // 12 secs + ErrorCode checkErrors(const QList &sslErrors, QNetworkReply *reply) { if (!sslErrors.empty()) { @@ -177,7 +179,7 @@ void ApiController::fillServerConfig(const QString &protocol, const ApiControlle QStringList ApiController::getProxyUrls() { QNetworkRequest request; - request.setTransferTimeout(7000); + request.setTransferTimeout(requestTimeoutMsecs); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QEventLoop wait; @@ -280,7 +282,7 @@ void ApiController::updateServerConfigFromApi(const QString &installationUuid, c if (serverConfig.value(config_key::configVersion).toInt()) { QNetworkRequest request; - request.setTransferTimeout(7000); + request.setTransferTimeout(requestTimeoutMsecs); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Authorization", "Api-Key " + serverConfig.value(configKey::accessToken).toString().toUtf8()); QString endpoint = serverConfig.value(configKey::apiEdnpoint).toString(); @@ -336,7 +338,7 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) #endif QNetworkRequest request; - request.setTransferTimeout(7000); + request.setTransferTimeout(requestTimeoutMsecs); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setUrl(QString("%1v1/services").arg(m_gatewayEndpoint)); @@ -377,6 +379,13 @@ ErrorCode ApiController::getServicesList(QByteArray &responseBody) auto errorCode = checkErrors(sslErrors, reply); reply->deleteLater(); + + if (errorCode == ErrorCode::NoError) { + if (!responseBody.contains("services")) { + return ErrorCode::ApiServicesMissingError; + } + } + return errorCode; } @@ -390,7 +399,7 @@ ErrorCode ApiController::getConfigForService(const QString &installationUuid, co #endif QNetworkRequest request; - request.setTransferTimeout(7000); + request.setTransferTimeout(requestTimeoutMsecs); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setUrl(QString("%1v1/config").arg(m_gatewayEndpoint)); diff --git a/client/core/controllers/serverController.cpp b/client/core/controllers/serverController.cpp index b6795a01..7219ff7d 100644 --- a/client/core/controllers/serverController.cpp +++ b/client/core/controllers/serverController.cpp @@ -346,7 +346,9 @@ bool ServerController::isReinstallContainerRequired(DockerContainer container, c } if (container == DockerContainer::Awg) { - if ((oldProtoConfig.value(config_key::port).toString(protocols::awg::defaultPort) + if ((oldProtoConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress) + != newProtoConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress)) + || (oldProtoConfig.value(config_key::port).toString(protocols::awg::defaultPort) != newProtoConfig.value(config_key::port).toString(protocols::awg::defaultPort)) || (oldProtoConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount) != newProtoConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount)) @@ -370,8 +372,10 @@ bool ServerController::isReinstallContainerRequired(DockerContainer container, c } if (container == DockerContainer::WireGuard) { - if (oldProtoConfig.value(config_key::port).toString(protocols::wireguard::defaultPort) - != newProtoConfig.value(config_key::port).toString(protocols::wireguard::defaultPort)) + if ((oldProtoConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress) + != newProtoConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress)) + || (oldProtoConfig.value(config_key::port).toString(protocols::wireguard::defaultPort) + != newProtoConfig.value(config_key::port).toString(protocols::wireguard::defaultPort))) return true; } @@ -607,6 +611,8 @@ ServerController::Vars ServerController::genVarsForScript(const ServerCredential vars.append({ { "$SFTP_PASSWORD", sftpConfig.value(config_key::password).toString() } }); // Amnezia wireguard vars + vars.append({ { "$AWG_SUBNET_IP", + amneziaWireguarConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress) } }); vars.append({ { "$AWG_SERVER_PORT", amneziaWireguarConfig.value(config_key::port).toString(protocols::awg::defaultPort) } }); vars.append({ { "$JUNK_PACKET_COUNT", amneziaWireguarConfig.value(config_key::junkPacketCount).toString() } }); diff --git a/client/core/defs.h b/client/core/defs.h index d00d347b..c0db2e12 100644 --- a/client/core/defs.h +++ b/client/core/defs.h @@ -109,6 +109,7 @@ namespace amnezia ApiConfigSslError = 1104, ApiMissingAgwPublicKey = 1105, ApiConfigDecryptionError = 1106, + ApiServicesMissingError = 1107, // QFile errors OpenError = 1200, diff --git a/client/core/errorstrings.cpp b/client/core/errorstrings.cpp index 49534606..70f433c6 100644 --- a/client/core/errorstrings.cpp +++ b/client/core/errorstrings.cpp @@ -63,7 +63,8 @@ QString errorString(ErrorCode code) { case (ErrorCode::ApiConfigTimeoutError): errorMessage = QObject::tr("Server response timeout on api request"); break; case (ErrorCode::ApiMissingAgwPublicKey): errorMessage = QObject::tr("Missing AGW public key"); break; case (ErrorCode::ApiConfigDecryptionError): errorMessage = QObject::tr("Failed to decrypt response payload"); break; - + case (ErrorCode::ApiServicesMissingError): errorMessage = QObject::tr("Missing list of available services"); break; + // QFile errors case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break; case(ErrorCode::ReadError): errorMessage = QObject::tr("QFile error: An error occurred when reading from the file"); break; diff --git a/client/core/serialization/vmess_new.cpp b/client/core/serialization/vmess_new.cpp index 6f3ec3e1..68d32203 100644 --- a/client/core/serialization/vmess_new.cpp +++ b/client/core/serialization/vmess_new.cpp @@ -104,7 +104,7 @@ QJsonObject Deserialize(const QString &vmessStr, QString *alias, QString *errMes server.users.first().security = "auto"; } - const static auto getQueryValue = [&query](const QString &key, const QString &defaultValue) { + const auto getQueryValue = [&query](const QString &key, const QString &defaultValue) { if (query.hasQueryItem(key)) return query.queryItemValue(key, QUrl::FullyDecoded); else diff --git a/client/daemon/daemon.cpp b/client/daemon/daemon.cpp index a234860b..081a7a90 100644 --- a/client/daemon/daemon.cpp +++ b/client/daemon/daemon.cpp @@ -114,12 +114,23 @@ bool Daemon::activate(const InterfaceConfig& config) { // Bring up the wireguard interface if not already done. if (!wgutils()->interfaceExists()) { + // Create the interface. if (!wgutils()->addInterface(config)) { logger.error() << "Interface creation failed."; return false; } } + // Bring the interface up. + if (supportIPUtils()) { + if (!iputils()->addInterfaceIPs(config)) { + return false; + } + if (!iputils()->setMTUAndUp(config)) { + return false; + } + } + // Configure routing for excluded addresses. for (const QString& i : config.m_excludedAddresses) { addExclusionRoute(IPAddress(i)); @@ -135,15 +146,6 @@ bool Daemon::activate(const InterfaceConfig& config) { return false; } - if (supportIPUtils()) { - if (!iputils()->addInterfaceIPs(config)) { - return false; - } - if (!iputils()->setMTUAndUp(config)) { - return false; - } - } - // set routing for (const IPAddress& ip : config.m_allowedIPAddressRanges) { if (!wgutils()->updateRoutePrefix(ip)) { diff --git a/client/daemon/daemon.h b/client/daemon/daemon.h index 3d418d70..757c9ff0 100644 --- a/client/daemon/daemon.h +++ b/client/daemon/daemon.h @@ -8,6 +8,8 @@ #include #include +#include "daemon/daemonerrors.h" +#include "daemonerrors.h" #include "dnsutils.h" #include "interfaceconfig.h" #include "iputils.h" @@ -51,7 +53,7 @@ class Daemon : public QObject { */ void activationFailure(); void disconnected(); - void backendFailure(); + void backendFailure(DaemonError reason = DaemonError::ERROR_FATAL); private: bool maybeUpdateResolvers(const InterfaceConfig& config); diff --git a/client/daemon/daemonerrors.h b/client/daemon/daemonerrors.h new file mode 100644 index 00000000..3d271244 --- /dev/null +++ b/client/daemon/daemonerrors.h @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#pragma once + +#include + +enum class DaemonError : uint8_t { + ERROR_NONE = 0u, + ERROR_FATAL = 1u, + ERROR_SPLIT_TUNNEL_INIT_FAILURE = 2u, + ERROR_SPLIT_TUNNEL_START_FAILURE = 3u, + ERROR_SPLIT_TUNNEL_EXCLUDE_FAILURE = 4u, + + DAEMON_ERROR_MAX = 5u, +}; diff --git a/client/daemon/daemonlocalserverconnection.cpp b/client/daemon/daemonlocalserverconnection.cpp index edbc4c9b..bc57c71f 100644 --- a/client/daemon/daemonlocalserverconnection.cpp +++ b/client/daemon/daemonlocalserverconnection.cpp @@ -159,9 +159,10 @@ void DaemonLocalServerConnection::disconnected() { write(obj); } -void DaemonLocalServerConnection::backendFailure() { +void DaemonLocalServerConnection::backendFailure(DaemonError err) { QJsonObject obj; obj.insert("type", "backendFailure"); + obj.insert("errorCode", static_cast(err)); write(obj); } diff --git a/client/daemon/daemonlocalserverconnection.h b/client/daemon/daemonlocalserverconnection.h index ec32df75..34170cb3 100644 --- a/client/daemon/daemonlocalserverconnection.h +++ b/client/daemon/daemonlocalserverconnection.h @@ -7,6 +7,8 @@ #include +#include "daemonerrors.h" + class QLocalSocket; class DaemonLocalServerConnection final : public QObject { @@ -23,7 +25,7 @@ class DaemonLocalServerConnection final : public QObject { void connected(const QString& pubkey); void disconnected(); - void backendFailure(); + void backendFailure(DaemonError err); void write(const QJsonObject& obj); diff --git a/client/daemon/wireguardutils.h b/client/daemon/wireguardutils.h index cdee40ef..b600a923 100644 --- a/client/daemon/wireguardutils.h +++ b/client/daemon/wireguardutils.h @@ -45,9 +45,11 @@ class WireguardUtils : public QObject { virtual bool updateRoutePrefix(const IPAddress& prefix) = 0; virtual bool deleteRoutePrefix(const IPAddress& prefix) = 0; - + virtual bool addExclusionRoute(const IPAddress& prefix) = 0; virtual bool deleteExclusionRoute(const IPAddress& prefix) = 0; + + virtual bool excludeLocalNetworks(const QList& addresses) = 0; }; #endif // WIREGUARDUTILS_H diff --git a/client/images/controls/external-link.svg b/client/images/controls/external-link.svg new file mode 100644 index 00000000..6a51c902 --- /dev/null +++ b/client/images/controls/external-link.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/mozilla/localsocketcontroller.cpp b/client/mozilla/localsocketcontroller.cpp index 5e9f0f97..1081bcae 100644 --- a/client/mozilla/localsocketcontroller.cpp +++ b/client/mozilla/localsocketcontroller.cpp @@ -1,9 +1,10 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#include "protocols/protocols_defs.h" #include "localsocketcontroller.h" +#include + #include #include #include @@ -17,6 +18,9 @@ #include "leakdetector.h" #include "logger.h" #include "models/server.h" +#include "daemon/daemonerrors.h" + +#include "protocols/protocols_defs.h" // How many times do we try to reconnect. constexpr int MAX_CONNECTION_RETRY = 10; @@ -451,8 +455,39 @@ void LocalSocketController::parseCommand(const QByteArray& command) { } if (type == "backendFailure") { - qCritical() << "backendFailure"; - return; + if (!obj.contains("errorCode")) { + // report a generic error if we dont know what it is. + logger.error() << "generic backend failure error"; + // REPORTERROR(ErrorHandler::ControllerError, "controller"); + return; + } + auto errorCode = static_cast(obj["errorCode"].toInt()); + if (errorCode >= (uint8_t)DaemonError::DAEMON_ERROR_MAX) { + // Also report a generic error if the code is invalid. + logger.error() << "invalid backend failure error code"; + // REPORTERROR(ErrorHandler::ControllerError, "controller"); + return; + } + switch (static_cast(errorCode)) { + case DaemonError::ERROR_NONE: + [[fallthrough]]; + case DaemonError::ERROR_FATAL: + logger.error() << "generic backend failure error (fatal or error none)"; + // REPORTERROR(ErrorHandler::ControllerError, "controller"); + break; + case DaemonError::ERROR_SPLIT_TUNNEL_INIT_FAILURE: + [[fallthrough]]; + case DaemonError::ERROR_SPLIT_TUNNEL_START_FAILURE: + [[fallthrough]]; + case DaemonError::ERROR_SPLIT_TUNNEL_EXCLUDE_FAILURE: + logger.error() << "split tunnel backend failure error"; + //REPORTERROR(ErrorHandler::SplitTunnelError, "controller"); + break; + case DaemonError::DAEMON_ERROR_MAX: + // We should not get here. + Q_ASSERT(false); + break; + } } if (type == "logs") { diff --git a/client/platforms/android/android_controller.cpp b/client/platforms/android/android_controller.cpp index 2790eb1b..d9195f87 100644 --- a/client/platforms/android/android_controller.cpp +++ b/client/platforms/android/android_controller.cpp @@ -163,9 +163,7 @@ QString AndroidController::openFile(const QString &filter) QString fileName; connect(this, &AndroidController::fileOpened, this, [&fileName, &wait](const QString &uri) { - qDebug() << "Android event: file opened; uri:" << uri; - fileName = QQmlFile::urlToLocalFileOrQrc(uri); - qDebug() << "Android opened filename:" << fileName; + fileName = uri; wait.quit(); }, static_cast(Qt::QueuedConnection | Qt::SingleShotConnection)); @@ -175,6 +173,25 @@ QString AndroidController::openFile(const QString &filter) return fileName; } +int AndroidController::getFd(const QString &fileName) +{ + return callActivityMethod("getFd", "(Ljava/lang/String;)I", + QJniObject::fromString(fileName).object()); +} + +void AndroidController::closeFd() +{ + callActivityMethod("closeFd", "()V"); +} + +QString AndroidController::getFileName(const QString &uri) +{ + auto fileName = callActivityMethod("getFileName", "(Ljava/lang/String;)Ljava/lang/String;", + QJniObject::fromString(uri).object()); + QJniEnvironment env; + return AndroidUtils::convertJString(env.jniEnv(), fileName.object()); +} + bool AndroidController::isCameraPresent() { return callActivityMethod("isCameraPresent", "()Z"); @@ -287,6 +304,11 @@ bool AndroidController::requestAuthentication() return result; } +void AndroidController::sendTouch(float x, float y) +{ + callActivityMethod("sendTouch", "(FF)V", x, y); +} + // Moving log processing to the Android side jclass AndroidController::log; jmethodID AndroidController::logDebug; diff --git a/client/platforms/android/android_controller.h b/client/platforms/android/android_controller.h index 759c9c3f..5707771e 100644 --- a/client/platforms/android/android_controller.h +++ b/client/platforms/android/android_controller.h @@ -34,6 +34,9 @@ public: void resetLastServer(int serverIndex); void saveFile(const QString &fileName, const QString &data); QString openFile(const QString &filter); + int getFd(const QString &fileName); + void closeFd(); + QString getFileName(const QString &uri); bool isCameraPresent(); bool isOnTv(); void startQrReaderActivity(); @@ -48,6 +51,7 @@ public: bool isNotificationPermissionGranted(); void requestNotificationPermission(); bool requestAuthentication(); + void sendTouch(float x, float y); static bool initLogging(); static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message); diff --git a/client/platforms/linux/daemon/linuxfirewall.cpp b/client/platforms/linux/daemon/linuxfirewall.cpp index 393c24f2..96194bc7 100644 --- a/client/platforms/linux/daemon/linuxfirewall.cpp +++ b/client/platforms/linux/daemon/linuxfirewall.cpp @@ -196,6 +196,8 @@ QStringList LinuxFirewall::getDNSRules(const QStringList& servers) result << QStringLiteral("-o amn0+ -d %1 -p tcp --dport 53 -j ACCEPT").arg(server); result << QStringLiteral("-o tun0+ -d %1 -p udp --dport 53 -j ACCEPT").arg(server); result << QStringLiteral("-o tun0+ -d %1 -p tcp --dport 53 -j ACCEPT").arg(server); + result << QStringLiteral("-o tun2+ -d %1 -p udp --dport 53 -j ACCEPT").arg(server); + result << QStringLiteral("-o tun2+ -d %1 -p tcp --dport 53 -j ACCEPT").arg(server); } return result; } @@ -277,6 +279,7 @@ void LinuxFirewall::install() installAnchor(Both, QStringLiteral("200.allowVPN"), { QStringLiteral("-o amn0+ -j ACCEPT"), QStringLiteral("-o tun0+ -j ACCEPT"), + QStringLiteral("-o tun2+ -j ACCEPT"), }); installAnchor(IPv4, QStringLiteral("120.blockNets"), {}); diff --git a/client/platforms/linux/daemon/wireguardutilslinux.cpp b/client/platforms/linux/daemon/wireguardutilslinux.cpp index 460a7fe1..1528d901 100644 --- a/client/platforms/linux/daemon/wireguardutilslinux.cpp +++ b/client/platforms/linux/daemon/wireguardutilslinux.cpp @@ -297,31 +297,6 @@ QList WireguardUtilsLinux::getPeerStatus() { return peerList; } - -void WireguardUtilsLinux::applyFirewallRules(FirewallParams& params) -{ - // double-check + ensure our firewall is installed and enabled - if (!LinuxFirewall::isInstalled()) LinuxFirewall::install(); - - // Note: rule precedence is handled inside IpTablesFirewall - LinuxFirewall::ensureRootAnchorPriority(); - - LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("000.allowLoopback"), true); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("100.blockAll"), params.blockAll); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("110.allowNets"), params.allowNets); - LinuxFirewall::updateAllowNets(params.allowAddrs); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("120.blockNets"), params.blockNets); - LinuxFirewall::updateBlockNets(params.blockAddrs); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("200.allowVPN"), true); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv6, QStringLiteral("250.blockIPv6"), true); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("290.allowDHCP"), true); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("300.allowLAN"), true); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("310.blockDNS"), true); - LinuxFirewall::updateDNSServers(params.dnsServers); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("320.allowDNS"), true); - LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("400.allowPIA"), true); -} - bool WireguardUtilsLinux::updateRoutePrefix(const IPAddress& prefix) { if (!m_rtmonitor) { return false; @@ -377,6 +352,26 @@ bool WireguardUtilsLinux::deleteExclusionRoute(const IPAddress& prefix) { return m_rtmonitor->deleteExclusionRoute(prefix); } +bool WireguardUtilsLinux::excludeLocalNetworks(const QList& routes) { + if (!m_rtmonitor) { + return false; + } + + // Explicitly discard LAN traffic that makes its way into the tunnel. This + // doesn't really exclude the LAN traffic, we just don't take any action to + // overrule the routes of other interfaces. + bool result = true; + for (const auto& prefix : routes) { + logger.error() << "Attempting to exclude:" << prefix.toString(); + if (!m_rtmonitor->insertRoute(prefix)) { + result = false; + } + } + + // TODO: A kill switch would be nice though :) + return result; +} + QString WireguardUtilsLinux::uapiCommand(const QString& command) { QLocalSocket socket; QTimer uapiTimeout; @@ -450,3 +445,27 @@ QString WireguardUtilsLinux::waitForTunnelName(const QString& filename) { return QString(); } + +void WireguardUtilsLinux::applyFirewallRules(FirewallParams& params) +{ + // double-check + ensure our firewall is installed and enabled + if (!LinuxFirewall::isInstalled()) LinuxFirewall::install(); + + // Note: rule precedence is handled inside IpTablesFirewall + LinuxFirewall::ensureRootAnchorPriority(); + + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("000.allowLoopback"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("100.blockAll"), params.blockAll); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("110.allowNets"), params.allowNets); + LinuxFirewall::updateAllowNets(params.allowAddrs); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("120.blockNets"), params.blockNets); + LinuxFirewall::updateBlockNets(params.blockAddrs); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("200.allowVPN"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv6, QStringLiteral("250.blockIPv6"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("290.allowDHCP"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("300.allowLAN"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("310.blockDNS"), true); + LinuxFirewall::updateDNSServers(params.dnsServers); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("320.allowDNS"), true); + LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("400.allowPIA"), true); +} diff --git a/client/platforms/linux/daemon/wireguardutilslinux.h b/client/platforms/linux/daemon/wireguardutilslinux.h index 9746ea4b..c324111d 100644 --- a/client/platforms/linux/daemon/wireguardutilslinux.h +++ b/client/platforms/linux/daemon/wireguardutilslinux.h @@ -37,6 +37,9 @@ public: bool addExclusionRoute(const IPAddress& prefix) override; bool deleteExclusionRoute(const IPAddress& prefix) override; + + bool excludeLocalNetworks(const QList& lanAddressRanges) override; + void applyFirewallRules(FirewallParams& params); signals: void backendFailure(); diff --git a/client/platforms/macos/daemon/macosroutemonitor.cpp b/client/platforms/macos/daemon/macosroutemonitor.cpp index 395f008a..bd991c01 100644 --- a/client/platforms/macos/daemon/macosroutemonitor.cpp +++ b/client/platforms/macos/daemon/macosroutemonitor.cpp @@ -358,8 +358,8 @@ void MacosRouteMonitor::rtmAppendAddr(struct rt_msghdr* rtm, size_t maxlen, } bool MacosRouteMonitor::rtmSendRoute(int action, const IPAddress& prefix, - unsigned int ifindex, - const void* gateway) { + unsigned int ifindex, const void* gateway, + int flags) { constexpr size_t rtm_max_size = sizeof(struct rt_msghdr) + sizeof(struct sockaddr_in6) * 2 + sizeof(struct sockaddr_storage); @@ -370,7 +370,7 @@ bool MacosRouteMonitor::rtmSendRoute(int action, const IPAddress& prefix, rtm->rtm_version = RTM_VERSION; rtm->rtm_type = action; rtm->rtm_index = ifindex; - rtm->rtm_flags = RTF_STATIC | RTF_UP; + rtm->rtm_flags = flags | RTF_STATIC | RTF_UP; rtm->rtm_addrs = 0; rtm->rtm_pid = 0; rtm->rtm_seq = m_rtseq++; @@ -490,7 +490,7 @@ bool MacosRouteMonitor::rtmFetchRoutes(int family) { return false; } -bool MacosRouteMonitor::insertRoute(const IPAddress& prefix) { +bool MacosRouteMonitor::insertRoute(const IPAddress& prefix, int flags) { struct sockaddr_dl datalink; memset(&datalink, 0, sizeof(datalink)); datalink.sdl_family = AF_LINK; @@ -502,11 +502,11 @@ bool MacosRouteMonitor::insertRoute(const IPAddress& prefix) { datalink.sdl_slen = 0; memcpy(&datalink.sdl_data, qPrintable(m_ifname), datalink.sdl_nlen); - return rtmSendRoute(RTM_ADD, prefix, m_ifindex, &datalink); + return rtmSendRoute(RTM_ADD, prefix, m_ifindex, &datalink, flags); } -bool MacosRouteMonitor::deleteRoute(const IPAddress& prefix) { - return rtmSendRoute(RTM_DELETE, prefix, m_ifindex, nullptr); +bool MacosRouteMonitor::deleteRoute(const IPAddress& prefix, int flags) { + return rtmSendRoute(RTM_DELETE, prefix, m_ifindex, nullptr, flags); } bool MacosRouteMonitor::addExclusionRoute(const IPAddress& prefix) { diff --git a/client/platforms/macos/daemon/macosroutemonitor.h b/client/platforms/macos/daemon/macosroutemonitor.h index b2483d76..78396690 100644 --- a/client/platforms/macos/daemon/macosroutemonitor.h +++ b/client/platforms/macos/daemon/macosroutemonitor.h @@ -24,8 +24,8 @@ class MacosRouteMonitor final : public QObject { MacosRouteMonitor(const QString& ifname, QObject* parent = nullptr); ~MacosRouteMonitor(); - bool insertRoute(const IPAddress& prefix); - bool deleteRoute(const IPAddress& prefix); + bool insertRoute(const IPAddress& prefix, int flags = 0); + bool deleteRoute(const IPAddress& prefix, int flags = 0); int interfaceFlags() { return m_ifflags; } bool addExclusionRoute(const IPAddress& prefix); @@ -37,7 +37,7 @@ class MacosRouteMonitor final : public QObject { void handleRtmUpdate(const struct rt_msghdr* msg, const QByteArray& payload); void handleIfaceInfo(const struct if_msghdr* msg, const QByteArray& payload); bool rtmSendRoute(int action, const IPAddress& prefix, unsigned int ifindex, - const void* gateway); + const void* gateway, int flags = 0); bool rtmFetchRoutes(int family); static void rtmAppendAddr(struct rt_msghdr* rtm, size_t maxlen, int rtaddr, const void* sa); diff --git a/client/platforms/macos/daemon/wireguardutilsmacos.cpp b/client/platforms/macos/daemon/wireguardutilsmacos.cpp index e2802ebc..eae22837 100644 --- a/client/platforms/macos/daemon/wireguardutilsmacos.cpp +++ b/client/platforms/macos/daemon/wireguardutilsmacos.cpp @@ -5,6 +5,7 @@ #include "wireguardutilsmacos.h" #include +#include #include #include @@ -130,7 +131,6 @@ bool WireguardUtilsMacos::addInterface(const InterfaceConfig& config) { } int err = uapiErrno(uapiCommand(message)); - if (err != 0) { logger.error() << "Interface configuration failed:" << strerror(err); } else { @@ -211,7 +211,6 @@ bool WireguardUtilsMacos::updatePeer(const InterfaceConfig& config) { logger.warning() << "Failed to create peer with no endpoints"; return false; } - out << config.m_serverPort << "\n"; out << "replace_allowed_ips=true\n"; @@ -323,10 +322,10 @@ bool WireguardUtilsMacos::deleteRoutePrefix(const IPAddress& prefix) { if (!m_rtmonitor) { return false; } - if (prefix.prefixLength() > 0) { - return m_rtmonitor->insertRoute(prefix); - } + if (prefix.prefixLength() > 0) { + return m_rtmonitor->deleteRoute(prefix); + } // Ensure that we do not replace the default route. if (prefix.type() == QAbstractSocket::IPv4Protocol) { return m_rtmonitor->deleteRoute(IPAddress("0.0.0.0/1")) && @@ -346,31 +345,6 @@ bool WireguardUtilsMacos::addExclusionRoute(const IPAddress& prefix) { return m_rtmonitor->addExclusionRoute(prefix); } -void WireguardUtilsMacos::applyFirewallRules(FirewallParams& params) -{ - // double-check + ensure our firewall is installed and enabled. This is necessary as - // other software may disable pfctl before re-enabling with their own rules (e.g other VPNs) - if (!MacOSFirewall::isInstalled()) MacOSFirewall::install(); - - MacOSFirewall::ensureRootAnchorPriority(); - MacOSFirewall::setAnchorEnabled(QStringLiteral("000.allowLoopback"), true); - MacOSFirewall::setAnchorEnabled(QStringLiteral("100.blockAll"), params.blockAll); - MacOSFirewall::setAnchorEnabled(QStringLiteral("110.allowNets"), params.allowNets); - MacOSFirewall::setAnchorTable(QStringLiteral("110.allowNets"), params.allowNets, - QStringLiteral("allownets"), params.allowAddrs); - - MacOSFirewall::setAnchorEnabled(QStringLiteral("120.blockNets"), params.blockNets); - MacOSFirewall::setAnchorTable(QStringLiteral("120.blockNets"), params.blockNets, - QStringLiteral("blocknets"), params.blockAddrs); - - MacOSFirewall::setAnchorEnabled(QStringLiteral("200.allowVPN"), true); - MacOSFirewall::setAnchorEnabled(QStringLiteral("250.blockIPv6"), true); - MacOSFirewall::setAnchorEnabled(QStringLiteral("290.allowDHCP"), true); - MacOSFirewall::setAnchorEnabled(QStringLiteral("300.allowLAN"), true); - MacOSFirewall::setAnchorEnabled(QStringLiteral("310.blockDNS"), true); - MacOSFirewall::setAnchorTable(QStringLiteral("310.blockDNS"), true, QStringLiteral("dnsaddr"), params.dnsServers); -} - bool WireguardUtilsMacos::deleteExclusionRoute(const IPAddress& prefix) { if (!m_rtmonitor) { return false; @@ -378,6 +352,26 @@ bool WireguardUtilsMacos::deleteExclusionRoute(const IPAddress& prefix) { return m_rtmonitor->deleteExclusionRoute(prefix); } +bool WireguardUtilsMacos::excludeLocalNetworks(const QList& routes) { + if (!m_rtmonitor) { + return false; + } + + // Explicitly discard LAN traffic that makes its way into the tunnel. This + // doesn't really exclude the LAN traffic, we just don't take any action to + // overrule the routes of other interfaces. + bool result = true; + for (const auto& prefix : routes) { + logger.error() << "Attempting to exclude:" << prefix.toString(); + if (!m_rtmonitor->insertRoute(prefix, RTF_IFSCOPE | RTF_REJECT)) { + result = false; + } + } + + // TODO: A kill switch would be nice though :) + return result; +} + QString WireguardUtilsMacos::uapiCommand(const QString& command) { QLocalSocket socket; QTimer uapiTimeout; @@ -454,3 +448,28 @@ QString WireguardUtilsMacos::waitForTunnelName(const QString& filename) { return QString(); } + +void WireguardUtilsMacos::applyFirewallRules(FirewallParams& params) +{ + // double-check + ensure our firewall is installed and enabled. This is necessary as + // other software may disable pfctl before re-enabling with their own rules (e.g other VPNs) + if (!MacOSFirewall::isInstalled()) MacOSFirewall::install(); + + MacOSFirewall::ensureRootAnchorPriority(); + MacOSFirewall::setAnchorEnabled(QStringLiteral("000.allowLoopback"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("100.blockAll"), params.blockAll); + MacOSFirewall::setAnchorEnabled(QStringLiteral("110.allowNets"), params.allowNets); + MacOSFirewall::setAnchorTable(QStringLiteral("110.allowNets"), params.allowNets, + QStringLiteral("allownets"), params.allowAddrs); + + MacOSFirewall::setAnchorEnabled(QStringLiteral("120.blockNets"), params.blockNets); + MacOSFirewall::setAnchorTable(QStringLiteral("120.blockNets"), params.blockNets, + QStringLiteral("blocknets"), params.blockAddrs); + + MacOSFirewall::setAnchorEnabled(QStringLiteral("200.allowVPN"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("250.blockIPv6"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("290.allowDHCP"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("300.allowLAN"), true); + MacOSFirewall::setAnchorEnabled(QStringLiteral("310.blockDNS"), true); + MacOSFirewall::setAnchorTable(QStringLiteral("310.blockDNS"), true, QStringLiteral("dnsaddr"), params.dnsServers); +} diff --git a/client/platforms/macos/daemon/wireguardutilsmacos.h b/client/platforms/macos/daemon/wireguardutilsmacos.h index 243f4b64..3f0d3391 100644 --- a/client/platforms/macos/daemon/wireguardutilsmacos.h +++ b/client/platforms/macos/daemon/wireguardutilsmacos.h @@ -35,6 +35,9 @@ class WireguardUtilsMacos final : public WireguardUtils { bool addExclusionRoute(const IPAddress& prefix) override; bool deleteExclusionRoute(const IPAddress& prefix) override; + + bool excludeLocalNetworks(const QList& lanAddressRanges) override; + void applyFirewallRules(FirewallParams& params); signals: diff --git a/client/platforms/windows/daemon/windowsdaemon.cpp b/client/platforms/windows/daemon/windowsdaemon.cpp index 00435f0b..620e0a1c 100644 --- a/client/platforms/windows/daemon/windowsdaemon.cpp +++ b/client/platforms/windows/daemon/windowsdaemon.cpp @@ -5,6 +5,7 @@ #include "windowsdaemon.h" #include +#include #include #include @@ -15,28 +16,34 @@ #include #include +#include "daemon/daemonerrors.h" #include "dnsutilswindows.h" #include "leakdetector.h" #include "logger.h" -#include "core/networkUtilities.h" +#include "platforms/windows/daemon/windowsfirewall.h" +#include "platforms/windows/daemon/windowssplittunnel.h" #include "platforms/windows/windowscommons.h" -#include "platforms/windows/windowsservicemanager.h" #include "windowsfirewall.h" +#include "core/networkUtilities.h" + namespace { Logger logger("WindowsDaemon"); } -WindowsDaemon::WindowsDaemon() : Daemon(nullptr), m_splitTunnelManager(this) { +WindowsDaemon::WindowsDaemon() : Daemon(nullptr) { MZ_COUNT_CTOR(WindowsDaemon); + m_firewallManager = WindowsFirewall::create(this); + Q_ASSERT(m_firewallManager != nullptr); - m_wgutils = new WireguardUtilsWindows(this); + m_wgutils = WireguardUtilsWindows::create(m_firewallManager, this); m_dnsutils = new DnsUtilsWindows(this); + m_splitTunnelManager = WindowsSplitTunnel::create(m_firewallManager); - connect(m_wgutils, &WireguardUtilsWindows::backendFailure, this, + connect(m_wgutils.get(), &WireguardUtilsWindows::backendFailure, this, &WindowsDaemon::monitorBackendFailure); connect(this, &WindowsDaemon::activationFailure, - []() { WindowsFirewall::instance()->disableKillSwitch(); }); + [this]() { m_firewallManager->disableKillSwitch(); }); } WindowsDaemon::~WindowsDaemon() { @@ -57,28 +64,42 @@ void WindowsDaemon::prepareActivation(const InterfaceConfig& config, int inetAda void WindowsDaemon::activateSplitTunnel(const InterfaceConfig& config, int vpnAdapterIndex) { if (config.m_vpnDisabledApps.length() > 0) { - m_splitTunnelManager.start(m_inetAdapterIndex, vpnAdapterIndex); - m_splitTunnelManager.setRules(config.m_vpnDisabledApps); + m_splitTunnelManager->start(m_inetAdapterIndex, vpnAdapterIndex); + m_splitTunnelManager->excludeApps(config.m_vpnDisabledApps); } else { - m_splitTunnelManager.stop(); + m_splitTunnelManager->stop(); } } bool WindowsDaemon::run(Op op, const InterfaceConfig& config) { - if (op == Down) { - m_splitTunnelManager.stop(); + if (!m_splitTunnelManager) { + if (config.m_vpnDisabledApps.length() > 0) { + // The Client has sent us a list of disabled apps, but we failed + // to init the the split tunnel driver. + // So let the client know this was not possible + emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_INIT_FAILURE); + } return true; } - if (op == Up) { - logger.debug() << "Tunnel UP, Starting SplitTunneling"; - if (!WindowsSplitTunnel::isInstalled()) { - logger.warning() << "Split Tunnel Driver not Installed yet, fixing this."; - WindowsSplitTunnel::installDriver(); - } + if (op == Down) { + m_splitTunnelManager->stop(); + return true; } - - activateSplitTunnel(config); + if (config.m_vpnDisabledApps.length() > 0) { + if (!m_splitTunnelManager->start(m_inetAdapterIndex)) { + emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_START_FAILURE); + }; + if (!m_splitTunnelManager->excludeApps(config.m_vpnDisabledApps)) { + emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_EXCLUDE_FAILURE); + }; + // Now the driver should be running (State == 4) + if (!m_splitTunnelManager->isRunning()) { + emit backendFailure(DaemonError::ERROR_SPLIT_TUNNEL_START_FAILURE); + } + return true; + } + m_splitTunnelManager->stop(); return true; } diff --git a/client/platforms/windows/daemon/windowsdaemon.h b/client/platforms/windows/daemon/windowsdaemon.h index 7e38c41e..b17dc811 100644 --- a/client/platforms/windows/daemon/windowsdaemon.h +++ b/client/platforms/windows/daemon/windowsdaemon.h @@ -5,8 +5,11 @@ #ifndef WINDOWSDAEMON_H #define WINDOWSDAEMON_H +#include + #include "daemon/daemon.h" #include "dnsutilswindows.h" +#include "windowsfirewall.h" #include "windowssplittunnel.h" #include "windowstunnelservice.h" #include "wireguardutilswindows.h" @@ -25,7 +28,7 @@ class WindowsDaemon final : public Daemon { protected: bool run(Op op, const InterfaceConfig& config) override; - WireguardUtils* wgutils() const override { return m_wgutils; } + WireguardUtils* wgutils() const override { return m_wgutils.get(); } DnsUtils* dnsutils() override { return m_dnsutils; } private: @@ -39,9 +42,10 @@ class WindowsDaemon final : public Daemon { int m_inetAdapterIndex = -1; - WireguardUtilsWindows* m_wgutils = nullptr; + std::unique_ptr m_wgutils; DnsUtilsWindows* m_dnsutils = nullptr; - WindowsSplitTunnel m_splitTunnelManager; + std::unique_ptr m_splitTunnelManager; + QPointer m_firewallManager; }; #endif // WINDOWSDAEMON_H diff --git a/client/platforms/windows/daemon/windowsfirewall.cpp b/client/platforms/windows/daemon/windowsfirewall.cpp index 3d45f228..03525387 100644 --- a/client/platforms/windows/daemon/windowsfirewall.cpp +++ b/client/platforms/windows/daemon/windowsfirewall.cpp @@ -9,11 +9,12 @@ #include #include #include -//#include -#include - +#include +#include #include #include +#include +#include "winsock.h" #include #include @@ -27,7 +28,6 @@ #include "leakdetector.h" #include "logger.h" #include "platforms/windows/windowsutils.h" -#include "winsock.h" #define IPV6_ADDRESS_SIZE 16 @@ -49,18 +49,13 @@ constexpr uint8_t HIGH_WEIGHT = 13; constexpr uint8_t MAX_WEIGHT = 15; } // namespace -WindowsFirewall* WindowsFirewall::instance() { - if (s_instance == nullptr) { - s_instance = new WindowsFirewall(qApp); +WindowsFirewall* WindowsFirewall::create(QObject* parent) { + if (s_instance != nullptr) { + // Only one instance of the firewall is allowed +// Q_ASSERT(false); + return s_instance; } - return s_instance; -} - -WindowsFirewall::WindowsFirewall(QObject* parent) : QObject(parent) { - MZ_COUNT_CTOR(WindowsFirewall); - Q_ASSERT(s_instance == nullptr); - - HANDLE engineHandle = NULL; + HANDLE engineHandle = nullptr; DWORD result = ERROR_SUCCESS; // Use dynamic sessions for efficiency and safety: // -> Filtering policy objects are deleted even when the application crashes/ @@ -71,15 +66,24 @@ WindowsFirewall::WindowsFirewall(QObject* parent) : QObject(parent) { logger.debug() << "Opening the filter engine."; - result = - FwpmEngineOpen0(NULL, RPC_C_AUTHN_WINNT, NULL, &session, &engineHandle); + result = FwpmEngineOpen0(nullptr, RPC_C_AUTHN_WINNT, nullptr, &session, + &engineHandle); if (result != ERROR_SUCCESS) { WindowsUtils::windowsLog("FwpmEngineOpen0 failed"); - return; + return nullptr; } logger.debug() << "Filter engine opened successfully."; - m_sessionHandle = engineHandle; + if (!initSublayer()) { + return nullptr; + } + s_instance = new WindowsFirewall(engineHandle, parent); + return s_instance; +} + +WindowsFirewall::WindowsFirewall(HANDLE session, QObject* parent) + : QObject(parent), m_sessionHandle(session) { + MZ_COUNT_CTOR(WindowsFirewall); } WindowsFirewall::~WindowsFirewall() { @@ -89,15 +93,8 @@ WindowsFirewall::~WindowsFirewall() { } } -bool WindowsFirewall::init() { - if (m_init) { - logger.warning() << "Alread initialised FW_WFP layer"; - return true; - } - if (m_sessionHandle == INVALID_HANDLE_VALUE) { - logger.error() << "Cant Init Sublayer with invalid wfp handle"; - return false; - } +// static +bool WindowsFirewall::initSublayer() { // If we were not able to aquire a handle, this will fail anyway. // We need to open up another handle because of wfp rules: // If a wfp resource was created with SESSION_DYNAMIC, @@ -157,11 +154,10 @@ bool WindowsFirewall::init() { return false; } logger.debug() << "Initialised Sublayer"; - m_init = true; return true; } -bool WindowsFirewall::enableKillSwitch(int vpnAdapterIndex) { +bool WindowsFirewall::enableInterface(int vpnAdapterIndex) { // Checks if the FW_Rule was enabled succesfully, // disables the whole killswitch and returns false if not. #define FW_OK(rule) \ @@ -184,7 +180,7 @@ bool WindowsFirewall::enableKillSwitch(int vpnAdapterIndex) { } \ } - logger.info() << "Enabling Killswitch Using Adapter:" << vpnAdapterIndex; + logger.info() << "Enabling firewall Using Adapter:" << vpnAdapterIndex; FW_OK(allowTrafficOfAdapter(vpnAdapterIndex, MED_WEIGHT, "Allow usage of VPN Adapter")); FW_OK(allowDHCPTraffic(MED_WEIGHT, "Allow DHCP Traffic")); @@ -200,6 +196,36 @@ bool WindowsFirewall::enableKillSwitch(int vpnAdapterIndex) { #undef FW_OK } +// Allow unprotected traffic sent to the following local address ranges. +bool WindowsFirewall::enableLanBypass(const QList& ranges) { + // Start the firewall transaction + auto result = FwpmTransactionBegin(m_sessionHandle, NULL); + if (result != ERROR_SUCCESS) { + disableKillSwitch(); + return false; + } + auto cleanup = qScopeGuard([&] { + FwpmTransactionAbort0(m_sessionHandle); + disableKillSwitch(); + }); + + // Blocking unprotected traffic + for (const IPAddress& prefix : ranges) { + if (!allowTrafficTo(prefix, LOW_WEIGHT + 1, "Allow LAN bypass traffic")) { + return false; + } + } + + result = FwpmTransactionCommit0(m_sessionHandle); + if (result != ERROR_SUCCESS) { + logger.error() << "FwpmTransactionCommit0 failed with error:" << result; + return false; + } + + cleanup.dismiss(); + return true; +} + bool WindowsFirewall::enablePeerTraffic(const InterfaceConfig& config) { // Start the firewall transaction auto result = FwpmTransactionBegin(m_sessionHandle, NULL); @@ -238,10 +264,10 @@ bool WindowsFirewall::enablePeerTraffic(const InterfaceConfig& config) { if (!config.m_excludedAddresses.empty()) { for (const QString& i : config.m_excludedAddresses) { - logger.debug() << "range: " << i; + logger.debug() << "excludedAddresses range: " << i; - if (!allowTrafficToRange(i, HIGH_WEIGHT, - "Allow Ecxlude route", config.m_serverPublicKey)) { + if (!allowTrafficTo(i, HIGH_WEIGHT, + "Allow Ecxlude route", config.m_serverPublicKey)) { return false; } } @@ -421,9 +447,59 @@ bool WindowsFirewall::allowTrafficOfAdapter(int networkAdapter, uint8_t weight, return true; } +bool WindowsFirewall::allowTrafficTo(const IPAddress& addr, int weight, + const QString& title, + const QString& peer) { + GUID layerKeyOut; + GUID layerKeyIn; + if (addr.type() == QAbstractSocket::IPv4Protocol) { + layerKeyOut = FWPM_LAYER_ALE_AUTH_CONNECT_V4; + layerKeyIn = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4; + } else { + layerKeyOut = FWPM_LAYER_ALE_AUTH_CONNECT_V6; + layerKeyIn = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6; + } + + // Match the IP address range. + FWPM_FILTER_CONDITION0 cond[1] = {}; + FWP_RANGE0 ipRange; + QByteArray lowIpV6Buffer; + QByteArray highIpV6Buffer; + + importAddress(addr.address(), ipRange.valueLow, &lowIpV6Buffer); + importAddress(addr.broadcastAddress(), ipRange.valueHigh, &highIpV6Buffer); + + cond[0].fieldKey = FWPM_CONDITION_IP_REMOTE_ADDRESS; + cond[0].matchType = FWP_MATCH_RANGE; + cond[0].conditionValue.type = FWP_RANGE_TYPE; + cond[0].conditionValue.rangeValue = &ipRange; + + // Assemble the Filter base + FWPM_FILTER0 filter; + memset(&filter, 0, sizeof(filter)); + filter.action.type = FWP_ACTION_PERMIT; + filter.weight.type = FWP_UINT8; + filter.weight.uint8 = weight; + filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY; + filter.numFilterConditions = 1; + filter.filterCondition = cond; + + // Send the filters down to the firewall. + QString description = "Permit traffic %1 " + addr.toString(); + filter.layerKey = layerKeyOut; + if (!enableFilter(&filter, title, description.arg("to"), peer)) { + return false; + } + filter.layerKey = layerKeyIn; + if (!enableFilter(&filter, title, description.arg("from"), peer)) { + return false; + } + return true; +} + bool WindowsFirewall::allowTrafficTo(const QHostAddress& targetIP, uint port, - int weight, const QString& title, - const QString& peer) { + int weight, const QString& title, + const QString& peer) { bool isIPv4 = targetIP.protocol() == QAbstractSocket::IPv4Protocol; GUID layerOut = isIPv4 ? FWPM_LAYER_ALE_AUTH_CONNECT_V4 : FWPM_LAYER_ALE_AUTH_CONNECT_V6; @@ -484,57 +560,6 @@ bool WindowsFirewall::allowTrafficTo(const QHostAddress& targetIP, uint port, return true; } -bool WindowsFirewall::allowTrafficToRange(const IPAddress& addr, uint8_t weight, - const QString& title, - const QString& peer) { - QString description("Allow traffic %1 %2 "); - - auto lower = addr.address(); - auto upper = addr.broadcastAddress(); - - const bool isV4 = addr.type() == QAbstractSocket::IPv4Protocol; - const GUID layerKeyOut = - isV4 ? FWPM_LAYER_ALE_AUTH_CONNECT_V4 : FWPM_LAYER_ALE_AUTH_CONNECT_V6; - const GUID layerKeyIn = isV4 ? FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4 - : FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6; - - // Assemble the Filter base - FWPM_FILTER0 filter; - memset(&filter, 0, sizeof(filter)); - filter.action.type = FWP_ACTION_PERMIT; - filter.weight.type = FWP_UINT8; - filter.weight.uint8 = weight; - filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY; - - FWPM_FILTER_CONDITION0 cond[1] = {0}; - FWP_RANGE0 ipRange; - QByteArray lowIpV6Buffer; - QByteArray highIpV6Buffer; - - importAddress(lower, ipRange.valueLow, &lowIpV6Buffer); - importAddress(upper, ipRange.valueHigh, &highIpV6Buffer); - - cond[0].fieldKey = FWPM_CONDITION_IP_REMOTE_ADDRESS; - cond[0].matchType = FWP_MATCH_RANGE; - cond[0].conditionValue.type = FWP_RANGE_TYPE; - cond[0].conditionValue.rangeValue = &ipRange; - - filter.numFilterConditions = 1; - filter.filterCondition = cond; - - filter.layerKey = layerKeyOut; - if (!enableFilter(&filter, title, description.arg("to").arg(addr.toString()), - peer)) { - return false; - } - filter.layerKey = layerKeyIn; - if (!enableFilter(&filter, title, - description.arg("from").arg(addr.toString()), peer)) { - return false; - } - return true; -} - bool WindowsFirewall::allowDHCPTraffic(uint8_t weight, const QString& title) { // Allow outbound DHCPv4 { @@ -734,7 +759,7 @@ bool WindowsFirewall::blockTrafficTo(const IPAddress& addr, uint8_t weight, filter.weight.uint8 = weight; filter.subLayerKey = ST_FW_WINFW_BASELINE_SUBLAYER_KEY; - FWPM_FILTER_CONDITION0 cond[1] = {0}; + FWPM_FILTER_CONDITION0 cond[1] = {}; FWP_RANGE0 ipRange; QByteArray lowIpV6Buffer; QByteArray highIpV6Buffer; diff --git a/client/platforms/windows/daemon/windowsfirewall.h b/client/platforms/windows/daemon/windowsfirewall.h index e0d5ebe8..55ee9417 100644 --- a/client/platforms/windows/daemon/windowsfirewall.h +++ b/client/platforms/windows/daemon/windowsfirewall.h @@ -26,18 +26,27 @@ struct FWP_CONDITION_VALUE0_; class WindowsFirewall final : public QObject { public: - ~WindowsFirewall(); + /** + * @brief Opens the Windows Filtering Platform, initializes the session, + * sublayer. Returns a WindowsFirewall object if successful, otherwise + * nullptr. If there is already a WindowsFirewall object, it will be returned. + * + * @param parent - parent QObject + * @return WindowsFirewall* - nullptr if failed to open the Windows Filtering + * Platform. + */ + static WindowsFirewall* create(QObject* parent); + ~WindowsFirewall() override; - static WindowsFirewall* instance(); - bool init(); - - bool enableKillSwitch(int vpnAdapterIndex); + bool enableInterface(int vpnAdapterIndex); + bool enableLanBypass(const QList& ranges); bool enablePeerTraffic(const InterfaceConfig& config); bool disablePeerTraffic(const QString& pubkey); bool disableKillSwitch(); private: - WindowsFirewall(QObject* parent); + static bool initSublayer(); + WindowsFirewall(HANDLE session, QObject* parent); HANDLE m_sessionHandle; bool m_init = false; QList m_activeRules; @@ -50,11 +59,10 @@ class WindowsFirewall final : public QObject { bool blockTrafficTo(const IPAddress& addr, uint8_t weight, const QString& title, const QString& peer = QString()); bool blockTrafficOnPort(uint port, uint8_t weight, const QString& title); + bool allowTrafficTo(const IPAddress& addr, int weight, const QString& title, + const QString& peer = QString()); bool allowTrafficTo(const QHostAddress& targetIP, uint port, int weight, const QString& title, const QString& peer = QString()); - bool allowTrafficToRange(const IPAddress& addr, uint8_t weight, - const QString& title, - const QString& peer); bool allowTrafficOfAdapter(int networkAdapter, uint8_t weight, const QString& title); bool allowDHCPTraffic(uint8_t weight, const QString& title); diff --git a/client/platforms/windows/daemon/windowsroutemonitor.cpp b/client/platforms/windows/daemon/windowsroutemonitor.cpp index 69967526..fb0fbf7e 100644 --- a/client/platforms/windows/daemon/windowsroutemonitor.cpp +++ b/client/platforms/windows/daemon/windowsroutemonitor.cpp @@ -13,6 +13,12 @@ namespace { Logger logger("WindowsRouteMonitor"); }; // namespace +// Attempt to mark routing entries that we create with a relatively +// high metric. This ensures that we can skip over routes of our own +// creation when processing route changes, and ensures that we give +// way to other routing entries. +constexpr const ULONG EXCLUSION_ROUTE_METRIC = 0x5e72; + // Called by the kernel on route changes - perform some basic filtering and // invoke the routeChanged slot to do the real work. static void routeChangeCallback(PVOID context, PMIB_IPFORWARD_ROW2 row, @@ -20,22 +26,17 @@ static void routeChangeCallback(PVOID context, PMIB_IPFORWARD_ROW2 row, WindowsRouteMonitor* monitor = (WindowsRouteMonitor*)context; Q_UNUSED(type); - // Ignore host route changes, and unsupported protocols. - if (row->DestinationPrefix.Prefix.si_family == AF_INET6) { - if (row->DestinationPrefix.PrefixLength >= 128) { - return; - } - } else if (row->DestinationPrefix.Prefix.si_family == AF_INET) { - if (row->DestinationPrefix.PrefixLength >= 32) { - return; - } - } else { + // Ignore route changes that we created. + if ((row->Protocol == MIB_IPPROTO_NETMGMT) && + (row->Metric == EXCLUSION_ROUTE_METRIC)) { + return; + } + if (monitor->getLuid() == row->InterfaceLuid.Value) { return; } - if (monitor->getLuid() != row->InterfaceLuid.Value) { - QMetaObject::invokeMethod(monitor, "routeChanged", Qt::QueuedConnection); - } + // Invoke the route changed signal to do the real work in Qt. + QMetaObject::invokeMethod(monitor, "routeChanged", Qt::QueuedConnection); } // Perform prefix matching comparison on IP addresses in host order. @@ -57,7 +58,8 @@ static int prefixcmp(const void* a, const void* b, size_t bits) { return 0; } -WindowsRouteMonitor::WindowsRouteMonitor(QObject* parent) : QObject(parent) { +WindowsRouteMonitor::WindowsRouteMonitor(quint64 luid, QObject* parent) + : QObject(parent), m_luid(luid) { MZ_COUNT_CTOR(WindowsRouteMonitor); logger.debug() << "WindowsRouteMonitor created."; @@ -67,11 +69,13 @@ WindowsRouteMonitor::WindowsRouteMonitor(QObject* parent) : QObject(parent) { WindowsRouteMonitor::~WindowsRouteMonitor() { MZ_COUNT_DTOR(WindowsRouteMonitor); CancelMibChangeNotify2(m_routeHandle); - flushExclusionRoutes(); + + flushRouteTable(m_exclusionRoutes); + flushRouteTable(m_clonedRoutes); logger.debug() << "WindowsRouteMonitor destroyed."; } -void WindowsRouteMonitor::updateValidInterfaces(int family) { +void WindowsRouteMonitor::updateInterfaceMetrics(int family) { PMIB_IPINTERFACE_TABLE table; DWORD result = GetIpInterfaceTable(family, &table); if (result != NO_ERROR) { @@ -82,10 +86,10 @@ void WindowsRouteMonitor::updateValidInterfaces(int family) { // Flush the list of interfaces that are valid for routing. if ((family == AF_INET) || (family == AF_UNSPEC)) { - m_validInterfacesIpv4.clear(); + m_interfaceMetricsIpv4.clear(); } if ((family == AF_INET6) || (family == AF_UNSPEC)) { - m_validInterfacesIpv6.clear(); + m_interfaceMetricsIpv6.clear(); } // Rebuild the list of interfaces that are valid for routing. @@ -101,12 +105,12 @@ void WindowsRouteMonitor::updateValidInterfaces(int family) { if (row->Family == AF_INET) { logger.debug() << "Interface" << row->InterfaceIndex << "is valid for IPv4 routing"; - m_validInterfacesIpv4.append(row->InterfaceLuid.Value); + m_interfaceMetricsIpv4[row->InterfaceLuid.Value] = row->Metric; } if (row->Family == AF_INET6) { logger.debug() << "Interface" << row->InterfaceIndex << "is valid for IPv6 routing"; - m_validInterfacesIpv6.append(row->InterfaceLuid.Value); + m_interfaceMetricsIpv6[row->InterfaceLuid.Value] = row->Metric; } } } @@ -126,72 +130,72 @@ void WindowsRouteMonitor::updateExclusionRoute(MIB_IPFORWARD_ROW2* data, if (row->InterfaceLuid.Value == m_luid) { continue; } - // Ignore host routes, and shorter potential matches. - if (row->DestinationPrefix.PrefixLength >= - data->DestinationPrefix.PrefixLength) { + if (row->DestinationPrefix.PrefixLength < bestMatch) { continue; } - if (row->DestinationPrefix.PrefixLength < bestMatch) { + // Ignore routes of our own creation. + if ((row->Protocol == data->Protocol) && (row->Metric == data->Metric)) { continue; } // Check if the routing table entry matches the destination. + if (!routeContainsDest(&row->DestinationPrefix, &data->DestinationPrefix)) { + continue; + } + + // Compute the combined interface and routing metric. + ULONG routeMetric = row->Metric; if (data->DestinationPrefix.Prefix.si_family == AF_INET6) { - if (row->DestinationPrefix.Prefix.Ipv6.sin6_family != AF_INET6) { - continue; - } - if (!m_validInterfacesIpv6.contains(row->InterfaceLuid.Value)) { - continue; - } - if (prefixcmp(&data->DestinationPrefix.Prefix.Ipv6.sin6_addr, - &row->DestinationPrefix.Prefix.Ipv6.sin6_addr, - row->DestinationPrefix.PrefixLength) != 0) { + if (!m_interfaceMetricsIpv6.contains(row->InterfaceLuid.Value)) { continue; } + routeMetric += m_interfaceMetricsIpv6[row->InterfaceLuid.Value]; } else if (data->DestinationPrefix.Prefix.si_family == AF_INET) { - if (row->DestinationPrefix.Prefix.Ipv4.sin_family != AF_INET) { - continue; - } - if (!m_validInterfacesIpv4.contains(row->InterfaceLuid.Value)) { - continue; - } - if (prefixcmp(&data->DestinationPrefix.Prefix.Ipv4.sin_addr, - &row->DestinationPrefix.Prefix.Ipv4.sin_addr, - row->DestinationPrefix.PrefixLength) != 0) { + if (!m_interfaceMetricsIpv4.contains(row->InterfaceLuid.Value)) { continue; } + routeMetric += m_interfaceMetricsIpv4[row->InterfaceLuid.Value]; } else { // Unsupported destination address family. continue; } + if (routeMetric < row->Metric) { + routeMetric = ULONG_MAX; + } // Prefer routes with lower metric if we find multiple matches // with the same prefix length. if ((row->DestinationPrefix.PrefixLength == bestMatch) && - (row->Metric >= bestMetric)) { + (routeMetric >= bestMetric)) { continue; } // If we got here, then this is the longest prefix match so far. memcpy(&nexthop, &row->NextHop, sizeof(SOCKADDR_INET)); - bestLuid = row->InterfaceLuid.Value; bestMatch = row->DestinationPrefix.PrefixLength; - bestMetric = row->Metric; + bestMetric = routeMetric; + if (bestMatch == data->DestinationPrefix.PrefixLength) { + bestLuid = 0; // Don't write to the table if we find an exact match. + } else { + bestLuid = row->InterfaceLuid.Value; + } } // If neither the interface nor next-hop have changed, then do nothing. - if ((data->InterfaceLuid.Value) == bestLuid && + if (data->InterfaceLuid.Value == bestLuid && memcmp(&nexthop, &data->NextHop, sizeof(SOCKADDR_INET)) == 0) { return; } - // Update the routing table entry. + // Delete the previous routing table entry, if any. if (data->InterfaceLuid.Value != 0) { DWORD result = DeleteIpForwardEntry2(data); if ((result != NO_ERROR) && (result != ERROR_NOT_FOUND)) { logger.error() << "Failed to delete route:" << result; } } + + // Update the routing table entry. data->InterfaceLuid.Value = bestLuid; memcpy(&data->NextHop, &nexthop, sizeof(SOCKADDR_INET)); if (data->InterfaceLuid.Value != 0) { @@ -202,10 +206,178 @@ void WindowsRouteMonitor::updateExclusionRoute(MIB_IPFORWARD_ROW2* data, } } +// static +bool WindowsRouteMonitor::routeContainsDest(const IP_ADDRESS_PREFIX* route, + const IP_ADDRESS_PREFIX* dest) { + if (route->Prefix.si_family != dest->Prefix.si_family) { + return false; + } + if (route->PrefixLength > dest->PrefixLength) { + return false; + } + if (route->Prefix.si_family == AF_INET) { + return prefixcmp(&route->Prefix.Ipv4.sin_addr, &dest->Prefix.Ipv4.sin_addr, + route->PrefixLength) == 0; + } else if (route->Prefix.si_family == AF_INET6) { + return prefixcmp(&route->Prefix.Ipv6.sin6_addr, + &dest->Prefix.Ipv6.sin6_addr, route->PrefixLength) == 0; + } else { + return false; + } +} + +// static +QHostAddress WindowsRouteMonitor::prefixToAddress( + const IP_ADDRESS_PREFIX* dest) { + if (dest->Prefix.si_family == AF_INET6) { + return QHostAddress(dest->Prefix.Ipv6.sin6_addr.s6_addr); + } else if (dest->Prefix.si_family == AF_INET) { + quint32 addr = htonl(dest->Prefix.Ipv4.sin_addr.s_addr); + return QHostAddress(addr); + } else { + return QHostAddress(); + } +} + +bool WindowsRouteMonitor::isRouteExcluded(const IP_ADDRESS_PREFIX* dest) const { + auto i = m_exclusionRoutes.constBegin(); + while (i != m_exclusionRoutes.constEnd()) { + const MIB_IPFORWARD_ROW2* row = i.value(); + if (routeContainsDest(&row->DestinationPrefix, dest)) { + return true; + } + i++; + } + return false; +} + +void WindowsRouteMonitor::updateCapturedRoutes(int family) { + if (!m_defaultRouteCapture) { + return; + } + + PMIB_IPFORWARD_TABLE2 table; + DWORD error = GetIpForwardTable2(family, &table); + if (error != NO_ERROR) { + updateCapturedRoutes(family, table); + FreeMibTable(table); + } +} + +void WindowsRouteMonitor::updateCapturedRoutes(int family, void* ptable) { + PMIB_IPFORWARD_TABLE2 table = reinterpret_cast(ptable); + if (!m_defaultRouteCapture) { + return; + } + + for (ULONG i = 0; i < table->NumEntries; i++) { + MIB_IPFORWARD_ROW2* row = &table->Table[i]; + // Ignore routes into the VPN interface. + if (row->InterfaceLuid.Value == m_luid) { + continue; + } + // Ignore the default route + if (row->DestinationPrefix.PrefixLength == 0) { + continue; + } + // Ignore routes of our own creation. + if ((row->Protocol == MIB_IPPROTO_NETMGMT) && + (row->Metric == EXCLUSION_ROUTE_METRIC)) { + continue; + } + // Ignore routes which should be excluded. + if (isRouteExcluded(&row->DestinationPrefix)) { + continue; + } + QHostAddress destination = prefixToAddress(&row->DestinationPrefix); + if (destination.isLoopback() || destination.isBroadcast() || + destination.isLinkLocal() || destination.isMulticast()) { + continue; + } + + // If we get here, this route should be cloned. + IPAddress prefix(destination, row->DestinationPrefix.PrefixLength); + MIB_IPFORWARD_ROW2* data = m_clonedRoutes.value(prefix, nullptr); + if (data != nullptr) { + // Count the number of matching entries in the main table. + data->Age++; + continue; + } + logger.debug() << "Capturing route to" + << logger.sensitive(prefix.toString()); + + // Clone the route and direct it into the VPN tunnel. + data = new MIB_IPFORWARD_ROW2; + InitializeIpForwardEntry(data); + data->InterfaceLuid.Value = m_luid; + data->DestinationPrefix = row->DestinationPrefix; + data->NextHop.si_family = data->DestinationPrefix.Prefix.si_family; + + // Set the rest of the flags for a static route. + data->ValidLifetime = 0xffffffff; + data->PreferredLifetime = 0xffffffff; + data->Metric = 0; + data->Protocol = MIB_IPPROTO_NETMGMT; + data->Loopback = false; + data->AutoconfigureAddress = false; + data->Publish = false; + data->Immortal = false; + data->Age = 0; + + // Route this traffic into the VPN tunnel. + DWORD result = CreateIpForwardEntry2(data); + if (result != NO_ERROR) { + logger.error() << "Failed to update route:" << result; + delete data; + } else { + m_clonedRoutes.insert(prefix, data); + data->Age++; + } + } + + // Finally scan for any routes which were removed from the table. We do this + // by reusing the age field to count the number of matching entries in the + // main table. + auto i = m_clonedRoutes.begin(); + while (i != m_clonedRoutes.end()) { + MIB_IPFORWARD_ROW2* data = i.value(); + if (data->Age > 0) { + // Entry is in use, don't delete it. + data->Age = 0; + i++; + continue; + } + if ((family != AF_UNSPEC) && + (data->DestinationPrefix.Prefix.si_family != family)) { + // We are not processing updates to this address family. + i++; + continue; + } + + logger.debug() << "Removing route capture for" + << logger.sensitive(i.key().toString()); + + // Otherwise, this route is no longer in use. + DWORD result = DeleteIpForwardEntry2(data); + if ((result != NO_ERROR) && (result != ERROR_NOT_FOUND)) { + logger.error() << "Failed to delete route:" << result; + } + delete data; + i = m_clonedRoutes.erase(i); + } +} + bool WindowsRouteMonitor::addExclusionRoute(const IPAddress& prefix) { logger.debug() << "Adding exclusion route for" << logger.sensitive(prefix.toString()); + // Silently ignore non-routeable addresses. + QHostAddress addr = prefix.address(); + if (addr.isLoopback() || addr.isBroadcast() || addr.isLinkLocal() || + addr.isMulticast()) { + return true; + } + if (m_exclusionRoutes.contains(prefix)) { logger.warning() << "Exclusion route already exists"; return false; @@ -232,7 +404,7 @@ bool WindowsRouteMonitor::addExclusionRoute(const IPAddress& prefix) { // Set the rest of the flags for a static route. data->ValidLifetime = 0xffffffff; data->PreferredLifetime = 0xffffffff; - data->Metric = 0; + data->Metric = EXCLUSION_ROUTE_METRIC; data->Protocol = MIB_IPPROTO_NETMGMT; data->Loopback = false; data->AutoconfigureAddress = false; @@ -254,7 +426,8 @@ bool WindowsRouteMonitor::addExclusionRoute(const IPAddress& prefix) { delete data; return false; } - updateValidInterfaces(family); + updateInterfaceMetrics(family); + updateCapturedRoutes(family, table); updateExclusionRoute(data, table); FreeMibTable(table); @@ -266,26 +439,28 @@ bool WindowsRouteMonitor::deleteExclusionRoute(const IPAddress& prefix) { logger.debug() << "Deleting exclusion route for" << logger.sensitive(prefix.address().toString()); - for (;;) { - MIB_IPFORWARD_ROW2* data = m_exclusionRoutes.take(prefix); - if (data == nullptr) { - break; - } - - DWORD result = DeleteIpForwardEntry2(data); - if ((result != ERROR_NOT_FOUND) && (result != NO_ERROR)) { - logger.error() << "Failed to delete route to" - << logger.sensitive(prefix.toString()) - << "result:" << result; - } - delete data; + MIB_IPFORWARD_ROW2* data = m_exclusionRoutes.take(prefix); + if (data == nullptr) { + return true; } + DWORD result = DeleteIpForwardEntry2(data); + if ((result != ERROR_NOT_FOUND) && (result != NO_ERROR)) { + logger.error() << "Failed to delete route to" + << logger.sensitive(prefix.toString()) + << "result:" << result; + } + + // Captured routes might have changed. + updateCapturedRoutes(data->DestinationPrefix.Prefix.si_family); + + delete data; return true; } -void WindowsRouteMonitor::flushExclusionRoutes() { - for (auto i = m_exclusionRoutes.begin(); i != m_exclusionRoutes.end(); i++) { +void WindowsRouteMonitor::flushRouteTable( + QHash& table) { + for (auto i = table.begin(); i != table.end(); i++) { MIB_IPFORWARD_ROW2* data = i.value(); DWORD result = DeleteIpForwardEntry2(data); if ((result != ERROR_NOT_FOUND) && (result != NO_ERROR)) { @@ -295,7 +470,17 @@ void WindowsRouteMonitor::flushExclusionRoutes() { } delete data; } - m_exclusionRoutes.clear(); + table.clear(); +} + +void WindowsRouteMonitor::setDetaultRouteCapture(bool enable) { + m_defaultRouteCapture = enable; + + // Flush any captured routes when disabling the feature. + if (!m_defaultRouteCapture) { + flushRouteTable(m_clonedRoutes); + return; + } } void WindowsRouteMonitor::routeChanged() { @@ -308,7 +493,8 @@ void WindowsRouteMonitor::routeChanged() { return; } - updateValidInterfaces(AF_UNSPEC); + updateInterfaceMetrics(AF_UNSPEC); + updateCapturedRoutes(AF_UNSPEC, table); for (MIB_IPFORWARD_ROW2* data : m_exclusionRoutes) { updateExclusionRoute(data, table); } diff --git a/client/platforms/windows/daemon/windowsroutemonitor.h b/client/platforms/windows/daemon/windowsroutemonitor.h index 0ae9a8a2..fa04f646 100644 --- a/client/platforms/windows/daemon/windowsroutemonitor.h +++ b/client/platforms/windows/daemon/windowsroutemonitor.h @@ -11,6 +11,8 @@ #include #include +#include +#include #include #include "ipaddress.h" @@ -19,28 +21,41 @@ class WindowsRouteMonitor final : public QObject { Q_OBJECT public: - WindowsRouteMonitor(QObject* parent); + WindowsRouteMonitor(quint64 luid, QObject* parent); ~WindowsRouteMonitor(); + void setDetaultRouteCapture(bool enable); + bool addExclusionRoute(const IPAddress& prefix); bool deleteExclusionRoute(const IPAddress& prefix); - void flushExclusionRoutes(); + void flushExclusionRoutes() { return flushRouteTable(m_exclusionRoutes); }; - void setLuid(quint64 luid) { m_luid = luid; } - quint64 getLuid() { return m_luid; } + quint64 getLuid() const { return m_luid; } public slots: void routeChanged(); private: + bool isRouteExcluded(const IP_ADDRESS_PREFIX* dest) const; + static bool routeContainsDest(const IP_ADDRESS_PREFIX* route, + const IP_ADDRESS_PREFIX* dest); + static QHostAddress prefixToAddress(const IP_ADDRESS_PREFIX* dest); + + void flushRouteTable(QHash& table); void updateExclusionRoute(MIB_IPFORWARD_ROW2* data, void* table); - void updateValidInterfaces(int family); + void updateInterfaceMetrics(int family); + void updateCapturedRoutes(int family); + void updateCapturedRoutes(int family, void* table); QHash m_exclusionRoutes; - QList m_validInterfacesIpv4; - QList m_validInterfacesIpv6; + QMap m_interfaceMetricsIpv4; + QMap m_interfaceMetricsIpv6; - quint64 m_luid = 0; + // Default route cloning + bool m_defaultRouteCapture = false; + QHash m_clonedRoutes; + + const quint64 m_luid = 0; HANDLE m_routeHandle = INVALID_HANDLE_VALUE; }; diff --git a/client/platforms/windows/daemon/windowssplittunnel.cpp b/client/platforms/windows/daemon/windowssplittunnel.cpp index c4e893b2..63de153d 100644 --- a/client/platforms/windows/daemon/windowssplittunnel.cpp +++ b/client/platforms/windows/daemon/windowssplittunnel.cpp @@ -4,9 +4,15 @@ #include "windowssplittunnel.h" +#include + +#include + #include "../windowscommons.h" #include "../windowsservicemanager.h" #include "logger.h" +#include "platforms/windows/daemon/windowsfirewall.h" +#include "platforms/windows/daemon/windowssplittunnel.h" #include "platforms/windows/windowsutils.h" #include "windowsfirewall.h" @@ -18,34 +24,252 @@ #include #include #include -#include + +#pragma region + +// Driver Configuration structures +using CONFIGURATION_ENTRY = struct { + // Offset into buffer region that follows all entries. + // The image name uses the device path. + SIZE_T ImageNameOffset; + // Length of the String + USHORT ImageNameLength; +}; + +using CONFIGURATION_HEADER = struct { + // Number of entries immediately following the header. + SIZE_T NumEntries; + + // Total byte length: header + entries + string buffer. + SIZE_T TotalLength; +}; + +// Used to Configure Which IP is network/vpn +using IP_ADDRESSES_CONFIG = struct { + IN_ADDR TunnelIpv4; + IN_ADDR InternetIpv4; + + IN6_ADDR TunnelIpv6; + IN6_ADDR InternetIpv6; +}; + +// Used to Define Which Processes are alive on activation +using PROCESS_DISCOVERY_HEADER = struct { + SIZE_T NumEntries; + SIZE_T TotalLength; +}; + +using PROCESS_DISCOVERY_ENTRY = struct { + HANDLE ProcessId; + HANDLE ParentProcessId; + + SIZE_T ImageNameOffset; + USHORT ImageNameLength; +}; + +using ProcessInfo = struct { + DWORD ProcessId; + DWORD ParentProcessId; + FILETIME CreationTime; + std::wstring DevicePath; +}; + +#ifndef CTL_CODE + +# define FILE_ANY_ACCESS 0x0000 + +# define METHOD_BUFFERED 0 +# define METHOD_IN_DIRECT 1 +# define METHOD_NEITHER 3 + +# define CTL_CODE(DeviceType, Function, Method, Access) \ + (((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method)) +#endif + +// Known ControlCodes +#define IOCTL_INITIALIZE CTL_CODE(0x8000, 1, METHOD_NEITHER, FILE_ANY_ACCESS) + +#define IOCTL_DEQUEUE_EVENT \ + CTL_CODE(0x8000, 2, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_REGISTER_PROCESSES \ + CTL_CODE(0x8000, 3, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_REGISTER_IP_ADDRESSES \ + CTL_CODE(0x8000, 4, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_GET_IP_ADDRESSES \ + CTL_CODE(0x8000, 5, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_SET_CONFIGURATION \ + CTL_CODE(0x8000, 6, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_GET_CONFIGURATION \ + CTL_CODE(0x8000, 7, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_CLEAR_CONFIGURATION \ + CTL_CODE(0x8000, 8, METHOD_NEITHER, FILE_ANY_ACCESS) + +#define IOCTL_GET_STATE CTL_CODE(0x8000, 9, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_QUERY_PROCESS \ + CTL_CODE(0x8000, 10, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_ST_RESET CTL_CODE(0x8000, 11, METHOD_NEITHER, FILE_ANY_ACCESS) + +constexpr static const auto DRIVER_SYMLINK = L"\\\\.\\MULLVADSPLITTUNNEL"; +constexpr static const auto DRIVER_FILENAME = "mullvad-split-tunnel.sys"; +constexpr static const auto DRIVER_SERVICE_NAME = L"AmneziaVPNSplitTunnel"; +constexpr static const auto MV_SERVICE_NAME = L"MullvadVPN"; + +#pragma endregion namespace { Logger logger("WindowsSplitTunnel"); + +ProcessInfo getProcessInfo(HANDLE process, const PROCESSENTRY32W& processMeta) { + ProcessInfo pi; + pi.ParentProcessId = processMeta.th32ParentProcessID; + pi.ProcessId = processMeta.th32ProcessID; + pi.CreationTime = {0, 0}; + pi.DevicePath = L""; + + FILETIME creationTime, null_time; + auto ok = GetProcessTimes(process, &creationTime, &null_time, &null_time, + &null_time); + if (ok) { + pi.CreationTime = creationTime; + } + wchar_t imagepath[MAX_PATH + 1]; + if (K32GetProcessImageFileNameW( + process, imagepath, sizeof(imagepath) / sizeof(*imagepath)) != 0) { + pi.DevicePath = imagepath; + } + return pi; } -WindowsSplitTunnel::WindowsSplitTunnel(QObject* parent) : QObject(parent) { +} // namespace + +std::unique_ptr WindowsSplitTunnel::create( + WindowsFirewall* fw) { + if (fw == nullptr) { + // Pre-Condition: + // Make sure the Windows Firewall has created the sublayer + // otherwise the driver will fail to initialize + logger.error() << "Failed to did not pass a WindowsFirewall obj" + << "The Driver cannot work with the sublayer not created"; + return nullptr; + } + // 00: Check if we conflict with mullvad, if so. if (detectConflict()) { logger.error() << "Conflict detected, abort Split-Tunnel init."; - uninstallDriver(); - return; + return nullptr; } - - m_tries = 0; - + // 01: Check if the driver is installed, if not do so. if (!isInstalled()) { logger.debug() << "Driver is not Installed, doing so"; auto handle = installDriver(); if (handle == INVALID_HANDLE_VALUE) { WindowsUtils::windowsLog("Failed to install Driver"); - return; + return nullptr; } logger.debug() << "Driver installed"; CloseServiceHandle(handle); } else { - logger.debug() << "Driver is installed"; + logger.debug() << "Driver was installed"; } - initDriver(); + // 02: Now check if the service is running + auto driver_manager = + WindowsServiceManager::open(QString::fromWCharArray(DRIVER_SERVICE_NAME)); + if (Q_UNLIKELY(driver_manager == nullptr)) { + // Let's be fair if we end up here, + // after checking it exists and installing it, + // this is super unlikeley + Q_ASSERT(false); + logger.error() + << "WindowsServiceManager was unable fo find Split Tunnel service?"; + return nullptr; + } + if (!driver_manager->isRunning()) { + logger.debug() << "Driver is not running, starting it"; + // Start the service + if (!driver_manager->startService()) { + logger.error() << "Failed to start Split Tunnel Service"; + return nullptr; + }; + } + // 03: Open the Driver Symlink + auto driverFile = CreateFileW(DRIVER_SYMLINK, GENERIC_READ | GENERIC_WRITE, 0, + nullptr, OPEN_EXISTING, 0, nullptr); + ; + if (driverFile == INVALID_HANDLE_VALUE) { + WindowsUtils::windowsLog("Failed to open Driver: "); + // Only once, if the opening did not work. Try to reboot it. # + logger.info() + << "Failed to open driver, attempting only once to reboot driver"; + if (!driver_manager->stopService()) { + logger.error() << "Unable stop driver"; + return nullptr; + }; + logger.info() << "Stopped driver, starting it again."; + if (!driver_manager->startService()) { + logger.error() << "Unable start driver"; + return nullptr; + }; + logger.info() << "Opening again."; + driverFile = CreateFileW(DRIVER_SYMLINK, GENERIC_READ | GENERIC_WRITE, 0, + nullptr, OPEN_EXISTING, 0, nullptr); + if (driverFile == INVALID_HANDLE_VALUE) { + logger.error() << "Opening Failed again, sorry!"; + return nullptr; + } + } + if (!initDriver(driverFile)) { + logger.error() << "Failed to init driver"; + return nullptr; + } + // We're ready to talk to the driver, it's alive and setup. + return std::make_unique(driverFile); +} + +bool WindowsSplitTunnel::initDriver(HANDLE driverIO) { + // We need to now check the state and init it, if required + auto state = getState(driverIO); + if (state == STATE_UNKNOWN) { + logger.debug() << "Cannot check if driver is initialized"; + return false; + } + if (state >= STATE_INITIALIZED) { + logger.debug() << "Driver already initialized: " << state; + // Reset Driver as it has wfp handles probably >:( + resetDriver(driverIO); + + auto newState = getState(driverIO); + logger.debug() << "New state after reset:" << newState; + if (newState >= STATE_INITIALIZED) { + logger.debug() << "Reset unsuccesfull"; + return false; + } + } + + DWORD bytesReturned; + auto ok = DeviceIoControl(driverIO, IOCTL_INITIALIZE, nullptr, 0, nullptr, 0, + &bytesReturned, nullptr); + if (!ok) { + auto err = GetLastError(); + logger.error() << "Driver init failed err -" << err; + logger.error() << "State:" << getState(driverIO); + + return false; + } + logger.debug() << "Driver initialized" << getState(driverIO); + return true; +} + +WindowsSplitTunnel::WindowsSplitTunnel(HANDLE driverIO) : m_driver(driverIO) { + logger.debug() << "Connected to the Driver"; + + Q_ASSERT(getState() == STATE_INITIALIZED); } WindowsSplitTunnel::~WindowsSplitTunnel() { @@ -53,73 +277,12 @@ WindowsSplitTunnel::~WindowsSplitTunnel() { uninstallDriver(); } -void WindowsSplitTunnel::initDriver() { - if (detectConflict()) { - logger.error() << "Conflict detected, abort Split-Tunnel init."; - return; - } - logger.debug() << "Try to open Split Tunnel Driver"; - // Open the Driver Symlink - m_driver = CreateFileW(DRIVER_SYMLINK, GENERIC_READ | GENERIC_WRITE, 0, - nullptr, OPEN_EXISTING, 0, nullptr); - ; - if (m_driver == INVALID_HANDLE_VALUE && m_tries < 500) { - WindowsUtils::windowsLog("Failed to open Driver: "); - m_tries++; - Sleep(100); - // If the handle is not present, try again after the serivce has started; - auto driver_manager = WindowsServiceManager(DRIVER_SERVICE_NAME); - QObject::connect(&driver_manager, &WindowsServiceManager::serviceStarted, - this, &WindowsSplitTunnel::initDriver); - driver_manager.startService(); - return; - } - - logger.debug() << "Connected to the Driver"; - // Reset Driver as it has wfp handles probably >:( - - if (!WindowsFirewall::instance()->init()) { - logger.error() << "Init WFP-Sublayer failed, driver won't be functional"; - return; - } - - // We need to now check the state and init it, if required - - auto state = getState(); - if (state == STATE_UNKNOWN) { - logger.debug() << "Cannot check if driver is initialized"; - } - if (state >= STATE_INITIALIZED) { - logger.debug() << "Driver already initialized: " << state; - reset(); - - auto newState = getState(); - logger.debug() << "New state after reset:" << newState; - if (newState >= STATE_INITIALIZED) { - logger.debug() << "Reset unsuccesfull"; - return; - } - } - - DWORD bytesReturned; - auto ok = DeviceIoControl(m_driver, IOCTL_INITIALIZE, nullptr, 0, nullptr, 0, - &bytesReturned, nullptr); - if (!ok) { - auto err = GetLastError(); - logger.error() << "Driver init failed err -" << err; - logger.error() << "State:" << getState(); - - return; - } - logger.debug() << "Driver initialized" << getState(); -} - -void WindowsSplitTunnel::setRules(const QStringList& appPaths) { +bool WindowsSplitTunnel::excludeApps(const QStringList& appPaths) { auto state = getState(); if (state != STATE_READY && state != STATE_RUNNING) { logger.warning() << "Driver is not in the right State to set Rules" << state; - return; + return false; } logger.debug() << "Pushing new Ruleset for Split-Tunnel " << state; @@ -133,12 +296,13 @@ void WindowsSplitTunnel::setRules(const QStringList& appPaths) { auto err = GetLastError(); WindowsUtils::windowsLog("Set Config Failed:"); logger.error() << "Failed to set Config err code " << err; - return; + return false; } - logger.debug() << "New Configuration applied: " << getState(); + logger.debug() << "New Configuration applied: " << stateString(); + return true; } -void WindowsSplitTunnel::start(int inetAdapterIndex, int vpnAdapterIndex) { +bool WindowsSplitTunnel::start(int inetAdapterIndex, int vpnAdapterIndex) { // To Start we need to send 2 things: // Network info (what is vpn what is network) logger.debug() << "Starting SplitTunnel"; @@ -151,7 +315,7 @@ void WindowsSplitTunnel::start(int inetAdapterIndex, int vpnAdapterIndex) { 0, &bytesReturned, nullptr); if (!ok) { logger.error() << "Driver init failed"; - return; + return false; } } @@ -164,16 +328,16 @@ void WindowsSplitTunnel::start(int inetAdapterIndex, int vpnAdapterIndex) { nullptr); if (!ok) { logger.error() << "Failed to set Process Config"; - return; + return false; } - logger.debug() << "Set Process Config ok || new State:" << getState(); + logger.debug() << "Set Process Config ok || new State:" << stateString(); } if (getState() == STATE_INITIALIZED) { logger.warning() << "Driver is still not ready after process list send"; - return; + return false; } - logger.debug() << "Driver is ready || new State:" << getState(); + logger.debug() << "Driver is ready || new State:" << stateString(); auto config = generateIPConfiguration(inetAdapterIndex, vpnAdapterIndex); auto ok = DeviceIoControl(m_driver, IOCTL_REGISTER_IP_ADDRESSES, &config[0], @@ -181,9 +345,10 @@ void WindowsSplitTunnel::start(int inetAdapterIndex, int vpnAdapterIndex) { nullptr); if (!ok) { logger.error() << "Failed to set Network Config"; - return; + return false; } - logger.debug() << "New Network Config Applied || new State:" << getState(); + logger.debug() << "New Network Config Applied || new State:" << stateString(); + return true; } void WindowsSplitTunnel::stop() { @@ -197,25 +362,27 @@ void WindowsSplitTunnel::stop() { logger.debug() << "Stopping Split tunnel successfull"; } -void WindowsSplitTunnel::reset() { +bool WindowsSplitTunnel::resetDriver(HANDLE driverIO) { DWORD bytesReturned; - auto ok = DeviceIoControl(m_driver, IOCTL_ST_RESET, nullptr, 0, nullptr, 0, + auto ok = DeviceIoControl(driverIO, IOCTL_ST_RESET, nullptr, 0, nullptr, 0, &bytesReturned, nullptr); if (!ok) { logger.error() << "Reset Split tunnel not successfull"; - return; + return false; } logger.debug() << "Reset Split tunnel successfull"; + return true; } -DRIVER_STATE WindowsSplitTunnel::getState() { - if (m_driver == INVALID_HANDLE_VALUE) { +// static +WindowsSplitTunnel::DRIVER_STATE WindowsSplitTunnel::getState(HANDLE driverIO) { + if (driverIO == INVALID_HANDLE_VALUE) { logger.debug() << "Can't query State from non Opened Driver"; return STATE_UNKNOWN; } DWORD bytesReturned; SIZE_T outBuffer; - bool ok = DeviceIoControl(m_driver, IOCTL_GET_STATE, nullptr, 0, &outBuffer, + bool ok = DeviceIoControl(driverIO, IOCTL_GET_STATE, nullptr, 0, &outBuffer, sizeof(outBuffer), &bytesReturned, nullptr); if (!ok) { WindowsUtils::windowsLog("getState response failure"); @@ -225,7 +392,10 @@ DRIVER_STATE WindowsSplitTunnel::getState() { WindowsUtils::windowsLog("getState response is empty"); return STATE_UNKNOWN; } - return static_cast(outBuffer); + return static_cast(outBuffer); +} +WindowsSplitTunnel::DRIVER_STATE WindowsSplitTunnel::getState() { + return getState(m_driver); } std::vector WindowsSplitTunnel::generateAppConfiguration( @@ -273,58 +443,59 @@ std::vector WindowsSplitTunnel::generateAppConfiguration( return outBuffer; } -std::vector WindowsSplitTunnel::generateIPConfiguration( +std::vector WindowsSplitTunnel::generateIPConfiguration( int inetAdapterIndex, int vpnAdapterIndex) { - std::vector out(sizeof(IP_ADDRESSES_CONFIG)); + std::vector out(sizeof(IP_ADDRESSES_CONFIG)); auto config = reinterpret_cast(&out[0]); auto ifaces = QNetworkInterface::allInterfaces(); - if (vpnAdapterIndex == 0) { + if (vpnAdapterIndex == 0) { vpnAdapterIndex = WindowsCommons::VPNAdapterIndex(); } - // Always the VPN - getAddress(vpnAdapterIndex, &config->TunnelIpv4, - &config->TunnelIpv6); - // 2nd best route - getAddress(inetAdapterIndex, &config->InternetIpv4, &config->InternetIpv6); + if (!getAddress(vpnAdapterIndex, &config->TunnelIpv4, + &config->TunnelIpv6)) { + return {}; + } + // 2nd best route is usually the internet adapter + if (!getAddress(inetAdapterIndex, &config->InternetIpv4, + &config->InternetIpv6)) { + return {}; + }; return out; } -void WindowsSplitTunnel::getAddress(int adapterIndex, IN_ADDR* out_ipv4, +bool WindowsSplitTunnel::getAddress(int adapterIndex, IN_ADDR* out_ipv4, IN6_ADDR* out_ipv6) { QNetworkInterface target = QNetworkInterface::interfaceFromIndex(adapterIndex); logger.debug() << "Getting adapter info for:" << target.humanReadableName(); - // take the first v4/v6 Adress and convert to in_addr - for (auto address : target.addressEntries()) { - if (address.ip().protocol() == QAbstractSocket::IPv4Protocol) { - auto adrr = address.ip().toString(); - std::wstring wstr = adrr.toStdWString(); - logger.debug() << "IpV4" << logger.sensitive(adrr); - PCWSTR w_str_ip = wstr.c_str(); - auto ok = InetPtonW(AF_INET, w_str_ip, out_ipv4); - if (ok != 1) { - logger.debug() << "Ipv4 Conversation error" << WSAGetLastError(); + auto get = [&target](QAbstractSocket::NetworkLayerProtocol protocol) { + for (auto address : target.addressEntries()) { + if (address.ip().protocol() != protocol) { + continue; } - break; + return address.ip().toString().toStdWString(); } + return std::wstring{}; + }; + auto ipv4 = get(QAbstractSocket::IPv4Protocol); + auto ipv6 = get(QAbstractSocket::IPv6Protocol); + + if (InetPtonW(AF_INET, ipv4.c_str(), out_ipv4) != 1) { + logger.debug() << "Ipv4 Conversation error" << WSAGetLastError(); + return false; } - for (auto address : target.addressEntries()) { - if (address.ip().protocol() == QAbstractSocket::IPv6Protocol) { - auto adrr = address.ip().toString(); - std::wstring wstr = adrr.toStdWString(); - logger.debug() << "IpV6" << logger.sensitive(adrr); - PCWSTR w_str_ip = wstr.c_str(); - auto ok = InetPtonW(AF_INET6, w_str_ip, out_ipv6); - if (ok != 1) { - logger.error() << "Ipv6 Conversation error" << WSAGetLastError(); - } - break; - } + if (ipv6.empty()) { + std::memset(out_ipv6, 0x00, sizeof(IN6_ADDR)); + return true; } + if (InetPtonW(AF_INET6, ipv6.c_str(), out_ipv6) != 1) { + logger.debug() << "Ipv6 Conversation error" << WSAGetLastError(); + } + return true; } std::vector WindowsSplitTunnel::generateProcessBlob() { @@ -411,33 +582,6 @@ std::vector WindowsSplitTunnel::generateProcessBlob() { return out; } -void WindowsSplitTunnel::close() { - CloseHandle(m_driver); - m_driver = INVALID_HANDLE_VALUE; -} - -ProcessInfo WindowsSplitTunnel::getProcessInfo( - HANDLE process, const PROCESSENTRY32W& processMeta) { - ProcessInfo pi; - pi.ParentProcessId = processMeta.th32ParentProcessID; - pi.ProcessId = processMeta.th32ProcessID; - pi.CreationTime = {0, 0}; - pi.DevicePath = L""; - - FILETIME creationTime, null_time; - auto ok = GetProcessTimes(process, &creationTime, &null_time, &null_time, - &null_time); - if (ok) { - pi.CreationTime = creationTime; - } - wchar_t imagepath[MAX_PATH + 1]; - if (K32GetProcessImageFileNameW( - process, imagepath, sizeof(imagepath) / sizeof(*imagepath)) != 0) { - pi.DevicePath = imagepath; - } - return pi; -} - // static SC_HANDLE WindowsSplitTunnel::installDriver() { LPCWSTR displayName = L"Amnezia Split Tunnel Service"; @@ -448,15 +592,15 @@ SC_HANDLE WindowsSplitTunnel::installDriver() { return (SC_HANDLE)INVALID_HANDLE_VALUE; } auto path = driver.absolutePath() + "/" + DRIVER_FILENAME; - LPCWSTR binPath = (const wchar_t*)path.utf16(); + auto binPath = (const wchar_t*)path.utf16(); auto scm_rights = SC_MANAGER_ALL_ACCESS; - auto serviceManager = OpenSCManager(NULL, // local computer - NULL, // servicesActive database + auto serviceManager = OpenSCManager(nullptr, // local computer + nullptr, // servicesActive database scm_rights); - auto service = CreateService(serviceManager, DRIVER_SERVICE_NAME, displayName, - SERVICE_ALL_ACCESS, SERVICE_KERNEL_DRIVER, - SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, - binPath, nullptr, 0, nullptr, nullptr, nullptr); + auto service = CreateService( + serviceManager, DRIVER_SERVICE_NAME, displayName, SERVICE_ALL_ACCESS, + SERVICE_KERNEL_DRIVER, SERVICE_AUTO_START, SERVICE_ERROR_NORMAL, binPath, + nullptr, nullptr, nullptr, nullptr, nullptr); CloseServiceHandle(serviceManager); return service; } @@ -554,3 +698,25 @@ bool WindowsSplitTunnel::detectConflict() { CloseServiceHandle(servicehandle); return err == ERROR_SERVICE_DOES_NOT_EXIST; } + +bool WindowsSplitTunnel::isRunning() { return getState() == STATE_RUNNING; } +QString WindowsSplitTunnel::stateString() { + switch (getState()) { + case STATE_UNKNOWN: + return "STATE_UNKNOWN"; + case STATE_NONE: + return "STATE_NONE"; + case STATE_STARTED: + return "STATE_STARTED"; + case STATE_INITIALIZED: + return "STATE_INITIALIZED"; + case STATE_READY: + return "STATE_READY"; + case STATE_RUNNING: + return "STATE_RUNNING"; + case STATE_ZOMBIE: + return "STATE_ZOMBIE"; + break; + } + return {}; +} diff --git a/client/platforms/windows/daemon/windowssplittunnel.h b/client/platforms/windows/daemon/windowssplittunnel.h index 466036d6..85c827f6 100644 --- a/client/platforms/windows/daemon/windowssplittunnel.h +++ b/client/platforms/windows/daemon/windowssplittunnel.h @@ -8,6 +8,7 @@ #include #include #include +#include // Note: the ws2tcpip.h import must come before the others. // clang-format off @@ -18,160 +19,78 @@ #include #include -// States for GetState -enum DRIVER_STATE { - STATE_UNKNOWN = -1, - STATE_NONE = 0, - STATE_STARTED = 1, - STATE_INITIALIZED = 2, - STATE_READY = 3, - STATE_RUNNING = 4, - STATE_ZOMBIE = 5, -}; +class WindowsFirewall; -#ifndef CTL_CODE - -# define FILE_ANY_ACCESS 0x0000 - -# define METHOD_BUFFERED 0 -# define METHOD_IN_DIRECT 1 -# define METHOD_NEITHER 3 - -# define CTL_CODE(DeviceType, Function, Method, Access) \ - (((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method)) -#endif - -// Known ControlCodes -#define IOCTL_INITIALIZE CTL_CODE(0x8000, 1, METHOD_NEITHER, FILE_ANY_ACCESS) - -#define IOCTL_DEQUEUE_EVENT \ - CTL_CODE(0x8000, 2, METHOD_BUFFERED, FILE_ANY_ACCESS) - -#define IOCTL_REGISTER_PROCESSES \ - CTL_CODE(0x8000, 3, METHOD_BUFFERED, FILE_ANY_ACCESS) - -#define IOCTL_REGISTER_IP_ADDRESSES \ - CTL_CODE(0x8000, 4, METHOD_BUFFERED, FILE_ANY_ACCESS) - -#define IOCTL_GET_IP_ADDRESSES \ - CTL_CODE(0x8000, 5, METHOD_BUFFERED, FILE_ANY_ACCESS) - -#define IOCTL_SET_CONFIGURATION \ - CTL_CODE(0x8000, 6, METHOD_BUFFERED, FILE_ANY_ACCESS) - -#define IOCTL_GET_CONFIGURATION \ - CTL_CODE(0x8000, 7, METHOD_BUFFERED, FILE_ANY_ACCESS) - -#define IOCTL_CLEAR_CONFIGURATION \ - CTL_CODE(0x8000, 8, METHOD_NEITHER, FILE_ANY_ACCESS) - -#define IOCTL_GET_STATE CTL_CODE(0x8000, 9, METHOD_BUFFERED, FILE_ANY_ACCESS) - -#define IOCTL_QUERY_PROCESS \ - CTL_CODE(0x8000, 10, METHOD_BUFFERED, FILE_ANY_ACCESS) - -#define IOCTL_ST_RESET CTL_CODE(0x8000, 11, METHOD_NEITHER, FILE_ANY_ACCESS) - -// Driver Configuration structures - -typedef struct { - // Offset into buffer region that follows all entries. - // The image name uses the device path. - SIZE_T ImageNameOffset; - // Length of the String - USHORT ImageNameLength; -} CONFIGURATION_ENTRY; - -typedef struct { - // Number of entries immediately following the header. - SIZE_T NumEntries; - - // Total byte length: header + entries + string buffer. - SIZE_T TotalLength; -} CONFIGURATION_HEADER; - -// Used to Configure Which IP is network/vpn -typedef struct { - IN_ADDR TunnelIpv4; - IN_ADDR InternetIpv4; - - IN6_ADDR TunnelIpv6; - IN6_ADDR InternetIpv6; -} IP_ADDRESSES_CONFIG; - -// Used to Define Which Processes are alive on activation -typedef struct { - SIZE_T NumEntries; - SIZE_T TotalLength; -} PROCESS_DISCOVERY_HEADER; - -typedef struct { - HANDLE ProcessId; - HANDLE ParentProcessId; - - SIZE_T ImageNameOffset; - USHORT ImageNameLength; -} PROCESS_DISCOVERY_ENTRY; - -typedef struct { - DWORD ProcessId; - DWORD ParentProcessId; - FILETIME CreationTime; - std::wstring DevicePath; -} ProcessInfo; - -class WindowsSplitTunnel final : public QObject { - Q_OBJECT - Q_DISABLE_COPY_MOVE(WindowsSplitTunnel) +class WindowsSplitTunnel final { public: - explicit WindowsSplitTunnel(QObject* parent); + /** + * @brief Installs and Initializes the Split Tunnel Driver. + * + * @param fw - + * @return std::unique_ptr - Is null on failure. + */ + static std::unique_ptr create(WindowsFirewall* fw); + + /** + * @brief Construct a new Windows Split Tunnel object + * + * @param driverIO - The Handle to the Driver's IO file, it assumes the driver + * is in STATE_INITIALIZED and the Firewall has been setup. + * Prefer using create() to get to this state. + */ + WindowsSplitTunnel(HANDLE driverIO); + /** + * @brief Destroy the Windows Split Tunnel object and uninstalls the Driver. + */ ~WindowsSplitTunnel(); // void excludeApps(const QStringList& paths); // Excludes an Application from the VPN - void setRules(const QStringList& appPaths); + bool excludeApps(const QStringList& appPaths); // Fetches and Pushed needed info to move to engaged mode - void start(int inetAdapterIndex, int vpnAdapterIndex = 0); + bool start(int inetAdapterIndex, int vpnAdapterIndex = 0); // Deletes Rules and puts the driver into passive mode void stop(); - // Resets the Whole Driver - void reset(); - // Just close connection, leave state as is - void close(); + // Returns true if the split-tunnel driver is now up and running. + bool isRunning(); + static bool detectConflict(); + + // States for GetState + enum DRIVER_STATE { + STATE_UNKNOWN = -1, + STATE_NONE = 0, + STATE_STARTED = 1, + STATE_INITIALIZED = 2, + STATE_READY = 3, + STATE_RUNNING = 4, + STATE_ZOMBIE = 5, + }; + + private: // Installes the Kernel Driver as Driver Service static SC_HANDLE installDriver(); static bool uninstallDriver(); static bool isInstalled(); - static bool detectConflict(); + static bool initDriver(HANDLE driverIO); + static DRIVER_STATE getState(HANDLE driverIO); + static bool resetDriver(HANDLE driverIO); - private slots: - void initDriver(); - - private: HANDLE m_driver = INVALID_HANDLE_VALUE; - constexpr static const auto DRIVER_SYMLINK = L"\\\\.\\MULLVADSPLITTUNNEL"; - constexpr static const auto DRIVER_FILENAME = "mullvad-split-tunnel.sys"; - constexpr static const auto DRIVER_SERVICE_NAME = L"AmneziaVPNSplitTunnel"; - constexpr static const auto MV_SERVICE_NAME = L"MullvadVPN"; DRIVER_STATE getState(); - - int m_tries; - // Initializes the WFP Sublayer - bool initSublayer(); + QString stateString(); // Generates a Configuration for Each APP std::vector generateAppConfiguration(const QStringList& appPaths); // Generates a Configuration which IP's are VPN and which network - std::vector generateIPConfiguration(int inetAdapterIndex, int vpnAdapterIndex = 0); + std::vector generateIPConfiguration(int inetAdapterIndex, int vpnAdapterIndex = 0); std::vector generateProcessBlob(); - void getAddress(int adapterIndex, IN_ADDR* out_ipv4, IN6_ADDR* out_ipv6); + [[nodiscard]] bool getAddress(int adapterIndex, IN_ADDR* out_ipv4, + IN6_ADDR* out_ipv6); // Collects info about an Opened Process - ProcessInfo getProcessInfo(HANDLE process, - const PROCESSENTRY32W& processMeta); // Converts a path to a Dos Path: // e.g C:/a.exe -> /harddisk0/a.exe diff --git a/client/platforms/windows/daemon/wireguardutilswindows.cpp b/client/platforms/windows/daemon/wireguardutilswindows.cpp index 1a220235..0823b9d7 100644 --- a/client/platforms/windows/daemon/wireguardutilswindows.cpp +++ b/client/platforms/windows/daemon/wireguardutilswindows.cpp @@ -24,8 +24,20 @@ namespace { Logger logger("WireguardUtilsWindows"); }; // namespace -WireguardUtilsWindows::WireguardUtilsWindows(QObject* parent) - : WireguardUtils(parent), m_tunnel(this), m_routeMonitor(this) { +std::unique_ptr WireguardUtilsWindows::create( + WindowsFirewall* fw, QObject* parent) { + if (!fw) { + logger.error() << "WireguardUtilsWindows::create: no wfp handle"; + return {}; + } + + // Can't use make_unique here as the Constructor is private :( + auto utils = new WireguardUtilsWindows(parent, fw); + return std::unique_ptr(utils); +} + +WireguardUtilsWindows::WireguardUtilsWindows(QObject* parent, WindowsFirewall* fw) + : WireguardUtils(parent), m_tunnel(this), m_firewall(fw) { MZ_COUNT_CTOR(WireguardUtilsWindows); logger.debug() << "WireguardUtilsWindows created."; @@ -114,13 +126,13 @@ bool WireguardUtilsWindows::addInterface(const InterfaceConfig& config) { return false; } m_luid = luid.Value; - m_routeMonitor.setLuid(luid.Value); + m_routeMonitor = new WindowsRouteMonitor(luid.Value, this); if (config.m_killSwitchEnabled) { // Enable the windows firewall NET_IFINDEX ifindex; ConvertInterfaceLuidToIndex(&luid, &ifindex); - WindowsFirewall::instance()->enableKillSwitch(ifindex); + m_firewall->enableInterface(ifindex); } logger.debug() << "Registration completed"; @@ -128,7 +140,11 @@ bool WireguardUtilsWindows::addInterface(const InterfaceConfig& config) { } bool WireguardUtilsWindows::deleteInterface() { - WindowsFirewall::instance()->disableKillSwitch(); + if (m_routeMonitor) { + m_routeMonitor->deleteLater(); + } + + m_firewall->disableKillSwitch(); m_tunnel.stop(); return true; } @@ -141,7 +157,7 @@ bool WireguardUtilsWindows::updatePeer(const InterfaceConfig& config) { if (config.m_killSwitchEnabled) { // Enable the windows firewall for this peer. - WindowsFirewall::instance()->enablePeerTraffic(config); + m_firewall->enablePeerTraffic(config); } logger.debug() << "Configuring peer" << publicKey.toHex() << "via" << config.m_serverIpv4AddrIn; @@ -171,9 +187,9 @@ bool WireguardUtilsWindows::updatePeer(const InterfaceConfig& config) { } // Exclude the server address, except for multihop exit servers. - if (config.m_hopType != InterfaceConfig::MultiHopExit) { - m_routeMonitor.addExclusionRoute(IPAddress(config.m_serverIpv4AddrIn)); - m_routeMonitor.addExclusionRoute(IPAddress(config.m_serverIpv6AddrIn)); + if (m_routeMonitor && config.m_hopType != InterfaceConfig::MultiHopExit) { + m_routeMonitor->addExclusionRoute(IPAddress(config.m_serverIpv4AddrIn)); + m_routeMonitor->addExclusionRoute(IPAddress(config.m_serverIpv6AddrIn)); } QString reply = m_tunnel.uapiCommand(message); @@ -186,13 +202,13 @@ bool WireguardUtilsWindows::deletePeer(const InterfaceConfig& config) { QByteArray::fromBase64(qPrintable(config.m_serverPublicKey)); // Clear exclustion routes for this peer. - if (config.m_hopType != InterfaceConfig::MultiHopExit) { - m_routeMonitor.deleteExclusionRoute(IPAddress(config.m_serverIpv4AddrIn)); - m_routeMonitor.deleteExclusionRoute(IPAddress(config.m_serverIpv6AddrIn)); + if (m_routeMonitor && config.m_hopType != InterfaceConfig::MultiHopExit) { + m_routeMonitor->deleteExclusionRoute(IPAddress(config.m_serverIpv4AddrIn)); + m_routeMonitor->deleteExclusionRoute(IPAddress(config.m_serverIpv6AddrIn)); } // Disable the windows firewall for this peer. - WindowsFirewall::instance()->disablePeerTraffic(config.m_serverPublicKey); + m_firewall->disablePeerTraffic(config.m_serverPublicKey); QString message; QTextStream out(&message); @@ -238,6 +254,13 @@ void WireguardUtilsWindows::buildMibForwardRow(const IPAddress& prefix, } bool WireguardUtilsWindows::updateRoutePrefix(const IPAddress& prefix) { + if (m_routeMonitor && (prefix.prefixLength() == 0)) { + // If we are setting up a default route, instruct the route monitor to + // capture traffic to all non-excluded destinations + m_routeMonitor->setDetaultRouteCapture(true); + } + // Build the route + MIB_IPFORWARD_ROW2 entry; buildMibForwardRow(prefix, &entry); @@ -255,6 +278,12 @@ bool WireguardUtilsWindows::updateRoutePrefix(const IPAddress& prefix) { } bool WireguardUtilsWindows::deleteRoutePrefix(const IPAddress& prefix) { + if (m_routeMonitor && (prefix.prefixLength() == 0)) { + // Deactivate the route capture feature. + m_routeMonitor->setDetaultRouteCapture(false); + } + // Build the route + MIB_IPFORWARD_ROW2 entry; buildMibForwardRow(prefix, &entry); @@ -272,9 +301,28 @@ bool WireguardUtilsWindows::deleteRoutePrefix(const IPAddress& prefix) { } bool WireguardUtilsWindows::addExclusionRoute(const IPAddress& prefix) { - return m_routeMonitor.addExclusionRoute(prefix); + return m_routeMonitor->addExclusionRoute(prefix); } bool WireguardUtilsWindows::deleteExclusionRoute(const IPAddress& prefix) { - return m_routeMonitor.deleteExclusionRoute(prefix); + return m_routeMonitor->deleteExclusionRoute(prefix); +} + +bool WireguardUtilsWindows::excludeLocalNetworks( + const QList& addresses) { + // If the interface isn't up then something went horribly wrong. + Q_ASSERT(m_routeMonitor); + // For each destination - attempt to exclude it from the VPN tunnel. + bool result = true; + for (const IPAddress& prefix : addresses) { + if (!m_routeMonitor->addExclusionRoute(prefix)) { + result = false; + } + } + // Permit LAN traffic through the firewall. + if (!m_firewall->enableLanBypass(addresses)) { + result = false; + } + + return result; } diff --git a/client/platforms/windows/daemon/wireguardutilswindows.h b/client/platforms/windows/daemon/wireguardutilswindows.h index 4fd67fad..276966b4 100644 --- a/client/platforms/windows/daemon/wireguardutilswindows.h +++ b/client/platforms/windows/daemon/wireguardutilswindows.h @@ -9,16 +9,21 @@ #include #include +#include #include "daemon/wireguardutils.h" #include "windowsroutemonitor.h" #include "windowstunnelservice.h" +class WindowsFirewall; +class WindowsRouteMonitor; + class WireguardUtilsWindows final : public WireguardUtils { Q_OBJECT public: - WireguardUtilsWindows(QObject* parent); + static std::unique_ptr create(WindowsFirewall* fw, + QObject* parent); ~WireguardUtilsWindows(); bool interfaceExists() override { return m_tunnel.isRunning(); } @@ -39,15 +44,19 @@ class WireguardUtilsWindows final : public WireguardUtils { bool addExclusionRoute(const IPAddress& prefix) override; bool deleteExclusionRoute(const IPAddress& prefix) override; + bool WireguardUtilsWindows::excludeLocalNetworks(const QList& addresses) override; + signals: void backendFailure(); private: + WireguardUtilsWindows(QObject* parent, WindowsFirewall* fw); void buildMibForwardRow(const IPAddress& prefix, void* row); quint64 m_luid = 0; WindowsTunnelService m_tunnel; - WindowsRouteMonitor m_routeMonitor; + QPointer m_routeMonitor; + QPointer m_firewall; }; #endif // WIREGUARDUTILSWINDOWS_H diff --git a/client/platforms/windows/windowsservicemanager.cpp b/client/platforms/windows/windowsservicemanager.cpp index 3a334224..d5a21170 100644 --- a/client/platforms/windows/windowsservicemanager.cpp +++ b/client/platforms/windows/windowsservicemanager.cpp @@ -4,6 +4,7 @@ #include "windowsservicemanager.h" +#include #include #include "Windows.h" @@ -16,35 +17,44 @@ namespace { Logger logger("WindowsServiceManager"); } -WindowsServiceManager::WindowsServiceManager(LPCWSTR serviceName) { +WindowsServiceManager::WindowsServiceManager(SC_HANDLE serviceManager, + SC_HANDLE service) + : QObject(qApp), m_serviceManager(serviceManager), m_service(service) { + m_timer.setSingleShot(false); +} + +std::unique_ptr WindowsServiceManager::open( + const QString serviceName) { + LPCWSTR service = (const wchar_t*)serviceName.utf16(); + DWORD err = NULL; auto scm_rights = SC_MANAGER_CONNECT | SC_MANAGER_ENUMERATE_SERVICE | SC_MANAGER_QUERY_LOCK_STATUS | STANDARD_RIGHTS_READ; - m_serviceManager = OpenSCManager(NULL, // local computer - NULL, // servicesActive database - scm_rights); + auto manager = OpenSCManager(NULL, // local computer + NULL, // servicesActive database + scm_rights); err = GetLastError(); if (err != NULL) { logger.error() << " OpenSCManager failed code: " << err; - return; + return {}; } logger.debug() << "OpenSCManager access given - " << err; - logger.debug() << "Opening Service - " - << QString::fromWCharArray(serviceName); + logger.debug() << "Opening Service - " << serviceName; // Try to get an elevated handle - m_service = OpenService(m_serviceManager, // SCM database - serviceName, // name of service - (GENERIC_READ | SERVICE_START | SERVICE_STOP)); + auto serviceHandle = + OpenService(manager, // SCM database + service, // name of service + (GENERIC_READ | SERVICE_START | SERVICE_STOP)); err = GetLastError(); if (err != NULL) { + CloseServiceHandle(manager); WindowsUtils::windowsLog("OpenService failed"); - return; + return {}; } - m_has_access = true; - m_timer.setSingleShot(false); logger.debug() << "Service manager execute access granted"; + return std::make_unique(manager, serviceHandle); } WindowsServiceManager::~WindowsServiceManager() { @@ -85,10 +95,6 @@ bool WindowsServiceManager::startPolling(DWORD goal_state, int max_wait_sec) { SERVICE_STATUS_PROCESS WindowsServiceManager::getStatus() { SERVICE_STATUS_PROCESS serviceStatus; - if (!m_has_access) { - logger.debug() << "Need read access to get service state"; - return serviceStatus; - } DWORD dwBytesNeeded; // Contains missing bytes if struct is too small? QueryServiceStatusEx(m_service, // handle to service SC_STATUS_PROCESS_INFO, // information level @@ -119,10 +125,6 @@ bool WindowsServiceManager::startService() { } bool WindowsServiceManager::stopService() { - if (!m_has_access) { - logger.error() << "Need execute access to stop services"; - return false; - } auto state = getStatus().dwCurrentState; if (state != SERVICE_RUNNING && state != SERVICE_START_PENDING) { logger.warning() << ("Service stop not possible, as its not running"); diff --git a/client/platforms/windows/windowsservicemanager.h b/client/platforms/windows/windowsservicemanager.h index 7638588f..31521245 100644 --- a/client/platforms/windows/windowsservicemanager.h +++ b/client/platforms/windows/windowsservicemanager.h @@ -12,7 +12,7 @@ #include "Winsvc.h" /** - * @brief The WindowsServiceManager provides control over the MozillaVPNBroker + * @brief The WindowsServiceManager provides control over the a * service via SCM */ class WindowsServiceManager : public QObject { @@ -20,7 +20,10 @@ class WindowsServiceManager : public QObject { Q_DISABLE_COPY_MOVE(WindowsServiceManager) public: - WindowsServiceManager(LPCWSTR serviceName); + // Creates a WindowsServiceManager for the Named service. + // returns nullptr if + static std::unique_ptr open(const QString serviceName); + WindowsServiceManager(SC_HANDLE serviceManager, SC_HANDLE service); ~WindowsServiceManager(); // true if the Service is running @@ -45,8 +48,6 @@ class WindowsServiceManager : public QObject { // See // SERVICE_STOPPED,SERVICE_STOP_PENDING,SERVICE_START_PENDING,SERVICE_RUNNING SERVICE_STATUS_PROCESS getStatus(); - bool m_has_access = false; - LPWSTR m_serviceName; SC_HANDLE m_serviceManager; SC_HANDLE m_service; // Service handle with r/w priv. DWORD m_state_target; diff --git a/client/protocols/xrayprotocol.cpp b/client/protocols/xrayprotocol.cpp index 2dfbcc21..7c69ccde 100755 --- a/client/protocols/xrayprotocol.cpp +++ b/client/protocols/xrayprotocol.cpp @@ -1,7 +1,6 @@ #include "xrayprotocol.h" #include "utilities.h" -#include "containers/containers_defs.h" #include "core/networkUtilities.h" #include @@ -22,9 +21,8 @@ XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent): XrayProtocol::~XrayProtocol() { + qDebug() << "XrayProtocol::~XrayProtocol()"; XrayProtocol::stop(); - QThread::msleep(200); - m_xrayProcess.close(); } ErrorCode XrayProtocol::start() @@ -36,10 +34,6 @@ ErrorCode XrayProtocol::start() return lastError(); } - if (Utils::processIsRunning(Utils::executable(xrayExecPath(), true))) { - Utils::killProcessByName(Utils::executable(xrayExecPath(), true)); - } - #ifdef QT_DEBUG m_xrayCfgFile.setAutoRemove(false); #endif @@ -54,9 +48,16 @@ ErrorCode XrayProtocol::start() qDebug().noquote() << "XrayProtocol::start()" << xrayExecPath() << args.join(" "); - m_xrayProcess.setProcessChannelMode(QProcess::MergedChannels); + + m_xrayProcess.setProcessChannelMode(QProcess::MergedChannels); m_xrayProcess.setProgram(xrayExecPath()); + + if (Utils::processIsRunning(Utils::executable("xray", false))) { + qDebug().noquote() << "kill previos xray"; + Utils::killProcessByName(Utils::executable("xray", false)); + } + m_xrayProcess.setArguments(args); connect(&m_xrayProcess, &QProcess::readyReadStandardOutput, this, [this]() { @@ -68,13 +69,9 @@ ErrorCode XrayProtocol::start() connect(&m_xrayProcess, QOverload::of(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) { qDebug().noquote() << "XrayProtocol finished, exitCode, exitStatus" << exitCode << exitStatus; setConnectionState(Vpn::ConnectionState::Disconnected); - if (exitStatus != QProcess::NormalExit) { - emit protocolError(amnezia::ErrorCode::XrayExecutableCrashed); - stop(); - } - if (exitCode != 0) { - emit protocolError(amnezia::ErrorCode::InternalError); - stop(); + if ((exitStatus != QProcess::NormalExit) || (exitCode != 0)) { + emit protocolError(amnezia::ErrorCode::XrayExecutableCrashed); + emit setConnectionState(Vpn::ConnectionState::Error); } }); @@ -177,14 +174,14 @@ void XrayProtocol::stop() IpcClient::Interface()->StartRoutingIpv6(); #endif qDebug() << "XrayProtocol::stop()"; - m_xrayProcess.terminate(); + m_xrayProcess.disconnect(); + m_xrayProcess.kill(); + m_xrayProcess.waitForFinished(3000); if (m_t2sProcess) { m_t2sProcess->stop(); } -#ifdef Q_OS_WIN - Utils::signalCtrl(m_xrayProcess.processId(), CTRL_C_EVENT); -#endif + setConnectionState(Vpn::ConnectionState::Disconnected); } QString XrayProtocol::xrayExecPath() diff --git a/client/resources.qrc b/client/resources.qrc index a10a784d..ff03a6e7 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -1,225 +1,229 @@ + fonts/pt-root-ui_vf.ttf + images/amneziaBigLogo.png + images/AmneziaVPN.png + images/controls/alert-circle.svg + images/controls/amnezia.svg + images/controls/app.svg + images/controls/archive-restore.svg + images/controls/arrow-left.svg + images/controls/arrow-right.svg + images/controls/bug.svg + images/controls/check.svg + images/controls/chevron-down.svg + images/controls/chevron-right.svg + images/controls/chevron-up.svg + images/controls/close.svg + images/controls/copy.svg + images/controls/delete.svg + images/controls/download.svg + images/controls/edit-3.svg + images/controls/eye-off.svg + images/controls/eye.svg + images/controls/external-link.svg + images/controls/file-check-2.svg + images/controls/file-cog-2.svg + images/controls/folder-open.svg + images/controls/folder-search-2.svg + images/controls/gauge.svg + images/controls/github.svg + images/controls/help-circle.svg + images/controls/history.svg + images/controls/home.svg + images/controls/info.svg + images/controls/mail.svg + images/controls/map-pin.svg + images/controls/more-vertical.svg + images/controls/plus.svg + images/controls/qr-code.svg + images/controls/radio-button-inner-circle-pressed.png + images/controls/radio-button-inner-circle.png + images/controls/radio-button-pressed.svg + images/controls/radio-button.svg + images/controls/radio.svg + images/controls/refresh-cw.svg + images/controls/save.svg + images/controls/scan-line.svg + images/controls/search.svg + images/controls/server.svg + images/controls/settings-2.svg + images/controls/settings.svg + images/controls/share-2.svg + images/controls/split-tunneling.svg + images/controls/tag.svg + images/controls/telegram.svg + images/controls/text-cursor.svg + images/controls/trash.svg + images/controls/x-circle.svg images/tray/active.png images/tray/default.png images/tray/error.png - images/AmneziaVPN.png - server_scripts/remove_container.sh - server_scripts/setup_host_firewall.sh - server_scripts/openvpn_cloak/Dockerfile + server_scripts/awg/configure_container.sh + server_scripts/awg/Dockerfile + server_scripts/awg/run_container.sh + server_scripts/awg/start.sh + server_scripts/awg/template.conf + server_scripts/build_container.sh + server_scripts/check_connection.sh + server_scripts/check_server_is_busy.sh + server_scripts/check_user_in_sudo.sh + server_scripts/dns/configure_container.sh + server_scripts/dns/Dockerfile + server_scripts/dns/run_container.sh + server_scripts/install_docker.sh + server_scripts/ipsec/configure_container.sh + server_scripts/ipsec/Dockerfile + server_scripts/ipsec/mobileconfig.plist + server_scripts/ipsec/run_container.sh + server_scripts/ipsec/start.sh + server_scripts/ipsec/strongswan.profile server_scripts/openvpn_cloak/configure_container.sh + server_scripts/openvpn_cloak/Dockerfile + server_scripts/openvpn_cloak/run_container.sh server_scripts/openvpn_cloak/start.sh server_scripts/openvpn_cloak/template.ovpn - server_scripts/install_docker.sh - server_scripts/build_container.sh - server_scripts/prepare_host.sh - server_scripts/check_connection.sh - server_scripts/remove_all_containers.sh - server_scripts/openvpn_cloak/run_container.sh - server_scripts/openvpn/configure_container.sh - server_scripts/openvpn/run_container.sh - server_scripts/openvpn/template.ovpn - server_scripts/openvpn/Dockerfile - server_scripts/openvpn/start.sh server_scripts/openvpn_shadowsocks/configure_container.sh server_scripts/openvpn_shadowsocks/Dockerfile server_scripts/openvpn_shadowsocks/run_container.sh server_scripts/openvpn_shadowsocks/start.sh server_scripts/openvpn_shadowsocks/template.ovpn + server_scripts/openvpn/configure_container.sh + server_scripts/openvpn/Dockerfile + server_scripts/openvpn/run_container.sh + server_scripts/openvpn/start.sh + server_scripts/openvpn/template.ovpn + server_scripts/prepare_host.sh + server_scripts/remove_all_containers.sh + server_scripts/remove_container.sh + server_scripts/setup_host_firewall.sh + server_scripts/sftp/configure_container.sh + server_scripts/sftp/Dockerfile + server_scripts/sftp/run_container.sh + server_scripts/socks5_proxy/configure_container.sh + server_scripts/socks5_proxy/Dockerfile + server_scripts/socks5_proxy/run_container.sh + server_scripts/socks5_proxy/start.sh + server_scripts/website_tor/configure_container.sh + server_scripts/website_tor/Dockerfile + server_scripts/website_tor/run_container.sh server_scripts/wireguard/configure_container.sh server_scripts/wireguard/Dockerfile server_scripts/wireguard/run_container.sh server_scripts/wireguard/start.sh server_scripts/wireguard/template.conf - server_scripts/website_tor/configure_container.sh - server_scripts/website_tor/run_container.sh - ui/qml/Config/GlobalConfig.qml - ui/qml/Config/qmldir - server_scripts/check_server_is_busy.sh - server_scripts/dns/configure_container.sh - server_scripts/dns/Dockerfile - server_scripts/dns/run_container.sh - server_scripts/sftp/configure_container.sh - server_scripts/sftp/Dockerfile - server_scripts/sftp/run_container.sh - server_scripts/ipsec/configure_container.sh - server_scripts/ipsec/Dockerfile - server_scripts/ipsec/run_container.sh - server_scripts/ipsec/start.sh - server_scripts/ipsec/mobileconfig.plist - server_scripts/ipsec/strongswan.profile - server_scripts/website_tor/Dockerfile - server_scripts/check_user_in_sudo.sh - ui/qml/Controls2/BasicButtonType.qml - ui/qml/Controls2/TextFieldWithHeaderType.qml - ui/qml/Controls2/LabelWithButtonType.qml - images/controls/arrow-right.svg - images/controls/chevron-right.svg - ui/qml/Controls2/ImageButtonType.qml - ui/qml/Controls2/CardType.qml - ui/qml/Controls2/CheckBoxType.qml - images/controls/check.svg - ui/qml/Controls2/DropDownType.qml - ui/qml/Pages2/PageSetupWizardStart.qml - ui/qml/main2.qml - images/amneziaBigLogo.png - ui/qml/Controls2/FlickableType.qml - ui/qml/Pages2/PageSetupWizardCredentials.qml - ui/qml/Controls2/HeaderType.qml - images/controls/arrow-left.svg - ui/qml/Pages2/PageSetupWizardProtocols.qml - ui/qml/Pages2/PageSetupWizardEasy.qml - images/controls/chevron-down.svg - images/controls/chevron-up.svg - ui/qml/Controls2/TextTypes/ParagraphTextType.qml - ui/qml/Controls2/TextTypes/Header2TextType.qml - ui/qml/Controls2/HorizontalRadioButton.qml - ui/qml/Controls2/VerticalRadioButton.qml - ui/qml/Controls2/SwitcherType.qml - ui/qml/Controls2/TabButtonType.qml - ui/qml/Pages2/PageSetupWizardProtocolSettings.qml - ui/qml/Pages2/PageSetupWizardInstalling.qml - ui/qml/Pages2/PageSetupWizardConfigSource.qml - images/controls/folder-open.svg - images/controls/qr-code.svg - images/controls/text-cursor.svg - ui/qml/Pages2/PageSetupWizardTextKey.qml - ui/qml/Pages2/PageStart.qml - ui/qml/Controls2/TabImageButtonType.qml - images/controls/home.svg - images/controls/settings-2.svg - images/controls/share-2.svg - ui/qml/Pages2/PageHome.qml - ui/qml/Pages2/PageSettingsServersList.qml - ui/qml/Pages2/PageShare.qml - ui/qml/Controls2/TextTypes/Header1TextType.qml - ui/qml/Controls2/TextTypes/LabelTextType.qml - ui/qml/Controls2/TextTypes/ButtonTextType.qml - ui/qml/Controls2/Header2Type.qml - images/controls/plus.svg - ui/qml/Components/ConnectButton.qml - images/controls/download.svg - ui/qml/Controls2/ProgressBarType.qml - ui/qml/Components/ConnectionTypeSelectionDrawer.qml - ui/qml/Components/HomeContainersListView.qml - ui/qml/Controls2/TextTypes/CaptionTextType.qml - images/controls/settings.svg - ui/qml/Pages2/PageSettingsServerInfo.qml - ui/qml/Controls2/PageType.qml - ui/qml/Controls2/PopupType.qml - images/controls/edit-3.svg - ui/qml/Pages2/PageSettingsServerData.qml - ui/qml/Components/SettingsContainersListView.qml - ui/qml/Controls2/TextTypes/ListItemTitleType.qml - ui/qml/Controls2/DividerType.qml - ui/qml/Controls2/StackViewType.qml - ui/qml/Pages2/PageSettings.qml - images/controls/amnezia.svg - images/controls/app.svg - images/controls/radio.svg - images/controls/save.svg - images/controls/server.svg - ui/qml/Pages2/PageSettingsServerProtocols.qml - ui/qml/Pages2/PageSettingsServerServices.qml - ui/qml/Pages2/PageSetupWizardViewConfig.qml - images/controls/file-cog-2.svg - ui/qml/Components/QuestionDrawer.qml - ui/qml/Pages2/PageDeinstalling.qml - ui/qml/Controls2/BackButtonType.qml - ui/qml/Pages2/PageSettingsServerProtocol.qml - ui/qml/Components/TransportProtoSelector.qml - ui/qml/Controls2/ListViewWithRadioButtonType.qml - images/controls/radio-button.svg - images/controls/radio-button-inner-circle.png - images/controls/radio-button-pressed.svg - images/controls/radio-button-inner-circle-pressed.png - ui/qml/Components/ShareConnectionDrawer.qml - ui/qml/Pages2/PageSettingsConnection.qml - ui/qml/Pages2/PageSettingsDns.qml - ui/qml/Pages2/PageSettingsApplication.qml - ui/qml/Pages2/PageSettingsBackup.qml - images/controls/delete.svg - ui/qml/Pages2/PageSettingsAbout.qml - images/controls/github.svg - images/controls/mail.svg - images/controls/telegram.svg - ui/qml/Controls2/TextTypes/SmallTextType.qml - ui/qml/Filters/ContainersModelFilters.qml - ui/qml/Components/SelectLanguageDrawer.qml - ui/qml/Controls2/BusyIndicatorType.qml - ui/qml/Pages2/PageProtocolOpenVpnSettings.qml - ui/qml/Pages2/PageProtocolShadowSocksSettings.qml - ui/qml/Pages2/PageProtocolCloakSettings.qml - ui/qml/Pages2/PageProtocolXraySettings.qml - ui/qml/Pages2/PageProtocolRaw.qml - ui/qml/Pages2/PageSettingsLogging.qml - ui/qml/Pages2/PageServiceSftpSettings.qml - images/controls/copy.svg - ui/qml/Pages2/PageServiceTorWebsiteSettings.qml - ui/qml/Pages2/PageSetupWizardQrReader.qml - images/controls/eye.svg - images/controls/eye-off.svg - ui/qml/Pages2/PageSettingsSplitTunneling.qml - ui/qml/Controls2/ContextMenuType.qml - ui/qml/Controls2/TextAreaType.qml - images/controls/trash.svg - images/controls/more-vertical.svg - ui/qml/Controls2/ListViewWithLabelsType.qml - ui/qml/Pages2/PageServiceDnsSettings.qml - ui/qml/Controls2/TopCloseButtonType.qml - images/controls/x-circle.svg - ui/qml/Pages2/PageProtocolAwgSettings.qml - server_scripts/awg/template.conf - server_scripts/awg/start.sh - server_scripts/awg/configure_container.sh - server_scripts/awg/run_container.sh - server_scripts/awg/Dockerfile - ui/qml/Pages2/PageShareFullAccess.qml - images/controls/close.svg - images/controls/search.svg server_scripts/xray/configure_container.sh server_scripts/xray/Dockerfile server_scripts/xray/run_container.sh server_scripts/xray/start.sh server_scripts/xray/template.json - ui/qml/Pages2/PageProtocolWireGuardSettings.qml + ui/qml/Components/AdLabel.qml + ui/qml/Components/ConnectButton.qml + ui/qml/Components/ConnectionTypeSelectionDrawer.qml + ui/qml/Components/HomeContainersListView.qml ui/qml/Components/HomeSplitTunnelingDrawer.qml - images/controls/split-tunneling.svg - ui/qml/Controls2/DrawerType2.qml - ui/qml/Pages2/PageSettingsAppSplitTunneling.qml ui/qml/Components/InstalledAppsDrawer.qml - images/controls/alert-circle.svg - images/controls/file-check-2.svg + ui/qml/Components/QuestionDrawer.qml + ui/qml/Components/SelectLanguageDrawer.qml + ui/qml/Components/ServersListView.qml + ui/qml/Components/SettingsContainersListView.qml + ui/qml/Components/ShareConnectionDrawer.qml + ui/qml/Components/TransportProtoSelector.qml + ui/qml/Config/GlobalConfig.qml + ui/qml/Config/qmldir + ui/qml/Controls2/BackButtonType.qml + ui/qml/Controls2/BasicButtonType.qml + ui/qml/Controls2/BusyIndicatorType.qml + ui/qml/Controls2/CardType.qml + ui/qml/Controls2/CardWithIconsType.qml + ui/qml/Controls2/CheckBoxType.qml + ui/qml/Controls2/ContextMenuType.qml + ui/qml/Controls2/DividerType.qml + ui/qml/Controls2/DrawerType2.qml + ui/qml/Controls2/DropDownType.qml + ui/qml/Controls2/FlickableType.qml + ui/qml/Controls2/Header2Type.qml + ui/qml/Controls2/HeaderType.qml + ui/qml/Controls2/HorizontalRadioButton.qml + ui/qml/Controls2/ImageButtonType.qml + ui/qml/Controls2/LabelWithButtonType.qml + ui/qml/Controls2/LabelWithImageType.qml + ui/qml/Controls2/ListViewWithLabelsType.qml + ui/qml/Controls2/ListViewWithRadioButtonType.qml + ui/qml/Controls2/PageType.qml + ui/qml/Controls2/PopupType.qml + ui/qml/Controls2/ProgressBarType.qml + ui/qml/Controls2/ScrollBarType.qml + ui/qml/Controls2/StackViewType.qml + ui/qml/Controls2/SwitcherType.qml + ui/qml/Controls2/TabButtonType.qml + ui/qml/Controls2/TabImageButtonType.qml + ui/qml/Controls2/TextAreaType.qml + ui/qml/Controls2/TextAreaWithFooterType.qml + ui/qml/Controls2/TextFieldWithHeaderType.qml + ui/qml/Controls2/TextTypes/ButtonTextType.qml + ui/qml/Controls2/TextTypes/CaptionTextType.qml + ui/qml/Controls2/TextTypes/Header1TextType.qml + ui/qml/Controls2/TextTypes/Header2TextType.qml + ui/qml/Controls2/TextTypes/LabelTextType.qml + ui/qml/Controls2/TextTypes/ListItemTitleType.qml + ui/qml/Controls2/TextTypes/ParagraphTextType.qml + ui/qml/Controls2/TextTypes/SmallTextType.qml + ui/qml/Controls2/TopCloseButtonType.qml + ui/qml/Controls2/VerticalRadioButton.qml ui/qml/Controls2/WarningType.qml - fonts/pt-root-ui_vf.ttf - ui/qml/Modules/Style/qmldir + ui/qml/Filters/ContainersModelFilters.qml + ui/qml/main2.qml ui/qml/Modules/Style/AmneziaStyle.qml + ui/qml/Modules/Style/qmldir + ui/qml/Pages2/PageDeinstalling.qml + ui/qml/Pages2/PageDevMenu.qml + ui/qml/Pages2/PageHome.qml + ui/qml/Pages2/PageProtocolAwgSettings.qml + ui/qml/Pages2/PageProtocolCloakSettings.qml + ui/qml/Pages2/PageProtocolOpenVpnSettings.qml + ui/qml/Pages2/PageProtocolRaw.qml + ui/qml/Pages2/PageProtocolShadowSocksSettings.qml + ui/qml/Pages2/PageProtocolWireGuardSettings.qml + ui/qml/Pages2/PageProtocolXraySettings.qml + ui/qml/Pages2/PageServiceDnsSettings.qml + ui/qml/Pages2/PageServiceSftpSettings.qml ui/qml/Pages2/PageServiceSocksProxySettings.qml - server_scripts/socks5_proxy/run_container.sh - server_scripts/socks5_proxy/Dockerfile - server_scripts/socks5_proxy/configure_container.sh - server_scripts/socks5_proxy/start.sh + ui/qml/Pages2/PageServiceTorWebsiteSettings.qml + ui/qml/Pages2/PageSettings.qml + ui/qml/Pages2/PageSettingsAbout.qml + ui/qml/Pages2/PageSettingsApiLanguageList.qml + ui/qml/Pages2/PageSettingsApiServerInfo.qml + ui/qml/Pages2/PageSettingsApplication.qml + ui/qml/Pages2/PageSettingsAppSplitTunneling.qml + ui/qml/Pages2/PageSettingsBackup.qml + ui/qml/Pages2/PageSettingsConnection.qml + ui/qml/Pages2/PageSettingsDns.qml + ui/qml/Pages2/PageSettingsLogging.qml + ui/qml/Pages2/PageSettingsServerData.qml + ui/qml/Pages2/PageSettingsServerInfo.qml + ui/qml/Pages2/PageSettingsServerProtocol.qml + ui/qml/Pages2/PageSettingsServerProtocols.qml + ui/qml/Pages2/PageSettingsServerServices.qml + ui/qml/Pages2/PageSettingsServersList.qml + ui/qml/Pages2/PageSettingsSplitTunneling.qml ui/qml/Pages2/PageProtocolAwgClientSettings.qml ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml - ui/qml/Pages2/PageSetupWizardApiServicesList.qml ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml - ui/qml/Controls2/CardWithIconsType.qml - images/controls/tag.svg - images/controls/history.svg - images/controls/gauge.svg - images/controls/map-pin.svg - ui/qml/Controls2/LabelWithImageType.qml - images/controls/info.svg - ui/qml/Controls2/TextAreaWithFooterType.qml - images/controls/scan-line.svg - images/controls/folder-search-2.svg - ui/qml/Pages2/PageSettingsApiServerInfo.qml - images/controls/bug.svg - ui/qml/Pages2/PageDevMenu.qml - images/controls/refresh-cw.svg - ui/qml/Pages2/PageSettingsApiLanguageList.qml - images/controls/archive-restore.svg - images/controls/help-circle.svg + ui/qml/Pages2/PageSetupWizardApiServicesList.qml + ui/qml/Pages2/PageSetupWizardConfigSource.qml + ui/qml/Pages2/PageSetupWizardCredentials.qml + ui/qml/Pages2/PageSetupWizardEasy.qml + ui/qml/Pages2/PageSetupWizardInstalling.qml + ui/qml/Pages2/PageSetupWizardProtocols.qml + ui/qml/Pages2/PageSetupWizardProtocolSettings.qml + ui/qml/Pages2/PageSetupWizardQrReader.qml + ui/qml/Pages2/PageSetupWizardStart.qml + ui/qml/Pages2/PageSetupWizardTextKey.qml + ui/qml/Pages2/PageSetupWizardViewConfig.qml + ui/qml/Pages2/PageShare.qml + ui/qml/Pages2/PageShareFullAccess.qml + ui/qml/Pages2/PageStart.qml images/flagKit/ZW.svg diff --git a/client/server_scripts/awg/configure_container.sh b/client/server_scripts/awg/configure_container.sh index 322cc38f..2000c965 100644 --- a/client/server_scripts/awg/configure_container.sh +++ b/client/server_scripts/awg/configure_container.sh @@ -12,7 +12,7 @@ echo $WIREGUARD_PSK > /opt/amnezia/awg/wireguard_psk.key cat > /opt/amnezia/awg/wg0.conf < + + AdLabel + + + Amnezia Premium - for access to any website + + + ApiServicesModel - + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s شبكة VPN كلاسيكية للعمل المريح وتنزيل الملفات الكبيرة ومشاهدة مقاطع الفيديو. تعمل مع أي موقع. تصل السرعة إلى %1 ميجابت/ثانية - + VPN to access blocked sites in regions with high levels of Internet censorship. شبكة VPN للولوج للمواقع المحظورة في بلاد ذو مستوي عالي من الرقابة علي الانترنت. - + <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> - + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. Amenzia Premium - شبكة VPN للعمل المريح, تحميل ملفات كبيرة الحجم, ومشاهدة مقاطع الفيديو ب جودة عالية. تعمل لجميع المواقع, حتي في البلاد ذو مستوي عالي من الرقابة علي الانترنت - + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship Amnezia Free هو VPN مجاني لتخطي الحظر في البلاد ذو مستوي عالي من الرقابة علي الانترنت - + %1 MBit/s %1 ميجابت/ثانية - + %1 days %1 ايام - + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> سيقوم VPN فقط بفتح المواقع المشهورة المحظورة في بلدك, مثل Instagram, Facebook, Twitter و مواقع اخري. المواقع الاخري ستٌفتح من عنوان ال IP الحقيقي الخاص بك, <a href="%1/free" style="color: #FBB26A;">معلومات اخري علي الموقع.</a> - + Free مجاني - + %1 $/month %1 دولار/الشهر @@ -80,7 +88,7 @@ ConnectButton - + Unable to disconnect during configuration preparation غير قادر علي قطع الاتصال اثناء إعداد التكوين @@ -89,60 +97,60 @@ ConnectionController - - - + + + Connect اتصل - + VPN Protocols is not installed. Please install VPN container at first لم يتم تثبيت بروتوكولات VPN, من فضلك قم بتنزيل حاوية VPN اولاً - + Connecting... اتصال... - + Connected تم الاتصال - + Reconnecting... إعادة الاتصال... - + Disconnecting... إنهاء الاتصال... - + Preparing... جاري التحضير... - + Settings updated successfully, reconnnection... تم تحديث الاعدادات بنجاح, جاري إعادة الاتصال... - + Settings updated successfully تم تحديث الاعدادات بنجاح - + The selected protocol is not supported on the current platform البروتوكول المحدد غير مدعوم علي المنصة الحالية - + unable to create configuration غير قادر علي إنشاء تكوين @@ -150,17 +158,17 @@ ConnectionTypeSelectionDrawer - + Add new connection إضافة اتصال جديد - + Configure your server قم بتهيئة الخادم الخاص بك - + Open config file, key or QR code افتح ملف تعريف, مفتاح تعريف او رمز QR @@ -198,7 +206,7 @@ HomeContainersListView - + Unable change protocol while there is an active connection غير قادر علي تغيير البروتوكول اثناء تواجد اتصال @@ -206,46 +214,46 @@ HomeSplitTunnelingDrawer - + Split tunneling تقسيم الانفاق - + Allows you to connect to some sites or applications through a VPN connection and bypass others يسمح لك بألاتصال ببعض المواقع او البرامج خلال اتصال VPN و تجاوز الاخرين - + Split tunneling on the server تقسيم الانفاق علي الخادم - + Enabled Can't be disabled for current server مٌفعل لا يمكن إقافة للخادم الحالي - + Site-based split tunneling انقسام الانفاق القائم علي الموقع - - + + Enabled مٌفعل - - + + Disabled مٌعطل - + App-based split tunneling انقسام الانفاق القائم علي التطبيق @@ -253,23 +261,20 @@ Can't be disabled for current server ImportController - Unable to open file - غير قادر علي فتح الملف + غير قادر علي فتح الملف - - Invalid configuration file - ملف تكوين غير صحيح + ملف تكوين غير صحيح - + Scanned %1 of %2. تم فحص%1 من %2. - + In the imported configuration, potentially dangerous lines were found: في التكوين المستورد، تم العثور على سطور يحتمل أن تكون خطرة: @@ -277,24 +282,24 @@ Can't be disabled for current server InstallController - + %1 installed successfully. %1 تم التثبيت بنجاح. - + %1 is already installed on the server. %1 بالفعل مٌثبت علي الخادم. - + Added containers that were already installed on the server تمت إضافة الحاويات التي كانت مٌثبتة بالفعل علي الخادم - + Already installed containers were found on the server. All installed containers have been added to the application @@ -302,62 +307,62 @@ Already installed containers were found on the server. All installed containers تمت إضافة جميع الحاويات المٌثبتة إلي التطبيق - + Settings updated successfully تم تحديث الاعدادات بنجاح - + Server '%1' was rebooted تمت إعادة تشغيل الخادم%1 - + Server '%1' was removed تمت إزالة الخادم '%1' - + All containers from server '%1' have been removed قد تم حذفها '%1' جميع الحاويات من الخادم - + %1 has been removed from the server '%2' %1 تم حدف '%2' اسم الخادم - + Api config removed تم حذف تكوين Api - + %1 cached profile cleared تم مسح ملف تعريف %1 المخزن مؤقتًا - + Please login as the user من فضلك قم بتسجيل الدخول كمستخدم - + Server added successfully تمت إضافة الخادم بنجاح - + %1 installed successfully. تم تحميل %1 بنجاح - + API config reloaded تمت إعادة تحميل تكوين API - + Successfully changed the country of connection to %1 تم تغيير بلد الاتصال بنجاح إلى %1 @@ -370,12 +375,12 @@ Already installed containers were found on the server. All installed containers اختر تطبيق - + application name اسم التطبيق - + Add selected اضف اختيارك @@ -443,12 +448,12 @@ Already installed containers were found on the server. All installed containers PageDevMenu - + Gateway endpoint نقطة نهاية البوابة - + Dev gateway environment @@ -456,85 +461,84 @@ Already installed containers were found on the server. All installed containers PageHome - + Logging enabled تم تمكين التسجيل - + Split tunneling enabled تقسيم الانفاق مٌفعل - + Split tunneling disabled تقسيم الانفاق مٌعطل - + VPN protocol بروتوكول VPN - + Servers الخوادم - Unable change server while there is an active connection - لا يمكن تغير الخادم بينما هناك اتصال مفعل + لا يمكن تغير الخادم بينما هناك اتصال مفعل PageProtocolAwgClientSettings - + AmneziaWG settings اعدادات AmneziaWG - + MTU - + Server settings - + Port منفذ - + Save احفظ - + Save settings? احفظ الإعدادات؟ - + Only the settings for this device will be changed - + Continue واصل - + Cancel إلغاء - + Unable change settings while there is an active connection لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط @@ -542,97 +546,102 @@ Already installed containers were found on the server. All installed containers PageProtocolAwgSettings - + AmneziaWG settings اعدادات AmneziaWG - + Port منفذ - + All users with whom you shared a connection with will no longer be able to connect to it. جميع المستخدمين الذين شاركت معهم اتصال لن يكونو قادرين علي الاتصال مرة اخري. - + Save احفظ - + + VPN address subnet + الشبكة الفرعية لعنوان VPN + + + Jc - Junk packet count Jc - عدد الحزم غير المرغوب فيها - + Jmin - Junk packet minimum size Jmin - الحجم الادني للحزم الغير مرغوب فيها - + Jmax - Junk packet maximum size Jmax - الحجم الاقصي للحزم الغير مرغوب فيها - + S1 - Init packet junk size S1 - حجم حزمة البيانات العشوائية الأولية - + S2 - Response packet junk size S2 - حجم حزمة الاستجابة غير المرغوب فيها - + H1 - Init packet magic header H1 - حزمة رأس سحرية مبدئية - + H2 - Response packet magic header H2 - رأس حزمة الاستجابة السحرية - + H4 - Transport packet magic header H4 - رأس حزمة النقل السحرية - + H3 - Underload packet magic header H3 - رأس حزمة السحر غير المحمل - + The values of the H1-H4 fields must be unique يجب أن تكون قيم الحقول H1-H4 فريدة - + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) يجب ألا تساوي قيمة الحقل S1 + حجم بدء الرسالة (148) S2 + حجم استجابة الرسالة (92) - + Save settings? احفظ الإعدادات؟ - + Continue واصل - + Cancel إلغاء - + Unable change settings while there is an active connection لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط @@ -640,33 +649,33 @@ Already installed containers were found on the server. All installed containers PageProtocolCloakSettings - + Cloak settings Cloak إعدادات - + Disguised as traffic from متنكراً في حركة مرور من - + Port منفذ - - + + Cipher الشفرة - + Save احفظ - + Unable change settings while there is an active connection لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط @@ -674,175 +683,175 @@ Already installed containers were found on the server. All installed containers PageProtocolOpenVpnSettings - + OpenVPN settings OpenVPN اعدادات - + VPN address subnet الشبكة الفرعية لعنوان VPN - + Network protocol بروتوكول الشبكة - + Port منفذ - + Auto-negotiate encryption التفاوض التلقائي علي الشبكة - - + + Hash - + SHA512 - + SHA384 - + SHA256 - + SHA3-512 - + SHA3-384 - + SHA3-256 - + whirlpool - + BLAKE2b512 - + BLAKE2s256 - + SHA1 - - + + Cipher شفرة - + AES-256-GCM - + AES-192-GCM - + AES-128-GCM - + AES-256-CBC - + AES-192-CBC - + AES-128-CBC - + ChaCha20-Poly1305 - + ARIA-256-CBC - + CAMELLIA-256-CBC - + none لا شئ - + TLS auth TLS مصادقة - + Block DNS requests outside of VPN احظر طلبات DNS خارج ال VPN - + Additional client configuration commands اوامر تكوين العميل الاضافية - - + + Commands: الاوامر: - + Additional server configuration commands اوامر تكوين الخادم الاضافية - + Unable change settings while there is an active connection لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط - + Save احفظ @@ -850,42 +859,42 @@ Already installed containers were found on the server. All installed containers PageProtocolRaw - + settings إعدادات - + Show connection options اظهر اختيارات الاتصال - + Connection options %1 %1 اختيارات الاتصال - + Remove احذف - + Remove %1 from server? احذف %1 من الخادم ? - + All users with whom you shared a connection with will no longer be able to connect to it. جميع المستخدمين الذين شاركت معهم اتصال لن يكونو قادرين علي الاتصال مرة اخري. - + Continue واصل - + Cancel إلغاء @@ -893,28 +902,28 @@ Already installed containers were found on the server. All installed containers PageProtocolShadowSocksSettings - + Shadowsocks settings Shadowsocks إعدادات - + Port منفذ - - + + Cipher تشفير - + Save احفظ - + Unable change settings while there is an active connection لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط @@ -922,52 +931,52 @@ Already installed containers were found on the server. All installed containers PageProtocolWireGuardClientSettings - + WG settings إعدادات WG - + MTU - + Server settings - + Port منفذ - + Save احفظ - + Save settings? احفظ الإعدادات؟ - + Only the settings for this device will be changed - + Continue واصل - + Cancel إلغاء - + Unable change settings while there is an active connection لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط @@ -975,42 +984,47 @@ Already installed containers were found on the server. All installed containers PageProtocolWireGuardSettings - + WG settings إعدادات WG - + + VPN address subnet + الشبكة الفرعية لعنوان VPN + + + Port منفذ - + Save settings? احفظ الإعدادات؟ - + All users with whom you shared a connection with will no longer be able to connect to it. جميع المستخدمين الذين شاركت معهم اتصال لن يكونو قادرين علي الاتصال مرة اخري. - + Continue واصل - + Cancel إلغاء - + Unable change settings while there is an active connection لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط - + Save احفظ @@ -1018,22 +1032,22 @@ Already installed containers were found on the server. All installed containers PageProtocolXraySettings - + XRay settings إعدادات XRay - + Disguised as traffic from متنكراً في حركة مرور من - + Save احفظ - + Unable change settings while there is an active connection لا يمكن تغيير الإعدادات أثناء وجود اتصال نشط @@ -1041,39 +1055,39 @@ Already installed containers were found on the server. All installed containers PageServiceDnsSettings - + A DNS service is installed on your server, and it is only accessible via VPN. تم تثبيت خدمة DNS علي الخادم الخاص بك, و فقط متاح من خلال VPN. - + The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. عنوان ال DNS متطابق لنفس عنوان الخادم بك, يمكنك تهيئة DNS في الاعدادات, تحت علامة تبويب الاتصال. - + Remove احذف - + Remove %1 from server? احذف %1 ? - + Cannot remove AmneziaDNS from running server لا يمكن إزالة AmneziaDNS من الخادم قيد التشغيل - + Continue واصل - + Cancel إلغاء @@ -1081,67 +1095,67 @@ Already installed containers were found on the server. All installed containers PageServiceSftpSettings - + Settings updated successfully تم تحديث الإعدادات بنجاح - + SFTP settings SFTP إعدادات - + Host استضافة - - - - + + + + Copied تم الاستنساخ - + Port منفذ - + User name اسم المستخدم - + Password كلمة المرور - + Mount folder on device قم بتثبيت المجلد علي الجهاز - + In order to mount remote SFTP folder as local drive, perform following steps: <br> لتثبيت مجلد SFTP كمحرك اقراص محلي, اتبع هذه الخطوات : <br> - - + + <br>1. Install the latest version of <br>1. تحميل اخر اصدار من - - + + <br>2. Install the latest version of <br>2. تحمير اخر اصدار من - + Detailed instructions تعليمات مفصلة @@ -1149,69 +1163,69 @@ Already installed containers were found on the server. All installed containers PageServiceSocksProxySettings - + Settings updated successfully تم تحديث الإعدادات بنجاح - - + + SOCKS5 settings إعدادات SOCKS5 - + Host استضافة - - - - + + + + Copied تم النسخ - - + + Port منفذ - + User name اسم المستخدم - - + + Password كلمة المرور - + Username اسم المستخدم - - + + Change connection settings تغيير إعدادات الاتصال - + The port must be in the range of 1 to 65535 يجب أن يكون المنفذ في النطاق من 1 إلى 65535 - + Password cannot be empty لا يمكن ان تكون كلمة المرور فارغة - + Username cannot be empty اسم المستخدم لا يمكن ان يكون فارغ @@ -1219,37 +1233,37 @@ Already installed containers were found on the server. All installed containers PageServiceTorWebsiteSettings - + Settings updated successfully تم تحديث الإعدادات بنجاح - + Tor website settings Tor إعدادات متصفح - + Website address عنوان المتصفح - + Copied تم الاستنساخ - + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. - + After creating your onion site, it takes a few minutes for the Tor network to make it available for use. - + When configuring WordPress set the this onion address as domain. عند تكوين WordPress قم بتعيين عنوان ال onion هذا ك domain. @@ -1257,42 +1271,42 @@ Already installed containers were found on the server. All installed containers PageSettings - + Settings إعدادات - + Servers الخوادم - + Connection الاتصال - + Application تطبيق - + Backup نسخة احتياطية - + About AmneziaVPN عن AmneziaVPN - + Dev console وحدة تحكم التطوير - + Close application إغلاق التطبيق @@ -1300,32 +1314,32 @@ Already installed containers were found on the server. All installed containers PageSettingsAbout - + Support Amnezia دعم Amenzia - + Amnezia is a free and open-source application. You can support the developers if you like it. هو تطبيق مجاني ومفتوح المصدر يمكنك دعم مطورين Amnezia إذا اعجبك. - + Contacts التواصل - + Telegram group مجموعة ال Telegram - + To discuss features لمناقشة الميزات - + https://t.me/amnezia_vpn_en @@ -1334,122 +1348,145 @@ Already installed containers were found on the server. All installed containers البريد - + support@amnezia.org - + For reviews and bug reports لل مراجعات والابلاغات عن المشاكل - + Copied - + GitHub GitHub - + + Discover the source code + + + + https://github.com/amnezia-vpn/amnezia-client - + Website موقع - + + Visit official website + + + + Software version: %1 %1 :إصدار البرنامج - + Check for updates تحقق من وجود تحديثات - + Privacy Policy سياسات الخصوصية + + PageSettingsApiLanguageList + + + Unable change server location while there is an active connection + + + PageSettingsApiServerInfo - + For the region للمنطقة - + Price السعر - + Work period مدة العمل - + + Valid until + + + + Speed السرعة - + Support tag علامة الدعم - + Copied تم النسخ - + Reload API config إعادة تحميل تكوين API - + Reload API config? إعادة تحميل تكوين API - - + + Continue واصل - - + + Cancel إلغاء - + Cannot reload API config during active connection لا يمكن إعادة تحميل تكوين API اثناء تواجد اتصال نشط - + Remove from application احذف من التطبيق - + Remove from application? احذف من التطبيق؟ - + Cannot remove server during active connection لا يمكن إزالة الخادم أثناء الاتصال النشط @@ -1457,12 +1494,12 @@ Already installed containers were found on the server. All installed containers PageSettingsAppSplitTunneling - + Cannot change split tunneling settings during active connection لا يمكن تغير إعدادات تقسيم الانفاق بينما هناك اتصال مٌفعل - + Only the apps from the list should have access via VPN يجب أن تتمتع التطبيقات الموجودة في القائمة فقط بإمكانية الوصول عبر VPN @@ -1472,42 +1509,42 @@ Already installed containers were found on the server. All installed containers لا يجب ان تتمتع التطبيقات في القائمة بولوج ل VPN - + App split tunneling تقسيم نفق التطبيق - + Mode وضع - + Remove احذف - + Continue واصل - + Cancel إلغاء - + application name اسم التطبيق - + Open executable file افتح ملف قابل للتنفيذ - + Executable files (*.*) ملفات قابلة للتنفيذ (*.*) @@ -1515,102 +1552,102 @@ Already installed containers were found on the server. All installed containers PageSettingsApplication - + Application تطبيق - + Allow application screenshots اسمح بلقطات شاشة التطبيق - + Enable notifications تفعيل الإشعارات - + Enable notifications to show the VPN state in the status bar تفعيل الإشعارات لإظهار حالة ال VPN في شريط الحالة - + Auto start تشغيل تلقائي - + Launch the application every time the device is starts قم بتشغيل التطبيق فكل مرة يتم فيها تشغيل الجهاز - + Auto connect اتصال تلقائي - + Connect to VPN on app start اتصل ب ال VPN عند تشغيل التطبيق - + Start minimized ابدأ ب الحجم الادني - + Launch application minimized تشغيل التطبيق في الحد الادني - + Language اللغة - + Logging تسجيل - + Enabled مٌفعل - + Disabled مٌعطل - + Reset settings and remove all data from the application إعادة ضبط الاعدادات ومسح جميع البيانات من التطبيق - + Reset settings and remove all data from the application? إعادة ضبط الاعدادات ومسح جميع البيانات من التطبيق؟ - + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. سيتم ضبط الاعدادات الافتراضية. جميع خدمات AmneziaVPN المٌثبتة ستبقي علي الخادم. - + Continue واصل - + Cancel إلغاء - + Cannot reset settings during active connection لا يمكن إعادة ضبط الإعدادات اثناء تواجد اتصال فعال @@ -1618,78 +1655,78 @@ Already installed containers were found on the server. All installed containers PageSettingsBackup - + Settings restored from backup file تم إعادة الاعدادات من ملف نسخة احتياطية - + Back up your configuration قم بعمل نسخة احتياطية - + You can save your settings to a backup file to restore them the next time you install the application. يمكنك حفظ الإعدادات في ملف نسخة احتياطية لأعادتهم في المرة القادمة التي تثبت فيها التطبيق. - + The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. ستحتوي النسخة الاحتياطية علي كلمات مرورك و المفاتيح الخاصة للخوادم المٌضافة إلي AmneziaVPN. احفظ هذه المعلومات في مكان امن. - + Make a backup إضافة نسخة احتياطية - + Save backup file احفظ ملف النسخه الاحتياطيه - - + + Backup files (*.backup) ملفات نٌسخ احتياطية (*.backup) - + Backup file saved تم حفظ ملف النسخ الاحتياطي - + Restore from backup استرجاع من ملف يحتوي علي نسخة احتياطية - + Open backup file افتح ملف نسخ احتياطي - + Import settings from a backup file? استرد الإعدادات من ملف نسخ احتياطي؟ - + All current settings will be reset ستتم إعادة ضبط جميع الإعدادات الحالية - + Continue واصل - + Cancel إلغاء - + Cannot restore backup settings during active connection لا يمكن استعادة إعدادات النسخ الاحتياطي أثناء الاتصال النشط @@ -1697,62 +1734,62 @@ Already installed containers were found on the server. All installed containers PageSettingsConnection - + Connection الاتصال - + When AmneziaDNS is not used or installed عندما يكون AmneziaDNS غير مٌثبت او غير مستخدم - + Allows you to use the VPN only for certain Apps يسمح لك بأستخدام ال VPN علي تطبيقات معينة - + Use AmneziaDNS استخدم AmneziaDNS - + If AmneziaDNS is installed on the server في حالة كان AmneziaDNS مٌثبت علي الخادم - + DNS servers خوادم DNS - + Site-based split tunneling انقسام الانفاق القائم علي الموقع - + Allows you to select which sites you want to access through the VPN يسمح لك بتحديد اي موقع تريد الوصول له عن طريق ال VPN - + App-based split tunneling انقسام الانفاق القائم علي التطبيق - + KillSwitch - + Disables your internet if your encrypted VPN connection drops out for any reason. يعطل اتصال الإنترنت الخاص بك إذا انقطع اتصال VPN المشفر لأي سبب من الأسباب. - + Cannot change killSwitch settings during active connection لا يمكن تغيير إعدادات KillSwitch اثناء تواجد اتصال فعال @@ -1760,62 +1797,62 @@ Already installed containers were found on the server. All installed containers PageSettingsDns - + Default server does not support custom DNS الخادم الافتراضي لا يدعم DNS مخصص - + DNS servers خوادم ال DNS - + If AmneziaDNS is not used or installed AmneziaVPN ليس مٌستخدم او مٌثبت - + Primary DNS الرئيسي DNS - + Secondary DNS الثانوي DNS - + Restore default استعادة الافتراضي - + Restore default DNS settings? قم بأعادة ضبط إعدادات ال DNS الافتراضية؟ - + Continue واصل - + Cancel إلغاء - + Settings have been reset لم يتم إعادة ضبط الإعدادات - + Save احفظ - + Settings saved تم حفظ الإعدادات @@ -1827,12 +1864,12 @@ Already installed containers were found on the server. All installed containers تم تمكين التسجيل. لاحظ أنه سيتم تعطيل السجلات تلقائيًا بعد 14 يومًا، وسيتم حذف جميع ملفات السجل. - + Logging التسجيل - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. سيتم حفظ سجلات البرنامج بشكل تلقائي عند تفعيل هذه الميزة, بشكل افتراضي, هذه الميزة مٌعطلة. قم بتفعيل هذه الميزة في حالة هناك خلل في التطبيق. @@ -1845,20 +1882,20 @@ Already installed containers were found on the server. All installed containers افتح مجلد يحتوي علي سجلات - - + + Save احفظ - - + + Logs files (*.log) ملفات الولوج (*.log) - - + + Logs file saved تم حفظ ملف السجل @@ -1867,64 +1904,62 @@ Already installed containers were found on the server. All installed containers احفظ السجلات في ملف - + Enable logs - + Clear logs? مسح السجلات؟ - + Continue واصل - + Cancel إلغاء - + Logs have been cleaned up تم مسح السجلات - + Client logs - + AmneziaVPN logs - - + Open logs folder - - + Export logs - + Service logs - + AmneziaVPN-service logs - + Clear logs احذف السجلات @@ -1942,12 +1977,12 @@ Already installed containers were found on the server. All installed containers لم يتم العثور علي اي خدمات مٌثبتة سابقاً - + Do you want to reboot the server? هل تريد إعادة تشغيل الخادم؟ - + Do you want to clear server from Amnezia software? هل تريد حذف الخادم من Amnezia? @@ -1957,18 +1992,18 @@ Already installed containers were found on the server. All installed containers - - - - + + + + Continue واصل - - - - + + + + Cancel إلغاء @@ -1983,67 +2018,67 @@ Already installed containers were found on the server. All installed containers اضفهم إلي التطبيق إذا لم يكونو ظاهرين - + Reboot server إعادة تشغيل الخادم - + The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? عملية إعادة التشغيل قد تستغرق 30 ثانية, هل تريد الاستكمال؟ - + Cannot reboot server during active connection لا يمكن إعادة تشغيل الخادم أثناء الاتصال النشط - + Remove server from application احذف خادم من التطبيق - + Do you want to remove the server from application? هل تريد حذف الخادم من التطبيق؟ - + Cannot remove server during active connection لا يمكن إزالة الخادم أثناء الاتصال النشط - + All users whom you shared a connection with will no longer be able to connect to it. جميع المستخدمين الذين شاركت معهم اتصال لن يستطيعو الاتصال مرة اخري. - + Cannot clear server from Amnezia software during active connection لا يمكن مسح الخادم من برنامج Amnezia أثناء الاتصال النشط - + Reset API config إعادة تكوين API - + Do you want to reset API config? هل تريد إعادة تكوين API? - + Cannot reset API config during active connection لا يمكن إعادة تعيين تكوين API أثناء الاتصال النشط - + All installed AmneziaVPN services will still remain on the server. جميع خدمات AmneziaVPN المٌثبتة ستظل علي الخادم. - + Clear server from Amnezia software احذف خادم من Amnezia @@ -2051,27 +2086,32 @@ Already installed containers were found on the server. All installed containers PageSettingsServerInfo - + + Subscription is valid until + + + + Server name اسم الخادم - + Save احفظ - + Protocols البروتوكولات - + Services الخدمات - + Management الإدارة @@ -2079,7 +2119,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServerProtocol - + settings الإعدادات @@ -2088,7 +2128,7 @@ Already installed containers were found on the server. All installed containers مسح ملف تعريف %1 - + Clear %1 profile? مسح ملف تعريف %1؟ @@ -2098,64 +2138,64 @@ Already installed containers were found on the server. All installed containers - + Unable to clear %1 profile while there is an active connection غير قادر على مسح ملف تعريف %1 أثناء وجود اتصال نشط - + Remove احذف - + Remove %1 from server? احذف %1 من الخادم؟ - + All users with whom you shared a connection will no longer be able to connect to it. جميع المستخدمين الذين شاركت معاهم اتصال لن يستطيعو الاتصال بعد الان. - + Cannot remove active container لا يمكن إزالة الحاوية النشطة - - + + Continue واصل - + connection settings - + Click the "connect" button to create a connection configuration - + server settings - + Clear profile - + The connection configuration will be deleted for this device only - - + + Cancel إلغاء @@ -2163,7 +2203,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServersList - + Servers الخوادم @@ -2171,100 +2211,100 @@ Already installed containers were found on the server. All installed containers PageSettingsSplitTunneling - + Default server does not support split tunneling function السرفر الافتراضي لا يدعم ميزة تقسيم الانفاق - + Addresses from the list should not be accessed via VPN لا يجب الولوج للعنواين المذكورة هنا من خلال ال VPN - + Split tunneling تقسيم الانفاق - + Mode وضع - + Remove احذف - + Continue واصل - + Cancel إلغاء - + Only the sites listed here will be accessed through the VPN سيتم الولوج للمواقع المذكورة هنا فقط عن طريق ال VPN - + Cannot change split tunneling settings during active connection لا يمكن تغير إعدادات تقسيم الانفاق بينما هناك اتصال مٌفعل - + website or IP موقع او IP - + Import / Export Sites - + Import استرد - + Save site list احفظ قائمة المواقع - + Save sites احفظ المواقع - - - + + + Sites files (*.json) - + Import a list of sites استرد قائمة من المواقع - + Replace site list تبديل قائمة المواقع - - + + Open sites file افتح ملف المواقع - + Add imported sites to existing ones إضافة المواقع المستردة للمواقع الموجودة @@ -2272,32 +2312,32 @@ Already installed containers were found on the server. All installed containers PageSetupWizardApiServiceInfo - + For the region للمنطقة - + Price السعر - + Work period مدة العمل - + Speed السرعة - + Features المميزات - + Connect اتصل @@ -2305,12 +2345,12 @@ Already installed containers were found on the server. All installed containers PageSetupWizardApiServicesList - + VPN by Amnezia VPN بواسطة Amnezia - + Choose a VPN service that suits your needs. اختر خدمة VPN تلبي احتياجاتك @@ -2318,7 +2358,7 @@ Already installed containers were found on the server. All installed containers PageSetupWizardConfigSource - + Connection الاتصال @@ -2328,155 +2368,190 @@ Already installed containers were found on the server. All installed containers إعدادات - + Enable logs - + + Support tag + علامة الدعم + + + + Copied + + + + Insert the key, add a configuration file or scan the QR-code أدخل المفتاح، أضف ملف تكوين أو امسح رمز الاستجابة السريعة - + Insert key أدخل مفتاح - + Insert أدخل - + Continue واصل - + Other connection options اختيارات اتصال اخري - + + Site Amnezia + + + + VPN by Amnezia VPN بواسطة Amnezia - + Connect to classic paid and free VPN services from Amnezia اتصل بخدمات VPN الكلاسيكية المدفوعة والمجانية من Amnezia - + Self-hosted VPN VPN ذاتية الاستضافة - + Configure Amnezia VPN on your own server قم بتكوين Amnezia VPN على الخادم الخاص بك - + Restore from backup استرجاع من ملف يحتوي علي نسخة احتياطية - + + + + + + Open backup file افتح ملف نسخ احتياطي - + Backup files (*.backup) ملفات نٌسخ احتياطية (*.backup) - + File with connection settings ملف إعدادات اتصال - + + + + + + Open config file افتح ملف تكوين - + QR code رمز QR - + + + + + + I have nothing ليس لدي اي شئ + + + + + PageSetupWizardCredentials - + Configure your server تكوين الخادم الخاص بك - + Server IP address [:port] عنوان خادم IP [:منفذ] - + Continue واصل - + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties ستظل جميع البيانات التي تدخلها سرية للغاية ولن تتم مشاركتها أو الكشف عنها ل Amnezia أو أي طرف ثالث - + 255.255.255.255:22 - + SSH Username - + Password or SSH private key كلمة مرور او مفتاح SSH خاص - + How to run your VPN server كيف تقوم بتشغيل خادم ال VPN الخاص بك - + Where to get connection data, step-by-step instructions for buying a VPS اين تحصل علي بيانات الاتصال, تعليمات خطوة ب خطوة لشراء VPS - + Ip address cannot be empty لا يمكن لعنوان IP ان يكون فارغ - + Enter the address in the format 255.255.255.255:88 ادخل العنوان في شكل 255.255.255.255:88 - + Login cannot be empty تسجيل دخول لا يمكن ان يكون فارغ - + Password/private key cannot be empty كلمة مرور/مفتاح خاص لأ يمكن ان يكونو فارغين @@ -2484,22 +2559,22 @@ Already installed containers were found on the server. All installed containers PageSetupWizardEasy - + What is the level of internet control in your region? ما هو مستوي التحكم في الانترنت في منطقتك؟ - + Choose a VPN protocol اختر بروتوكول VPN - + Skip setup تخطي الإعداد - + Continue واصل @@ -2546,37 +2621,37 @@ Already installed containers were found on the server. All installed containers PageSetupWizardProtocolSettings - + Installing %1 تثبيت %1 - + More detailed اكثر تفصيلاً - + Close اغلق - + Network protocol بروتوكول شبكة - + Port منفذ - + Install تثبيت - + The port must be in the range of 1 to 65535 يجب أن يكون المنفذ في النطاق من 1 إلى 65535 @@ -2584,12 +2659,12 @@ Already installed containers were found on the server. All installed containers PageSetupWizardProtocols - + VPN protocol VPN بروتوكول - + Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP. اختر بالنسبة للأولوية القصوى بالنسبة لك. ويمكنك لاحقًا تثبيت بروتوكولات وخدمات إضافية أخرى، مثل وكيل DNS وSFTP. @@ -2605,7 +2680,7 @@ Already installed containers were found on the server. All installed containers PageSetupWizardStart - + Let's get started هيا نبدأ @@ -2613,28 +2688,28 @@ Already installed containers were found on the server. All installed containers PageSetupWizardTextKey - + Connection key مفتاح اتصال - + A line that starts with vpn://... يجب ان تٌكتب بهذه الطريقة حتي بوجود التحذير كي تظهر بشكل صحيح داخل التطبيق سطر يبدأ ب ...//:vpn - + Key مفتاح - + Insert ادخل - + Continue واصل @@ -2642,32 +2717,32 @@ Already installed containers were found on the server. All installed containers PageSetupWizardViewConfig - + New connection اتصال جديد - + Collapse content طي المحتوي - + Show content اظهر المحتوي - + Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. تمكين تشويش WireGuard. قد يكون من المفيد إذا تم حظر WireGuard على مزود الخدمة الخاص بك. - + Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. استخدم رموز اتصال فقط من المصادر التي تثق بها, ربما تم إنشاء رموز من مصادر عامة لاعتراض بياناتك. - + Connect اتصل @@ -2675,207 +2750,212 @@ Already installed containers were found on the server. All installed containers PageShare - + Save OpenVPN config احفظ تكوين OpenVPN - + Save WireGuard config احفظ تكوين WireGuard - + Save AmneziaWG config احفظ تكوين AmneziaWG - + Save Shadowsocks config احفظ تكوين Shadowsocks - + Save Cloak config احفظ تكوين Cloak - + Save XRay config حفظ تكوين XRay - + For the AmneziaVPN app AmneziaVPN من اجل تطبيق - + OpenVPN native format تنسيق OpenVPN الاصلي - + WireGuard native format تنسيق WireGuard الاصلي - + AmneziaWG native format تنسيق AmneziaWG اصلي - + Shadowsocks native format تنسيق Shadowsocks الاصلي - + Cloak native format تنسيق Cloak الاصلي - + XRay native format الشكل الاصلي ل XRay - + Share VPN Access شارك اتصال VPN - + Share full access to the server and VPN شارك ولوج كامل للخادم و ال VPN - + Use for your own devices, or share with those you trust to manage the server. استخدمه للأجهزة الخاصة بك، أو شاركه مع من تثق بهم لإدارة الخادم. - - + + Users المستخدمين - + Share VPN access without the ability to manage the server شارك اتصال VPN بدون القدرة علي إدارة الخادم - + Search ابحث - + Creation date: %1 تاريخ الإنشاء: %1 - + Latest handshake: %1 اخر تصافح: %1 - + Data received: %1 البيانات المستلمة: %1 - + Data sent: %1 البيانات المٌرسلة: %1 - + + Allowed IPs: %1 + + + + Rename إعادة التسمية - + Client name اسم العميل - + Save احفظ - + Revoke سحب وإبطال - + Revoke the config for a user - %1? سحب وإبطال للمستخدم - %1? - + The user will no longer be able to connect to your server. المستخدم لن يكون قادر علي الاتصال بعد الان. - + Continue واصل - + Cancel إلغاء - + Connection الاتصال - - + + Server خادم - + File with connection settings to ملف بإعدادات إلي - - + + Protocol بروتوكول - + Connection to اتصال إلي - + Config revoked تم سحب وإبطال التكوين - + User name اسم المستخدم - - + + Connection format تنسيق الاتصال - - + + Share شارك @@ -2883,55 +2963,55 @@ Already installed containers were found on the server. All installed containers PageShareFullAccess - + Full access to the server and VPN ولوج كامل للخادم و ال VPN - + We recommend that you use full access to the server only for your own additional devices. نحن ننصحك بأستخدام ولوج كامل للخادم فقط لأجهزتك الاضافية. - + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. إذا شاركت ولوج كامل مع الاشخاص, سيكونو قادرين علي حذف وإضافة بروتوكولات و خدمات إلي الخادم, والذي سيجعل VPN يعمل بشكل غير صحيح لجميع المستخدمين. - - + + Server خادم - + Accessing التواصل - + File with accessing settings to ملف مع إعدادات الوصول إلي - + Share مشاركة - + Access error! خطأ في الوصول! - + Connection to اتصال إلي - + File with connection settings to معلف مع إعدادات الاتصال إلي @@ -2939,17 +3019,17 @@ Already installed containers were found on the server. All installed containers PageStart - + Logging was disabled after 14 days, log files were deleted تم تعطيل التسجيل بعد 14 يومًا، وتم حذف ملفات السجل - + Settings restored from backup file تم تحميل الإعدادات من ملف نسخة احتياطية - + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. @@ -2957,7 +3037,7 @@ Already installed containers were found on the server. All installed containers PopupType - + Close اغلاق @@ -3288,22 +3368,22 @@ Already installed containers were found on the server. All installed containers انتهت مدة الاتصال بالخادم - + VPN connection error - + Error when retrieving configuration from API خطأ عند استرداد التكوين من API - + This config has already been added to the application هذا التكوين بالفعل تمت إضافتة للبرنامج - + ErrorCode: %1. @@ -3378,57 +3458,72 @@ Already installed containers were found on the server. All installed containers التكوين لا يحتوي علي اي حاويات و اعتماد للأتصال بالخادم - + + Unable to open config file + + + + In the response from the server, an empty config was received في الاستجابة من الخادم، تم تلقي تكوين فارغ - + SSL error occurred حدث خطأ SSL - + Server response timeout on api request انتهت مهلة استجابة الخادم عند طلب واجهة برمجة التطبيقات - + Missing AGW public key مفتاح AGW عام مفقود + + + Failed to decrypt response payload + + + Missing list of available services + + + + QFile error: The file could not be opened خطأ QFile: لا يمكن فتح الملف - + QFile error: An error occurred when reading from the file خطأ QFile: ظهر خطأ اثناء القراءه من الملف - + QFile error: The file could not be accessed خطأ QFile: لا يمكن الوصول للملف - + QFile error: An unspecified error occurred خطأ QFile: ظهر خطأ غير محدد - + QFile error: A fatal error occurred خطأ QFile: حدث خطأ فادح - + QFile error: The operation was aborted خطأ QFile: تم إحباط العملية - + Internal error خطأ داخلي @@ -3867,11 +3962,19 @@ While it offers a blend of security, stability, and speed, it's essential t SelectLanguageDrawer - + Choose language اختر لغة + + ServersListView + + + Unable change server while there is an active connection + لا يمكن تغير الخادم بينما هناك اتصال مفعل + + Settings @@ -3889,12 +3992,12 @@ While it offers a blend of security, stability, and speed, it's essential t SettingsController - + Backup file is corrupted ملف النسخه الاحتياطيه تالف - + All settings have been reset to default values تم استرجاع جميع الإعدادات للإعدادات الافتراضية @@ -3908,33 +4011,33 @@ While it offers a blend of security, stability, and speed, it's essential t احفظ تكوين AmneziaVPN - + Share شارك - + Copy انسخ - - + + Copied تم النسخ - + Copy config string انسخ نص التكوين - + Show connection settings اظهر إعدادات الاتصال - + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" حتي تقرأ رمز ال QR في تطبيق Amnezia, اختار "إضافة خادم" - "لدي بيانات الاتصال" - "رمز Qr, او مفتاح تعريف او ملف إعدادات" @@ -3957,27 +4060,27 @@ While it offers a blend of security, stability, and speed, it's essential t تم حذف الموقع: %1 - + Can't open file: %1 لا يمكن فتح ملف: %1 - + Failed to parse JSON data from file: %1 فشل قراءه بيانات JSON من الملف: %1 - + The JSON data is not an array in file: %1 بيانات ال JSON ليست مصفوفة في الملف: %1 - + Import completed اكتمل الاستيراد - + Export completed اكتمل التصدير @@ -4018,7 +4121,7 @@ While it offers a blend of security, stability, and speed, it's essential t TextFieldWithHeaderType - + The field can't be empty الحقل لا يمكن ان يكون فارغ @@ -4026,7 +4129,7 @@ While it offers a blend of security, stability, and speed, it's essential t VpnConnection - + Mbps @@ -4100,12 +4203,12 @@ While it offers a blend of security, stability, and speed, it's essential t main2 - + Private key passphrase عبارة المرور الخاصة بالمفتاح - + Save احفظ diff --git a/client/translations/amneziavpn_fa_IR.ts b/client/translations/amneziavpn_fa_IR.ts index 6cd78e77..c48606be 100644 --- a/client/translations/amneziavpn_fa_IR.ts +++ b/client/translations/amneziavpn_fa_IR.ts @@ -1,55 +1,63 @@ + + AdLabel + + + Amnezia Premium - for access to any website + + + ApiServicesModel - + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s برای کار راحت، دانلود فایل‌های بزرگ و تماشای ویدیوها، از VPN کلاسیک استفاده کنید. این VPN برای هر سایتی کار می‌کند و سرعت آن تا %1 مگابیت بر ثانیه است. - + VPN to access blocked sites in regions with high levels of Internet censorship. وی پی ان برای دسترسی به سایت‌های مسدود شده در مناطق با سانسور شدید اینترنت. - + <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> - + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. امنزیا پریمیوم - یک وی پی ان کلاسیک برای کار راحت، دانلود فایل‌های بزرگ و تماشای ویدیو با کیفیت بالا. قابل استفاده برای تمامی سایت‌ها، حتی در کشورهایی با بالاترین سطح سانسور اینترنت. - + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship امنزیا رایگان یک وی پی ان رایگان برای دور زدن مسدودیت‌ها در کشورهایی با سطح بالای سانسور اینترنت است. - + %1 MBit/s %1 MBit/s - + %1 days %1 روز - + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> وی پی ان فقط سایت‌های محبوبی را که در منطقه شما مسدود شده‌اند، مانند اینستاگرام، فیسبوک، توییتر و غیره باز می‌کند. سایر سایت‌ها با آدرس آی‌پی واقعی شما باز خواهند شد. <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> - + Free رایگان - + %1 $/month %1 $/ماه @@ -80,7 +88,7 @@ ConnectButton - + Unable to disconnect during configuration preparation در هنگام آماده‌سازی پیکربندی، نمی‌توان از اتصال خارج شد. @@ -88,63 +96,63 @@ ConnectionController - + VPN Protocols is not installed. Please install VPN container at first پروتکل وی‎پی‎ان نصب نشده است لطفا کانتینر وی‎پی‎ان را نصب کنید - + Connecting... در حال ارتباط... - + Connected متصل - + Preparing... در حال آماده‌سازی... - + Settings updated successfully, reconnnection... تنظیمات به روز رسانی شد در حال اتصال دوباره... - + Settings updated successfully تنظیمات با موفقیت به‎روز‎رسانی شدند - + The selected protocol is not supported on the current platform پروتکل انتخاب‌شده در پلتفرم فعلی پشتیبانی نمی‌شود. - + unable to create configuration نمی‌توان پیکربندی را ایجاد کرد. - + Reconnecting... اتصال دوباره... - - - + + + Connect اتصال - + Disconnecting... قطع ارتباط... @@ -152,17 +160,17 @@ ConnectionTypeSelectionDrawer - + Add new connection ایجاد ارتباط جدید - + Configure your server تنظیم سرور - + Open config file, key or QR code بارگذاری فایل تنظیمات، کلید یا QR Code @@ -200,7 +208,7 @@ HomeContainersListView - + Unable change protocol while there is an active connection امکان تغییر پروتکل در هنگام متصل بودن وجود ندارد @@ -212,45 +220,45 @@ HomeSplitTunnelingDrawer - + Split tunneling جداسازی ترافیک - + Allows you to connect to some sites or applications through a VPN connection and bypass others اجازه می‌دهد به شما که از طریق اتصال VPN به برخی از وب‌سایت‌ها یا برنامه‌ها وصل شوید و از دیگران عبور کنید - + Split tunneling on the server تقسیم تونل‌ها در سرور - + Enabled Can't be disabled for current server فعال - + Site-based split tunneling جداسازی ترافیک بر اساس سایت - - + + Enabled فعال - - + + Disabled غیر فعال - + App-based split tunneling جداسازی ترافیک بر اساس نرم‎افزار @@ -258,23 +266,20 @@ Can't be disabled for current server ImportController - Unable to open file - نمی‌توان فایل را باز کرد. + نمی‌توان فایل را باز کرد. - - Invalid configuration file - فایل پیکربندی نامعتبر است. + فایل پیکربندی نامعتبر است. - + Scanned %1 of %2. ارزیابی %1 از %2. - + In the imported configuration, potentially dangerous lines were found: در پیکربندی وارد شده، خطوطی که ممکن است خطرناک باشند، یافت شدند: @@ -282,86 +287,86 @@ Can't be disabled for current server InstallController - + %1 installed successfully. %1 با موفقیت نصب شد. - + %1 is already installed on the server. %1 در حال حاضر بر روی سرور نصب شده است. - + Added containers that were already installed on the server کانتینرهایی که بر روی سرور موجود بودند اضافه شدند - + Already installed containers were found on the server. All installed containers have been added to the application کانتینرهای نصب شده بر روی سرور شناسایی شدند. تمام کانتینترهای نصب شده به نرم افزار اضافه شدند - + Settings updated successfully تنظیمات با موفقیت به‎روز‎رسانی شدند - + Server '%1' was rebooted سرور %1 راه اندازی مجدد شد - + Server '%1' was removed سرور %1 حذف شد - + All containers from server '%1' have been removed تمام کانتینترها از سرور %1 حذف شدند - + %1 has been removed from the server '%2' %1 از سرور %2 حذف شد - + Api config removed پیکربندی API حذف شد. - + %1 cached profile cleared %1 پروفایل ذخیره شده پاک شد. - + Please login as the user لطفا به عنوان کاربر وارد شوید - + Server added successfully سرور با موفقیت اضافه شد - + %1 installed successfully. %1 با موفقیت نصب شد. - + API config reloaded پیکربندی API دوباره بارگذاری شد. - + Successfully changed the country of connection to %1 کشور اتصال با موفقیت به %1 تغییر یافت. @@ -374,12 +379,12 @@ Already installed containers were found on the server. All installed containers انتخاب برنامه - + application name نام برنامه - + Add selected اضافه کردن انتخاب شده @@ -447,12 +452,12 @@ Already installed containers were found on the server. All installed containers PageDevMenu - + Gateway endpoint - + Dev gateway environment @@ -460,85 +465,84 @@ Already installed containers were found on the server. All installed containers PageHome - + Logging enabled لاگ‌برداری فعال شد - + Split tunneling enabled فعال شدن تونل تقسیم‌شده - + Split tunneling disabled تونل تقسیم‌شده غیرفعال شده - + VPN protocol پروتکل وی‎پی‎ان - + Servers سرورها - Unable change server while there is an active connection - امکان تغییر سرور در هنگام متصل بودن وجود ندارد + امکان تغییر سرور در هنگام متصل بودن وجود ندارد PageProtocolAwgClientSettings - + AmneziaWG settings تنظیمات AmneziaWG - + MTU - + Server settings - + Port پورت - + Save ذخیره - + Save settings? تنظیمات را ذخیره کن? - + Only the settings for this device will be changed - + Continue - + Cancel - + Unable change settings while there is an active connection نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. @@ -546,12 +550,12 @@ Already installed containers were found on the server. All installed containers PageProtocolAwgSettings - + AmneziaWG settings تنظیمات AmneziaWG - + Port پورت @@ -564,87 +568,92 @@ Already installed containers were found on the server. All installed containers آیا میخواهید AmneziaWG از سرور حذف شود؟ - + All users with whom you shared a connection with will no longer be able to connect to it. همه کاربرانی که با آن‌ها ارتباطی به اشتراک گذاشته‌اید دیگر قادر به اتصال به آن نخواهند بود. - + Save ذخیره - + + VPN address subnet + زیرشبکه آدرس VPN + + + Jc - Junk packet count - + Jmin - Junk packet minimum size - + Jmax - Junk packet maximum size - + S1 - Init packet junk size - + S2 - Response packet junk size - + H1 - Init packet magic header - + H2 - Response packet magic header - + H4 - Transport packet magic header - + H3 - Underload packet magic header - + The values of the H1-H4 fields must be unique - + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) - + Save settings? تنظیمات را ذخیره کن? - + Continue ادامه - + Cancel کنسل - + Unable change settings while there is an active connection نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. @@ -652,33 +661,33 @@ Already installed containers were found on the server. All installed containers PageProtocolCloakSettings - + Cloak settings تنظیمات Cloak - + Disguised as traffic from پنهان کردن به عنوان ترافیک از - + Port پورت - - + + Cipher رمزگذاری - + Save ذخیره - + Unable change settings while there is an active connection نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. @@ -686,170 +695,170 @@ Already installed containers were found on the server. All installed containers PageProtocolOpenVpnSettings - + OpenVPN settings تنظیمات OpenVPN - + VPN address subnet زیرشبکه آدرس VPN - + Network protocol پروتکل شبکه - + Port پورت - + Auto-negotiate encryption رمزگذاری خودکار - - + + Hash هش - + SHA512 SHA512 - + SHA384 SHA384 - + SHA256 SHA256 - + SHA3-512 SHA3-512 - + SHA3-384 SHA3-384 - + SHA3-256 SHA3-256 - + whirlpool whirlpool - + BLAKE2b512 BLAKE2b512 - + BLAKE2s256 BLAKE2s256 - + SHA1 SHA1 - - + + Cipher رمزگذاری - + AES-256-GCM AES-256-GCM - + AES-192-GCM AES-192-GCM - + AES-128-GCM AES-128-GCM - + AES-256-CBC AES-256-CBC - + AES-192-CBC AES-192-CBC - + AES-128-CBC AES-128-CBC - + ChaCha20-Poly1305 ChaCha20-Poly1305 - + ARIA-256-CBC ARIA-256-CBC - + CAMELLIA-256-CBC CAMELLIA-256-CBC - + none none - + TLS auth اعتبار TLS - + Block DNS requests outside of VPN مسدود کردن درخواست‎های DNS خارج از وی‎پی‎ان - + Additional client configuration commands تنظیمات و دستورات اضافه برنامه متصل شونده - - + + Commands: دستورات: - + Additional server configuration commands تنظیمات و دستورات اضافه سرور - + Unable change settings while there is an active connection نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. @@ -874,7 +883,7 @@ Already installed containers were found on the server. All installed containers کنسل - + Save ذخیره @@ -882,32 +891,32 @@ Already installed containers were found on the server. All installed containers PageProtocolRaw - + settings تنظیمات - + Show connection options نمایش تنظیمات اتصال - + Connection options %1 تنظیمات اتصال %1 - + Remove حذف - + Remove %1 from server? %1 از سرور حذف شود؟ - + All users with whom you shared a connection with will no longer be able to connect to it. همه کاربرانی که با آن‌ها ارتباطی به اشتراک گذاشته‌اید دیگر قادر به اتصال به آن نخواهند بود. @@ -916,12 +925,12 @@ Already installed containers were found on the server. All installed containers همه کاربرانی که با آن این پروتکل VPN را به اشتراک گذاشته‌اید دیگر نمی‌توانند به آن متصل شوند. - + Continue ادامه - + Cancel کنسل @@ -929,28 +938,28 @@ Already installed containers were found on the server. All installed containers PageProtocolShadowSocksSettings - + Shadowsocks settings تنظیمات Shadowsocks - + Port پورت - - + + Cipher رمزگذاری - + Save ذخیره - + Unable change settings while there is an active connection نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. @@ -958,52 +967,52 @@ Already installed containers were found on the server. All installed containers PageProtocolWireGuardClientSettings - + WG settings تنظیمات WG - + MTU - + Server settings - + Port پورت - + Save ذخیره - + Save settings? تنظیمات را ذخیره کن? - + Only the settings for this device will be changed - + Continue - + Cancel - + Unable change settings while there is an active connection نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. @@ -1011,32 +1020,37 @@ Already installed containers were found on the server. All installed containers PageProtocolWireGuardSettings - + WG settings تنظیمات WG - + + VPN address subnet + زیرشبکه آدرس VPN + + + Port پورت - + Save settings? تنظیمات را ذخیره کن? - + All users with whom you shared a connection with will no longer be able to connect to it. همه کاربرانی که با آن‌ها ارتباطی به اشتراک گذاشته‌اید دیگر قادر به اتصال به آن نخواهند بود. - + Continue - + Unable change settings while there is an active connection نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. @@ -1045,12 +1059,12 @@ Already installed containers were found on the server. All installed containers تمام کاربرانی که این ارتباط را با آنها به اشتراک گذاشته‎اید دیگر نمی‎توانند به آن متصل شوند. - + Cancel کنسل - + Save ذخیره @@ -1058,22 +1072,22 @@ Already installed containers were found on the server. All installed containers PageProtocolXraySettings - + XRay settings تنظیمات XRay - + Disguised as traffic from به‌عنوان ترافیک از طرف زیر نمایش داده می‌شود - + Save ذخیره - + Unable change settings while there is an active connection نمی‌توان تنظیمات را تغییر داد در حالی که اتصال فعال است. @@ -1088,39 +1102,39 @@ Already installed containers were found on the server. All installed containers PageServiceDnsSettings - + A DNS service is installed on your server, and it is only accessible via VPN. یک سرویس DSN بر روی سرور شما نصب شده و فقط از طریق وی‎پی‎ان قابل دسترسی می‎باشد. - + The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. آدرس DSN همان آدرس سرور شماست. میتوانید از قسمت تنظیمات و تب اتصالات DSN خود را تنظیم کنید. - + Remove جذف - + Remove %1 from server? %1 از سرور حذف شود؟ - + Continue ادامه - + Cancel کنسل - + Cannot remove AmneziaDNS from running server نمی‌توان AmneziaDNS را از سرور در حال اجرا حذف کرد. @@ -1128,67 +1142,67 @@ Already installed containers were found on the server. All installed containers PageServiceSftpSettings - + Settings updated successfully تنظیمات با موفقیت به‎روز‎رسانی شد - + SFTP settings تنظیمات SFTP - + Host هاست - - - - + + + + Copied کپی شد - + Port پورت - + User name نام کاربری - + Password رمز عبور - + Mount folder on device بارگذاری پوشه بر روی دستگاه - + In order to mount remote SFTP folder as local drive, perform following steps: <br> برای بارگذاری پوشه SFTP بر روی درایو محلی قدم‎های زیر را انجام دهید: <br> - - + + <br>1. Install the latest version of <br> 1. آخرین نسخه را نصب کنید - - + + <br>2. Install the latest version of <br> 2. آخرین نسخه را نصب کنید - + Detailed instructions جزییات دستورالعمل‎ها @@ -1212,69 +1226,69 @@ Already installed containers were found on the server. All installed containers PageServiceSocksProxySettings - + Settings updated successfully تنظیمات با موفقیت به‌روزرسانی شد. - - + + SOCKS5 settings تنظیمات SOCKS5 - + Host هاستمیزبان - - - - + + + + Copied کپی شد - - + + Port پورت - + User name نام کاربری - - + + Password رمز عبور - + Username نام کاربری - - + + Change connection settings تغییر تنظیمات اتصال - + The port must be in the range of 1 to 65535 پورت باید در محدوده ۱ تا ۶۵۵۳۵ باشد - + Password cannot be empty رمز عبور نمی‌تواند خالی باشد - + Username cannot be empty نام کاربری نمی‌تواند خالی باشد @@ -1282,37 +1296,37 @@ Already installed containers were found on the server. All installed containers PageServiceTorWebsiteSettings - + Settings updated successfully تنظیمات با موفقیت به‎روز‎‌رسانی شد - + Tor website settings تنظیمات وب‎سایت Tor - + Website address آدرس وب‎سایت - + Copied کپی شد - + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. - + After creating your onion site, it takes a few minutes for the Tor network to make it available for use. پس از ایجاد سایت پیاز خود، چند دقیقه طول می‌کشد تا شبکه تور آن را برای استفاده فراهم کند. - + When configuring WordPress set the this onion address as domain. زمانی که سایت وردپرس را تنظیم میکنید این آدرس پیازی را به عنوان دامنه قرار دهید. @@ -1336,42 +1350,42 @@ Already installed containers were found on the server. All installed containers PageSettings - + Settings تنظیمات - + Servers سرورها - + Connection ارتباط - + Application نرم‎افزار - + Backup بک‎آپ - + About AmneziaVPN درباره Amnezia - + Dev console - + Close application بستن نرم‎افزار @@ -1379,37 +1393,37 @@ Already installed containers were found on the server. All installed containers PageSettingsAbout - + Support Amnezia پشتیبانی از Amnezia - + Amnezia is a free and open-source application. You can support the developers if you like it. Amnezia یک برنامه رایگان و متن باز است. اگر دوست دارید می توانید از توسعه دهندگان حمایت کنید. - + Contacts مخاطب - + Telegram group گروه تلگرام - + To discuss features برای گفتگو در مورد ویژگی‎ها - + https://t.me/amnezia_vpn_en https://t.me/amnezia_vpn_ir - + support@amnezia.org @@ -1418,121 +1432,144 @@ Already installed containers were found on the server. All installed containers ایمیل - + For reviews and bug reports برای ارائه نظرات و گزارشات باگ - + Copied کپی شد - + GitHub GitHub - + + Discover the source code + + + + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website وب سایت + + + Visit official website + + https://amnezia.org https://amnezia.org - + Software version: %1 %1 :نسخه نرم‎افزار - + Check for updates بررسی بروز‎رسانی - + Privacy Policy + + PageSettingsApiLanguageList + + + Unable change server location while there is an active connection + + + PageSettingsApiServerInfo - + For the region برای منطقه - + Price قیمت - + Work period مدت زمان کار - + + Valid until + + + + Speed سرعت - + Support tag - + Copied کپی شد - + Reload API config بارگذاری مجدد پیکربندی API - + Reload API config? آیا می‌خواهید پیکربندی API را دوباره بارگذاری کنید؟ - - + + Continue ادامه دهید - - + + Cancel لغو - + Cannot reload API config during active connection نمی‌توان پیکربندی API را در حین اتصال فعال دوباره بارگذاری کرد. - + Remove from application حذف از برنامه - + Remove from application? آیا می‌خواهید از برنامه حذف کنید؟ - + Cannot remove server during active connection نمی‌توان سرور را در حین اتصال فعال حذف کرد. @@ -1540,12 +1577,12 @@ Already installed containers were found on the server. All installed containers PageSettingsAppSplitTunneling - + Cannot change split tunneling settings during active connection نمی توان تنظیمات تونل تقسیم را در طول اتصال فعال تغییر دادنمی‌توان تنظیمات تقسیم تونلینگ را در حین اتصال فعال تغییر داد. - + Only the apps from the list should have access via VPN فقط برنامه‌های موجود در لیست باید از طریق VPN دسترسی داشته باشند. @@ -1555,42 +1592,42 @@ Already installed containers were found on the server. All installed containers برنامه‌های موجود در لیست نباید از طریق VPN دسترسی داشته باشند. - + App split tunneling تقسیم تونلینگ برنامه‌ها - + Mode حالت - + Remove حذف - + Continue ادامه دهید - + Cancel کنسل - + application name نام برنامه - + Open executable file فایل اجرایی را باز کنید - + Executable files (*.*) فایل‌های اجرایی (*.*) @@ -1598,102 +1635,102 @@ Already installed containers were found on the server. All installed containers PageSettingsApplication - + Application نرم افزار - + Allow application screenshots مجوز اسکرین‎شات در برنامه - + Enable notifications فعال کردن اعلان‌ها - + Enable notifications to show the VPN state in the status bar اعلان ها را فعال کنید تا وضعیت VPN را در نوار وضعیت ببینید - + Auto start شروع خودکار - + Launch the application every time the device is starts راه‎اندازی نرم‎افزار با هر بار روشن شدن دستگاه - + Auto connect اتصال خودکار - + Connect to VPN on app start اتصال به وی‎‎پی‎ان با شروع نرم‎افزار - + Start minimized شروع به صورت کوچک - + Launch application minimized راه‎اندازی برنامه به صورت کوچک - + Language زبان - + Logging گزارشات - + Enabled فعال - + Disabled غیر فعال - + Reset settings and remove all data from the application ریست کردن تنظیمات و حذف تمام داده‎ها از نرم‎افزار - + Reset settings and remove all data from the application? ریست کردن تنظیمات و حذف تمام داده‎ها از نرم‎افزار؟ - + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. تمام تنظیمات به حالت پیش‎فرض ریست می‎شوند. تمام سرویس‎های Amnezia بر روی سرور باقی می‎مانند. - + Continue ادامه - + Cancel کنسل - + Cannot reset settings during active connection نمی‌توان تنظیمات را در حین اتصال فعال بازنشانی کرد. @@ -1701,78 +1738,78 @@ Already installed containers were found on the server. All installed containers PageSettingsBackup - + Settings restored from backup file تنظیمات از فایل پشتیبان بازیابی شد - + Back up your configuration یک نسخه پشتیبان از تنظیمات خود تهیه - + You can save your settings to a backup file to restore them the next time you install the application. می‎توانید تنظیمات را در یک فایل پشتیبان ذخیره کرده و دفعه بعد که نرم‎افزار را نصب کردید آن‎ها را بازیابی کنید. - + The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. پشتیبان حاوی رمزهای عبور و کلیدهای خصوصی شما برای تمام سرورهای اضافه شده به AmneziaVPN خواهد بود. این اطلاعات را در یک مکان امن نگه دارید - + Make a backup ایجاد یک پشتیبان - + Save backup file ذخیره فایل پشتیبان - - + + Backup files (*.backup) Backup files (*.backup) - + Backup file saved فایل پشتیبان ذخیره شد - + Restore from backup بازیابی از پشتیبان - + Open backup file باز کردن فایل پشتیبان - + Import settings from a backup file? ورود تنظیمات از فایل پشتیبان؟ - + All current settings will be reset تمام تنظیمات جاری ریست خواهد شد - + Continue ادامه - + Cancel کنسل - + Cannot restore backup settings during active connection نمی‌توان تنظیمات پشتیبان را در حین اتصال فعال بازیابی کرد. @@ -1780,62 +1817,62 @@ Already installed containers were found on the server. All installed containers PageSettingsConnection - + Connection ارتباط - + Use AmneziaDNS استفاده از AmneziaDNS - + If AmneziaDNS is installed on the server اگر AmneziaDNS بر روی سرور نصب شده باشد - + DNS servers سرورهای DNS - + When AmneziaDNS is not used or installed وقتی AmneziaDNS استفاده نشده یا نصب نشده است - + Allows you to use the VPN only for certain Apps به شما امکان می دهد از VPN فقط برای برخی برنامه ها استفاده کنید - + KillSwitch KillSwitch - + Disables your internet if your encrypted VPN connection drops out for any reason. اگر به هر دلیلی اتصال VPN رمزگذاری شده شما قطع شود، اینترنت شما را غیرفعال می‌کند. - + Cannot change killSwitch settings during active connection نمی‌توان تنظیمات Kill Switch را در حین اتصال فعال تغییر داد. - + Site-based split tunneling جداسازی ترافیک بر اساس سایت - + Allows you to select which sites you want to access through the VPN میتوانید مشخص کنید که چه سایت‎هایی از وی‎پی‎ان استفاده کنند - + App-based split tunneling جداسازی ترافیک بر اساس نرم‎افزار @@ -1843,62 +1880,62 @@ Already installed containers were found on the server. All installed containers PageSettingsDns - + Default server does not support custom DNS سرور پیش‌فرض از دی‌ان‌اس سفارشی پشتیبانی نمی‌کند - + DNS servers سرورهای DNS - + If AmneziaDNS is not used or installed اگر AmneziaDNS نصب نباشد یا استفاده نشود - + Primary DNS DNS اصلی - + Secondary DNS DNS ثانویه - + Restore default بازگشت به پیش‎فرض - + Restore default DNS settings? بازگشت به تنظیمات پیش‎فرض DNS؟ - + Continue ادامه - + Cancel کنسل - + Settings have been reset تنظیمات ریست شد - + Save ذخیره - + Settings saved ذخیره تنظیمات @@ -1910,12 +1947,12 @@ Already installed containers were found on the server. All installed containers ثبت وقایع فعال است. توجه داشته باشید که ثبت وقایع به‌طور خودکار پس از ۱۴ روز غیرفعال شده و تمام فایل‌های ثبت وقایع حذف خواهند شد. - + Logging گزارشات - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. فعال کردن این عملکرد باعث ذخیره خودکار لاگ‌های برنامه می‌شود. به طور پیش‌فرض، قابلیت ثبت لاگ غیرفعال است. در صورت بروز خطا در برنامه، ذخیره لاگ را فعال کنید. @@ -1928,20 +1965,20 @@ Already installed containers were found on the server. All installed containers باز کردن پوشه گزارشات - - + + Save ذخیره - - + + Logs files (*.log) Logs files (*.log) - - + + Logs file saved فایل گزارشات ذخیره شد @@ -1950,64 +1987,62 @@ Already installed containers were found on the server. All installed containers ذخیره گزارشات در فایل - + Enable logs - + Clear logs? پاک کردن گزارشات؟ - + Continue ادامه - + Cancel کنسل - + Logs have been cleaned up گزارشات پاک شدند - + Client logs - + AmneziaVPN logs - - + Open logs folder - - + Export logs - + Service logs - + AmneziaVPN-service logs - + Clear logs پاک کردن گزارشات @@ -2042,18 +2077,18 @@ Already installed containers were found on the server. All installed containers - - - - + + + + Continue ادامه - - - - + + + + Cancel کنسل @@ -2068,77 +2103,77 @@ Already installed containers were found on the server. All installed containers اضافه کردن آنها به نرم‎افزار اگر نمایش داده نشده‎اند - + Reboot server سرور را دوباره راه‌اندازی کنید - + Do you want to reboot the server? آیا می‌خواهید سرور را دوباره راه‌اندازی کنید؟ - + The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? فرآیند راه‌اندازی ممکن است حدود ۳۰ ثانیه طول بکشد. آیا مطمئن هستید که می‌خواهید ادامه دهید؟ - + Cannot reboot server during active connection نمی‌توان سرور را در حین اتصال فعال راه‌اندازی مجدد کرد. - + Do you want to remove the server from application? آیا می‌خواهید سرور را از برنامه حذف کنید؟ - + Cannot remove server during active connection نمی‌توان سرور را در حین اتصال فعال حذف کرد. - + Do you want to clear server from Amnezia software? آیا می‌خواهید سرور را از نرم‌افزار Amnezia پاک کنید؟ - + All users whom you shared a connection with will no longer be able to connect to it. همه کاربرانی که با آن‌ها ارتباطی به اشتراک گذاشته‌اید دیگر قادر به اتصال به آن نخواهند بود. - + Cannot clear server from Amnezia software during active connection نمی‌توان سرور را در حین اتصال فعال از نرم‌افزار Amnezia پاک کرد. - + Reset API config تنظیمات API را بازنشانی کنید - + Do you want to reset API config? آیا می خواهید پیکربندی API را بازنشانی کنید؟ - + Cannot reset API config during active connection نمی‌توان پیکربندی API را در حین اتصال فعال بازنشانی کرد. - + Remove server from application حذف کردن سرور از نرم‎افزار - + All installed AmneziaVPN services will still remain on the server. تمام سرویس‎های نصب‎شده Amnezia همچنان بر روی سرور باقی خواهند ماند. - + Clear server from Amnezia software پاک کردن سرور از نرم‎افزار Amnezia @@ -2146,27 +2181,32 @@ Already installed containers were found on the server. All installed containers PageSettingsServerInfo - + + Subscription is valid until + + + + Server name نام سرور - + Save ذخیره - + Protocols پروتکل‎ها - + Services سرویس‎ها - + Management مدیریت @@ -2174,7 +2214,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServerProtocol - + settings تنظیمات @@ -2183,7 +2223,7 @@ Already installed containers were found on the server. All installed containers پاک کردن پروفایل %1 - + Clear %1 profile? آیا می‌خواهید پروفایل %1 را پاک کنید؟ @@ -2193,64 +2233,64 @@ Already installed containers were found on the server. All installed containers - + Unable to clear %1 profile while there is an active connection نمی‌توان پروفایل %1 را در حین اتصال فعال پاک کرد. - + Remove حذف - + Remove %1 from server? حذف %1 از سرور؟ - + All users with whom you shared a connection will no longer be able to connect to it. تمام کاربرانی که این ارتباط را با آنها به اشتراک گذاشته‎اید دیگر نمی‎توانند به آن متصل شوند. - + Cannot remove active container نمی‌توان کانتینر فعال را حذف کرد. - - + + Continue ادامه - + connection settings - + Click the "connect" button to create a connection configuration - + server settings - + Clear profile - + The connection configuration will be deleted for this device only - - + + Cancel کنسل @@ -2258,7 +2298,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServersList - + Servers سرورها @@ -2266,100 +2306,100 @@ Already installed containers were found on the server. All installed containers PageSettingsSplitTunneling - + Default server does not support split tunneling function سرور پیش‌فرض از عملکرد تونل‌سازی تقسیم شده پشتیبانی نمی‌کند - + Addresses from the list should not be accessed via VPN دسترسی به آدرس‎های لیست بدون وی‎پی‎ان - + Split tunneling جداسازی ترافیک - + Mode حالت - + Remove حذف - + Continue ادامه - + Cancel کنسل - + Cannot change split tunneling settings during active connection نمی توان تنظیمات تونل تقسیم را در طول اتصال فعال تغییر داد - + Only the sites listed here will be accessed through the VPN تنها سایت‌های موجود در اینجا از طریق VPN دسترسی داده خواهند شد - + website or IP وب‌سایت یا آدرس IP - + Import / Export Sites وارد کردن / صادر کردن وب‌سایت‌ها - + Import بارگذاری - + Save site list ذخیره لیست سایت‎ها - + Save sites ذخیره سایت‎ها - - - + + + Sites files (*.json) Sites files (*.json) - + Import a list of sites بارگذاری لیست سایت‎ها - + Replace site list جایگزین کردن لیست سایت - - + + Open sites file باز کردن فایل سایت‎ها - + Add imported sites to existing ones اضافه کردن سایت‎های بارگذاری شده به سایت‎های موجود @@ -2367,32 +2407,32 @@ Already installed containers were found on the server. All installed containers PageSetupWizardApiServiceInfo - + For the region برای منطقه - + Price قیمت - + Work period مدت زمان کار - + Speed سرعت - + Features ویژگی‌ها - + Connect اتصال @@ -2400,12 +2440,12 @@ Already installed containers were found on the server. All installed containers PageSetupWizardApiServicesList - + VPN by Amnezia VPN توسط Amnezia - + Choose a VPN service that suits your needs. یک سرویس VPN که مناسب نیازهای شما باشد را انتخاب کنید. @@ -2433,7 +2473,7 @@ It's okay as long as it's from someone you trust. چی داری؟ - + File with connection settings فایل شامل تنظیمات اتصال @@ -2442,7 +2482,7 @@ It's okay as long as it's from someone you trust. فایل شامل تنظیمات اتصال یا بک‎آپ - + Connection ارتباط @@ -2452,85 +2492,120 @@ It's okay as long as it's from someone you trust. تنظیمات - + Enable logs - + + Support tag + + + + + Copied + کپی شد + + + Insert the key, add a configuration file or scan the QR-code کلید را وارد کنید، فایل پیکربندی را اضافه کنید یا کد QR را اسکن کنید - + Insert key کلید را وارد کنید - + Insert وارد کردن - + Continue ادامه دهید - + Other connection options گزینه‌های اتصال دیگر - + + Site Amnezia + + + + VPN by Amnezia VPN توسط Amnezia - + Connect to classic paid and free VPN services from Amnezia اتصال به سرویس‌های VPN کلاسیک پولی و رایگان از Amnezia - + Self-hosted VPN Self-hosted VPN - + Configure Amnezia VPN on your own server پیکربندی VPN Amnezia بر روی سرور خودتان - + Restore from backup بازیابی از پشتیبان - + + + + + + Open backup file باز کردن فایل پشتیبان - + Backup files (*.backup) Backup files (*.backup) - + + + + + + Open config file باز کردن فایل تنظیمات - + QR code QR-Code - + + + + + + I have nothing من هیچی ندارم + + + + + Key as text متن شامل کلید @@ -2539,67 +2614,67 @@ It's okay as long as it's from someone you trust. PageSetupWizardCredentials - + Server IP address [:port] آدرس آی‎پی سرور (:پورت) - + Continue ادامه - + Enter the address in the format 255.255.255.255:88 آدرس را با فرمت 255.255.255.255:88 وارد کنید - + Configure your server سرور خود را پیکربندی کنید - + 255.255.255.255:22 - + SSH Username - + Password or SSH private key رمز عبور یا کلید خصوصی SSH - + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties تمام داده‎هایی که شما وارد می‎کنید به شدت محرمانه‎ است و با Amnezia یا هر شخص ثالث دیگری به اشتراک گذاشته نمی‎شود - + How to run your VPN server چگونه سرور VPN خود را اجرا کنید - + Where to get connection data, step-by-step instructions for buying a VPS داده‌های اتصال را از کجا دریافت کنید و دستورالعمل‌های مرحله به مرحله برای خرید یک VPS - + Ip address cannot be empty آدرس آی‎پی نمی‎تواند خالی باشد - + Login cannot be empty نام‎کاربری نمی‎تواند خالی باشد - + Password/private key cannot be empty پسورد یا کلید خصوصی نمی‎تواند خالی باشد @@ -2607,22 +2682,22 @@ It's okay as long as it's from someone you trust. PageSetupWizardEasy - + What is the level of internet control in your region? سطح کنترل اینترنت در منطقه شما چگونه است؟ - + Choose a VPN protocol یک پروتکل VPN را انتخاب کنید - + Skip setup رد شدن از تنظیم - + Continue ادامه @@ -2669,37 +2744,37 @@ It's okay as long as it's from someone you trust. PageSetupWizardProtocolSettings - + Installing %1 در حال نصب %1 - + More detailed جزییات بیشتر - + Close بستن - + Network protocol پروتکل شبکه - + Port پورت - + Install نصب - + The port must be in the range of 1 to 65535 پورت باید در محدوده ۱ تا ۶۵۵۳۵ باشد @@ -2707,12 +2782,12 @@ It's okay as long as it's from someone you trust. PageSetupWizardProtocols - + VPN protocol پروتکل وی‎پی‎ان - + Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP. پروتکلی که بیشترین اولویت را برای شما دارد انتخاب کنید. بعدا، میتوانید پروتکل‎ها و سرویس‎های اضافه مانند پروکسی DNS و SFTP را هم نصب کنید. @@ -2748,7 +2823,7 @@ It's okay as long as it's from someone you trust. من هیچی ندارم - + Let's get started بیایید شروع کنیم @@ -2756,27 +2831,27 @@ It's okay as long as it's from someone you trust. PageSetupWizardTextKey - + Connection key کلید ارتباط - + A line that starts with vpn://... خطی که با آن شروع می شود vpn://... - + Key کلید - + Insert وارد کردن - + Continue ادامه @@ -2784,32 +2859,32 @@ It's okay as long as it's from someone you trust. PageSetupWizardViewConfig - + New connection ارتباط جدید - + Collapse content جمع کردن محتوا - + Show content نمایش محتوا - + Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. فعال‌سازی استتار WireGuard. این ممکن است مفید باشد اگر WireGuard توسط ارائه‌دهنده شما مسدود شده باشد. - + Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. از کدهای اتصال فقط از منابع مورد اعتماد خود استفاده کنید. ممکن است کدهایی از منابع عمومی برای رهگیری داده های شما ایجاد شده باشند - + Connect اتصال @@ -2817,211 +2892,216 @@ It's okay as long as it's from someone you trust. PageShare - + OpenVPN native format فرمت OpenVPN - + WireGuard native format فرمت WireGuard - + Connection ارتباط - - + + Server سرور - + Config revoked تنظیمات ابطال‎شد - + Connection to ارتباط با - + File with connection settings to فایل شامل تنظیمات ارتباط با - + Save OpenVPN config ذخیره تنظیمات OpenVPN - + Save WireGuard config ذخیره تنظیمات WireGuard - + Save AmneziaWG config تنظیمات AmneziaWG را ذخیره کنید - + Save Shadowsocks config ذخیره تنظیمات Shadowsocks - + Save Cloak config ذخیره تنظیمات Cloak - + Save XRay config ذخیره پیکربندی XRay - + For the AmneziaVPN app برای نرم‎افزار AmneziaVPN - + AmneziaWG native format فرمت بومی AmneziaWG - + Shadowsocks native format فرمت Shadowsocks - + Cloak native format فرمت Cloak - + XRay native format فرمت بومی XRay - + Share VPN Access اتصال vpn را به اشتراک بگذارید - + Share full access to the server and VPN به اشتراک گذاشتن دسترسی کامل به سرور و وی‎پی‎ان - + Use for your own devices, or share with those you trust to manage the server. برای دستگاه‎های خودتان استفاده کنید یا با آنهایی که برای مدیریت سرور به آن‎ها اعتماد دارید به اشتراک بگذارید. - - + + Users کاربران - + User name نام کاربری - + Search جستجو - + Creation date: %1 تاریخ ایجاد: %1 - + Latest handshake: %1 آخرین ارتباط: %1 - + Data received: %1 داده‌های دریافت شده: %1 - + Data sent: %1 داده‌های ارسال شده: %1 + + + Allowed IPs: %1 + + Creation date: تاریخ ایجاد: - + Rename تغییر نام - + Client name نام کلاینت - + Save ذخیره - + Revoke ابطال - + Revoke the config for a user - %1? لغو پیکربندی برای یک کاربر - %1? - + The user will no longer be able to connect to your server. کاربر دیگر نمی‎تواند به سرور وصل شود. - + Continue ادامه - + Cancel کنسل - + Share VPN access without the ability to manage the server به اشتراک گذاشتن دسترسی وی‎پی‎ان بدون امکان مدیریت سرور - - + + Protocol پروتکل - - + + Connection format فرمت ارتباط - - + + Share اشتراک‎گذاری @@ -3029,55 +3109,55 @@ It's okay as long as it's from someone you trust. PageShareFullAccess - + Full access to the server and VPN دسترسی کامل به سرور و وی‎پی‎ان - + We recommend that you use full access to the server only for your own additional devices. ما پیشنهاد میکنیم که ازحالت دسترسی کامل به سرور فقط برای دستگاه‎های دیگر خودتان استفاده کنید. - + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. اگر دسترسی کامل را با دیگران به اشتراک بگذارید، آن‎ها می‎توانند پروتکل‎ها و سرویس‎ها را حذف یا اضافه کنند که باعث می‎شود که وی‎پی‎ان دیگر برای سایر کاربران کار نکند. - - + + Server سرور - + Accessing در حال دسترسی به - + File with accessing settings to فایل شامل تنظیمات دسترسی به - + Share اشتراک‎گذاری - + Access error! خطای دسترسی! - + Connection to ارتباط با - + File with connection settings to فایل شامل تنظیمات ارتباط با @@ -3085,17 +3165,17 @@ It's okay as long as it's from someone you trust. PageStart - + Logging was disabled after 14 days, log files were deleted ثبت وقایع پس از ۱۴ روز غیرفعال شد و فایل‌های ثبت وقایع حذف شدند - + Settings restored from backup file تنظیمات از فایل پشتیبان بازیابی شد - + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. @@ -3103,7 +3183,7 @@ It's okay as long as it's from someone you trust. PopupType - + Close بستن @@ -3479,22 +3559,22 @@ It's okay as long as it's from someone you trust. تنظیمات شامل هیچ کانتینر یا اعتبارنامه‎ای برای اتصال به سرور نیست - + VPN connection error خطای اتصال VPN - + Error when retrieving configuration from API خطا هنگام بازیابی پیکربندی از API - + This config has already been added to the application این پیکربندی قبلاً به برنامه اضافه شده است - + ErrorCode: %1. کد خطا: %1. @@ -3564,57 +3644,72 @@ It's okay as long as it's from someone you trust. VPN pool error: no available addresses - + + Unable to open config file + + + + In the response from the server, an empty config was received در پاسخ از سرور، پیکربندی خالی دریافت شد - + SSL error occurred SSL error occurred - + Server response timeout on api request Server response timeout on api request - + Missing AGW public key + + + Failed to decrypt response payload + + - QFile error: The file could not be opened - - - - - QFile error: An error occurred when reading from the file - - - - - QFile error: The file could not be accessed + Missing list of available services - QFile error: An unspecified error occurred + QFile error: The file could not be opened - QFile error: A fatal error occurred + QFile error: An error occurred when reading from the file + QFile error: The file could not be accessed + + + + + QFile error: An unspecified error occurred + + + + + QFile error: A fatal error occurred + + + + QFile error: The operation was aborted - + Internal error Internal error @@ -4071,11 +4166,19 @@ For more detailed information, you can SelectLanguageDrawer - + Choose language انتخاب زبان + + ServersListView + + + Unable change server while there is an active connection + امکان تغییر سرور در هنگام متصل بودن وجود ندارد + + Settings @@ -4093,7 +4196,7 @@ For more detailed information, you can SettingsController - + All settings have been reset to default values تمام تنظیمات به مقادیر پیش فرض ریست شد @@ -4102,7 +4205,7 @@ For more detailed information, you can پروفایل ذخیره شده پاک شد - + Backup file is corrupted فایل بک‎آپ خراب شده است @@ -4116,33 +4219,33 @@ For more detailed information, you can ذخیره تنظیمات AmneziaVPN - + Share اشتراک‎گذاری - + Copy کپی - - + + Copied کپی شد - + Copy config string کپی‎کردن متن تنظیمات - + Show connection settings نمایش تنظیمات ارتباط - + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" برای خواندن QR Code در نرم‎افزار AmneziaVPN "اضافه کردن سرور" -> "من داده برای اتصال دارم" -> "QR Code، کلید یا فایل تنظیمات" @@ -4165,27 +4268,27 @@ For more detailed information, you can سایت حذف شد: %1 - + Can't open file: %1 فایل باز نشد: %1 - + Failed to parse JSON data from file: %1 مشکل در تحلیل داده‎های JSON در فایل: %1 - + The JSON data is not an array in file: %1 داده‎های JSON در فایل به صورت آرایه نیستند: %1 - + Import completed بارگذاری کامل شد - + Export completed خروجی گرفتن کامل شد @@ -4226,7 +4329,7 @@ For more detailed information, you can TextFieldWithHeaderType - + The field can't be empty این فیلد نمی‌تواند خالی باشد. @@ -4234,7 +4337,7 @@ For more detailed information, you can VpnConnection - + Mbps Mbps @@ -4316,12 +4419,12 @@ For more detailed information, you can main2 - + Private key passphrase عبارت کلید خصوصی - + Save ذخیره diff --git a/client/translations/amneziavpn_hi_IN.ts b/client/translations/amneziavpn_hi_IN.ts index ab459b7c..db095d5c 100644 --- a/client/translations/amneziavpn_hi_IN.ts +++ b/client/translations/amneziavpn_hi_IN.ts @@ -1,55 +1,63 @@ + + AdLabel + + + Amnezia Premium - for access to any website + + + ApiServicesModel - + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s - + VPN to access blocked sites in regions with high levels of Internet censorship. - + <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> - + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. - + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship - + %1 MBit/s - + %1 days - + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> - + Free - + %1 $/month @@ -80,7 +88,7 @@ ConnectButton - + Unable to disconnect during configuration preparation कॉन्फ़िगरेशन तैयारी के दौरान डिस्कनेक्ट करने में असमर्थ @@ -89,61 +97,61 @@ ConnectionController - - - + + + Connect कनेक्ट - + VPN Protocols is not installed. Please install VPN container at first पीएन प्रोटोकॉल स्थापित नहीं है. कृपया पहले वीपीएन कंटेनर स्थापित करें - + Connected जुड़ा हुआ - + The selected protocol is not supported on the current platform चयनित प्रोटोकॉल वर्तमान प्लेटफ़ॉर्म पर समर्थित नहीं है - + unable to create configuration कॉन्फ़िगरेशन बनाने में असमर्थ - + Connecting... कनेक्ट... - + Reconnecting... पुनः कनेक्ट हो रहा है... - + Disconnecting... डिस्कनेक्ट हो रहा है... - + Preparing... तैयार कर रहे हैं... - + Settings updated successfully, reconnnection... सेटिंग्स सफलतापूर्वक अपडेट हो गईं... - + Settings updated successfully सेटिंग्स सफलतापूर्वक अपडेट हो गईं @@ -151,17 +159,17 @@ ConnectionTypeSelectionDrawer - + Add new connection नया कनेक्शन जोड़ें - + Configure your server अपना सर्वर कॉन्फ़िगर करें - + Open config file, key or QR code कॉन्फ़िग फ़ाइल, कुंजी या QR कोड खोलें @@ -199,7 +207,7 @@ HomeContainersListView - + Unable change protocol while there is an active connection सक्रिय कनेक्शन होने पर प्रोटोकॉल बदलने में असमर्थ @@ -207,46 +215,46 @@ HomeSplitTunnelingDrawer - + Split tunneling विभाजित सुरंग - + Allows you to connect to some sites or applications through a VPN connection and bypass others आपको वीपीएन कनेक्शन के माध्यम से कुछ साइटों या एप्लिकेशन से जुड़ने और अन्य को बायपास करने की अनुमति देता है - + Split tunneling on the server सर्वर पर स्प्लिट टनलिंग - + Enabled Can't be disabled for current server सक्रिय वर्तमान सर्वर के लिए अक्षम नहीं किया जा सकता - + Site-based split tunneling साइट-आधारित विभाजित टनलिंग - - + + Enabled सक्षम किया - - + + Disabled अक्षम - + App-based split tunneling ऐप-आधारित स्प्लिट टनलिंग @@ -254,23 +262,20 @@ Can't be disabled for current server ImportController - Unable to open file - फाइल खोलने में असमर्थ + फाइल खोलने में असमर्थ - - Invalid configuration file - अमान्य कॉन्फ़िगरेशन फ़ाइल + अमान्य कॉन्फ़िगरेशन फ़ाइल - + Scanned %1 of %2. %2 में से %1 स्कैन किया गया. - + In the imported configuration, potentially dangerous lines were found: @@ -278,86 +283,86 @@ Can't be disabled for current server InstallController - + %1 installed successfully. %1 सफलतापूर्वक स्थापित हुआ. - + %1 is already installed on the server. %1 पहले से ही सर्वर पर स्थापित है. - + Added containers that were already installed on the server सर्वर पर पहले से स्थापित कंटेनर जोड़े गए - + Already installed containers were found on the server. All installed containers have been added to the application सर्वर पर पहले से स्थापित कंटेनर पाए गए। सभी स्थापित कंटेनरों को एप्लिकेशन में जोड़ दिया गया है - + Settings updated successfully सेटिंग्स सफलतापूर्वक अपडेट हो गईं - + Server '%1' was rebooted सर्वर '%1' रीबूट किया गया था - + Server '%1' was removed सर्वर '%1' रीबूट किया गया था - + All containers from server '%1' have been removed सर्वर '%1' से सभी कंटेनर हटा दिए गए हैं - + %1 has been removed from the server '%2' %1 को सर्वर '%2' से हटा दिया गया है - + Api config removed - + %1 cached profile cleared %1 कैश्ड प्रोफ़ाइल साफ़ की गई - + Please login as the user कृपया उपयोगकर्ता के रूप में लॉगिन करें - + Server added successfully सर्वर सफलतापूर्वक जोड़ा गया - + %1 installed successfully. - + API config reloaded - + Successfully changed the country of connection to %1 @@ -370,12 +375,12 @@ Already installed containers were found on the server. All installed containers एप्लिकेशन चुनें - + application name आवेदन का नाम - + Add selected चुने हुए को जोड़ो @@ -443,12 +448,12 @@ Already installed containers were found on the server. All installed containers PageDevMenu - + Gateway endpoint - + Dev gateway environment @@ -456,85 +461,84 @@ Already installed containers were found on the server. All installed containers PageHome - + Logging enabled लॉगिंग सक्षम - + Split tunneling enabled स्प्लिट टनलिंग सक्षम - + Split tunneling disabled स्प्लिट टनलिंग अक्षम - + VPN protocol VPN प्रोटोकॉल - + Servers सर्वर - Unable change server while there is an active connection - सक्रिय कनेक्शन होने पर सर्वर बदलने में असमर्थ + सक्रिय कनेक्शन होने पर सर्वर बदलने में असमर्थ PageProtocolAwgClientSettings - + AmneziaWG settings Amneziaडब्ल्यूजी सेटिंग्स - + MTU एमटीयू - + Server settings - + Port - + Save सहेजें - + Save settings? सेटिंग्स सेव करें? - + Only the settings for this device will be changed - + Continue जारी रखना - + Cancel रद्द करना - + Unable change settings while there is an active connection सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ @@ -542,12 +546,17 @@ Already installed containers were found on the server. All installed containers PageProtocolAwgSettings - + AmneziaWG settings Amneziaडब्ल्यूजी सेटिंग्स - + + VPN address subnet + VPN एड्रेस सबनेट + + + Port पोर्ट @@ -556,87 +565,87 @@ Already installed containers were found on the server. All installed containers एमटीयू - + Jc - Junk packet count - + Jmin - Junk packet minimum size - + Jmax - Junk packet maximum size - + S1 - Init packet junk size - + S2 - Response packet junk size - + H1 - Init packet magic header - + H2 - Response packet magic header - + H4 - Transport packet magic header - + H3 - Underload packet magic header - + Save सहेजें - + The values of the H1-H4 fields must be unique H1-H4 फ़ील्ड का मान अद्वितीय होना चाहिए - + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) फ़ील्ड S1 + संदेश आरंभ आकार (148) का मान S2 + संदेश प्रतिक्रिया आकार (92) के बराबर नहीं होना चाहिए - + Save settings? सेटिंग्स सेव करें? - + All users with whom you shared a connection with will no longer be able to connect to it. वे सभी उपयोगकर्ता जिनके साथ आपने कनेक्शन साझा किया था, वे अब इससे कनेक्ट नहीं हो पाएंगे. - + Unable change settings while there is an active connection सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ - + Continue जारी रखना - + Cancel रद्द करना @@ -644,33 +653,33 @@ Already installed containers were found on the server. All installed containers PageProtocolCloakSettings - + Cloak settings लबादा सेटिंग - + Disguised as traffic from से यातायात के रूप में प्रच्छन्न - + Port पोर्ट - - + + Cipher साइफर - + Save सहेजें - + Unable change settings while there is an active connection सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ @@ -678,175 +687,175 @@ Already installed containers were found on the server. All installed containers PageProtocolOpenVpnSettings - + OpenVPN settings OpenVPN सेटिंग्स - + VPN address subnet VPN एड्रेस सबनेट - + Network protocol नेटवर्क प्रोटोकॉल - + Port पोर्ट - + Auto-negotiate encryption स्वतः-निगोशिएट एन्क्रिप्शन - - + + Hash - + SHA512 - + SHA384 - + SHA256 - + SHA3-512 - + SHA3-384 - + SHA3-256 - + whirlpool - + BLAKE2b512 - + BLAKE2s256 अक्षम - + SHA1 - - + + Cipher साइफर - + AES-256-GCM - + AES-192-GCM - + AES-128-GCM एईएस-128-जीसीएम - + AES-256-CBC - + AES-192-CBC - + AES-128-CBC एईएस-128-सीबीसी - + ChaCha20-Poly1305 चाचा20-पॉली1305 - + ARIA-256-CBC एआरआईए-256-सीबीसी - + CAMELLIA-256-CBC कैमेलिया-256-सीबीसी - + none कोई नहीं - + TLS auth टीएलएस प्राधिकरण - + Block DNS requests outside of VPN VPN के बाहर डीएनएस अनुरोधों को ब्लॉक करें - + Additional client configuration commands अतिरिक्त क्लाइंट कॉन्फ़िगरेशन आदेश - - + + Commands: आदेश: - + Additional server configuration commands अतिरिक्त सर्वर कॉन्फ़िगरेशन आदेश - + Save सहेजें - + Unable change settings while there is an active connection सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ @@ -854,42 +863,42 @@ Already installed containers were found on the server. All installed containers PageProtocolRaw - + settings समायोजन - + Show connection options कनेक्शन विकल्प दिखाएँ - + Connection options %1 कनेक्शन विकल्प%1 - + Remove निकालना - + Remove %1 from server? सर्वर से %1 हटाएँ? - + All users with whom you shared a connection with will no longer be able to connect to it. वे सभी उपयोगकर्ता जिनके साथ आपने कनेक्शन साझा किया था, वे अब इससे कनेक्ट नहीं हो पाएंगे. - + Continue जारी रखना - + Cancel रद्द करना @@ -897,28 +906,28 @@ Already installed containers were found on the server. All installed containers PageProtocolShadowSocksSettings - + Shadowsocks settings शैडोसॉक्स सेटिंग्स - + Port पोर्ट - - + + Cipher साइफर - + Save सहेजें - + Unable change settings while there is an active connection सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ @@ -926,52 +935,52 @@ Already installed containers were found on the server. All installed containers PageProtocolWireGuardClientSettings - + WG settings डब्ल्यूजी सेटिंग्स - + MTU एमटीयू - + Server settings - + Port - + Save सहेजें - + Save settings? सेटिंग्स सेव करें? - + Only the settings for this device will be changed - + Continue जारी रखना - + Cancel रद्द करना - + Unable change settings while there is an active connection सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ @@ -979,12 +988,17 @@ Already installed containers were found on the server. All installed containers PageProtocolWireGuardSettings - + WG settings डब्ल्यूजी सेटिंग्स - + + VPN address subnet + VPN एड्रेस सबनेट + + + Port बंदरगाह @@ -993,32 +1007,32 @@ Already installed containers were found on the server. All installed containers एमटीयू - + Save सहेजें - + Save settings? सेटिंग्स सेव करें? - + All users with whom you shared a connection with will no longer be able to connect to it. वे सभी उपयोगकर्ता जिनके साथ आपने कनेक्शन साझा किया था, वे अब इससे कनेक्ट नहीं हो पाएंगे. - + Continue जारी रखना - + Cancel रद्द करना - + Unable change settings while there is an active connection सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ @@ -1026,22 +1040,22 @@ Already installed containers were found on the server. All installed containers PageProtocolXraySettings - + XRay settings एक्सरे सेटिंग्स - + Disguised as traffic from से यातायात के रूप में प्रच्छन्न - + Save सहेजें - + Unable change settings while there is an active connection सक्रिय कनेक्शन होने पर सेटिंग बदलने में असमर्थ @@ -1049,39 +1063,39 @@ Already installed containers were found on the server. All installed containers PageServiceDnsSettings - + A DNS service is installed on your server, and it is only accessible via VPN. आपके सर्वर पर एक DNS सेवा स्थापित है, और यह केवल वीपीएन के माध्यम से पहुंच योग्य है. - + The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. DNS पता आपके सर्वर के पते के समान है। आप कनेक्शन टैब के अंतर्गत सेटिंग्स में DNS को कॉन्फ़िगर कर सकते हैं. - + Remove निकालना - + Remove %1 from server? सर्वर से %1 हटाएँ? - + Continue जारी रखना - + Cancel रद्द करना - + Cannot remove AmneziaDNS from running server चल रहे सर्वर से एम्नेज़िया डीएनएस को नहीं हटाया जा सकता @@ -1089,67 +1103,67 @@ Already installed containers were found on the server. All installed containers PageServiceSftpSettings - + Settings updated successfully सेटिंग्स सफलतापूर्वक अपडेट हो गईं - + SFTP settings एसएफटीपी सेटिंग्स - + Host मेज़बान - - - - + + + + Copied कॉपी किया गया - + Port पोर्ट - + User name उपयोगकर्ता नाम - + Password पासवर्ड - + Mount folder on device डिवाइस पर फ़ोल्डर माउंट करें - + In order to mount remote SFTP folder as local drive, perform following steps: <br> दूरस्थ SFTP फ़ोल्डर को स्थानीय ड्राइव के रूप में माउंट करने के लिए, निम्नलिखित चरण निष्पादित करें: <br> - - + + <br>1. Install the latest version of <br>1. का नवीनतम संस्करण स्थापित करें - - + + <br>2. Install the latest version of <br>2. का नवीनतम संस्करण स्थापित करें - + Detailed instructions विस्तृत निर्देश @@ -1173,69 +1187,69 @@ Already installed containers were found on the server. All installed containers PageServiceSocksProxySettings - + Settings updated successfully सेटिंग्स सफलतापूर्वक अपडेट हो गईं - - + + SOCKS5 settings - + Host मेज़बान - - - - + + + + Copied कॉपी किया गया - - + + Port - + User name उपयोगकर्ता नाम - - + + Password पासवर्ड - + Username - - + + Change connection settings - + The port must be in the range of 1 to 65535 - + Password cannot be empty - + Username cannot be empty @@ -1243,37 +1257,37 @@ Already installed containers were found on the server. All installed containers PageServiceTorWebsiteSettings - + Settings updated successfully सेटिंग्स सफलतापूर्वक अपडेट हो गईं - + Tor website settings टोर वेबसाइट सेटिंग्स - + Website address वेबसाइट का पता - + Copied कॉपी किया गया - + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. इस यूआरएल को खोलने के लिए <a href='https://www.torproject.org/download/' style='color: #FBB26A;'>Tor ब्राउज़र</a> का उपयोग करें। - + After creating your onion site, it takes a few minutes for the Tor network to make it available for use. आपकी onionसाइट बनाने के बाद, टोर नेटवर्क को इसे उपयोग के लिए उपलब्ध कराने में कुछ मिनट लगते हैं. - + When configuring WordPress set the this onion address as domain. वर्डप्रेस को कॉन्फ़िगर करते समय इस प्याज पते को डोमेन के रूप में सेट करें. @@ -1297,42 +1311,42 @@ Already installed containers were found on the server. All installed containers PageSettings - + Settings समायोजन - + Servers सर्वर - + Connection कनेक्शन - + Application एप्लिकेशन - + Backup बैकअप - + About AmneziaVPN AmneziaVPN के बारे में - + Dev console - + Close application एप्लिकेशन बंद करो @@ -1340,32 +1354,32 @@ Already installed containers were found on the server. All installed containers PageSettingsAbout - + Support Amnezia Amnezia का समर्थन करें - + Amnezia is a free and open-source application. You can support the developers if you like it. एमनेज़िया एक निःशुल्क और ओपन-सोर्स एप्लिकेशन है। यदि आपको यह पसंद है तो आप डेवलपर्स का समर्थन कर सकते हैं।. - + Contacts संपर्क - + Telegram group टेलीग्राम समूह - + To discuss features सुविधाओं पर चर्चा करना - + https://t.me/amnezia_vpn_en @@ -1374,122 +1388,145 @@ Already installed containers were found on the server. All installed containers मेल - + support@amnezia.org - + For reviews and bug reports समीक्षाओं और बग रिपोर्टों के लिए - + Copied कॉपी किया गया - + GitHub GitHub - + + Discover the source code + + + + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website वेबसाइट - + + Visit official website + + + + Software version: %1 सॉफ़्टवेयर संस्करण: %1 - + Check for updates अद्यतन के लिए जाँच - + Privacy Policy गोपनीयता नीति + + PageSettingsApiLanguageList + + + Unable change server location while there is an active connection + + + PageSettingsApiServerInfo - + For the region - + Price - + Work period - + + Valid until + + + + Speed - + Support tag - + Copied कॉपी किया गया - + Reload API config - + Reload API config? - - + + Continue जारी रखना - - + + Cancel रद्द करना - + Cannot reload API config during active connection - + Remove from application - + Remove from application? - + Cannot remove server during active connection सक्रिय कनेक्शन के दौरान सर्वर को हटाया नहीं जा सकता @@ -1497,7 +1534,7 @@ Already installed containers were found on the server. All installed containers PageSettingsAppSplitTunneling - + Cannot change split tunneling settings during active connection सक्रिय कनेक्शन के दौरान स्प्लिट टनलिंग सेटिंग्स को नहीं बदला जा सकता @@ -1510,7 +1547,7 @@ Already installed containers were found on the server. All installed containers सूची के ऐप्स को वीपीएन के माध्यम से एक्सेस नहीं किया जाना चाहिए - + Only the apps from the list should have access via VPN @@ -1520,42 +1557,42 @@ Already installed containers were found on the server. All installed containers - + App split tunneling ऐप स्प्लिट टनलिंग - + Mode तरीका - + Remove निकालना - + Continue जारी रखना - + Cancel रद्द करना - + application name आवेदन का नाम - + Open executable file निष्पादन योग्य फ़ाइल खोलें - + Executable files (*.*) निष्पादनीय फाइल (*.*) @@ -1563,102 +1600,102 @@ Already installed containers were found on the server. All installed containers PageSettingsApplication - + Application एप्लिकेशन - + Allow application screenshots एप्लिकेशन स्क्रीनशॉट की अनुमति दें - + Enable notifications - + Enable notifications to show the VPN state in the status bar - + Auto start ऑटो स्टार्ट - + Launch the application every time the device is starts हर बार डिवाइस चालू होने पर एप्लिकेशन लॉन्च करें - + Auto connect ऑटो कनेक्ट - + Connect to VPN on app start ऐप शुरू होने पर वीपीएन से कनेक्ट करें - + Start minimized स्टार्ट को मिनिमाइज किया गया - + Launch application minimized लॉन्च एप्लिकेशन को न्यूनतम किया गया - + Language भाषा - + Logging लॉगिंग - + Enabled सक्रिय किया - + Disabled अक्षम - + Reset settings and remove all data from the application सेटिंग्स रीसेट करें और एप्लिकेशन से सभी डेटा हटा दें - + Reset settings and remove all data from the application? सेटिंग्स रीसेट करें और एप्लिकेशन से सभी डेटा हटा दें? - + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. सभी सेटिंग्स डिफ़ॉल्ट पर रीसेट हो जाएंगी. सभी स्थापित AmneziaVPN सेवाएँ अभी भी सर्वर पर रहेंगी।. - + Continue जारी रखना - + Cancel रद्द करना - + Cannot reset settings during active connection सक्रिय कनेक्शन के दौरान सेटिंग्स रीसेट नहीं की जा सकतीं @@ -1666,78 +1703,78 @@ Already installed containers were found on the server. All installed containers PageSettingsBackup - + Settings restored from backup file बैकअप फ़ाइल से सेटिंग्स पुनर्स्थापित की गईं - + Back up your configuration अपने कॉन्फ़िगरेशन का बैकअप लें - + You can save your settings to a backup file to restore them the next time you install the application. अगली बार जब आप एप्लिकेशन इंस्टॉल करेंगे तो उन्हें पुनर्स्थापित करने के लिए आप अपनी सेटिंग्स को बैकअप फ़ाइल में सहेज सकते हैं. - + The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. बैकअप में AmneziaVPN में जोड़े गए सभी सर्वरों के लिए आपके पासवर्ड और निजी कुंजी शामिल होंगी। इस जानकारी को सुरक्षित स्थान पर रखें. - + Make a backup बैकअप बनाएं - + Save backup file बैकअप फ़ाइल सहेजें - - + + Backup files (*.backup) बैकअप फ़ाइलें (*.backup) - + Backup file saved बैकअप फ़ाइल सहेजी गई - + Restore from backup बैकअप से बहाल करना - + Open backup file बैकअप फ़ाइल खोलें - + Import settings from a backup file? बैकअप फ़ाइल से सेटिंग्स आयात करें? - + All current settings will be reset सभी मौजूदा सेटिंग्स रीसेट कर दी जाएंगी - + Continue जारी रखना - + Cancel रद्द करना - + Cannot restore backup settings during active connection सक्रिय कनेक्शन के दौरान बैकअप सेटिंग्स को पुनर्स्थापित नहीं किया जा सकता @@ -1745,62 +1782,62 @@ Already installed containers were found on the server. All installed containers PageSettingsConnection - + Connection कनेक्शन - + When AmneziaDNS is not used or installed जब AmneziaDNS का उपयोग या स्थापित नहीं किया जाता है - + Allows you to use the VPN only for certain Apps आपको केवल कुछ ऐप्स के लिए वीपीएन का उपयोग करने की अनुमति देता है - + Use AmneziaDNS Amneziaडीएनएस का प्रयोग करें - + If AmneziaDNS is installed on the server यदि AmneziaDNS सर्वर पर स्थापित है - + DNS servers DNS सर्वर - + Site-based split tunneling साइट-आधारित विभाजित टनलिंग - + Allows you to select which sites you want to access through the VPN आपको यह चुनने की अनुमति देता है कि आप वीपीएन के माध्यम से किन साइटों तक पहुंचना चाहते हैं - + App-based split tunneling ऐप-आधारित स्प्लिट टनलिंग - + KillSwitch स्विच बन्द कर दो - + Disables your internet if your encrypted VPN connection drops out for any reason. यदि आपका एन्क्रिप्टेड वीपीएन कनेक्शन किसी भी कारण से बंद हो जाता है तो आपका इंटरनेट अक्षम कर देता है. - + Cannot change killSwitch settings during active connection सक्रिय कनेक्शन के दौरान किलस्विच सेटिंग्स को नहीं बदला जा सकता @@ -1808,62 +1845,62 @@ Already installed containers were found on the server. All installed containers PageSettingsDns - + Default server does not support custom DNS डिफ़ॉल्ट सर्वर कस्टम डीएनएस का समर्थन नहीं करता है - + DNS servers DNS सर्वर - + If AmneziaDNS is not used or installed यदि AmneziaDNS का उपयोग या स्थापित नहीं किया गया है - + Primary DNS प्राथमिक डीएनएस - + Secondary DNS द्वितीयक डीएनएस - + Restore default डिफ़ॉल्ट बहाल - + Restore default DNS settings? डिफ़ॉल्ट DNS सेटिंग्स पुनर्स्थापित करें? - + Continue जारी रखना - + Cancel रद्द करना - + Settings have been reset सेटिंग्स रीसेट कर दी गई हैं - + Save सहेजें - + Settings saved सेटिंग्स को सहेजा गया @@ -1875,12 +1912,12 @@ Already installed containers were found on the server. All installed containers लॉगिंग सक्षम है. ध्यान दें कि 14 दिनों के बाद लॉग स्वचालित रूप से अक्षम हो जाएंगे, और सभी लॉग फ़ाइलें हटा दी जाएंगी. - + Logging लॉगिंग - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. इस फ़ंक्शन को सक्षम करने से एप्लिकेशन के लॉग स्वचालित रूप से सहेजे जाएंगे, डिफ़ॉल्ट रूप से, लॉगिंग कार्यक्षमता अक्षम है। एप्लिकेशन की खराबी की स्थिति में लॉग सेविंग सक्षम करें. @@ -1893,20 +1930,20 @@ Already installed containers were found on the server. All installed containers लॉग के साथ फ़ोल्डर खोलें - - + + Save सहेजें - - + + Logs files (*.log) लॉग फ़ाइलें (*.log) - - + + Logs file saved लॉग फ़ाइल सहेजी गई @@ -1915,64 +1952,62 @@ Already installed containers were found on the server. All installed containers फ़ाइल में लॉग सहेजें - + Enable logs - + Clear logs? लॉग साफ़ करें? - + Continue जारी रखना - + Cancel रद्द करना - + Logs have been cleaned up लॉग साफ़ कर दिए गए हैं - + Client logs - + AmneziaVPN logs - - + Open logs folder - - + Export logs - + Service logs - + AmneziaVPN-service logs - + Clear logs लॉग साफ़ करें @@ -1990,12 +2025,12 @@ Already installed containers were found on the server. All installed containers कोई नया स्थापित कंटेनर नहीं मिला - + Do you want to reboot the server? क्या आप सर्वर को रीबूट करना चाहते हैं? - + Do you want to clear server from Amnezia software? क्या आप एमनेज़िया सॉफ़्टवेयर से सर्वर साफ़ करना चाहते हैं? @@ -2005,18 +2040,18 @@ Already installed containers were found on the server. All installed containers - - - - + + + + Continue जारी रखना - - - - + + + + Cancel रद्द करना @@ -2031,67 +2066,67 @@ Already installed containers were found on the server. All installed containers यदि वे प्रदर्शित नहीं थे तो उन्हें एप्लिकेशन में जोड़ें - + Reboot server सर्वर रीबूट करें - + The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? रीबूट प्रक्रिया में लगभग 30 सेकंड लग सकते हैं। आप निश्चित है आप आगे बढ़ना चाहते है? - + Cannot reboot server during active connection सक्रिय कनेक्शन के दौरान सर्वर को रीबूट नहीं किया जा सकता - + Remove server from application एप्लिकेशन से सर्वर हटाएं - + Do you want to remove the server from application? क्या आप एप्लिकेशन से सर्वर हटाना चाहते हैं? - + Cannot remove server during active connection सक्रिय कनेक्शन के दौरान सर्वर को हटाया नहीं जा सकता - + All users whom you shared a connection with will no longer be able to connect to it. वे सभी उपयोगकर्ता जिनके साथ आपने कनेक्शन साझा किया था, वे अब इससे कनेक्ट नहीं हो पाएंगे. - + Cannot clear server from Amnezia software during active connection सक्रिय कनेक्शन के दौरान एमनेज़िया सॉफ़्टवेयर से सर्वर साफ़ नहीं किया जा सकता - + Reset API config एपीआई कॉन्फिगरेशन रीसेट करें - + Do you want to reset API config? क्या आप एपीआई कॉन्फिगरेशन रीसेट करना चाहते हैं? - + Cannot reset API config during active connection सक्रिय कनेक्शन के दौरान एपीआई कॉन्फिगरेशन को रीसेट नहीं किया जा सकता - + All installed AmneziaVPN services will still remain on the server. सभी स्थापित AmneziaVPN सेवाएँ अभी भी सर्वर पर रहेंगी. - + Clear server from Amnezia software एमनेज़िया सॉफ़्टवेयर से सर्वर साफ़ करें @@ -2099,27 +2134,32 @@ Already installed containers were found on the server. All installed containers PageSettingsServerInfo - + + Subscription is valid until + + + + Server name सर्वर का नाम - + Save सहेजें - + Protocols प्रोटोकॉल - + Services सेवाएं - + Management प्रबंध @@ -2127,7 +2167,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServerProtocol - + settings समायोजन @@ -2136,7 +2176,7 @@ Already installed containers were found on the server. All installed containers %1 प्रोफ़ाइल साफ़ करें - + Clear %1 profile? %1 प्रोफ़ाइल साफ़ करें? @@ -2146,64 +2186,64 @@ Already installed containers were found on the server. All installed containers - + Unable to clear %1 profile while there is an active connection सक्रिय कनेक्शन होने पर %1 प्रोफ़ाइल साफ़ करने में असमर्थ - + Remove निकालना - + All users with whom you shared a connection will no longer be able to connect to it. वे सभी उपयोगकर्ता जिनके साथ आपने कनेक्शन साझा किया था, वे अब इससे कनेक्ट नहीं हो पाएंगे. - + Cannot remove active container सक्रिय कंटेनर को हटाया नहीं जा सकता - + Remove %1 from server? सर्वर से %1 हटाएँ? - + connection settings - + Click the "connect" button to create a connection configuration - + server settings - + Clear profile - + The connection configuration will be deleted for this device only - - + + Continue जारी रखना - - + + Cancel रद्द करना @@ -2211,7 +2251,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServersList - + Servers सर्वर @@ -2219,100 +2259,100 @@ Already installed containers were found on the server. All installed containers PageSettingsSplitTunneling - + Default server does not support split tunneling function डिफ़ॉल्ट सर्वर स्प्लिट टनलिंग फ़ंक्शन का समर्थन नहीं करता है - + Addresses from the list should not be accessed via VPN सूची के पतों को वीपीएन के माध्यम से एक्सेस नहीं किया जाना चाहिए - + Split tunneling विभाजित सुरंग - + Mode तरीका - + Remove निकालना - + Continue जारी रखना - + Cancel रद्द करना - + Only the sites listed here will be accessed through the VPN केवल यहां सूचीबद्ध साइटों को ही वीपीएन के माध्यम से एक्सेस किया जाएगा - + Cannot change split tunneling settings during active connection सक्रिय कनेक्शन के दौरान स्प्लिट टनलिंग सेटिंग्स को नहीं बदला जा सकता - + website or IP वेबसाइट या आईपी - + Import / Export Sites आयात/निर्यात साइटें - + Import आयात - + Save site list साइट सूची सहेजें - + Save sites साइटें सहेजें - - - + + + Sites files (*.json) - + Import a list of sites साइटों की सूची आयात करें - + Replace site list साइट सूची बदलें - - + + Open sites file साइट फ़ाइल खोलें - + Add imported sites to existing ones आयातित साइटों को मौजूदा साइटों में जोड़ें @@ -2320,32 +2360,32 @@ Already installed containers were found on the server. All installed containers PageSetupWizardApiServiceInfo - + For the region - + Price - + Work period - + Speed - + Features - + Connect कनेक्ट @@ -2353,12 +2393,12 @@ Already installed containers were found on the server. All installed containers PageSetupWizardApiServicesList - + VPN by Amnezia - + Choose a VPN service that suits your needs. @@ -2382,7 +2422,7 @@ Already installed containers were found on the server. All installed containers कनेक्शन सेटिंग्स वाली फ़ाइल - + Connection कनेक्शन @@ -2392,90 +2432,125 @@ Already installed containers were found on the server. All installed containers समायोजन - + Enable logs - + + Support tag + + + + + Copied + कॉपी किया गया + + + Insert the key, add a configuration file or scan the QR-code - + Insert key - + Insert डालना - + Continue जारी रखना - + Other connection options - + + Site Amnezia + + + + VPN by Amnezia - + Connect to classic paid and free VPN services from Amnezia - + Self-hosted VPN - + Configure Amnezia VPN on your own server - + Restore from backup बैकअप से बहाल करना - + + + + + + Open backup file बैकअप फ़ाइल खोलें - + Backup files (*.backup) बैकअप फ़ाइलें (*.backup) - + File with connection settings कनेक्शन सेटिंग्स वाली फ़ाइल - + + + + + + Open config file कॉन्फ़िग फ़ाइल खोलें - + QR code क्यू आर संहिता - + + + + + + I have nothing मेरे पास कुछ नहीं है + + + + + Key as text पाठ के रूप में कुंजी @@ -2484,67 +2559,67 @@ Already installed containers were found on the server. All installed containers PageSetupWizardCredentials - + Configure your server अपना सर्वर कॉन्फ़िगर करें - + Server IP address [:port] सर्वर आईपी पता [:पोर्ट] - + Continue जारी रखना - + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties आपके द्वारा दर्ज किया गया सभी डेटा पूरी तरह से गोपनीय रहेगा और एमनेज़िया या किसी तीसरे पक्ष को साझा या प्रकट नहीं किया जाएगा - + 255.255.255.255:22 255.255.255.255:22 - + SSH Username SSH उपयोगकर्ता नाम - + Password or SSH private key पासवर्ड या SSH निजी कुंजी - + How to run your VPN server - + Where to get connection data, step-by-step instructions for buying a VPS - + Ip address cannot be empty आईपी ​​पता खाली नहीं हो सकता - + Enter the address in the format 255.255.255.255:88 पता 255.255.255.255:88 प्रारूप में दर्ज करें - + Login cannot be empty लॉगिन खाली नहीं हो सकता - + Password/private key cannot be empty पासवर्ड/निजी कुंजी खाली नहीं हो सकती @@ -2552,22 +2627,22 @@ Already installed containers were found on the server. All installed containers PageSetupWizardEasy - + What is the level of internet control in your region? आपके क्षेत्र में इंटरनेट नियंत्रण का स्तर क्या है? - + Choose a VPN protocol एक वीपीएन प्रोटोकॉल चुनें - + Continue जारी रखना - + Skip setup सेटअप छोड़ें @@ -2614,37 +2689,37 @@ Already installed containers were found on the server. All installed containers PageSetupWizardProtocolSettings - + Installing %1 %1 स्थापित किया जा रहा है - + More detailed अधिक विवरण - + Close बंद करना - + Network protocol नेटवर्क प्रोटोकॉल - + Port منفذ - + Install स्थापित करना - + The port must be in the range of 1 to 65535 @@ -2652,12 +2727,12 @@ Already installed containers were found on the server. All installed containers PageSetupWizardProtocols - + VPN protocol VPN प्रोटोकॉल - + Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP. वह चुनें जो आपके लिए सर्वोच्च प्राथमिकता हो। बाद में, आप अन्य प्रोटोकॉल और अतिरिक्त सेवाएँ, जैसे DNS प्रॉक्सी और SFTP स्थापित कर सकते हैं. @@ -2693,7 +2768,7 @@ Already installed containers were found on the server. All installed containers मेरे पास कुछ नहीं है - + Let's get started @@ -2701,27 +2776,27 @@ Already installed containers were found on the server. All installed containers PageSetupWizardTextKey - + Connection key कनेक्शन कुंजी - + A line that starts with vpn://... एक लाइन जो vpn://... से शुरू होती है... - + Key चाबी - + Insert डालना - + Continue जारी रखना @@ -2729,32 +2804,32 @@ Already installed containers were found on the server. All installed containers PageSetupWizardViewConfig - + New connection नया कनेक्शन - + Collapse content सामग्री संक्षिप्त करें - + Show content सामग्री दिखाओ - + Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. वायरगार्ड अस्पष्टीकरण सक्षम करें. यदि आपके प्रदाता पर वायरगार्ड अवरुद्ध है तो यह उपयोगी हो सकता है. - + Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. केवल उन स्रोतों से कनेक्शन कोड का उपयोग करें जिन पर आपको भरोसा है। हो सकता है कि आपके डेटा को इंटरसेप्ट करने के लिए सार्वजनिक स्रोतों से कोड बनाए गए हों. - + Connect कनेक्ट @@ -2762,37 +2837,37 @@ Already installed containers were found on the server. All installed containers PageShare - + Save OpenVPN config OpenVPN कॉन्फ़िगरेशन सहेजें - + Save WireGuard config वायरगार्ड कॉन्फ़िगरेशन सहेजें - + Save AmneziaWG config AmneziaWG कॉन्फ़िगरेशन सहेजें - + Save Shadowsocks config शैडोसॉक्स कॉन्फ़िगरेशन सहेजें - + Save Cloak config क्लोक कॉन्फ़िगरेशन सहेजें - + Save XRay config एक्सरे कॉन्फिगरेशन सहेजें - + For the AmneziaVPN app AmneziaVPN ऐप के लिए @@ -2801,176 +2876,181 @@ Already installed containers were found on the server. All installed containers OpenVpn मूल स्वरूप - + WireGuard native format वायरगार्ड मूल प्रारूप - + AmneziaWG native format AmneziaWG मूल प्रारूप - + Shadowsocks native format शैडोसॉक्स मूल प्रारूप - + Cloak native format लबादा देशी स्वरूप - + XRay native format एक्सरे देशी प्रारूप - + Share VPN Access VPN एक्सेस साझा करें - + Share full access to the server and VPN सर्वर और वीपीएन तक पूर्ण पहुंच साझा करें - + Use for your own devices, or share with those you trust to manage the server. अपने स्वयं के उपकरणों के लिए उपयोग करें, या सर्वर को प्रबंधित करने के लिए उन लोगों के साथ साझा करें जिन पर आप भरोसा करते हैं. - - + + Users उपयोगकर्ताओं - + Share VPN access without the ability to manage the server सर्वर को प्रबंधित करने की क्षमता के बिना वीपीएन एक्सेस साझा करें - + Search खोज - + Creation date: %1 निर्माण दिनांक: %1 - + Latest handshake: %1 नवीनतम हाथ मिलाना: %1 - + Data received: %1 प्राप्त डेटा: %1 - + Data sent: %1 डेटा भेजा गया: %1 + + + Allowed IPs: %1 + + Creation date: निर्माण तिथि: - + Rename नाम बदलें - + Client name ग्राहक नाम - + Save सहेजें - + Revoke निरस्त करें - + Revoke the config for a user - %1? किसी उपयोक्ता के लिए कॉन्फ़िगरेशन निरस्त करें - %1? - + The user will no longer be able to connect to your server. उपयोगकर्ता अब आपके सर्वर से कनेक्ट नहीं हो पाएगा. - + Continue जारी रखना - + Cancel रद्द करना - + Connection कनेक्शन - - + + Server सर्वर - + File with connection settings to कनेक्शन सेटिंग्स वाली फ़ाइल - - + + Protocol शिष्टाचार - + Connection to कनेक्शन के लिए - + Config revoked कॉन्फ़िगरेशन निरस्त कर दिया गया - + OpenVPN native format - + User name उपयोगकर्ता नाम - - + + Connection format कनेक्शन प्रारूप - - + + Share शेयर करना @@ -2978,55 +3058,55 @@ Already installed containers were found on the server. All installed containers PageShareFullAccess - + Full access to the server and VPN सर्वर और वीपीएन तक पूर्ण पहुंच - + We recommend that you use full access to the server only for your own additional devices. हम अनुशंसा करते हैं कि आप सर्वर तक पूर्ण पहुंच का उपयोग केवल अपने अतिरिक्त उपकरणों के लिए करें. - + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. यदि आप अन्य लोगों के साथ पूर्ण पहुंच साझा करते हैं, तो वे सर्वर पर प्रोटोकॉल और सेवाओं को हटा और जोड़ सकते हैं, जिससे वीपीएन सभी उपयोगकर्ताओं के लिए गलत तरीके से काम करेगा. - - + + Server सर्वर - + Accessing एक्सेस करना - + File with accessing settings to एक्सेस सेटिंग्स वाली फ़ाइल - + Share शेयर करना - + Access error! प्रवेश त्रुटि! - + Connection to के लिए कनेक्शन - + File with connection settings to कनेक्शन सेटिंग्स वाली फ़ाइल @@ -3034,17 +3114,17 @@ Already installed containers were found on the server. All installed containers PageStart - + Logging was disabled after 14 days, log files were deleted 14 दिनों के बाद लॉगिंग अक्षम कर दी गई, लॉग फ़ाइलें हटा दी गईं - + Settings restored from backup file बैकअप फ़ाइल से सेटिंग्स पुनर्स्थापित की गईं - + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. @@ -3052,7 +3132,7 @@ Already installed containers were found on the server. All installed containers PopupType - + Close बंद करना @@ -3393,42 +3473,57 @@ Already installed containers were found on the server. All installed containers सर्वर से कनेक्ट होने का समय समाप्त - + + Unable to open config file + + + + VPN connection error VPN कनेक्शन त्रुटि - + Error when retrieving configuration from API एपीआई से कॉन्फ़िगरेशन पुनर्प्राप्त करते समय त्रुटि - + This config has already been added to the application यह कॉन्फ़िगरेशन पहले ही एप्लिकेशन में जोड़ा जा चुका है - + In the response from the server, an empty config was received - + SSL error occurred - + Server response timeout on api request - + Missing AGW public key - + + Failed to decrypt response payload + + + + + Missing list of available services + + + + ErrorCode: %1. ErrorCode: %1. @@ -3493,37 +3588,37 @@ Already installed containers were found on the server. All installed containers कॉन्फ़िगरेशन में सर्वर से कनेक्ट करने के लिए कोई कंटेनर और क्रेडेंशियल नहीं है - + QFile error: The file could not be opened Qफ़ाइल त्रुटि: फ़ाइल खोली नहीं जा सकी - + QFile error: An error occurred when reading from the file Qफ़ाइल त्रुटि: फ़ाइल से पढ़ते समय एक त्रुटि उत्पन्न हुई - + QFile error: The file could not be accessed Qफ़ाइल त्रुटि: फ़ाइल तक नहीं पहुंचा जा सका - + QFile error: An unspecified error occurred Qफ़ाइल त्रुटि: एक अनिर्दिष्ट त्रुटि उत्पन्न हुई - + QFile error: A fatal error occurred Qफ़ाइल त्रुटि: एक घातक त्रुटि उत्पन्न हुई - + QFile error: The operation was aborted Qफ़ाइल त्रुटि: ऑपरेशन निरस्त कर दिया गया था - + Internal error आंतरिक त्रुटि @@ -3963,11 +4058,19 @@ While it offers a blend of security, stability, and speed, it's essential t SelectLanguageDrawer - + Choose language भाषा चुनें + + ServersListView + + + Unable change server while there is an active connection + सक्रिय कनेक्शन होने पर सर्वर बदलने में असमर्थ + + Settings @@ -3985,12 +4088,12 @@ While it offers a blend of security, stability, and speed, it's essential t SettingsController - + Backup file is corrupted बैकअप फ़ाइल दूषित है - + All settings have been reset to default values सभी सेटिंग्स को डिफ़ॉल्ट मानों पर रीसेट कर दिया गया है @@ -4004,33 +4107,33 @@ While it offers a blend of security, stability, and speed, it's essential t AmneziaVPN कॉन्फ़िगरेशन सहेजें - + Share शेयर करना - + Copy कॉपी - - + + Copied कॉपी किया गया - + Copy config string कॉन्फिग स्ट्रिंग कॉपी करें - + Show connection settings कनेक्शन सेटिंग दिखाएं - + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" एमनेज़िया ऐप में क्यूआर कोड पढ़ने के लिए, "सर्वर जोड़ें" → "मेरे पास कनेक्ट करने के लिए डेटा है" → "क्यूआर कोड, कुंजी या सेटिंग्स फ़ाइल" चुनें। @@ -4053,27 +4156,27 @@ While it offers a blend of security, stability, and speed, it's essential t साइट हटाई गई: %1 - + Can't open file: %1 फ़ाइल नहीं खुल सकती: %1 - + Failed to parse JSON data from file: %1 फ़ाइल से JSON डेटा पार्स करने में विफल:%1 - + The JSON data is not an array in file: %1 JSON डेटा फ़ाइल में कोई सरणी नहीं है: %1 - + Import completed आयात पूरा हुआ - + Export completed निर्यात पूरा हुआ @@ -4114,7 +4217,7 @@ While it offers a blend of security, stability, and speed, it's essential t TextFieldWithHeaderType - + The field can't be empty फ़ील्ड खाली नहीं हो सकती @@ -4122,7 +4225,7 @@ While it offers a blend of security, stability, and speed, it's essential t VpnConnection - + Mbps @@ -4208,12 +4311,12 @@ While it offers a blend of security, stability, and speed, it's essential t main2 - + Private key passphrase निजी कुंजी पासफ़्रेज़ - + Save सहेजें diff --git a/client/translations/amneziavpn_my_MM.ts b/client/translations/amneziavpn_my_MM.ts index 3e964cc9..55243d1b 100644 --- a/client/translations/amneziavpn_my_MM.ts +++ b/client/translations/amneziavpn_my_MM.ts @@ -1,55 +1,63 @@ + + AdLabel + + + Amnezia Premium - for access to any website + + + ApiServicesModel - + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s သက်တောင့်သက်သာအလုပ်လုပ်နိုင်ဖို့အတွက်နှင့် ကြီးမားသောဖိုင်များကိုဒေါင်းလုဒ်လုပ်ခြင်းနှင့် ဗီဒီယိုများကြည့်ရှုခြင်းတို့အတွက် အသုံးပြုနိုင်သော VPN ဖြစ်ပါတယ်။ မည်သည့်ဆိုက်များအတွက်မဆိုအလုပ်လုပ်ပြီး လိုင်းအရှိန် %1 MBit/s အထိအသုံးပြုနိုင်ပါတယ်။ - + VPN to access blocked sites in regions with high levels of Internet censorship. အင်တာနက် ဆင်ဆာဖြတ်တောက်မှု မြင့်မားသော ဒေသများရှိ ပိတ်ဆို့ထားသော ဆိုက်များကို ဝင်ရောက်ရန် VPN။. - + <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> - + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. Amnezia Premium - သက်တောင့်သက်သာအလုပ်လုပ်နိုင်ဖို့အတွက်နှင့် ကြီးမားသောဖိုင်များကိုဒေါင်းလုဒ်လုပ်ခြင်းနှင့် ဗီဒီယိုများကိုကြည်လင်ပြတ်သားစွာကြည့်ရှုခြင်းတို့အတွက် အသုံးပြုနိုင်သော VPN ဖြစ်ပါတယ်။ အင်တာနက်ဆင်ဆာဖြတ်မှု အဆင့်အမြင့်ဆုံးနိုင်ငံများတွင်ပင် မည်သည့်ဆိုက်များအတွက်မဆို အလုပ်လုပ်ပါသည်။. - + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship Amnezia Free သည် အင်တာနက်ဆင်ဆာဖြတ်တောက်မှု မြင့်မားသောနိုင်ငံများတွင် ပိတ်ဆို့ခြင်းကို ကျော်ဖြတ်ရန်အတွက် အခမဲ့ VPN တစ်ခုဖြစ်ပါသည်။ - + %1 MBit/s %1 MBit/s - + %1 days %1 ရက် - + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> ဤ VPN သည် သင့်ဒေသရှိ Instagram၊ Facebook၊ Twitter နှင့် အခြားသော လူကြိုက်များသော ဆိုက်များကိုသာ ဖွင့်ပေးပါမည်။ အခြားဝဘ်ဆိုက်များကိုမူ သင်၏ IP လိပ်စာအစစ်အမှန်ဖြင့်သာ ဖွင့်ပေးပါမည်၊ <a href="%1/free" style="color: #FBB26A;">နောက်ထပ်အသေးစိတ်အချက်အလက်များကို ဝဘ်ဆိုဒ်ပေါ်တွင်ကြည့်ရန်</a> - + Free အခမဲ့ - + %1 $/month %1 $/တစ်လ @@ -80,7 +88,7 @@ ConnectButton - + Unable to disconnect during configuration preparation Configuration ပြင်ဆင်ခြင်းလုပ်ဆောင်နေချိန်အတွင်း ချိတ်ဆက်မှုဖြတ်တောက်၍မရပါ @@ -88,62 +96,62 @@ ConnectionController - + VPN Protocols is not installed. Please install VPN container at first VPN ပရိုတိုကောများကို မထည့်သွင်းရသေးပါ။ ကျေးဇူးပြု၍ VPN ကွန်တိန်နာကို အရင်ထည့်သွင်းပါ။ - + Connecting... ချိတ်ဆက်နေပါပြီ... - + Connected ချိတ်ဆက်ပြီးသွားပါပြီ - + Preparing... ပြင်ဆင်နေသည်... - + Settings updated successfully, reconnnection... ဆက်တင်များကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ပြီးပါပြီ၊ ပြန်လည်ချိတ်ဆက်နေပါသည်... - + Settings updated successfully ဆက်တင်များကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ပြီးပါပြီ - + The selected protocol is not supported on the current platform ရွေးချယ်ထားသော ပရိုတိုကောကို လက်ရှိပလက်ဖောင်းပေါ်တွင် အ‌ထောက်အပံ့မပေးထားပါ - + unable to create configuration configuration ဖန်တီး၍မရပါ - + Reconnecting... ပြန်လည်ချိတ်ဆက်နေပါသည်... - - - + + + Connect ချိတ်ဆက်မည် - + Disconnecting... အဆက်အသွယ်ဖြတ်နေပါသည်... @@ -151,17 +159,17 @@ ConnectionTypeSelectionDrawer - + Add new connection ချိတ်ဆက်မှုအသစ်ထည့်သွင်းမည် - + Configure your server သင်၏ဆာဗာကို စီစဉ်ချိန်ညှိမည် - + Open config file, key or QR code config ဖိုင်၊ key သို့မဟုတ် QR ကုဒ်ကို ဖွင့်မည် @@ -199,7 +207,7 @@ HomeContainersListView - + Unable change protocol while there is an active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ပရိုတိုကောကို ပြောင်းလဲ၍မရပါ။ @@ -207,46 +215,46 @@ HomeSplitTunnelingDrawer - + Split tunneling Split tunneling - + Allows you to connect to some sites or applications through a VPN connection and bypass others VPN ချိတ်ဆက်မှုကြားခံ၍ အချို့သောဆိုက်များ သို့မဟုတ် အပလီကေးရှင်းများသို့ ချိတ်ဆက်ဖို့ရန်နှင့် အခြားအရာများကို ကျော်ဖြတ်ရန် လုပ်ဆောင်ပေးသည်။ - + Split tunneling on the server ဆာဗာပေါ်တွင် split tunneling အသုံးပြုထားပါသည်။ - + Enabled Can't be disabled for current server ဖွင့်ထားပါသည်။ လက်ရှိဆာဗာအတွက် ပိတ်၍မရပါ။ - + Site-based split tunneling ဝက်ဆိုဒ်အခြေပြု split tunneling - - + + Enabled ဖွင့်ထားပါသည်။ - - + + Disabled ပိတ်ထားပါသည်။ - + App-based split tunneling App အခြေပြု split tunneling @@ -254,23 +262,20 @@ Can't be disabled for current server ImportController - Unable to open file - ဖိုင်ကိုဖွင့်၍မရပါ + ဖိုင်ကိုဖွင့်၍မရပါ - - Invalid configuration file - Configuration ဖိုင် မမှန်ကန်ပါ + Configuration ဖိုင် မမှန်ကန်ပါ - + Scanned %1 of %2. %2 ၏ %1 ကို စကင်န်ဖတ်ထားသည်. - + In the imported configuration, potentially dangerous lines were found: တင်သွင်းသည့် configuration တွင်၊ အန္တရာယ်ရှိနိုင်သည့်စာလိုင်းများကို တွေ့ရှိခဲ့သည်: @@ -278,86 +283,86 @@ Can't be disabled for current server InstallController - + %1 installed successfully. %1 ကို အောင်မြင်စွာ ထည့်သွင်းပြီးပါပြီ. - + %1 is already installed on the server. %1 ကို ဆာဗာတွင် ထည့်သွင်းပြီးဖြစ်သည်. - + Added containers that were already installed on the server ဆာဗာတွင် ထည့်သွင်းပြီးသား ကွန်တိန်နာများကို ပေါင်းထည့်ပြီးပါပြီ။ - + Already installed containers were found on the server. All installed containers have been added to the application ထည့်သွင်းပြီးသား ကွန်တိန်နာများကို ဆာဗာပေါ်တွင် တွေ့ရှိခဲ့သည်။ ထည့်သွင်းထားသည့် ကွန်တိန်နာအားလုံးကို အပလီကေးရှင်းထဲသို့ ပေါင်းထည့်ပြီးပါပြီ။ - + Settings updated successfully ဆက်တင်များကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ပြီးပါပြီ။ - + Server '%1' was rebooted ဆာဗာ '%1' ကို ပြန်လည်စတင်ခဲ့သည်။ - + Server '%1' was removed ဆာဗာ '%1' ကို ဖယ်ရှားခဲ့သည်။ - + All containers from server '%1' have been removed ဆာဗာ '%1' မှ ကွန်တိန်နာအားလုံးကို ဖယ်ရှားလိုက်ပါပြီ။ - + %1 has been removed from the server '%2' %1 ကို ဆာဗာ '%2' မှ ဖယ်ရှားလိုက်ပါပြီ - + Api config removed Api config ကိုဖယ်ရှားလိုက်သည် - + %1 cached profile cleared ကက်ရှ်လုပ်ထားတဲ့ ပရိုဖိုင် %1 ခုကို ရှင်းပြီးပါပြီ - + Please login as the user အသုံးပြုသူအဖြစ် log in ဝင်ရောက်ပါ - + Server added successfully ဆာဗာကို အောင်မြင်စွာ ထည့်သွင်းပြီးပါပြီ - + %1 installed successfully. %1 ခုကို အောင်မြင်စွာ ထည့်သွင်းပြီးပါပြီ. - + API config reloaded API config ကို ပြန်လည်စတင်လိုက်ပါပြီ - + Successfully changed the country of connection to %1 ချိတ်ဆက်မှုနိုင်ငံကို %1 သို့ အောင်မြင်စွာ ပြောင်းလဲလိုက်ပါပြီ @@ -370,12 +375,12 @@ Already installed containers were found on the server. All installed containers အပလီကေးရှင်းရွေးမည် - + application name အပလီကေးရှင်းအမည် - + Add selected ရွေးချယ်ထားသည်များကိုထည့်မည် @@ -443,12 +448,12 @@ Already installed containers were found on the server. All installed containers PageDevMenu - + Gateway endpoint Gateway အဆုံးမှတ် - + Dev gateway environment @@ -456,85 +461,84 @@ Already installed containers were found on the server. All installed containers PageHome - + Logging enabled Logging ဖွင့်ထားပါသည် - + Split tunneling enabled split tunnelling ဖွင့်ထားပါသည် - + Split tunneling disabled split tunnelling ပိတ်ထားပါသည် - + VPN protocol VPN ပရိုတိုကော - + Servers ဆာဗာများ - Unable change server while there is an active connection - လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆာဗာကို ပြောင်းလဲ၍မရပါ + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆာဗာကို ပြောင်းလဲ၍မရပါ PageProtocolAwgClientSettings - + AmneziaWG settings AmneziaWG ဆက်တင်များ - + MTU MTU - + Server settings - + Port Port - + Save သိမ်းဆည်းမည် - + Save settings? ဆက်တင်များကို သိမ်းဆည်းမည်လား? - + Only the settings for this device will be changed - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် - + Unable change settings while there is an active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ @@ -542,12 +546,12 @@ Already installed containers were found on the server. All installed containers PageProtocolAwgSettings - + AmneziaWG settings AmneziaWG ဆက်တင်များ - + Port Port @@ -556,87 +560,92 @@ Already installed containers were found on the server. All installed containers MTU - + All users with whom you shared a connection with will no longer be able to connect to it. သင်နှင့်အတူချိတ်ဆက်မှုတစ်ခုကို မျှဝေထားသည့် အသုံးပြုသူအားလုံး ချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. - + Save သိမ်းဆည်းမည် - + + VPN address subnet + VPN လိပ်စာ ကွန်ရက်ခွဲ + + + Jc - Junk packet count Jc - Junk packet အရေအတွက် - + Jmin - Junk packet minimum size Jmin - Junk packet အသေးငယ်ဆုံးလက်ခံနိုင်မှုအရွယ်အစား - + Jmax - Junk packet maximum size Jmax - Junk packet အကြီးဆုံးလက်ခံနိုင်မှုအရွယ်အစား - + S1 - Init packet junk size S1 - Init packet junk အရွယ်အစား - + S2 - Response packet junk size S2 - Response packet junk အရွယ်အစား - + H1 - Init packet magic header H1 - Init packet magic header - + H2 - Response packet magic header H2 - Response packet magic header - + H4 - Transport packet magic header H4 - Transport packet magic header - + H3 - Underload packet magic header H3 - Underload packet magic header - + The values of the H1-H4 fields must be unique H1-H4 အကွက်များ၏ တန်ဖိုးများသည် အခြားတန်ဖိုးများနှင့်မတူ တမူထူးခြားနေရပါမည် - + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) အကွက် S1 + မက်ဆေ့ချ် စတင်ခြင်း အရွယ်အစား (148) ၏ တန်ဖိုးသည် S2 + မက်ဆေ့ချ် တုံ့ပြန်မှု အရွယ်အစား (92) နှင့် မညီမျှရပါ - + Save settings? ဆက်တင်များကို သိမ်းဆည်းမည်လား? - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် - + Unable change settings while there is an active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ @@ -644,33 +653,33 @@ Already installed containers were found on the server. All installed containers PageProtocolCloakSettings - + Cloak settings ဖုံးကွယ်အသွင်ယူမှု ဆက်တင်များ - + Disguised as traffic from traffic အဖြစ် အသွင်ယူထားသည် - + Port Port - - + + Cipher စာဝှက် - + Save သိမ်းဆည်းမည် - + Unable change settings while there is an active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ @@ -678,175 +687,175 @@ Already installed containers were found on the server. All installed containers PageProtocolOpenVpnSettings - + OpenVPN settings OpenVPN ဆက်တင်များ - + VPN address subnet VPN လိပ်စာ ကွန်ရက်ခွဲ - + Network protocol ကွန်ယက် ပရိုတိုကော - + Port Port - + Auto-negotiate encryption အလိုအလျောက် ညှိနှိုင်း ကုဒ်ဝှက်ခြင်း - - + + Hash Hash - + SHA512 SHA512 - + SHA384 SHA384 - + SHA256 SHA256 - + SHA3-512 SHA3-512 - + SHA3-384 SHA3-384 - + SHA3-256 SHA3-256 - + whirlpool whirlpool - + BLAKE2b512 BLAKE2b512 - + BLAKE2s256 BLAKE2s256 - + SHA1 SHA1 - - + + Cipher စာဝှက် - + AES-256-GCM AES-256-GCM - + AES-192-GCM AES-192-GCM - + AES-128-GCM AES-128-GCM - + AES-256-CBC AES-256-CBC - + AES-192-CBC AES-192-CBC - + AES-128-CBC AES-128-CBC - + ChaCha20-Poly1305 ChaCha20-Poly1305 - + ARIA-256-CBC ARIA-256-CBC - + CAMELLIA-256-CBC CAMELLIA-256-CBC - + none none - + TLS auth TLS auth - + Block DNS requests outside of VPN VPN ပြင်ပရှိ DNS တောင်းဆိုမှုများကို ပိတ်ပင်မည်။ - + Additional client configuration commands ထပ်တိုး client ဖွဲ့စည်းမှုဆိုင်ရာ ညွှန်ကြားချက်များ - - + + Commands: အမိန့်ပေးခိုင်းစေချက်များ: - + Additional server configuration commands ထပ်တိုး ဆာဗာ ဖွဲ့စည်းမှုဆိုင်ရာ ညွှန်ကြားချက်များ - + Unable change settings while there is an active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ - + Save သိမ်းဆည်းမည် @@ -854,42 +863,42 @@ Already installed containers were found on the server. All installed containers PageProtocolRaw - + settings ဆက်တင်များ - + Show connection options ချိတ်ဆက်မှုရွေးချယ်စရာများကို ပြပါ။ - + Connection options %1 ချိတ်ဆက်မှုရွေးချယ်စရာများ %1 - + Remove ဖယ်ရှားမည် - + Remove %1 from server? %1 ကို ဆာဗာမှ ဖယ်ရှားမည်လား? - + All users with whom you shared a connection with will no longer be able to connect to it. သင်နှင့်အတူချိတ်ဆက်မှုတစ်ခုကို မျှဝေထားသည့် အသုံးပြုသူအားလုံး ချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် @@ -897,28 +906,28 @@ Already installed containers were found on the server. All installed containers PageProtocolShadowSocksSettings - + Shadowsocks settings Shadowsocks ဆက်တင်များ - + Port Port - - + + Cipher စာဝှက် - + Save သိမ်းဆည်းမည် - + Unable change settings while there is an active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ @@ -926,52 +935,52 @@ Already installed containers were found on the server. All installed containers PageProtocolWireGuardClientSettings - + WG settings WG ဆက်တင်များ - + MTU MTU - + Server settings - + Port Port - + Save သိမ်းဆည်းမည် - + Save settings? ဆက်တင်များကို သိမ်းဆည်းမည်လား? - + Only the settings for this device will be changed - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် - + Unable change settings while there is an active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ @@ -979,32 +988,37 @@ Already installed containers were found on the server. All installed containers PageProtocolWireGuardSettings - + WG settings WG ဆက်တင်များ - + + VPN address subnet + VPN လိပ်စာ ကွန်ရက်ခွဲ + + + Port Port - + Save settings? ဆက်တင်များကို သိမ်းဆည်းမည်လား? - + All users with whom you shared a connection with will no longer be able to connect to it. သင်နှင့်အတူချိတ်ဆက်မှုတစ်ခုကို မျှဝေထားသည့် အသုံးပြုသူအားလုံး ချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် @@ -1013,12 +1027,12 @@ Already installed containers were found on the server. All installed containers MTU - + Unable change settings while there is an active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ - + Save သိမ်းဆည်းမည် @@ -1026,22 +1040,22 @@ Already installed containers were found on the server. All installed containers PageProtocolXraySettings - + XRay settings XRay ဆက်တင်များ - + Disguised as traffic from traffic အဖြစ် အသွင်ယူထားသည် - + Save သိမ်းဆည်းမည် - + Unable change settings while there is an active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆက်တင်များကို ပြောင်းလဲ၍မရပါ @@ -1049,39 +1063,39 @@ Already installed containers were found on the server. All installed containers PageServiceDnsSettings - + A DNS service is installed on your server, and it is only accessible via VPN. DNS ဝန်ဆောင်မှုကို သင့်ဆာဗာတွင် ထည့်သွင်းထားပြီးဖြစ်ပြီး ၎င်းကို VPN မှတစ်ဆင့်သာ အသုံးပြုနိုင်သည်. - + The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. DNS လိပ်စာသည် သင့်ဆာဗာလိပ်စာနှင့် အတူတူပင်ဖြစ်ပါသည်။ ချိတ်ဆက်မှုတက်ဘ်အောက်ရှိ ဆက်တင်များတွင် DNS ကို ပြင်ဆင်ချိန်ညှိနိုင်ပါသည်. - + Remove ဖယ်ရှားမည် - + Remove %1 from server? %1 ကို ဆာဗာမှ ဖယ်ရှားမည်လား? - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် - + Cannot remove AmneziaDNS from running server AmneziaDNS ကို လည်ပတ်နေသည့်ဆာဗာမှ ဖယ်ရှား၍မရပါ @@ -1089,67 +1103,67 @@ Already installed containers were found on the server. All installed containers PageServiceSftpSettings - + Settings updated successfully ဆက်တင်များကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ပြီးပါပြီ။ - + SFTP settings SFTP ဆက်တင်များ - + Host Host - - - - + + + + Copied ကူးယူပြီးပါပြီ - + Port Port - + User name အသုံးပြုသူနာမည် - + Password စကားဝှက် - + Mount folder on device ဖိုင်တွဲကို စက်တွင် တပ်ဆင်မည်။ - + In order to mount remote SFTP folder as local drive, perform following steps: <br> အဝေးမှ SFTP ဖိုင်တွဲကို စက်တွင်း drive အဖြစ် တပ်ဆင်ရန်အတွက် အောက်ပါအဆင့်များကို လုပ်ဆောင်ပါ: <br> - - + + <br>1. Install the latest version of <br>၁။ နောက်ဆုံးထွက်ဗားရှင်းကို ထည့်သွင်းမည် - - + + <br>2. Install the latest version of <br>၂။ နောက်ဆုံးထွက်ဗားရှင်းကို ထည့်သွင်းမည် - + Detailed instructions အသေးစိတ်ညွှန်ကြားချက်များ @@ -1157,69 +1171,69 @@ Already installed containers were found on the server. All installed containers PageServiceSocksProxySettings - + Settings updated successfully ဆက်တင်များကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ပြီးပါပြီ - - + + SOCKS5 settings SOCKS5 ဆက်တင်များ - + Host Host - - - - + + + + Copied ကူးယူပြီးပါပြီ - - + + Port Port - + User name အသုံးပြုသူနာမည် - - + + Password စကားဝှက် - + Username အသုံးပြုသူနာမည် - - + + Change connection settings ချက်ဆက်မှုဆက်တင်များကို ပြောင်းလဲမည် - + The port must be in the range of 1 to 65535 Port သည် 1 မှ 65535 အတွင်း ဖြစ်ရမည် - + Password cannot be empty စကားဝှက် သည် ဗလာမဖြစ်ရပါ - + Username cannot be empty အသုံးပြုသူနာမည် သည် ဗလာမဖြစ်ရပါ @@ -1227,37 +1241,37 @@ Already installed containers were found on the server. All installed containers PageServiceTorWebsiteSettings - + Settings updated successfully ဆက်တင်များကို အောင်မြင်စွာ အပ်ဒိတ်လုပ်ပြီးပါပြီ။ - + Tor website settings Tor ဝဘ်ဆိုက်ဆက်တင်များ - + Website address ဝဘ်ဆိုဒ်လိပ်စာ - + Copied ကူးယူပြီးပါပြီ - + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. ဤ URL ကိုဖွင့်ရန် <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> ကို အသုံးပြုပါ. - + After creating your onion site, it takes a few minutes for the Tor network to make it available for use. သင်၏ onion ဆိုက်ကို ဖန်တီးပြီးနောက်၊ Tor ကွန်ရက်က ၄င်းကိုအသုံးပြုနိုင်အောင်ပြုလုပ်‌ပေးရန် မိနစ်အနည်းငယ်အချိန်ယူသည်. - + When configuring WordPress set the this onion address as domain. WordPress ကို ချိန်ညှိသည့်အခါ ဤ onion လိပ်စာကို domain အဖြစ် သတ်မှတ်ပါ. @@ -1265,42 +1279,42 @@ Already installed containers were found on the server. All installed containers PageSettings - + Settings ဆက်တင်များ - + Servers ဆာဗာများ - + Connection ချိတ်ဆက်မှု - + Application အပလီကေးရှင်း - + Backup backup ယူမည် - + About AmneziaVPN AmneziaVPN အကြောင်း - + Dev console ဒက်ဗယ်လော်ပါ console - + Close application အပလီကေးရှင်းကို ပိတ်မည် @@ -1308,32 +1322,32 @@ Already installed containers were found on the server. All installed containers PageSettingsAbout - + Support Amnezia Amnezia ကိုကူညီပံ့ပိုးမည် - + Amnezia is a free and open-source application. You can support the developers if you like it. Amnezia သည် အခမဲ့ open-source application တစ်ခုဖြစ်သည်။ သင်နှစ်သက်ပါက developer များကို ပံ့ပိုးနိုင်ပါသည်. - + Contacts ဆက်သွယ်ရန်လိပ်စာများ - + Telegram group Telegram ဂရု - + To discuss features feature များကိုဆွေးနွေးရန် - + https://t.me/amnezia_vpn_en https://t.me/amnezia_vpn @@ -1342,122 +1356,145 @@ Already installed containers were found on the server. All installed containers မေးလ် - + support@amnezia.org - + For reviews and bug reports သုံးသပ်ချက်များနှင့် ချွတ်ယွင်းချက်အစီရင်ခံစာများအတွက် - + Copied ကူးယူပြီးပါပြီ - + GitHub GitHub - + + Discover the source code + + + + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website ဝဘ်ဆိုက် - + + Visit official website + + + + Software version: %1 ဆော့ဖ်ဝဲဗားရှင်း: %1 - + Check for updates အပ်ဒိတ်များရှိမရှိ စစ်ဆေးမည် - + Privacy Policy ကိုယ်ရေးအချက်အလက်မူဝါဒ + + PageSettingsApiLanguageList + + + Unable change server location while there is an active connection + + + PageSettingsApiServerInfo - + For the region ဒေသအတွက် - + Price စျေးနှုန်း - + Work period အလုပ်လုပ်မည့်ကာလ - + + Valid until + + + + Speed မြန်နှုန်း - + Support tag ကူညီပံ့ပိုးမှု tag - + Copied ကူးယူပြီးပါပြီ - + Reload API config API config ကို ပြန်လည်စတင်မည် - + Reload API config? API config ကို ပြန်လည်စတင်မည်လား? - - + + Continue ဆက်လက်လုပ်ဆောင်မည် - - + + Cancel ပယ်ဖျက်မည် - + Cannot reload API config during active connection ချိတ်ဆက်မှုရှိနေချိန်အတွင်း API config ကို ပြန်လည်စတင်၍မရပါ - + Remove from application အပလီကေးရှင်းမှဖယ်ရှားမည် - + Remove from application? အပလီကေးရှင်းမှဖယ်ရှားမည်လား? - + Cannot remove server during active connection ချိတ်ဆက်မှုရှိနေချိန်အတွင်း ဆာဗာကို ဖယ်ရှား၍မရပါ @@ -1465,12 +1502,12 @@ Already installed containers were found on the server. All installed containers PageSettingsAppSplitTunneling - + Cannot change split tunneling settings during active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် split tunneling ဆက်တင်များကို ပြောင်းလဲ၍မရပါ - + Only the apps from the list should have access via VPN စာရင်းတွင်းပါဝင်သောအက်ပ်များသာလျှင် VPN မှတစ်ဆင့် ဝင်ရောက်ခွင့်ရှိလိမ့်မည်ဖြစ်သည် @@ -1480,42 +1517,42 @@ Already installed containers were found on the server. All installed containers စာရင်းတွင်းပါဝင်သောအက်ပ်များကို VPN မှတစ်ဆင့် ဝင်ရောက်ခွင့်ရရှိလိမ့်မည်မဟုတ်ပေ - + App split tunneling App split tunneling - + Mode Mode - + Remove ဖယ်ရှားမည် - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် - + application name အပလီကေးရှင်းအမည် - + Open executable file စီမံလုပ်ဆောင်နိုင်မှုဖိုင်ကိုဖွင့်မည် - + Executable files (*.*) စီမံလုပ်ဆောင်နိုင်မှုဖိုင်များ (*.*) @@ -1523,102 +1560,102 @@ Already installed containers were found on the server. All installed containers PageSettingsApplication - + Application အပလီကေးရှင်း - + Allow application screenshots အပလီကေးရှင်းကို screenshot ရိုက်ရန်ခွင့်ပြုမည် - + Enable notifications နိုတီများဖွင့်မည် - + Enable notifications to show the VPN state in the status bar စတေးတပ်ဘားတွင် VPN အခြေအနေကိုပြသရန် နိုတီများကို ဖွင့်မည် - + Auto start အလိုအ‌လျှောက်စတင်မည် - + Launch the application every time the device is starts စက်စတင်ချိန်တိုင်း အပလီကေးရှင်းကို စတင်မည် - + Auto connect အလိုအ‌လျှောက်ချိတ်ဆက်မည် - + Connect to VPN on app start အက်ပ်စတင်ချိန်တွင် VPN သို့ ချိတ်ဆက်မည် - + Start minimized အက်ပ်စတင်သည့်အခါ minimized ထားပြီးစတင်မည် - + Launch application minimized အက်ပ်ဖွင့်သည့်အခါ minimized ထားပြီးဖွင့်မည် - + Language ဘာသာစကား - + Logging Logging - + Enabled ဖွင့်ထားပါသည် - + Disabled ပိတ်ထားပါသည် - + Reset settings and remove all data from the application ဆက်တင်များနဂိုတိုင်းထားပြီး အပလီကေးရှင်းမှဒေတာအားလုံးဖယ်ရှားမည် - + Reset settings and remove all data from the application? ဆက်တင်များကို ပြန်လည်သတ်မှတ်ပြီး အပလီကေးရှင်းမှ ဒေတာအားလုံးကို ဖယ်ရှားမည်လား? - + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. ဆက်တင်အားလုံးကို မူရင်းအတိုင်း ပြန်လည်သတ်မှတ်ပါမည်။ ထည့်သွင်းထားသော AmneziaVPN ဝန်ဆောင်မှုများအားလုံးသည် ဆာဗာပေါ်တွင် ဆက်လက်ရှိနေမည်ဖြစ်သည်။. - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် - + Cannot reset settings during active connection ချိတ်ဆက်မှုရှိနေချိန်အတွင်း ဆက်တင်များကို မူရင်းအတိုင်း ပြန်လည်သတ်မှတ်၍မရပါ @@ -1626,78 +1663,78 @@ Already installed containers were found on the server. All installed containers PageSettingsBackup - + Settings restored from backup file ဆက်တင်များကို အရန်ဖိုင်မှ ပြန်လည်ရယူပြီးပါပြီ - + Back up your configuration သင်၏ဖွဲ့စည်းပုံကို အရန်သိမ်းပါ။ - + You can save your settings to a backup file to restore them the next time you install the application. သင်၏ဆက်တင်များကို အရန်ဖိုင်တွင် သိမ်းဆည်းထားခြင်းဖြင့် အပလီကေးရှင်းကို နောက်တစ်ကြိမ်ထည့်သွင်းသည့်အခါ ၎င်းဆက်တင်များကို ပြန်လည်ရယူနိုင်သည်. - + The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. အရံဖိုင်တွင် AmneziaVPN သို့ ထည့်ထားသော ဆာဗာအားလုံးအတွက် သင့်စကားဝှက်များနှင့် လျှို့ဝှက်သော့များ ပါဝင်ပါမည်။ ဤအချက်အလက်ကို လုံခြုံသောနေရာတွင် ထားပါ။. - + Make a backup အရံဖိုင်တစ်ခု ပြုလုပ်မည် - + Save backup file အရံဖိုင်ကို သိမ်းဆည်းမည် - - + + Backup files (*.backup) အရံဖိုင်များ (*.backup) - + Backup file saved အရံဖိုင်ကိုသိမ်းဆည်းပြီးပါပြီ - + Restore from backup အရံဖိုင်မှ ပြန်လည်ရယူမည် - + Open backup file အရံဖိုင်ကို ဖွင့်မည် - + Import settings from a backup file? ဆက်တင်များကို အရံဖိုင်တစ်ခုမှ ပြန်လည်တင်သွင်းမည်လား? - + All current settings will be reset လက်ရှိဆက်တင်များအားလုံးကို ပြန်လည်သတ်မှတ်ပါမည် - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် - + Cannot restore backup settings during active connection ချိတ်ဆက်မှုရှိနေချိန်အတွင်း အရံဆက်တင်များကို ပြန်လည်ရယူ၍မရပါ @@ -1705,62 +1742,62 @@ Already installed containers were found on the server. All installed containers PageSettingsConnection - + Connection ချိတ်ဆက်မှု - + Use AmneziaDNS AmneziaDNS ကို အသုံးပြုမည် - + If AmneziaDNS is installed on the server အကယ်၍ AmneziaDNS ကို ဆာဗာတွင် ထည့်သွင်းထားလျှင် - + DNS servers DNS ဆာဗာများ - + When AmneziaDNS is not used or installed AmneziaDNS ကို အသုံးမပြု သို့မဟုတ် ထည့်သွင်းခြင်းမပြုသည့်အခါ - + Allows you to use the VPN only for certain Apps အချို့သောအက်ပ်များအတွက်သာ VPN ကို အသုံးပြုခွင့်ပေးသည် - + KillSwitch KillSwitch - + Disables your internet if your encrypted VPN connection drops out for any reason. အကြောင်းတစ်ခုခုကြောင့် VPN ချိတ်ဆက်မှု ပျက်သွားပါက သင့်အင်တာနက်ကို ချက်ချင်းရပ်ဆိုင်းပေးသည်. - + Cannot change killSwitch settings during active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် killSwitch ဆက်တင်များကို ပြောင်းလဲ၍မရပါ - + Site-based split tunneling ဝက်ဆိုဒ်အခြေပြု split tunneling - + Allows you to select which sites you want to access through the VPN VPN မှတဆင့် သင်ဝင်ရောက်လိုသည့်ဆိုဒ်များကို ရွေးချယ်စေနိုင်သည် - + App-based split tunneling App အခြေပြု split tunneling @@ -1768,62 +1805,62 @@ Already installed containers were found on the server. All installed containers PageSettingsDns - + Default server does not support custom DNS မူရင်းဆာဗာသည် စိတ်ကြိုက်ထားနိုင်သည့် DNS ကို အထောက်အပံ့မပေးပါ - + DNS servers DNS ဆာဗာများ - + If AmneziaDNS is not used or installed AmneziaDNS ကို အသုံးမပြု သို့မဟုတ် ထည့်သွင်းခြင်းမပြုသည့်အခါ - + Primary DNS Primary DNS - + Secondary DNS Secondary DNS - + Restore default မူရင်းအတိုင်းပြန်လည်ထားရှိမည် - + Restore default DNS settings? မူရင်း DNS ဆက်တင်များကို ပြန်လည်ရယူလိုပါသလား? - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် - + Settings have been reset ဆက်တင်များကို ပြန်လည်သတ်မှတ်ပြီးပါပြီ - + Save သိမ်းဆည်းမည် - + Settings saved ဆက်တင်များကို သိမ်းဆည်းပြီးပြီ @@ -1835,12 +1872,12 @@ Already installed containers were found on the server. All installed containers Logging ကို ဖွင့်ထားသည်။ မှတ်တမ်းများကို ၁၄ ရက်အကြာတွင် အလိုအလျောက်ပိတ်ထားမည်ဖြစ်ပြီး မှတ်တမ်းဖိုင်များအားလုံး ပျက်သွားမည်ဖြစ်ကြောင်း သတိပြုပါ။. - + Logging Logging - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. ဤလုပ်ဆောင်ချက်ကို ဖွင့်ခြင်းဖြင့် အပလီကေးရှင်း၏ မှတ်တမ်းများကို အလိုအလျောက် သိမ်းဆည်းပေးမည် ဖြစ်သည်။ ပုံမှန်အတိုင်းဆိုလျှင် Logging လုပ်ဆောင်ချက်ကို ပိတ်ထားမည်ဖြစ်သည်။ အပလီကေးရှင်းချို့ယွင်းချက်ရှိခဲ့ပါသော် မှတ်တမ်းကိုပြန်လည်ကြည့်ရှုနိုင်ရန် မှတ်တမ်းသိမ်းဆည်းမှုကို ဖွင့်ထားလိုက်ပါ။. @@ -1853,21 +1890,21 @@ Already installed containers were found on the server. All installed containers မှတ်တမ်းများရှိသောဖိုင်တွဲကိုဖွင့်မည် - - + + Save သိမ်းဆည်းမည် - - + + Logs files (*.log) မှတ်တမ်းဖိုင်များ (*.log) မှတ်တမ်းဖိုင်များ (*.log) - - + + Logs file saved မှတ်တမ်းဖိုင်များသိမ်းဆည်းပြီးပါပြီ @@ -1876,64 +1913,62 @@ Already installed containers were found on the server. All installed containers မှတ်တမ်းများကို ဖိုင်တွင်သိမ်းဆည်းမည် - + Enable logs - + Clear logs? မှတ်တမ်းများရှင်းလင်းမည်လား? - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် - + Logs have been cleaned up မှတ်တမ်းများကို ရှင်းလင်းပြီးပါပြီ - + Client logs - + AmneziaVPN logs - - + Open logs folder - - + Export logs - + Service logs - + AmneziaVPN-service logs - + Clear logs မှတ်တမ်းများရှင်းလင်းမည် @@ -1956,18 +1991,18 @@ Already installed containers were found on the server. All installed containers - - - - + + + + Continue ဆက်လက်လုပ်ဆောင်မည် - - - - + + + + Cancel ပယ်ဖျက်မည် @@ -1982,77 +2017,77 @@ Already installed containers were found on the server. All installed containers ဖော်ဆောင်ပြသခြင်းမရှိပါက ၎င်းတို့ကို အပလီကေးရှင်းထဲသို့ ထည့်မည် - + Reboot server ဆာဗာကို ပြန်လည်စတင်မည် - + Do you want to reboot the server? ဆာဗာကို ပြန်လည်စတင်ချင်ပါသလား? - + The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? ပြန်လည်စတင်သည့် လုပ်ငန်းစဉ်သည် စက္ကန့် 30 ခန့် ကြာနိုင်သည်. ဆက်လက်လုပ်ဆောင်လိုပါသလား? - + Cannot reboot server during active connection ချိတ်ဆက်မှုရှိနေချိန်အတွင်း ဆာဗာကို ပြန်လည်စတင်၍မရပါ - + Do you want to remove the server from application? ဆာဗာကို အပလီကေးရှင်းမှဖယ်ရှားချင်ပါသလား? - + Cannot remove server during active connection ချိတ်ဆက်မှုရှိနေချိန်အတွင်း ဆာဗာကို ဖယ်ရှား၍မရပါ - + Do you want to clear server from Amnezia software? ဆာဗာကို Amnezia ဆော့ဖ်ဝဲလ်မှ ရှင်းလင်းလိုပါသလား? - + All users whom you shared a connection with will no longer be able to connect to it. သင်၏ချိတ်ဆက်မှကို မျှဝေထားသည့် အသုံးပြုသူအားလုံး ချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. - + Cannot clear server from Amnezia software during active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆာဗာကို Amnezia ဆော့ဖ်ဝဲလ်မှ ရှင်းလင်း၍မရပါ - + Reset API config API config ကို ပြန်လည်သတ်မှတ်မည် - + Do you want to reset API config? API config ကို ပြန်လည်သတ်မှတ်ချင်ပါသလား? - + Cannot reset API config during active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် API config ကို ပြန်လည်သတ်မှတ်၍မရပါ - + Remove server from application ဆာဗာကို အပလီကေးရှင်းမှဖယ်ရှားမည် - + All installed AmneziaVPN services will still remain on the server. ထည့်သွင်းထားသော AmneziaVPN ဝန်ဆောင်မှုများအားလုံးသည် ဆာဗာပေါ်တွင် ဆက်လက်ရှိနေမည်ဖြစ်သည်. - + Clear server from Amnezia software ဆာဗာကို Amnezia ဆော့ဖ်ဝဲလ်မှ ရှင်းလင်းမည် @@ -2060,27 +2095,32 @@ Already installed containers were found on the server. All installed containers PageSettingsServerInfo - + + Subscription is valid until + + + + Server name ဆာဗာအမည် - + Save သိမ်းဆည်းမည် - + Protocols ပရိုတိုကောများ - + Services ဝန်ဆောင်မှုများ - + Management စီမံခန့်ခွဲမှု @@ -2088,7 +2128,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServerProtocol - + settings ဆက်တင်များ @@ -2097,7 +2137,7 @@ Already installed containers were found on the server. All installed containers %1 ပရိုဖိုင်ကို ရှင်းလင်းမည် - + Clear %1 profile? %1 ပရိုဖိုင်ကို ရှင်းလင်းမည်လား? @@ -2107,64 +2147,64 @@ Already installed containers were found on the server. All installed containers - + Unable to clear %1 profile while there is an active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် %1 ပရိုဖိုင်ကို ရှင်းလင်း၍မရပါ - + Remove ဖယ်ရှားမည် - + Remove %1 from server? %1 ကို ဆာဗာမှ ဖယ်ရှားမည်လား? - + All users with whom you shared a connection will no longer be able to connect to it. သင်နှင့်အတူချိတ်ဆက်မှုတစ်ခုကို မျှဝေထားသည့် အသုံးပြုသူအားလုံး ဤချိတ်ဆက်မှုကိုချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. - + Cannot remove active container Active container ကိုဖယ်ရှား၍မရပါ - - + + Continue ဆက်လက်လုပ်ဆောင်မည် - + connection settings - + Click the "connect" button to create a connection configuration - + server settings - + Clear profile - + The connection configuration will be deleted for this device only - - + + Cancel ပယ်ဖျက်မည် @@ -2172,7 +2212,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServersList - + Servers ဆာဗာများ @@ -2180,100 +2220,100 @@ Already installed containers were found on the server. All installed containers PageSettingsSplitTunneling - + Default server does not support split tunneling function မူရင်းဆာဗာသည် split tunneling လုပ်ဆောင်ချက်ကို အထောက်အပံ့မပေးပါ - + Addresses from the list should not be accessed via VPN စာရင်းတွင်ဖော်ပြထားသောလိပ်စာများကို VPN ဖြင့် ဝင်ရောက်ခြင်းပြုနိုင်လိမ့်မည် မဟုတ်ပေ - + Split tunneling Split tunneling - + Mode Mode - + Remove ဖယ်ရှားမည် - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် - + Import / Export Sites ဆိုက်များ သွင်း/ထုတ်မည် - + Only the sites listed here will be accessed through the VPN ဤနေရာတွင်ဖော်ပြထားသောဆိုက်များကိုသာ VPN မှတဆင့်ဝင်ရောက်ပါမည် - + Cannot change split tunneling settings during active connection လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် split tunneling ဆက်တင်များကို ပြောင်းလဲ၍မရပါ - + website or IP ဝဘ်ဆိုက် သို့မဟုတ် IP - + Import တင်သွင်းမည် - + Save site list ဆိုက်စာရင်းကို သိမ်းဆည်းမည် - + Save sites ဆိုက်များသိမ်းဆည်းမည် - - - + + + Sites files (*.json) ဆိုက်ဖိုင်များ (*.json) - + Import a list of sites ဆိုက်စာရင်းတစ်ခု တင်သွင်းမည် - + Replace site list ဆိုက်စာရင်းကို အစားထိုးမည် - - + + Open sites file ဆိုက်ဖိုင်များ ဖွင့်မည် - + Add imported sites to existing ones တင်သွင်းထားသော ဆိုက်များကို ရှိပြီးသားဆိုက်များထဲသို့ ထည့်မည် @@ -2281,32 +2321,32 @@ Already installed containers were found on the server. All installed containers PageSetupWizardApiServiceInfo - + For the region ဒေသအတွက် - + Price စျေးနှုန်း - + Work period အလုပ်လုပ်မည့်ကာလ - + Speed မြန်နှုန်း - + Features Feature များ - + Connect ချိတ်ဆက်မည် @@ -2314,12 +2354,12 @@ Already installed containers were found on the server. All installed containers PageSetupWizardApiServicesList - + VPN by Amnezia Amnezia မှ VPN - + Choose a VPN service that suits your needs. သင့်လိုအပ်ချက်များနှင့် ကိုက်ညီသော VPN ဝန်ဆောင်မှုကို ရွေးချယ်ပါ. @@ -2327,12 +2367,12 @@ Already installed containers were found on the server. All installed containers PageSetupWizardConfigSource - + File with connection settings ချိတ်ဆက်မှုဆက်တင်များပါဝင်သောဖိုင် - + Connection ချိတ်ဆက်မှု @@ -2342,150 +2382,185 @@ Already installed containers were found on the server. All installed containers ဆက်တင်များ - + Enable logs - + + Support tag + ကူညီပံ့ပိုးမှု tag + + + + Copied + ကူးယူပြီးပါပြီ + + + Insert the key, add a configuration file or scan the QR-code Key ကိုထည့်မည်၊ ဖွဲ့စည်းမှုဖိုင်တစ်ခုကိုထည့်မည် သို့မဟုတ် QR-ကုဒ်ကို စကင်န်ဖတ်မည် - + Insert key Key ကိုထည့်သွင်းမည် - + Insert ထည့်သွင်းမည် - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Other connection options အခြားချိတ်ဆက်မှုရွေးချယ်စရာများ - + + Site Amnezia + + + + VPN by Amnezia Amnezia မှ VPN - + Connect to classic paid and free VPN services from Amnezia Amnezia မှ အခပေးနှင့် အခမဲ့ မူလ VPN ဝန်ဆောင်မှုများသို့ ချိတ်ဆက်မည် - + Self-hosted VPN ကိုယ်တိုင် host လုပ်ထားသော VPN - + Configure Amnezia VPN on your own server Amnezia VPN ကို သင်၏ကိုယ်ပိုင်ဆာဗာပေါ်တွင် စီစဥ်ချိန်ညှိမည် - + Restore from backup အရံဖိုင်မှ ပြန်လည်ရယူမည် - + + + + + + Open backup file အရံဖိုင်ကို ဖွင့်မည် - + Backup files (*.backup) အရံဖိုင်များ (*.backup) - + + + + + + Open config file config ဖိုင်ကိုဖွင့်မည် - + QR code QR-ကုဒ် - + + + + + + I have nothing ကျွန်ုပ်တွင်ဘာမှမရှိပါ + + + + + PageSetupWizardCredentials - + Server IP address [:port] ဆာဗာ IP လိပ်စာ [:port] - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Enter the address in the format 255.255.255.255:88 လိပ်စာကို 255.255.255.255:88 ဖော်မတ်ဖြင့် ထည့်ပါ - + Configure your server သင်၏ဆာဗာကို စီစဉ်ချိန်ညှိမည် - + 255.255.255.255:22 255.255.255.255:22 - + SSH Username SSH အသုံးပြုသူအမည် - + Password or SSH private key စကားဝှက် သိုမဟုတ် SSH private key - + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties သင်ထည့်သွင်းသည့်ဒေတာအားလုံးကို တင်းကြပ်လုံခြုံစွာလျှို့ဝှက်ထားမည်ဖြစ်ပြီး Amnezia သို့မဟုတ် မည်သည့်ပြင်ပအဖွဲ့အစည်းကိုမျှ မျှဝေမည် သို့မဟုတ် ထုတ်ဖော်မည်မဟုတ်ပါ - + How to run your VPN server သင်၏ဆာဗာကို လည်ပတ်ပုံလည်ပတ်နည်း - + Where to get connection data, step-by-step instructions for buying a VPS ချိတ်ဆက်မှုဒေတာကို ဘယ်မှာရနိုင်မလဲ၊ VPS ဝယ်ယူပုံဝယ်ယူနည်းအတွက် အဆင့်ဆင့် ညွှန်ကြားချက်များ - + Ip address cannot be empty IP လိပ်စာသည် ဗလာမဖြစ်ရပါ - + Login cannot be empty လော့ဂ်အင်အချက်အလက်သည် ဗလာမဖြစ်ရပါ - + Password/private key cannot be empty စကားဝှက်/private key သည် ဗလာမဖြစ်ရပါ @@ -2493,22 +2568,22 @@ Already installed containers were found on the server. All installed containers PageSetupWizardEasy - + What is the level of internet control in your region? သင့်ဒေသရှိ အင်တာနက်ထိန်းချုပ်မှုအဆင့်က ဘယ်လောက်ရှိပါသလဲ? - + Choose a VPN protocol VPN ပရိုတိုကောကို ရွေးပါ - + Skip setup စနစ်ထည့်သွင်းမှုကို ကျော်မည် - + Continue ဆက်လက်လုပ်ဆောင်မည် @@ -2555,37 +2630,37 @@ Already installed containers were found on the server. All installed containers PageSetupWizardProtocolSettings - + Installing %1 %1 ကိုထည့်သွင်းနေသည် - + More detailed ပိုမိုအသေးစိတ် - + Close ပိတ်မည် - + Network protocol ကွန်ရက်ပရိုတိုကော - + Port Port - + Install ထည်သွင်းမည် - + The port must be in the range of 1 to 65535 Port သည် 1 မှ 65535 အတွင်း ဖြစ်ရမည် @@ -2593,12 +2668,12 @@ Already installed containers were found on the server. All installed containers PageSetupWizardProtocols - + VPN protocol VPN ပရိုတိုကော - + Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP. သင့်အတွက် ဦးစားပေးအဖြစ်ဆုံးကို ရွေးချယ်ပါ။ နောက်ပိုင်းတွင် DNS proxy နှင့် SFTP ကဲ့သို့သော အခြားပရိုတိုကောများနှင့် ထပ်ဆောင်းဝန်ဆောင်မှုများကို ထည့်သွင်းနိုင်သည်။. @@ -2614,7 +2689,7 @@ Already installed containers were found on the server. All installed containers PageSetupWizardStart - + Let's get started စတင်လိုက်ကြရအောင် @@ -2622,27 +2697,27 @@ Already installed containers were found on the server. All installed containers PageSetupWizardTextKey - + Connection key ချိန်ဆက်မှု key - + A line that starts with vpn://... vpn://... ဖြင့် စတင်သော စာကြောင်း... - + Key Key - + Insert ထည်သွင်းမည် - + Continue ဆက်လက်လုပ်ဆောင်မည် @@ -2650,32 +2725,32 @@ Already installed containers were found on the server. All installed containers PageSetupWizardViewConfig - + New connection ချိတ်ဆက်မှုအသစ် - + Collapse content အကြောင်းအရာများကိုဖြန့်ချမည် - + Show content အကြောင်းအရာများကိုပြမည် - + Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. WireGuard obfuscation ကိုဖွင့်ထားပါ။ အကယ်၍ သင်၏အင်တာနက်ဝန်ဆောင်မှုပေးသောကုမ္ပဏီက WireGuard ပိတ်ဆို့ထားသော် ၎င်းကိုဖွင့်ထားခြင်းအားဖြင့်အသုံးဝင်နိုင်သည်။. - + Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. သင်ယုံကြည်ရသော ရင်းမြစ်များမှရရှိသော ချိတ်ဆက်ကုဒ်များကိုသာ အသုံးပြုပါ။ လူတိုင်းဝင်ရောက်ရယူနေနိုင်သော ရင်းမြစ်များမှကုဒ်များသည် သင့်ဒေတာကို ကြားဖြတ်ရယူရန် ဖန်တီးထားသောကုဒ်များဖြစ်နေနိုင်သည်။. - + Connect ချိတ်ဆက်မည် @@ -2683,207 +2758,212 @@ Already installed containers were found on the server. All installed containers PageShare - + OpenVPN native format OpenVPN မူရင်းဖောမတ် - + WireGuard native format WireGuard မူရင်းဖော်မတ် - + Connection ချိတ်ဆက်မှု - - + + Server ဆာဗာ - + Config revoked Config ကိုပြန်ရုပ်သိမ်းလိုက်ပါပြီ - + Connection to ဤဆာဗာသို့ချိတ်ဆက်မှု - + File with connection settings to ဤဆာဗာနှင့်ချိတ်ဆက်မှု ဆက်တင်များပါရှိသော ဖိုင် - + Save OpenVPN config OpenVPN config ကိုသိမ်းဆည်းမည် - + Save WireGuard config WireGuard config ကိုသိမ်းဆည်းမည် - + Save AmneziaWG config AmneziaWG config ကိုသိမ်းဆည်းမည် - + Save Shadowsocks config Shadowsocks config ကိုသိမ်းဆည်းမည် - + Save Cloak config Cloak config ကိုသိမ်းဆည်းမည် - + Save XRay config XRay config ကိုသိမ်းဆည်းမည် - + For the AmneziaVPN app AmneziaVPN အက်ပ်အတွက် - + AmneziaWG native format AmneziaWG မူရင်းဖော်မတ် - + Shadowsocks native format Shadowsocks မူရင်းဖောမတ် - + Cloak native format Cloak မူရင်းဖော်မတ် - + XRay native format XRay မူရင်းဖော်မတ် - + Share VPN Access VPN အသုံးပြုခွင့်ကိုမျှဝေမည် - + Share full access to the server and VPN ဆာဗာနှင့် VPN သို့ အပြည့်အဝဝင်ရောက်ခွင့်ကို မျှဝေမည် - + Use for your own devices, or share with those you trust to manage the server. သင့်ကိုယ်ပိုင်စက်ပစ္စည်းများအတွက် အသုံးပြုရန် သို့မဟုတ် ဆာဗာကို စီမံခန့်ခွဲရန် သင်ယုံကြည်ရသူများနှင့် မျှဝေရန်. - - + + Users အသုံးပြုသူများ - + User name အသုံးပြုသူနာမည် - + Search ရှာဖွေမည် - + Creation date: %1 ဖန်တီးပြုလုပ်သည့်ရက်စွဲ: %1 - + Latest handshake: %1 နောက်ဆုံး handshake လုပ်ခြင်း: %1 - + Data received: %1 လက်ခံရရှိသည့်ဒေတာ: %1 - + Data sent: %1 ပေးပို့လိုက်သည့်ဒေတာ: %1 - + + Allowed IPs: %1 + + + + Rename အမည်ပြောင်းမည် - + Client name ကလိုင်းရင့်အမည် - + Save သိမ်းဆည်းမည် - + Revoke ပြန်ရုပ်သိမ်းမည် - + Revoke the config for a user - %1? အသုံးပြုသူ %1 အတွက် config ကို ပြန်လည်ရုပ်သိမ်းမည်လား? - + The user will no longer be able to connect to your server. ဤအသုံးပြုသူသည် သင့်ဆာဗာသို့ ချိတ်ဆက်နိုင်တော့မည်မဟုတ်ပါ. - + Continue ဆက်လက်လုပ်ဆောင်မည် - + Cancel ပယ်ဖျက်မည် - + Share VPN access without the ability to manage the server ဆာဗာကို စီမံခန့်ခွဲနိုင်စွမ်းမပါရှိဘဲ VPN အသုံးပြုခွင့်ကို မျှဝေမည် - - + + Protocol ပရိုတိုကော - - + + Connection format ချိတ်ဆက်မှုဖောမတ် - - + + Share မျှဝေမည် @@ -2891,55 +2971,55 @@ Already installed containers were found on the server. All installed containers PageShareFullAccess - + Full access to the server and VPN ဆာဗာနှင့် VPN ကို အပြည့်အဝဝင်ရောက်ခွင့် - + We recommend that you use full access to the server only for your own additional devices. သင့်ကိုယ်ပိုင်အပိုပစ္စည်းများအတွက်သာ ဆာဗာသို့ အပြည့်အဝဝင်ရောက်ခွင့်ကို အသုံးပြုရန် ကျွန်ုပ်တို့ အကြံပြုပါသည်. - + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. သင်သည် အခြားသူများနှင့် အပြည့်အဝဝင်ရောက်ခွင့်ကို မျှဝေပါက၊ ၎င်းတို့သည် ပရိုတိုကောများနှင့် ဝန်ဆောင်မှုများကို ဆာဗာသို့ထည့်သွင်းခြင်း ဆာဗာမှဖယ်ရှားခြင်းများ ပြုလုပ်နိုင်သောကြောင့် အသုံးပြုသူများအားလုံးအတွက် VPN မှားယွင်းစွာ လုပ်ဆောင်ခြင်းများဖြစ်စေနိုင်ပါသည်. - - + + Server ဆာဗာ - + Accessing ဝင်ရောက်နေသည် - + File with accessing settings to ဤဆာဗာနှင့်ဝင်ရောက်နိုင်မှု ဆက်တင်များပါရှိသော ဖိုင် - + Share မျှဝေမည် - + Access error! အသုံးပြုခွင့်တွင်အမှားပါနေပါသည်! - + Connection to ဤဆာဗာသို့ချိတ်ဆက်မှု - + File with connection settings to ဤဆာဗာနှင့်ချိတ်ဆက်မှု ဆက်တင်များပါရှိသော ဖိုင် @@ -2947,17 +3027,17 @@ Already installed containers were found on the server. All installed containers PageStart - + Logging was disabled after 14 days, log files were deleted ၁၄ ရက်အကြာတွင် Logging ကို ပိတ်ခဲ့သည်၊ မှတ်တမ်းဖိုင်များကို ဖျက်ပစ်လိုက်ပြီဖြစ်သည် - + Settings restored from backup file ဆက်တင်များကို အရံဖိုင်မှ ပြန်လည်ရယူပြီးပါပြီ - + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. @@ -2965,7 +3045,7 @@ Already installed containers were found on the server. All installed containers PopupType - + Close ပိတ်မည် @@ -3289,17 +3369,17 @@ Already installed containers were found on the server. All installed containers Config တွင် ဆာဗာသို့ချိတ်ဆက်ရန်အတွက် ကွန်တိန်နာများနှင့် အထောက်အထားများ မပါဝင်ပါ - + Error when retrieving configuration from API API မှ စီစဉ်သတ်မှတ်မှုကို ရယူသည့်အခါ အမှားအယွင်းဖြစ်ပေါ်နေသည် - + This config has already been added to the application ဤ config ကို အပလီကေးရှင်းထဲသို့ ထည့်သွင်းပြီးဖြစ်သည် - + ErrorCode: %1. မှားယွင်းမှုကုတ်: %1. @@ -3369,62 +3449,77 @@ Already installed containers were found on the server. All installed containers VPN pool မှားယွင်းမှု: ရရှိနိုင်သောလိပ်စာများမရှိပါ - + + Unable to open config file + + + + VPN connection error VPN ချိတ်ဆက်မှုမှားယွင်းနေပါသည် - + In the response from the server, an empty config was received ဆာဗာမှ တုံ့ပြန်မှုတွင်၊ config အလွတ်တစ်ခုကို လက်ခံရရှိခဲ့သည် - + SSL error occurred SSL မှားယွင်းမှုဖြစ်သွားသည် - + Server response timeout on api request Api တောင်းဆိုမှုတွင် ဆာဗာတုံ့ပြန်မှု အချိန်ကုန်သွားသည် - + Missing AGW public key AGW public key ပျောက်ဆုံးနေသည် + + + Failed to decrypt response payload + + + Missing list of available services + + + + QFile error: The file could not be opened QFile မှားယွင်းမှု: ဖိုင်ကို ဖွင့်၍မရပါ - + QFile error: An error occurred when reading from the file QFile မှားယွင်းမှု: ဖိုင်ကိုဖတ်နေစဥ်အတွင်း မှားယွင်းမှုဖြစ်သွားသည် - + QFile error: The file could not be accessed QFile မှားယွင်းမှု: ဖိုင်ကို ဝင်၍မရပါ - + QFile error: An unspecified error occurred QFile မှားယွင်းမှု: သတ်မှတ်မထားသော မှားယွင်းမှုတစ်ခု ဖြစ်ပွားခဲ့သည် - + QFile error: A fatal error occurred QFile မှားယွင်းမှု: ကြီးမားသော မှားယွင်းမှုတစ်ခု ဖြစ်ပွားခဲ့သည် - + QFile error: The operation was aborted QFile မှားယွင်းမှု: လုပ်ငန်းစဥ်ကို ဖျက်သိမ်းလိုက်ရသည် - + Internal error စက်တွင်းဖြစ်သော မှားယွင်းမှု @@ -3872,11 +3967,19 @@ For more detailed information, you can SelectLanguageDrawer - + Choose language ဘာသာစကားကို ရွေးချယ်ပါ + + ServersListView + + + Unable change server while there is an active connection + လက်ရှိချိတ်ဆက်မှုတစ်ခုရှိနေချိန်တွင် ဆာဗာကို ပြောင်းလဲ၍မရပါ + + Settings @@ -3894,12 +3997,12 @@ For more detailed information, you can SettingsController - + All settings have been reset to default values ဆက်တင်အားလုံးကို မူရင်းတန်ဖိုးများအဖြစ် ပြန်လည်သတ်မှတ်ထားသည် - + Backup file is corrupted အရံဖိုင်ပျက်ဆီးနေသည် @@ -3913,33 +4016,33 @@ For more detailed information, you can AmneziaWG config ကိုသိမ်းဆည်းမည် - + Share မျှဝေမည် - + Copy ကူးယူမည် - - + + Copied ကူးယူပြီးပါပြီ - + Copy config string config string ကိုကူးယူမည် - + Show connection settings ချိတ်ဆက်မှုဆက်တင်များကို ပြပါ - + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" Amnezia အက်ပ်ရှိ QR ကုဒ်ကိုဖတ်ရန်အတွက်အောက်ပါအတိုင်း ရွေးချယ်ပါ "ဆာဗာထည့်ရန်" → "ချိတ်ဆက်ရန် ဒေတာရှိသည်" → "QR ကုဒ်၊ key သို့မဟုတ် ဆက်တင်ဖိုင်" @@ -3962,27 +4065,27 @@ For more detailed information, you can ဆိုက်ကို ဖယ်ရှားလိုက်သည်: %1 - + Can't open file: %1 ဖိုင်ကိုဖွင့်၍မရပါ: %1 - + Failed to parse JSON data from file: %1 JSON ဒေတာကို ဖိုင်မှ ခွဲခြမ်းထုပ်ယူမှု မအောင်မြင်ပါ: %1 - + The JSON data is not an array in file: %1 JSON ဒေတာသည် ဖိုင်ထဲရှိ array တစ်ခုမဟုတ်ပါ: %1 - + Import completed တင်သွင်းခြင်းပြီးဆုံးသွားပါပြီ - + Export completed ထုတ်ယူခြင်းပြီးဆုံးသွားပါပြီ @@ -4023,7 +4126,7 @@ For more detailed information, you can TextFieldWithHeaderType - + The field can't be empty ဖြည့်သွင်းရမည့်နေရာသည် အလွတ်မဖြစ်ရပါ @@ -4031,7 +4134,7 @@ For more detailed information, you can VpnConnection - + Mbps Mbps @@ -4105,12 +4208,12 @@ For more detailed information, you can main2 - + Private key passphrase ကိုယ်ပိုင် key စကားဝှက် - + Save သိမ်းဆည်းမည် diff --git a/client/translations/amneziavpn_ru_RU.ts b/client/translations/amneziavpn_ru_RU.ts index 2fb21259..95b66fdc 100644 --- a/client/translations/amneziavpn_ru_RU.ts +++ b/client/translations/amneziavpn_ru_RU.ts @@ -1,55 +1,63 @@ + + AdLabel + + + Amnezia Premium - for access to any website + Amnezia Premium - для доступа к любым сайтам + + ApiServicesModel - + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s Классический VPN для комфортной работы, загрузки больших файлов и просмотра видео. Работает для любых сайтов. Скорость до %1 Мбит/с - + VPN to access blocked sites in regions with high levels of Internet censorship. VPN для доступа к заблокированным сайтам в регионах с высоким уровнем интернет-цензуры. - + <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> - + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. Amnezia Premium — классический VPN для комфортной работы, загрузки больших файлов и просмотра видео в высоком разрешении. Работает на всех сайтах, даже в странах с самым высоким уровнем интернет-цензуры. - + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship Amnezia Free - это бесплатный VPN для обхода блокировок в странах с высоким уровнем интернет-цензуры - + %1 MBit/s - + %1 days %1 дней - + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> Через VPN будут открываться только популярные сайты, заблокированные в вашем регионе, такие как Instagram, Facebook, Twitter и другие. Остальные сайты будут открываться с вашего реального IP-адреса, <a href="%1/free" style="color: #FBB26A;">подробности на сайте.</a> - + Free Бесплатно - + %1 $/month %1 $/месяц @@ -80,7 +88,7 @@ ConnectButton - + Unable to disconnect during configuration preparation Невозможно отключиться во время подготовки конфигурации @@ -88,62 +96,62 @@ ConnectionController - + VPN Protocols is not installed. Please install VPN container at first VPN-протоколы не установлены. Пожалуйста, установите протокол - + Connecting... Подключение... - + Connected Подключено - + Preparing... Подготовка... - + Settings updated successfully, reconnnection... Настройки успешно обновлены, переподключение... - + Settings updated successfully Настройки успешно обновлены - + The selected protocol is not supported on the current platform Выбранный протокол не поддерживается на данном устройстве - + unable to create configuration не удалось создать конфигурацию - + Reconnecting... Переподключение... - - - + + + Connect Подключиться - + Disconnecting... Отключение... @@ -151,17 +159,17 @@ ConnectionTypeSelectionDrawer - + Add new connection Добавить новое соединение - + Configure your server Настроить свой сервер - + Open config file, key or QR code Открыть файл конфигурации, ключ или QR-код @@ -199,7 +207,7 @@ HomeContainersListView - + Unable change protocol while there is an active connection Невозможно изменить протокол во время активного соединения @@ -211,46 +219,46 @@ HomeSplitTunnelingDrawer - + Split tunneling Раздельное VPN-туннелирование - + Allows you to connect to some sites or applications through a VPN connection and bypass others Позволяет подключаться к одним сайтам или приложениям через VPN-соединение, а к другим — в обход него - + Split tunneling on the server Раздельное туннелирование на сервере - + Enabled Can't be disabled for current server Включено Невозможно отключить для текущего сервера - + Site-based split tunneling Раздельное туннелирование сайтов - - + + Enabled Включено - - + + Disabled Отключено - + App-based split tunneling Раздельное туннелирование приложений @@ -258,23 +266,20 @@ Can't be disabled for current server ImportController - Unable to open file - Невозможно открыть файл + Невозможно открыть файл - - Invalid configuration file - Неверный файл конфигурации + Неверный файл конфигурации - + Scanned %1 of %2. Отсканировано %1 из %2. - + In the imported configuration, potentially dangerous lines were found: В импортированной конфигурации были обнаружены потенциально опасные строки: @@ -282,86 +287,86 @@ Can't be disabled for current server InstallController - + %1 installed successfully. %1 успешно установлен. - + %1 is already installed on the server. %1 уже установлен на сервер. - + Added containers that were already installed on the server Добавлены сервисы и протоколы, которые были ранее установлены на сервер - + Already installed containers were found on the server. All installed containers have been added to the application На сервере обнаружены установленные протоколы и сервисы. Все они были добавлены в приложение - + Settings updated successfully Настройки успешно обновлены - + Server '%1' was rebooted Сервер '%1' был перезагружен - + Server '%1' was removed Сервер '%1' был удален - + All containers from server '%1' have been removed Все протоколы и сервисы были удалены с сервера '%1' - + %1 has been removed from the server '%2' %1 был удален с сервера '%2' - + Api config removed Конфигурация API удалена - + %1 cached profile cleared %1 закэшированный профиль очищен - + Please login as the user Пожалуйста, войдите в систему от имени пользователя - + Server added successfully Сервер успешно добавлен - + %1 installed successfully. %1 успешно установлен. - + API config reloaded Конфигурация API перезагружена - + Successfully changed the country of connection to %1 Изменение страны подключения на %1 @@ -374,12 +379,12 @@ Already installed containers were found on the server. All installed containers Выберите приложение - + application name название приложения - + Add selected Добавить выбранные @@ -447,12 +452,12 @@ Already installed containers were found on the server. All installed containers PageDevMenu - + Gateway endpoint - + Dev gateway environment @@ -460,98 +465,97 @@ Already installed containers were found on the server. All installed containers PageHome - + Logging enabled Логирование включено - + Split tunneling enabled Раздельное туннелирование включено - + Split tunneling disabled Раздельное туннелирование выключено - + VPN protocol VPN-протокол - + Servers Серверы - Unable change server while there is an active connection - Невозможно изменить сервер во время активного соединения + Невозможно изменить сервер во время активного соединения PageProtocolAwgClientSettings - + AmneziaWG settings Настройки AmneziaWG - + MTU MTU - + Server settings - + Настройки сервера - + Port Порт - + Save - Сохранить + Сохранить + + + + Save settings? + Сохранить настройки? + + + + Only the settings for this device will be changed + Будут изменены настройки только для этого устройства. - Save settings? - Сохранить настройки? + Continue + Продолжить - Only the settings for this device will be changed - - - - - Continue - Продолжить - - - Cancel - Отменить + Отменить - + Unable change settings while there is an active connection - Невозможно изменить настройки во время активного соединения + Невозможно изменить настройки во время активного соединения PageProtocolAwgSettings - + AmneziaWG settings Настройки AmneziaWG - + Port Порт @@ -568,87 +572,92 @@ Already installed containers were found on the server. All installed containers Удалить AmneziaWG с сервера? - + All users with whom you shared a connection with will no longer be able to connect to it. Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. - + Save Сохранить - + + VPN address subnet + Подсеть VPN-адресов + + + Jc - Junk packet count - + Jmin - Junk packet minimum size - + Jmax - Junk packet maximum size - + S1 - Init packet junk size - + S2 - Response packet junk size - + H1 - Init packet magic header - + H2 - Response packet magic header - + H4 - Transport packet magic header - + H3 - Underload packet magic header - + The values of the H1-H4 fields must be unique Значения в полях H1-H4 должны быть уникальными - + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) Значение в поле S1 + размер инициации сообщения (148) не должно равняться значению в поле S2 + размер ответа на сообщение (92) - + Save settings? Сохранить настройки? - + Continue Продолжить - + Cancel Отменить - + Unable change settings while there is an active connection Невозможно изменить настройки во время активного соединения @@ -656,33 +665,33 @@ Already installed containers were found on the server. All installed containers PageProtocolCloakSettings - + Cloak settings Настройки Cloak - + Disguised as traffic from Замаскировать трафик под - + Port Порт - - + + Cipher Шифрование - + Save Сохранить - + Unable change settings while there is an active connection Невозможно изменить настройки во время активного соединения @@ -690,170 +699,170 @@ Already installed containers were found on the server. All installed containers PageProtocolOpenVpnSettings - + OpenVPN settings Настройки OpenVPN - + VPN address subnet Подсеть VPN-адресов - + Network protocol Сетевой протокол - + Port Порт - + Auto-negotiate encryption Шифрование с автоматическим согласованием - - + + Hash Хэш - + SHA512 SHA512 - + SHA384 SHA384 - + SHA256 SHA256 - + SHA3-512 SHA3-512 - + SHA3-384 SHA3-384 - + SHA3-256 SHA3-256 - + whirlpool whirlpool - + BLAKE2b512 BLAKE2b512 - + BLAKE2s256 BLAKE2s256 - + SHA1 SHA1 - - + + Cipher Шифрование - + AES-256-GCM AES-256-GCM - + AES-192-GCM AES-192-GCM - + AES-128-GCM AES-128-GCM - + AES-256-CBC AES-256-CBC - + AES-192-CBC AES-192-CBC - + AES-128-CBC AES-128-CBC - + ChaCha20-Poly1305 ChaCha20-Poly1305 - + ARIA-256-CBC ARIA-256-CBC - + CAMELLIA-256-CBC CAMELLIA-256-CBC - + none none - + TLS auth TLS авторизация - + Block DNS requests outside of VPN Блокировать DNS-запросы за пределами VPN - + Additional client configuration commands Дополнительные команды конфигурации клиента - - + + Commands: Команды: - + Additional server configuration commands Дополнительные команды конфигурации сервера - + Unable change settings while there is an active connection Невозможно изменить настройки во время активного соединения @@ -878,7 +887,7 @@ Already installed containers were found on the server. All installed containers Отменить - + Save Сохранить @@ -886,32 +895,32 @@ Already installed containers were found on the server. All installed containers PageProtocolRaw - + settings настройки - + Show connection options Показать параметры подключения - + Connection options %1 Параметры подключения %1 - + Remove Удалить - + Remove %1 from server? Удалить %1 с сервера? - + All users with whom you shared a connection with will no longer be able to connect to it. Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. @@ -920,12 +929,12 @@ Already installed containers were found on the server. All installed containers Все пользователи, с которыми вы поделились этим VPN-протоколом, больше не смогут к нему подключаться. - + Continue Продолжить - + Cancel Отменить @@ -933,28 +942,28 @@ Already installed containers were found on the server. All installed containers PageProtocolShadowSocksSettings - + Shadowsocks settings Настройки Shadowsocks - + Port Порт - - + + Cipher Шифрование - + Save Сохранить - + Unable change settings while there is an active connection Невозможно изменить настройки во время активного соединения @@ -962,84 +971,89 @@ Already installed containers were found on the server. All installed containers PageProtocolWireGuardClientSettings - + WG settings Настройки WG - + MTU MTU - + Server settings - + Настройки сервера - + Port Порт - + Save Сохранить - + Save settings? - Сохранить настройки? + Сохранить настройки? - + Only the settings for this device will be changed - + Будут изменены настройки только для этого устройства - + Continue - Продолжить + Продолжить + + + + Cancel + Отменить - Cancel - Отменить - - - Unable change settings while there is an active connection - Невозможно изменить настройки во время активного соединения + Невозможно изменить настройки во время активного соединения PageProtocolWireGuardSettings - + WG settings Настройки WG - + + VPN address subnet + Подсеть VPN-адресов + + + Port Порт - + Save settings? - Сохранить настройки? + Сохранить настройки? - + All users with whom you shared a connection with will no longer be able to connect to it. - Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. + Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. MTU MTU - + Unable change settings while there is an active connection Невозможно изменить настройки во время активного соединения @@ -1056,17 +1070,17 @@ Already installed containers were found on the server. All installed containers Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. - + Continue Продолжить - + Cancel Отменить - + Save Сохранить @@ -1074,22 +1088,22 @@ Already installed containers were found on the server. All installed containers PageProtocolXraySettings - + XRay settings Настройки XRay - + Disguised as traffic from Замаскировать трафик под - + Save Сохранить - + Unable change settings while there is an active connection Невозможно изменить настройки во время активного соединения @@ -1104,39 +1118,39 @@ Already installed containers were found on the server. All installed containers PageServiceDnsSettings - + A DNS service is installed on your server, and it is only accessible via VPN. На вашем сервере установлен DNS-сервис, доступ к нему возможен только через VPN. - + The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. Адрес DNS совпадает с адресом вашего сервера. Настроить DNS можно во вкладке "Соединение" настроек приложения. - + Remove Удалить - + Remove %1 from server? Удалить %1 с сервера? - + Continue Продолжить - + Cancel Отменить - + Cannot remove AmneziaDNS from running server Невозможно удалить AmneziaDNS с работающего сервера @@ -1144,67 +1158,67 @@ Already installed containers were found on the server. All installed containers PageServiceSftpSettings - + Settings updated successfully Настройки успешно обновлены - + SFTP settings Настройки SFTP - + Host Хост - - - - + + + + Copied Скопировано - + Port Порт - + User name Имя пользователя - + Password Пароль - + Mount folder on device Смонтировать папку на устройстве - + In order to mount remote SFTP folder as local drive, perform following steps: <br> Чтобы смонтировать SFTP-папку как локальный диск, выполните следующие действия: <br> - - + + <br>1. Install the latest version of <br>1. Установите последнюю версию - - + + <br>2. Install the latest version of <br>2. Установите последнюю версию - + Detailed instructions Подробные инструкции @@ -1228,69 +1242,69 @@ Already installed containers were found on the server. All installed containers PageServiceSocksProxySettings - + Settings updated successfully Настройки успешно обновлены - - + + SOCKS5 settings Настройки SOCKS5 - + Host Хост - - - - + + + + Copied Скопировано - - + + Port Порт - + User name Имя пользователя - - + + Password Пароль - + Username Имя пользователя - - + + Change connection settings Изменить настройки соединения - + The port must be in the range of 1 to 65535 Порт должен быть в диапазоне от 1 до 65535 - + Password cannot be empty Пароль не может быть пустым - + Username cannot be empty Имя пользователя не может быть пустым @@ -1298,37 +1312,37 @@ Already installed containers were found on the server. All installed containers PageServiceTorWebsiteSettings - + Settings updated successfully Настройки успешно обновлены - + Tor website settings Настройки сайта в сети Тоr - + Website address Адрес сайта - + Copied Скопировано - + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. Используйте <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> для открытия этой ссылки. - + After creating your onion site, it takes a few minutes for the Tor network to make it available for use. Через несколько минут после установки ваш onion-сайт станет доступен в сети Tor. - + When configuring WordPress set the this onion address as domain. При настройке WordPress укажите этот onion-адрес в качестве домена. @@ -1352,42 +1366,42 @@ Already installed containers were found on the server. All installed containers PageSettings - + Settings Настройки - + Servers Серверы - + Connection Соединение - + Application Приложение - + Backup Резервное копирование - + About AmneziaVPN Об AmneziaVPN - + Dev console - + Close application Закрыть приложение @@ -1395,7 +1409,7 @@ Already installed containers were found on the server. All installed containers PageSettingsAbout - + Support Amnezia Поддержите Amnezia @@ -1404,32 +1418,32 @@ Already installed containers were found on the server. All installed containers Показать другие способы на GitHub - + Amnezia is a free and open-source application. You can support the developers if you like it. Amnezia — это бесплатное приложение с открытым исходным кодом. Поддержите разработчиков, если оно вам нравится. - + Contacts Контакты - + Telegram group Группа в Telegram - + To discuss features Для обсуждения возможностей - + https://t.me/amnezia_vpn_en https://t.me/amnezia_vpn - + support@amnezia.org @@ -1438,121 +1452,144 @@ Already installed containers were found on the server. All installed containers Почта - + For reviews and bug reports Для отзывов и сообщений об ошибках - + Copied - Скопировано + Скопировано - + GitHub GitHub - + + Discover the source code + + + + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website Веб-сайт + + + Visit official website + + https://amnezia.org https://amnezia.org - + Software version: %1 Версия ПО: %1 - + Check for updates Проверить обновления - + Privacy Policy Политика конфиденциальности + + PageSettingsApiLanguageList + + + Unable change server location while there is an active connection + Невозможно изменить локацию во время активного соединения + + PageSettingsApiServerInfo - + For the region Для региона - + Price Цена - + Work period Период работы - + + Valid until + + + + Speed Скорость - + Support tag - + Copied Скопировано - + Reload API config Перезагрузить конфигурацию API - + Reload API config? Перезагрузить конфигурацию API? - - + + Continue Продолжить - - + + Cancel Отменить - + Cannot reload API config during active connection Невозможно перзагрузить API конфигурацию при активном соединении - + Remove from application Удалить из приложения - + Remove from application? Удалить из приложения? - + Cannot remove server during active connection Невозможно удалить сервер во время активного соединения @@ -1560,12 +1597,12 @@ Already installed containers were found on the server. All installed containers PageSettingsAppSplitTunneling - + Cannot change split tunneling settings during active connection Невозможно изменить настройки раздельного туннелирования во время активного соединения - + Only the apps from the list should have access via VPN Только приложения из списка должны работать через VPN @@ -1575,42 +1612,42 @@ Already installed containers were found on the server. All installed containers Приложения из списка не должны работать через VPN - + App split tunneling Раздельное туннелирование приложений - + Mode Режим - + Remove Удалить - + Continue Продолжить - + Cancel Отменить - + application name название приложения - + Open executable file Открыть исполняемый файл - + Executable files (*.*) Исполняемые файлы (*.*) @@ -1618,102 +1655,102 @@ Already installed containers were found on the server. All installed containers PageSettingsApplication - + Application Приложение - + Allow application screenshots Разрешить скриншоты приложения - + Auto start Автозапуск - + Launch the application every time the device is starts Запускать приложение при загрузке устройства - + Auto connect Автоподключение - + Connect to VPN on app start Подключаться к VPN при запуске приложения - + Start minimized Запускать в свернутом виде - + Launch application minimized Запускать приложение в свернутом виде - + Language Язык - + Enable notifications Включить уведомления - + Enable notifications to show the VPN state in the status bar Включить уведомления для отображения статуса VPN в строке состояния - + Logging Логирование - + Enabled Включено - + Disabled Отключено - + Reset settings and remove all data from the application Сбросить настройки и удалить все данные из приложения - + Reset settings and remove all data from the application? Сбросить настройки и удалить все данные из приложения? - + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. Все настройки будут сброшены до значений по умолчанию. Все установленные сервисы AmneziaVPN останутся на сервере. - + Continue Продолжить - + Cancel Отменить - + Cannot reset settings during active connection Невозможно сбросить настройки во время активного соединения @@ -1725,12 +1762,12 @@ Already installed containers were found on the server. All installed containers Резервное копирование - + Settings restored from backup file Настройки восстановлены из файла резервной копии - + Back up your configuration Создать резервную копию конфигурации @@ -1739,68 +1776,68 @@ Already installed containers were found on the server. All installed containers Резервная копия конфигурации - + You can save your settings to a backup file to restore them the next time you install the application. Вы можете сохранить настройки в файл резервной копии, чтобы восстановить их при следующей установке приложения. - + The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. Резервная копия будет содержать ваши пароли и закрытые ключи для всех серверов, добавленных в AmneziaVPN. Храните эту информацию в надежном месте. - + Make a backup Создать резервную копию - + Save backup file Сохранить резервную копию - - + + Backup files (*.backup) Файлы резервных копий (*.backup) - + Backup file saved Резервная копия сохранена - + Restore from backup Восстановить из резервной копии - + Open backup file Открыть резервную копию - + Import settings from a backup file? Импортировать настройки из резервной копии? - + All current settings will be reset Все текущие настройки будут сброшены - + Continue Продолжить - + Cancel Отменить - + Cannot restore backup settings during active connection Невозможно восстановить настройки из резервной копии во время активного соединения @@ -1808,7 +1845,7 @@ Already installed containers were found on the server. All installed containers PageSettingsConnection - + Connection Соединение @@ -1821,57 +1858,57 @@ Already installed containers were found on the server. All installed containers Подключение к VPN при запуске приложения - + Use AmneziaDNS Использовать AmneziaDNS - + If AmneziaDNS is installed on the server Если AmneziaDNS установлен на сервере - + DNS servers DNS-серверы - + When AmneziaDNS is not used or installed Когда AmneziaDNS не используется или не установлен - + Allows you to use the VPN only for certain Apps Позволяет использовать VPN только для определенных приложений - + KillSwitch KillSwitch - + Disables your internet if your encrypted VPN connection drops out for any reason. Отключает ваше интернет-соединение, если ваше зашифрованное VPN-соединение по какой-либо причине прерывается. - + Cannot change killSwitch settings during active connection Невозможно изменить настройки аварийного выключателя во время активного соединения - + Site-based split tunneling Раздельное туннелирование сайтов - + Allows you to select which sites you want to access through the VPN Позволяет выбирать, к каким сайтам подключаться через VPN - + App-based split tunneling Раздельное туннелирование приложений @@ -1883,12 +1920,12 @@ Already installed containers were found on the server. All installed containers PageSettingsDns - + Default server does not support custom DNS Сервер по умолчанию не поддерживает пользовательские DNS - + DNS servers DNS-серверы @@ -1897,52 +1934,52 @@ Already installed containers were found on the server. All installed containers Когда AmneziaDNS не используется или не установлен - + If AmneziaDNS is not used or installed Если AmneziaDNS не используется или не установлен - + Primary DNS Первичный DNS - + Secondary DNS Вторичный DNS - + Restore default Восстановить по умолчанию - + Restore default DNS settings? Восстановить настройки DNS по умолчанию? - + Continue Продолжить - + Cancel Отменить - + Settings have been reset Настройки сброшены - + Save Сохранить - + Settings saved Настройки сохранены @@ -1954,12 +1991,12 @@ Already installed containers were found on the server. All installed containers Логирование включено. Обратите внимание, что логирование будет автоматически отключено через 14 дней, и все логи будут удалены. - + Logging Логирование - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. Включение этой функции позволяет сохранять логи на вашем устройстве. По умолчанию она отключена. Включите сохранение логов в случае сбоев в работе приложения. @@ -1972,20 +2009,20 @@ Already installed containers were found on the server. All installed containers Открыть папку с логами - - + + Save Сохранить - - + + Logs files (*.log) Файлы логов (*.log) - - + + Logs file saved Файл с логами сохранен @@ -1994,64 +2031,62 @@ Already installed containers were found on the server. All installed containers Сохранить логи в файл - + Enable logs - + Включить запись логов - + Clear logs? Очистить логи? - + Continue Продолжить - + Cancel Отменить - + Logs have been cleaned up Логи очищены - + Client logs - + AmneziaVPN logs - - + Open logs folder - + Открыть папку с логами - - + Export logs - + Сохранить логи - + Service logs - + AmneziaVPN-service logs - + Clear logs Очистить логи @@ -2086,18 +2121,18 @@ Already installed containers were found on the server. All installed containers - - - - + + + + Continue Продолжить - - - - + + + + Cancel Отменить @@ -2112,67 +2147,67 @@ Already installed containers were found on the server. All installed containers Добавить их в приложение, если они не отображаются - + Reboot server Перезагрузить сервер - + Do you want to reboot the server? Вы уверены, что хотите перезагрузить сервер? - + The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? Процесс перезагрузки может занять около 30 секунд. Вы уверены, что хотите продолжить? - + Cannot reboot server during active connection Невозможно перезагрузить сервер во время активного соединения - + Do you want to remove the server from application? Вы уверены, что хотите удалить сервер из приложения? - + Cannot remove server during active connection Невозможно удалить сервер во время активного соединения - + Do you want to clear server from Amnezia software? Вы хотите очистить сервер от всех сервисов Amnezia? - + All users whom you shared a connection with will no longer be able to connect to it. Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. - + Cannot clear server from Amnezia software during active connection Невозможно очистить сервер от сервисов Amnezia во время активного соединения - + Reset API config Сбросить конфигурацию API - + Do you want to reset API config? Вы хотите сбросить конфигурацию API? - + Cannot reset API config during active connection Невозможно сбросить конфигурацию API во время активного соединения - + Remove server from application Удалить сервер из приложения @@ -2181,12 +2216,12 @@ Already installed containers were found on the server. All installed containers Удалить сервер? - + All installed AmneziaVPN services will still remain on the server. Все установленные сервисы и протоколы Amnezia останутся на сервере. - + Clear server from Amnezia software Очистить сервер от протоколов и сервисов Amnezia @@ -2202,27 +2237,32 @@ Already installed containers were found on the server. All installed containers PageSettingsServerInfo - + + Subscription is valid until + Подписка заканчивается через + + + Server name Имя сервера - + Save Сохранить - + Protocols Протоколы - + Services Сервисы - + Management Управление @@ -2234,7 +2274,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServerProtocol - + settings настройки @@ -2243,7 +2283,7 @@ Already installed containers were found on the server. All installed containers Очистить профиль %1 - + Clear %1 profile? Очистить профиль %1? @@ -2253,52 +2293,52 @@ Already installed containers were found on the server. All installed containers - + connection settings - + Click the "connect" button to create a connection configuration - + server settings - + настройки сервера - + Clear profile - + Очистить профиль - + The connection configuration will be deleted for this device only - + Конфигурация подключения будет удалена только для этого устройства - + Unable to clear %1 profile while there is an active connection Невозможно очистить профиль %1 во время активного соединения - + Remove Удалить - + Remove %1 from server? Удалить %1 с сервера? - + All users with whom you shared a connection will no longer be able to connect to it. Все пользователи, с которыми вы поделились конфигурацией вашего VPN, больше не смогут к нему подключаться. - + Cannot remove active container Невозможно удалить активный контейнер @@ -2307,14 +2347,14 @@ Already installed containers were found on the server. All installed containers Все пользователи, с которыми вы поделились VPN, больше не смогут к нему подключаться. - - + + Continue Продолжить - - + + Cancel Отменить @@ -2322,7 +2362,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServersList - + Servers Серверы @@ -2330,7 +2370,7 @@ Already installed containers were found on the server. All installed containers PageSettingsSplitTunneling - + Default server does not support split tunneling function Сервер по умолчанию не поддерживает раздельное туннелирование @@ -2339,32 +2379,32 @@ Already installed containers were found on the server. All installed containers Только адреса из списка должны открываться через VPN - + Addresses from the list should not be accessed via VPN Адреса из списка не должны открываться через VPN - + Split tunneling - Раздельное туннелирование + Раздельное туннелирование сайтов - + Mode Режим - + Remove Удалить - + Continue Продолжить - + Cancel Отменить @@ -2373,65 +2413,65 @@ Already installed containers were found on the server. All installed containers Сайт или IP - + Import / Export Sites Импорт/экспорт сайтов - + Only the sites listed here will be accessed through the VPN Только адреса из списка должны открываться через VPN - + Cannot change split tunneling settings during active connection Невозможно изменить настройки раздельного туннелирования во время активного соединения - + website or IP веб-сайт или IP - + Import Импорт - + Save site list Сохранить список сайтов - + Save sites Сохранить сайты - - - + + + Sites files (*.json) Файлы сайтов (*.json) - + Import a list of sites Импортировать список с сайтами - + Replace site list Заменить список с сайтами - - + + Open sites file Открыть список с сайтами - + Add imported sites to existing ones Добавить импортированные сайты к существующим @@ -2439,32 +2479,32 @@ Already installed containers were found on the server. All installed containers PageSetupWizardApiServiceInfo - + For the region Для региона - + Price Цена - + Work period Период работы - + Speed Скорость - + Features Особенности - + Connect Подключиться @@ -2472,12 +2512,12 @@ Already installed containers were found on the server. All installed containers PageSetupWizardApiServicesList - + VPN by Amnezia VPN от Amnezia - + Choose a VPN service that suits your needs. Выберите VPN-сервис, который подходит именно вам. @@ -2505,7 +2545,7 @@ It's okay as long as it's from someone you trust. Что у вас есть? - + File with connection settings Файл с настройками подключения @@ -2514,95 +2554,130 @@ It's okay as long as it's from someone you trust. Файл с настройками подключения или резервной копией - + Connection Соединение Settings - Настройки + Настройки - + Enable logs + Включить запись логов + + + + Support tag - + + Copied + Скопировано + + + Insert the key, add a configuration file or scan the QR-code Вставьте ключ, добавьте файл конфигурации или отсканируйте QR-код - + Insert key Вставьте ключ - + Insert Вставить - + Continue Продолжить - + Other connection options Другие варианты подключения - + + Site Amnezia + Сайт Amnezia + + + VPN by Amnezia VPN от Amnezia - + Connect to classic paid and free VPN services from Amnezia Подключайтесь к классическим платным и бесплатным VPN-сервисам от Amnezia - + Self-hosted VPN Self-hosted VPN - + Configure Amnezia VPN on your own server Настроить VPN на собственном сервере - + Restore from backup Восстановить из резервной копии - + + + + + + Open backup file Открыть резервную копию - + Backup files (*.backup) Файлы резервных копий (*.backup) - + + + + + + Open config file Открыть файл с конфигурацией - + QR code QR-код - + + + + + + I have nothing У меня ничего нет + + + + + Key as text Ключ в виде текста @@ -2615,7 +2690,7 @@ It's okay as long as it's from someone you trust. Подключение к серверу - + Server IP address [:port] IP-адрес[:порт] сервера @@ -2628,7 +2703,7 @@ It's okay as long as it's from someone you trust. Password / SSH private key - + Continue Продолжить @@ -2638,7 +2713,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Все данные, которые вы вводите, останутся строго конфиденциальными и не будут переданы или раскрыты Amnezia или каким-либо третьим лицам - + Enter the address in the format 255.255.255.255:88 Введите адрес в формате 255.255.255.255:88 @@ -2647,52 +2722,52 @@ and will not be shared or disclosed to the Amnezia or any third parties Login to connect via SSH - + Configure your server Настроить ваш сервер - + 255.255.255.255:22 255.255.255.255:22 - + SSH Username Имя пользователя SSH - + Password or SSH private key Пароль или закрытый ключ SSH - + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties Все данные, которые вы вводите, останутся строго конфиденциальными и не будут переданы или раскрыты Amnezia или каким-либо третьим лицам - + How to run your VPN server Как создать VPN на собственном сервере - + Where to get connection data, step-by-step instructions for buying a VPS - Где взять данные для подключения, пошаговые инстуркции по покупке VPS + Где взять данные для подключения, пошаговые инструкции по покупке VPS - + Ip address cannot be empty Поле с IP-адресом не может быть пустым - + Login cannot be empty Поле с логином не может быть пустым - + Password/private key cannot be empty Поле с паролем/закрытым ключом не может быть пустым @@ -2700,17 +2775,17 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardEasy - + What is the level of internet control in your region? Какой уровень контроля над интернетом в вашем регионе? - + Choose a VPN protocol Выберите VPN-протокол - + Skip setup Пропустить настройку @@ -2723,7 +2798,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Выбрать VPN-протокол - + Continue Продолжить @@ -2774,37 +2849,37 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardProtocolSettings - + Installing %1 Устанавливается %1 - + More detailed Подробнее - + Close Закрыть - + Network protocol Сетевой протокол - + Port Порт - + Install Установить - + The port must be in the range of 1 to 65535 Порт должен быть в диапазоне от 1 до 65535 @@ -2812,12 +2887,12 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardProtocols - + VPN protocol VPN-протокол - + Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP. Выберите протокол, который вам больше подходит. В дальнейшем можно установить другие протоколы и дополнительные сервисы, такие как DNS-прокси и SFTP. @@ -2857,7 +2932,7 @@ and will not be shared or disclosed to the Amnezia or any third parties https://amnezia.org/ru/starter-guide - + Let's get started Приступим @@ -2865,27 +2940,27 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardTextKey - + Connection key Ключ для подключения - + A line that starts with vpn://... Строка, которая начинается с vpn://... - + Key Ключ - + Insert Вставить - + Continue Продолжить @@ -2893,7 +2968,7 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardViewConfig - + New connection Новое соединение @@ -2902,27 +2977,27 @@ and will not be shared or disclosed to the Amnezia or any third parties Не используйте код подключения из публичных источников. Его могли создать, чтобы перехватить ваши данные. - + Collapse content Свернуть - + Show content Показать - + Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. Включить обфускацию WireGuard. Это может быть полезно, если WireGuard блокируется вашим провайдером. - + Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. Используйте файлы конфигурации только из тех источников, которым вы доверяете. Файлы из общедоступных источников могли быть созданы с целью перехвата ваших личных данных. - + Connect Подключиться @@ -2930,12 +3005,12 @@ and will not be shared or disclosed to the Amnezia or any third parties PageShare - + OpenVPN native format Оригинальный формат OpenVPN - + WireGuard native format Оригинальный формат WireGuard @@ -2944,7 +3019,7 @@ and will not be shared or disclosed to the Amnezia or any third parties VPN-Доступ - + Connection Соединение @@ -2957,8 +3032,8 @@ and will not be shared or disclosed to the Amnezia or any third parties Доступ к управлению сервером. Пользователь, с которым вы делитесь полным доступом к соединению, сможет добавлять и удалять ваши протоколы и службы на сервере, а также изменять настройки. - - + + Server Сервер @@ -2967,167 +3042,172 @@ and will not be shared or disclosed to the Amnezia or any third parties Доступ - + Config revoked Конфигурация отозвана - + Connection to Подключение к - + File with connection settings to Файл с настройками подключения к - + Save OpenVPN config Сохранить конфигурацию OpenVPN - + Save WireGuard config Сохранить конфигурацию WireGuard - + Save AmneziaWG config Сохранить конфигурацию AmneziaWG - + Save Shadowsocks config Сохранить конфигурацию Shadowsocks - + Save Cloak config Сохранить конфигурацию Cloak - + Save XRay config Сохранить конфигурацию XRay - + For the AmneziaVPN app Для приложения AmneziaVPN - + AmneziaWG native format Оригинальный формат AmneziaWG - + Shadowsocks native format Оригинальный формат Shadowsocks - + Cloak native format Оригинальный формат Cloak - + XRay native format Оригинальный формат XRay - + Share VPN Access Поделиться VPN - + Share full access to the server and VPN Поделиться полным доступом к серверу и VPN - + Use for your own devices, or share with those you trust to manage the server. Используйте для собственных устройств или передайте управление сервером тем, кому вы доверяете. - - + + Users Пользователи - + User name Имя пользователя - + Search Поиск - + Creation date: %1 Дата создания: %1 - + Latest handshake: %1 Последнее рукопожатие: %1 - + Data received: %1 Получено данных: %1 - + Data sent: %1 Отправлено данных: %1 + + + Allowed IPs: %1 + + Creation date: Дата создания: - + Rename Переименовать - + Client name Имя клиента - + Save Сохранить - + Revoke Отозвать - + Revoke the config for a user - %1? Отозвать конфигурацию для пользователя - %1? - + The user will no longer be able to connect to your server. Пользователь больше не сможет подключаться к вашему серверу. - + Continue Продолжить - + Cancel Отменить @@ -3136,25 +3216,25 @@ and will not be shared or disclosed to the Amnezia or any third parties Полный доступ - + Share VPN access without the ability to manage the server Поделиться доступом к VPN без возможности управления сервером - - + + Protocol Протокол - - + + Connection format Формат подключения - - + + Share Поделиться @@ -3162,55 +3242,55 @@ and will not be shared or disclosed to the Amnezia or any third parties PageShareFullAccess - + Full access to the server and VPN Полный доступ к серверу и VPN - + We recommend that you use full access to the server only for your own additional devices. Мы рекомендуем использовать полный доступ к серверу только для собственных устройств. - + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. Если вы поделитесь полным доступом с другими людьми, то они смогут удалять и добавлять протоколы и сервисы на сервер, что приведет к некорректной работе VPN для всех пользователей. - - + + Server Сервер - + Accessing Доступ - + File with accessing settings to Файл с настройками доступа к - + Share Поделиться - + Access error! Ошибка доступа! - + Connection to Подключение к - + File with connection settings to Файл с настройками подключения к @@ -3218,25 +3298,25 @@ and will not be shared or disclosed to the Amnezia or any third parties PageStart - + Logging was disabled after 14 days, log files were deleted Логирование было отключено по прошествии 14 дней, файлы логов были удалены. - + Settings restored from backup file Настройки восстановлены из бэкап файла - + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. - + Логирование включено. Обратите внимание, что через 14 дней оно будет автоматически отключено, а все файлы логов будут удалены. PopupType - + Close Закрыть @@ -3625,17 +3705,17 @@ and will not be shared or disclosed to the Amnezia or any third parties Конфигурация не содержит каких-либо контейнеров и учетных данных для подключения к серверу - + Error when retrieving configuration from API Ошибка при получении конфигурации из API - + This config has already been added to the application Данная конфигурация уже была добавлена в приложение - + ErrorCode: %1. Код ошибки: %1. @@ -3694,62 +3774,77 @@ and will not be shared or disclosed to the Amnezia or any third parties Ошибка пула VPN: нет доступных адресов - + + Unable to open config file + Не удалось открыть файл конфигурации + + + VPN connection error Ошибка VPN-соединения - + In the response from the server, an empty config was received В ответе от сервера была получена пустая конфигурация - + SSL error occurred Произошла ошибка SSL - + Server response timeout on api request Тайм-аут ответа сервера на запрос API - + Missing AGW public key + + + Failed to decrypt response payload + + + Missing list of available services + + + + QFile error: The file could not be opened Ошибка QFile: не удалось открыть файл - + QFile error: An error occurred when reading from the file Ошибка QFile: произошла ошибка при чтении из файла - + QFile error: The file could not be accessed Ошибка QFile: не удалось получить доступ к файлу - + QFile error: An unspecified error occurred Ошибка QFile: произошла неизвестная ошибка - + QFile error: A fatal error occurred Ошибка QFile: произошла фатальная ошибка - + QFile error: The operation was aborted Ошибка QFile: операция была прервана - + Internal error Внутренняя ошибка @@ -4286,11 +4381,19 @@ This means that AmneziaWG keeps the fast performance of the original while addin SelectLanguageDrawer - + Choose language Выберите язык + + ServersListView + + + Unable change server while there is an active connection + Невозможно изменить сервер во время активного соединения + + Settings @@ -4308,7 +4411,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin SettingsController - + All settings have been reset to default values Все настройки сброшены до значений по умолчанию @@ -4317,7 +4420,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin Закэшированные профили очищены - + Backup file is corrupted Файл резервной копии поврежден @@ -4331,33 +4434,33 @@ This means that AmneziaWG keeps the fast performance of the original while addin Сохранить конфигурацию AmneziaVPN - + Share Поделиться - + Copy Скопировать - - + + Copied Скопировано - + Copy config string Скопировать строку конфигурации - + Show connection settings Показать настройки подключения - + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" Для считывания QR-кода в приложении Amnezia выберите "Добавить сервер" → "У меня есть данные для подключения" → "Открыть файл конфигурации, ключ или QR-код" @@ -4380,27 +4483,27 @@ This means that AmneziaWG keeps the fast performance of the original while addin Сайт удален: %1 - + Can't open file: %1 Невозможно открыть файл: %1 - + Failed to parse JSON data from file: %1 Не удалось разобрать JSON-данные из файла: %1 - + The JSON data is not an array in file: %1 JSON-данные не являются массивом в файле: %1 - + Import completed Импорт завершен - + Export completed Экспорт завершен @@ -4441,7 +4544,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin TextFieldWithHeaderType - + The field can't be empty Поле не может быть пустым @@ -4449,7 +4552,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin VpnConnection - + Mbps Мбит/с @@ -4547,12 +4650,12 @@ This means that AmneziaWG keeps the fast performance of the original while addin main2 - + Private key passphrase Парольная фраза для закрытого ключа - + Save Сохранить diff --git a/client/translations/amneziavpn_uk_UA.ts b/client/translations/amneziavpn_uk_UA.ts index c7195119..3709e30a 100644 --- a/client/translations/amneziavpn_uk_UA.ts +++ b/client/translations/amneziavpn_uk_UA.ts @@ -1,6 +1,14 @@ + + AdLabel + + + Amnezia Premium - for access to any website + + + AmneziaApplication @@ -27,52 +35,52 @@ ApiServicesModel - + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s Звичайний VPN для комфортної роботи, завантаження великих файлів та перегляду відео. Працює для будь-яких сайтів. Швидкість до %1 MBit/s - + VPN to access blocked sites in regions with high levels of Internet censorship. VPN для доступу до заблокованих сайтів у регіонах з високим рівнем інтернет-цензури. - + <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> - + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. Amnezia Premium - звичайний VPN для комфортної роботи, завантаження великих файлів та перегляду відео у високій роздільній здатності. Працює для всіх вебсайтів, навіть у країнах з найвищим рівнем інтернет-цензури. - + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship Amnezia Free — це безкоштовний VPN для обходу блокувань у країнах з високим рівнем інтернет-цензури - + %1 MBit/s %1 MBit/s - + %1 days %1 днів - + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> Лише популярні сайти, які заблоковані у вашому регіоні, будуть відкриватись за допомогою VPN підключення (Instagram, Facebook, Twitter та ін.). Звичайні сайти будуть відкриватися без використання VPN, <a href="%1/free" style="color: #FBB26A;">більш детально на нашому сайті.</a> - + Free Безкоштовно - + %1 $/month %1 $/місяць @@ -103,7 +111,7 @@ ConnectButton - + Unable to disconnect during configuration preparation Неможливо відключитися під час підготовки конфігурації @@ -111,62 +119,62 @@ ConnectionController - + VPN Protocols is not installed. Please install VPN container at first VPN протоколи не встановлено. Будь-ласка, встановіть VPN контейнер - + unable to create configuration Неможливо створити конфігурацію - + Connecting... Підключення... - + Connected Підключено - + Preparing... Підготовка... - + Settings updated successfully, reconnnection... Налаштування оновлено, підключення... - + Settings updated successfully Налаштування оновлено - + The selected protocol is not supported on the current platform Вибраний протокол не підтримується на цьому пристрої - + Reconnecting... Перепідключення... - - - + + + Connect Підключитись - + Disconnecting... Відключаємось... @@ -174,17 +182,17 @@ ConnectionTypeSelectionDrawer - + Add new connection Додати нове з'єднання - + Configure your server Налаштувати свій сервер - + Open config file, key or QR code Відкрити файл конфігурації, ключ або QR код @@ -222,7 +230,7 @@ HomeContainersListView - + Unable change protocol while there is an active connection Неможливо змінити протокол при активному підключенні @@ -238,46 +246,46 @@ HomeSplitTunnelingDrawer - + Split tunneling Роздільне тунелювання - + Allows you to connect to some sites or applications through a VPN connection and bypass others Дозволяє підключатись до одних сайтів та застосунків через захищене з'єднання, а іншим в обхід нього - + Split tunneling on the server Роздільне тунелювання на сервері - + Enabled Can't be disabled for current server Увімкнено. Не може бути вимкнено для даного сервера - + Site-based split tunneling Роздільне тунелювання по сайтам - - + + Enabled Увімкнено - - + + Disabled Вимкнено - + App-based split tunneling Роздільне тунелювання застосунків @@ -285,23 +293,20 @@ Can't be disabled for current server ImportController - Unable to open file - Неможливо відкрити файл + Неможливо відкрити файл - - Invalid configuration file - Недійсний файл конфігурації + Недійсний файл конфігурації - + Scanned %1 of %2. Відскановано %1 з %2. - + In the imported configuration, potentially dangerous lines were found: У імпортованій конфігурації знайдено потенційно небезпечні рядки: @@ -309,85 +314,85 @@ Can't be disabled for current server InstallController - + %1 installed successfully. %1 встановлено. - + %1 is already installed on the server. %1 вже встановлено на сервері. - + Added containers that were already installed on the server Додані сервіси і протоколи, які були раніше встановлені на сервері - + Already installed containers were found on the server. All installed containers have been added to the application На сервері знайдені сервіси та протоколи, всі вони додані в застосунок - + Settings updated successfully Налаштування оновлено - + Server '%1' was rebooted Сервер '%1' перезавантажено - + Server '%1' was removed Сервер '%1' був видалений - + All containers from server '%1' have been removed Всі сервіси та протоколи були видалені з сервера '%1' - + %1 has been removed from the server '%2' %1 був видалений з сервера '%2' - + Api config removed Конфігурацію API видалено - + %1 cached profile cleared Кешований профіль %1 очищено - + Please login as the user Буль-ласка, увійдіть в систему від імені користувача - + Server added successfully Сервер додано - + %1 installed successfully. %1 встановлено успішно. - + API config reloaded Конфігурацію API перезавантажено - + Successfully changed the country of connection to %1 Успішно змінено країну підключення на %1 @@ -400,12 +405,12 @@ Already installed containers were found on the server. All installed containers Виберіть застосунок - + application name назва застосунку - + Add selected Додати вибране @@ -473,12 +478,12 @@ Already installed containers were found on the server. All installed containers PageDevMenu - + Gateway endpoint - + Dev gateway environment @@ -486,85 +491,84 @@ Already installed containers were found on the server. All installed containers PageHome - + Logging enabled Логування увімкнено - + Split tunneling enabled Роздільне тунелювання увімкнено - + Split tunneling disabled Роздільне тунелювання вимкнено - + VPN protocol VPN протокол - + Servers Сервери - Unable change server while there is an active connection - Не можна змінити сервер при активному підключенні + Не можна змінити сервер при активному підключенні PageProtocolAwgClientSettings - + AmneziaWG settings налаштування AmneziaWG - + MTU MTU - + Server settings - + Port Порт - + Save Зберегти - + Save settings? Зберегти налаштування? - + Only the settings for this device will be changed - + Continue Продовжити - + Cancel Відмінити - + Unable change settings while there is an active connection Неможливо змінити налаштування, поки є активне підключення @@ -572,87 +576,92 @@ Already installed containers were found on the server. All installed containers PageProtocolAwgSettings - + AmneziaWG settings налаштування AmneziaWG - + + VPN address subnet + VPN address subnet + + + Port Порт - + Jc - Junk packet count - + Jmin - Junk packet minimum size - + Jmax - Junk packet maximum size - + S1 - Init packet junk size - + S2 - Response packet junk size - + H1 - Init packet magic header - + H2 - Response packet magic header - + H4 - Transport packet magic header - + H3 - Underload packet magic header - + Save Зберегти - + The values of the H1-H4 fields must be unique Значення полів H1-H4 мають бути унікальними - + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) Значення поля S1 + розмір повідомлення ініціалізації (148) не має бути рівним значенню S2 + розмір повідомлення відповіді (92) - + Save settings? Зберегти налаштування? - + All users with whom you shared a connection with will no longer be able to connect to it. Усі користувачі, з якими ви поділилися підключенням, більше не зможуть підключитися до нього. - + Unable change settings while there is an active connection Неможливо змінити налаштування, поки є активне підключення @@ -673,12 +682,12 @@ Already installed containers were found on the server. All installed containers Користувачі, з якими ви поділились цим протоколм, більше не зможуть до нього підключитись. - + Continue Продовжити - + Cancel Відмінити @@ -690,33 +699,33 @@ Already installed containers were found on the server. All installed containers PageProtocolCloakSettings - + Cloak settings Налаштування Cloak - + Disguised as traffic from Замаскувати трафік під - + Port Порт - - + + Cipher Шифрування - + Save Зберегти - + Unable change settings while there is an active connection Неможливо змінити налаштування, поки є активне підключення @@ -728,7 +737,7 @@ Already installed containers were found on the server. All installed containers PageProtocolOpenVpnSettings - + OpenVPN settings налаштування OpenVPN @@ -737,170 +746,170 @@ Already installed containers were found on the server. All installed containers Підмережа для VPN - + VPN address subnet VPN address subnet - + Network protocol Мережевий притокол - + Port Порт - + Auto-negotiate encryption Автоматично отримувати шифрування - - + + Hash Хеш - + SHA512 SHA512 - + SHA384 SHA384 - + SHA256 SHA256 - + SHA3-512 SHA3-512 - + SHA3-384 SHA3-384 - + SHA3-256 SHA3-256 - + whirlpool whirlpool - + BLAKE2b512 BLAKE2b512 - + BLAKE2s256 BLAKE2s256 - + SHA1 SHA1 - - + + Cipher Шифрування - + AES-256-GCM AES-256-GCM - + AES-192-GCM AES-192-GCM - + AES-128-GCM AES-128-GCM - + AES-256-CBC AES-256-CBC - + AES-192-CBC AES-192-CBC - + AES-128-CBC AES-128-CBC - + ChaCha20-Poly1305 ChaCha20-Poly1305 - + ARIA-256-CBC ARIA-256-CBC - + CAMELLIA-256-CBC CAMELLIA-256-CBC - + none none - + TLS auth TLS авторизація - + Block DNS requests outside of VPN Блокувати DNS запити за межами VPN тунеля - + Additional client configuration commands Додаткові команди конфігурації клієнта - - + + Commands: Команди: - + Additional server configuration commands Додаткові команти конфігурації сервера - + Save Зберегти - + Unable change settings while there is an active connection Неможливо змінити налаштування, поки є активне підключення @@ -936,32 +945,32 @@ Already installed containers were found on the server. All installed containers PageProtocolRaw - + settings налаштування - + Show connection options Показати параметри підключення - + Connection options %1 Параметри підключення %1 - + Remove Видалити - + Remove %1 from server? Видалити %1 з сервера? - + All users with whom you shared a connection with will no longer be able to connect to it. Усі користувачі, з якими ви поділилися підключенням, більше не зможуть підключитися до нього. @@ -974,12 +983,12 @@ Already installed containers were found on the server. All installed containers Користувачі, з якими ви поділились цим протоколм, більше не зможуть до нього підключитись. - + Continue Продовжити - + Cancel Відмінити @@ -987,28 +996,28 @@ Already installed containers were found on the server. All installed containers PageProtocolShadowSocksSettings - + Shadowsocks settings Налаштування Shadowsocks - + Port Порт - - + + Cipher Шифрування - + Save Зберегти - + Unable change settings while there is an active connection Неможливо змінити налаштування, поки є активне підключення @@ -1020,52 +1029,52 @@ Already installed containers were found on the server. All installed containers PageProtocolWireGuardClientSettings - + WG settings - + MTU MTU - + Server settings - + Port Порт - + Save Зберегти - + Save settings? Зберегти налаштування? - + Only the settings for this device will be changed - + Continue Продовжити - + Cancel Відмінити - + Unable change settings while there is an active connection Неможливо змінити налаштування, поки є активне підключення @@ -1073,12 +1082,17 @@ Already installed containers were found on the server. All installed containers PageProtocolWireGuardSettings - + WG settings - + + VPN address subnet + VPN address subnet + + + Port Порт @@ -1087,32 +1101,32 @@ Already installed containers were found on the server. All installed containers MTU - + Save Зберегти - + Save settings? Зберегти налаштування? - + All users with whom you shared a connection with will no longer be able to connect to it. Усі користувачі, з якими ви поділилися підключенням, більше не зможуть підключитися до нього. - + Continue Продовжити - + Cancel Відмінити - + Unable change settings while there is an active connection Неможливо змінити налаштування, поки є активне підключення @@ -1120,22 +1134,22 @@ Already installed containers were found on the server. All installed containers PageProtocolXraySettings - + XRay settings Налаштування XRay - + Disguised as traffic from Замаскувати трафік під - + Save Зберегти - + Unable change settings while there is an active connection Неможливо змінити налаштування, поки є активне підключення @@ -1150,39 +1164,39 @@ Already installed containers were found on the server. All installed containers PageServiceDnsSettings - + A DNS service is installed on your server, and it is only accessible via VPN. На вашому сервері встановлено DNS-сервіс, доступ до нього можливо тільки через VPN. - + The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. Адреса DNS сервера співпадає з адресою вашого сервера. Налаштувати DNS можливо на вкладці "Підключення" налаштувань застосунку. - + Remove Видалити - + Remove %1 from server? Видалити %1 з сервера? - + Continue Продовжити - + Cancel Відмінити - + Cannot remove AmneziaDNS from running server Не вдається видалити AmneziaDNS з працюючого сервера @@ -1190,30 +1204,30 @@ Already installed containers were found on the server. All installed containers PageServiceSftpSettings - + Settings updated successfully Налаштування оновлено - + SFTP settings Налаштування SFTP - + Host Хост - - - - + + + + Copied Скопійовано - + Port Порт @@ -1222,39 +1236,39 @@ Already installed containers were found on the server. All installed containers Логін - + User name Імя користувача - + Password Пароль - + Mount folder on device Змонтувати папку з вашого пристрою - + In order to mount remote SFTP folder as local drive, perform following steps: <br> Для того щоб додати SFTP-папку, як локальний диск на вашому пристрої, виконайте наступні дії: <br> - - + + <br>1. Install the latest version of <br>1. Встановіть останню версію - - + + <br>2. Install the latest version of <br>2. Встановіть останню версію - + Detailed instructions Детальні інструкції @@ -1278,69 +1292,69 @@ Already installed containers were found on the server. All installed containers PageServiceSocksProxySettings - + Settings updated successfully Налаштування успішно оновлено - - + + SOCKS5 settings Налаштування SOCKS5 - + Host Хост - - - - + + + + Copied Скопійовано - - + + Port Порт - + User name User name - - + + Password Пароль - + Username Username - - + + Change connection settings Змінити налаштування підключення - + The port must be in the range of 1 to 65535 Порт повинен бути в межах від 1 до 65535 - + Password cannot be empty Пароль не може бути порожнім - + Username cannot be empty Ім'я користувача не може бути порожнім @@ -1348,37 +1362,37 @@ Already installed containers were found on the server. All installed containers PageServiceTorWebsiteSettings - + Settings updated successfully Налаштування оновлено - + Tor website settings Налаштування сайту в мережі Тоr - + Website address Адреса сайту - + Copied Скопійовано - + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. Використовуйте <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> для відкриття цього посилання. - + After creating your onion site, it takes a few minutes for the Tor network to make it available for use. Через кілька хвилин після встановлення ваш сайт Onion стане доступним у мережі Tor. - + When configuring WordPress set the this onion address as domain. При налаштуванні WordPress, вкажіть цей Onion в якості домена. @@ -1406,42 +1420,42 @@ Already installed containers were found on the server. All installed containers PageSettings - + Settings Налаштування - + Servers Сервери - + Connection Підключення - + Application Застосунок - + Backup Резервне копіювання - + About AmneziaVPN Про AmneziaVPN - + Dev console - + Close application Закрити застосунок @@ -1453,7 +1467,7 @@ Already installed containers were found on the server. All installed containers Підтримайте проект донатом - + Support Amnezia Підтримайте Amnezia @@ -1478,32 +1492,32 @@ Already installed containers were found on the server. All installed containers Показати інші способи на Github - + Amnezia is a free and open-source application. You can support the developers if you like it. Amnezia — це безкоштовний додаток з відкритим кодом. Якщо вам подобається цей додаток, ви можете підтримати розробників. - + Contacts Контакти - + Telegram group Група в Telegram - + To discuss features Для дискусій - + https://t.me/amnezia_vpn_en https://t.me/amnezia_vpn - + support@amnezia.org @@ -1512,121 +1526,144 @@ Already installed containers were found on the server. All installed containers Пошта - + For reviews and bug reports Для відгуків і повідомлень про помилки - + Copied Скопійовано - + GitHub GitHub - + + Discover the source code + + + + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website Веб-сайт + + + Visit official website + + https://amnezia.org https://amnezia.org - + Software version: %1 Версія ПЗ: %1 - + Check for updates Перевірити оновлення - + Privacy Policy Політика конфіденційності + + PageSettingsApiLanguageList + + + Unable change server location while there is an active connection + + + PageSettingsApiServerInfo - + For the region Для регіону - + Price Ціна - + Work period Період роботи - + + Valid until + + + + Speed Швидкість - + Support tag - + Copied Скопійовано - + Reload API config Перезавантажити конфігурацію API - + Reload API config? Перезавантажити конфігурацію API? - - + + Continue Продовжити - - + + Cancel Відмінити - + Cannot reload API config during active connection Неможливо перезавантажити конфігурацію API під час активного підключення - + Remove from application Видалити з додатку - + Remove from application? Видалити з додатку? - + Cannot remove server during active connection Неможливо видалити сервер під час активного підключення @@ -1634,12 +1671,12 @@ Already installed containers were found on the server. All installed containers PageSettingsAppSplitTunneling - + Cannot change split tunneling settings during active connection Не можна змінити налаштування роздільного тунелювання при підключеному VPN - + Only the apps from the list should have access via VPN Доступ через VPN мають лише програми зі списку @@ -1649,42 +1686,42 @@ Already installed containers were found on the server. All installed containers Програми зі списку не мають доступ через VPN - + App split tunneling Split tunneling для додатка - + Mode Режим - + Remove Видалити - + Continue Продовжити - + Cancel Відмінити - + application name назва додатка - + Open executable file Відкрити виконуваний файл - + Executable files (*.*) Виконувані файли (*.*) @@ -1692,102 +1729,102 @@ Already installed containers were found on the server. All installed containers PageSettingsApplication - + Application Застосунок - + Allow application screenshots Дозволити скріншоти у застосунку - + Enable notifications Увімкнути сповіщення - + Enable notifications to show the VPN state in the status bar Увімкнути сповіщення (показує стан VPN у статус барі) - + Auto start Автозапуск - + Launch the application every time the device is starts Запускати застосунок при старті - + Auto connect Автопідключення - + Connect to VPN on app start Підключення до VPN при старті застосунку - + Start minimized Запускати в згорнутому вигляді - + Launch application minimized Запускати застосунок в згорнутому вигляді - + Language Мова - + Logging Логування - + Enabled Увімкнено - + Disabled Вимкнено - + Reset settings and remove all data from the application Скинути налаштування і видалити всі дані із застосунку - + Reset settings and remove all data from the application? Скинути налаштування і видалити всі дані із застосунку? - + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. Всі дані із застосунку будуть видалені, всі встановлені сервіси AmneziaVPN залишаться на сервері. - + Continue Продовжити - + Cancel Відмінити - + Cannot reset settings during active connection Неможливо скинути налаштування під час активного підключення @@ -1799,7 +1836,7 @@ Already installed containers were found on the server. All installed containers Резервне копіювання - + Settings restored from backup file Відновлення налаштувань із бекап файлу @@ -1808,73 +1845,73 @@ Already installed containers were found on the server. All installed containers Бекап конфігурація - + Back up your configuration Резервне копіювання вашої конфігурації - + You can save your settings to a backup file to restore them the next time you install the application. Ви можете зберегти свої налаштування у бекап файл (резервну копію), щоб відновити їх під час наступного встановлення програми. - + The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. Резервна копія міститиме ваші паролі та приватні ключі для всіх серверів, доданих до AmneziaVPN. Зберігайте цю інформацію у безпечному місці. - + Make a backup Зробити бекап (резервну копію) - + Save backup file Зберегти бекап файл - - + + Backup files (*.backup) Файли резервної копії (*.backup) - + Backup file saved Бекап файл збережено - + Restore from backup Відновити із бекапа - + Open backup file Відкрити бекап файл - + Import settings from a backup file? Імпортувати налаштування із бекап файлу? - + All current settings will be reset Всі поточні налаштування будуть скинуті - + Continue Продовжити - + Cancel Відмінити - + Cannot restore backup settings during active connection Неможливо відновити резервну копію налаштувань під час активного підключення @@ -1882,7 +1919,7 @@ Already installed containers were found on the server. All installed containers PageSettingsConnection - + Connection З'єднання @@ -1895,57 +1932,57 @@ Already installed containers were found on the server. All installed containers Підключення до VPN при старті застосунку - + Use AmneziaDNS Використовувати AmneziaDNS - + If AmneziaDNS is installed on the server Якщо він встановлений на сервері - + DNS servers DNS сервер - + When AmneziaDNS is not used or installed Ці адреси будуть використовуватись коли вимкнений AmneziaDNS - + Allows you to use the VPN only for certain Apps Дозволяє використовувати VPN тільки для вибраних застосунків - + KillSwitch KillSwitch - + Disables your internet if your encrypted VPN connection drops out for any reason. Вимикає ваш інтернет, якщо ваше захищене VPN-підключення зникає з будь-якої причини. - + Cannot change killSwitch settings during active connection Неможливо змінити налаштування killSwitch під час активного підключення - + Site-based split tunneling Роздільне тунелювання сайтів - + Allows you to select which sites you want to access through the VPN Дозволяє доступ до одних сайтів через VPN, а для інших в обхід VPN - + App-based split tunneling Роздільне VPN-тунелювання застосунків @@ -1957,12 +1994,12 @@ Already installed containers were found on the server. All installed containers PageSettingsDns - + Default server does not support custom DNS Сервер за замовчуванням не підтримує користувацький DNS - + DNS servers DNS сервер @@ -1971,52 +2008,52 @@ Already installed containers were found on the server. All installed containers Ці адреси будуть використовуватись, коли вимкнено або не встановлено AmneziaDNS - + If AmneziaDNS is not used or installed Якщо AmneziaDNS вимкнено або не встановлено - + Primary DNS Основний DNS - + Secondary DNS Допоміжний DNS - + Restore default Відновити за замовчуванням - + Restore default DNS settings? Відновити налаштування DNS за замовчуванням? - + Continue Продовжити - + Cancel Відмінити - + Settings have been reset Налаштування скинуті - + Save Зберегти - + Settings saved Зберегти налаштування @@ -2028,12 +2065,12 @@ Already installed containers were found on the server. All installed containers Логування увімкнене. Зверніть увагу, що логування буде автоматично вимкнене через 14 днів, а всі файли журналів будуть видалені. - + Logging Логування - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. Увімкнення цієї функції автоматично зберігатиме журнали додатка. За замовчуванням функція логування вимкнена. Увімкніть збереження журналів у випадку збою додатка. @@ -2046,20 +2083,20 @@ Already installed containers were found on the server. All installed containers Відкрити папку з логами - - + + Save Зберегти - - + + Logs files (*.log) Logs files (*.log) - - + + Logs file saved Файл з логами збережено @@ -2068,64 +2105,62 @@ Already installed containers were found on the server. All installed containers Зберегти логи в файл - + Enable logs - + Clear logs? Очистити логи? - + Continue Продовжити - + Cancel Відмінити - + Logs have been cleaned up Логи видалено - + Client logs - + AmneziaVPN logs - - + Open logs folder - - + Export logs - + Service logs - + AmneziaVPN-service logs - + Clear logs Видалити логи @@ -2160,18 +2195,18 @@ Already installed containers were found on the server. All installed containers - - - - + + + + Continue Продовжити - - - - + + + + Cancel Відмінити @@ -2186,62 +2221,62 @@ Already installed containers were found on the server. All installed containers Додати їх в застосунок, якщо вони не були відображені - + Reboot server Перезавантажити сервер - + Do you want to reboot the server? Ви впевнені, що хочете перезавантажити сервер? - + The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? Процес перезавантаження може зайняти близько 30 сек. Ви впевені, що хочете продовжити? - + Cannot reboot server during active connection Неможливо перезавантажити сервер під час активного підключення - + Remove server from application Видалити сервер з додатка - + Do you want to remove the server from application? Ви впевнені, що хочете видалити сервер із застосунку? - + Cannot remove server during active connection Неможливо видалити сервер під час активного підключення - + Clear server from Amnezia software Очистити сервер від програмного забезпечення Amnezia - + Do you want to clear server from Amnezia software? Ви дійсно хочете очистити сервер від програмного забезпечення Amnezia? - + All users whom you shared a connection with will no longer be able to connect to it. Усі користувачі, з якими ви поділилися підключенням, більше не зможуть підключитися до нього. - + Cannot clear server from Amnezia software during active connection Неможливо очистити сервер від програмного забезпечення Amnezia під час активного підключення - + Cannot reset API config during active connection Неможливо скинути конфігурацію API під час активного підключення @@ -2250,12 +2285,12 @@ Already installed containers were found on the server. All installed containers Ви хочете очистити сервер від сервісів Amnezia? - + Reset API config Скинути API конфігурацію - + Do you want to reset API config? Ви хочете скинути API конфігурацію? @@ -2268,7 +2303,7 @@ Already installed containers were found on the server. All installed containers Видалити сервер із застосунку? - + All installed AmneziaVPN services will still remain on the server. Всі встановлені сервіси та протоколи Amnezia все ще залишаться на сервері. @@ -2288,27 +2323,32 @@ Already installed containers were found on the server. All installed containers PageSettingsServerInfo - + + Subscription is valid until + + + + Server name Імя сервера - + Save Зберегти - + Protocols Протоколи - + Services Сервіси - + Management Управління @@ -2320,7 +2360,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServerProtocol - + settings Налаштування @@ -2329,42 +2369,42 @@ Already installed containers were found on the server. All installed containers Очистити профіль %1 - + connection settings - + Click the "connect" button to create a connection configuration - + server settings - + Clear profile - + Clear %1 profile? Очистити профіль %1? - + The connection configuration will be deleted for this device only - + Unable to clear %1 profile while there is an active connection Неможливо очистити профіль %1 під час активного підключення - + Cannot remove active container Неможливо видалити активний контейнер @@ -2374,17 +2414,17 @@ Already installed containers were found on the server. All installed containers - + Remove Видалити - + Remove %1 from server? Видалити %1 з сервера? - + All users with whom you shared a connection will no longer be able to connect to it. Користувачі, з якими ви поділились цим протоколм, більше не зможуть до нього підключитись. @@ -2393,14 +2433,14 @@ Already installed containers were found on the server. All installed containers Користувачі, з якими ви поділились цим протоколм, більше не зможуть до нього підключитись. - - + + Continue Продовжити - - + + Cancel Відмінити @@ -2408,7 +2448,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServersList - + Servers Сервери @@ -2420,32 +2460,32 @@ Already installed containers were found on the server. All installed containers Тільки адреси із списку мають відкриватись через VPN - + Addresses from the list should not be accessed via VPN Адреси із списку не повинні відкриватись через VPN - + Split tunneling Роздільне VPN-тунелювання - + Mode Режим - + Remove Видалити - + Continue Продовжити - + Cancel Відмінити @@ -2454,70 +2494,70 @@ Already installed containers were found on the server. All installed containers Сайт чи IP - + Import / Export Sites Імпорт / Експорт Сайтів - + Only the sites listed here will be accessed through the VPN Тільки адреси зі списку повинні відкриватись через VPN - + Cannot change split tunneling settings during active connection Не можна змінити налаштування роздільного тунелювання при підключеному VPN - + Default server does not support split tunneling function - + website or IP вебсайт або IP - + Import Імпорт - + Save site list Зберегти список сайтів - + Save sites Зберегти - - - + + + Sites files (*.json) Sites files (*.json) - + Import a list of sites Імпортувати список із сайтами - + Replace site list Замінити список із сайтами - - + + Open sites file Відкрити список із сайтами - + Add imported sites to existing ones Додати імпортовані сайти до існуючих @@ -2525,32 +2565,32 @@ Already installed containers were found on the server. All installed containers PageSetupWizardApiServiceInfo - + For the region Для регіону - + Price Ціна - + Work period Період роботи - + Speed Швидкість - + Features Особливості - + Connect Підключитись @@ -2558,12 +2598,12 @@ Already installed containers were found on the server. All installed containers PageSetupWizardApiServicesList - + VPN by Amnezia VPN від Amnezia - + Choose a VPN service that suits your needs. Виберіть VPN-сервіс, який відповідає вашим потребам. @@ -2591,7 +2631,7 @@ It's okay as long as it's from someone you trust. Виберіть що у вас є - + File with connection settings Файл з налаштуваннями підключення @@ -2600,7 +2640,7 @@ It's okay as long as it's from someone you trust. Файл з налаштуваннями підключення або бекап - + Connection Підключення @@ -2610,85 +2650,120 @@ It's okay as long as it's from someone you trust. Налаштування - + Enable logs - + + Support tag + + + + + Copied + Скопійовано + + + Insert the key, add a configuration file or scan the QR-code Вставте ключ, додайте файл конфігурації або відскануйте QR-код - + Insert key Вставити ключ - + Insert Вставити - + Continue Продовжити - + Other connection options Інші параметри підключення - + + Site Amnezia + + + + VPN by Amnezia VPN від Amnezia - + Connect to classic paid and free VPN services from Amnezia Підключайтеся до звичайних платних та безкоштовних VPN-сервісів від Amnezia - + Self-hosted VPN Self-hosted VPN - + Configure Amnezia VPN on your own server Налаштуйте Amnezia VPN на власному сервері - + Restore from backup Відновити із бекапа - + + + + + + Open backup file Відкрити бекап файл - + Backup files (*.backup) Файли резервної копії (*.backup) - + + + + + + Open config file Відкрити файл з конфігурацією - + QR code QR-код - + + + + + + I have nothing У мене нічого нема + + + + + Key as text Ключ у вигляді тексту @@ -2701,7 +2776,7 @@ It's okay as long as it's from someone you trust. Підключення до сервера - + Server IP address [:port] Server IP address [:port] @@ -2714,7 +2789,7 @@ It's okay as long as it's from someone you trust. Password / SSH private key - + Continue Продовжити @@ -2725,7 +2800,7 @@ and will not be shared or disclosed to the Amnezia or any third parties і не будуть передані чи розголошені Amnezia або будь-яким третім особам - + Enter the address in the format 255.255.255.255:88 Введіть адресу в форматі 255.255.255.255:88 @@ -2734,52 +2809,52 @@ and will not be shared or disclosed to the Amnezia or any third parties Login to connect via SSH - + Configure your server Налаштувати свій сервер - + 255.255.255.255:22 255.255.255.255:22 - + SSH Username SSH Username - + Password or SSH private key Пароль або SSH ключ - + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties Усі дані, які ви вводите, залишатимуться суворо конфіденційними та не будуть передані чи розголошені Amnezia або будь-яким третім особам - + How to run your VPN server Як запустити ваш VPN-сервер - + Where to get connection data, step-by-step instructions for buying a VPS Де отримати дані для підключення: покрокові інструкції з придбання VPS - + Ip address cannot be empty Поле IP address не може бути пустим - + Login cannot be empty Поле Login не може бути пустим - + Password/private key cannot be empty Поле Password/Private key не може бути пустим @@ -2787,17 +2862,17 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardEasy - + What is the level of internet control in your region? Який рівень контроля над інтернетом у вашому регіоні? - + Choose a VPN protocol Виберіть протокол VPN - + Skip setup Пропустити налаштування @@ -2810,7 +2885,7 @@ and will not be shared or disclosed to the Amnezia or any third parties Вибрати VPN-протокол - + Continue Продовжити @@ -2869,37 +2944,37 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardProtocolSettings - + Installing %1 Встановити %1 - + More detailed Детальніше - + Close Закрити - + Network protocol Мережевий протокол - + Port Порт - + Install Встановити - + The port must be in the range of 1 to 65535 Порт повинен бути в межах від 1 до 65535 @@ -2907,12 +2982,12 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardProtocols - + VPN protocol VPN протокол - + Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP. Виберіть протокол, який вам більше підходить. Пізніше можна встановити інші протоколи і додаткові сервіси, такі як DNS-проксі, TOR-сайт и SFTP. @@ -2948,7 +3023,7 @@ and will not be shared or disclosed to the Amnezia or any third parties У мене нічого нема - + Let's get started Почнемо @@ -2956,27 +3031,27 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardTextKey - + Connection key Ключ для підключення - + A line that starts with vpn://... Стрічка, яка починається з vpn://... - + Key Ключ - + Insert Вставити - + Continue Продовжити @@ -2984,7 +3059,7 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardViewConfig - + New connection Нове підключення @@ -2997,27 +3072,27 @@ and will not be shared or disclosed to the Amnezia or any third parties Не використовуйте код підключення з загальнодоступних джерел. Він може бути створений для перехоплення ваших даних. - + Collapse content Згорнути - + Show content Показати вміст ключа - + Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. Увімкніть обфускацію WireGuard. Це може бути корисним, якщо WireGuard заблокований у вашого провайдера. - + Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. Використовуйте коди підключення тільки з джерел, яким ви довіряєте. Коди з публічних джерел можуть бути створені для перехоплення ваших даних. - + Connect Підключитись @@ -3025,12 +3100,12 @@ and will not be shared or disclosed to the Amnezia or any third parties PageShare - + OpenVPN native format OpenVPN нативний формат - + WireGuard native format WireGuard нативний формат @@ -3039,7 +3114,7 @@ and will not be shared or disclosed to the Amnezia or any third parties VPN-Доступ - + Connection З'єднання @@ -3052,8 +3127,8 @@ and will not be shared or disclosed to the Amnezia or any third parties Доступ до керування сервером. Користувач, з яким ви ділитесь повним доступом до підключення, зможе додавати та видаляти протоколи і служби на сервері, а також змінювати налаштування. - - + + Server Сервер @@ -3062,167 +3137,172 @@ and will not be shared or disclosed to the Amnezia or any third parties Доступ - + Config revoked Кофігурацію відкликано - + Connection to Підключення до - + File with connection settings to Файл з налаштуванням доступу до - + Save OpenVPN config Зберегти OpenVPN конфігурацію - + Save WireGuard config Збергти WireGuard конфігурацію - + Save AmneziaWG config Зберегти AmneziaWG конфігурацію - + Save Shadowsocks config Зберегти конфігурацію Shadowsocks - + Save Cloak config Зберегти конфігурацію Cloak - + Save XRay config Зберегти конфігурацію XRay - + For the AmneziaVPN app Для AmneziaVPN - + AmneziaWG native format нативний формат AmneziaWG - + Shadowsocks native format Shadowsocks нативний формат - + Cloak native format Cloak нативний формат - + XRay native format XRay нативний формат - + Share VPN Access Поділитись VPN з'єднанням - + Share full access to the server and VPN Поділитись повним доступом до серверу - + Use for your own devices, or share with those you trust to manage the server. Використовуйте для власних пристроїв або передайте керування сервером тим, кому довіряєте. - - + + Users Користувачі - + User name Ім'я користувача - + Search Пошук - + Creation date: %1 Дата створення: %1 - + Latest handshake: %1 Останнє з'єднання: %1 - + Data received: %1 Отримано даних: %1 - + Data sent: %1 Відправлено даних: %1 + + + Allowed IPs: %1 + + Creation date: Дата створення: - + Rename Перейменувати - + Client name Назва клієнта - + Save Зберегти - + Revoke Відкликати - + Revoke the config for a user - %1? Відкликати доступ для користувача - %1? - + The user will no longer be able to connect to your server. Користувач більше не зможе підключатись до вашого сервера - + Continue Продовжити - + Cancel Відмінити @@ -3231,25 +3311,25 @@ and will not be shared or disclosed to the Amnezia or any third parties Повний доступ - + Share VPN access without the ability to manage the server Поділитись доступом до VPN, без можливості керування сервером - - + + Protocol Протокол - - + + Connection format Формат підключення - - + + Share Поділитись @@ -3257,54 +3337,54 @@ and will not be shared or disclosed to the Amnezia or any third parties PageShareFullAccess - + Full access to the server and VPN Повний доступ до серверу та VPN - + We recommend that you use full access to the server only for your own additional devices. Ми рекомендуємо використовувати повний доступ тілки для власних пристроїв. - + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. Якщо ви ділитеся повним доступом з іншими людьми, вони можуть видаляти та додавати протоколи та служби на сервер, що призведе до некоректної роботи VPN для всіх користувачів. - - + + Server Сервер - + Accessing Доступ - + File with accessing settings to Файл з налаштуваннями доступу до - + Share Поділитись - + Access error! Помилка доступу! - + Connection to Підключення до - + File with connection settings to Файл з налаштуванням доступу до @@ -3312,17 +3392,17 @@ and will not be shared or disclosed to the Amnezia or any third parties PageStart - + Logging was disabled after 14 days, log files were deleted Логування було вимкнене через 14 днів, файли журналів були видалені - + Settings restored from backup file Відновлення налаштувань із бекап файлу - + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. @@ -3330,7 +3410,7 @@ and will not be shared or disclosed to the Amnezia or any third parties PopupType - + Close Закрити @@ -3721,17 +3801,17 @@ and will not be shared or disclosed to the Amnezia or any third parties Конфігурація не містить контейнерів і облікових даних для підключення до серверу - + Error when retrieving configuration from API - + This config has already been added to the application Ця конфігурація вже була додана в застосунок - + ErrorCode: %1. @@ -3790,62 +3870,77 @@ and will not be shared or disclosed to the Amnezia or any third parties VPN pool error: no available addresses - + + Unable to open config file + + + + VPN connection error - + In the response from the server, an empty config was received - + SSL error occurred - + Server response timeout on api request - + Missing AGW public key + + + Failed to decrypt response payload + + - QFile error: The file could not be opened - - - - - QFile error: An error occurred when reading from the file - - - - - QFile error: The file could not be accessed + Missing list of available services - QFile error: An unspecified error occurred + QFile error: The file could not be opened - QFile error: A fatal error occurred + QFile error: An error occurred when reading from the file + QFile error: The file could not be accessed + + + + + QFile error: An unspecified error occurred + + + + + QFile error: A fatal error occurred + + + + QFile error: The operation was aborted - + Internal error Internal error @@ -4363,11 +4458,19 @@ This means that AmneziaWG keeps the fast performance of the original while addin SelectLanguageDrawer - + Choose language Выберите язык + + ServersListView + + + Unable change server while there is an active connection + Не можна змінити сервер при активному підключенні + + Settings @@ -4385,7 +4488,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin SettingsController - + All settings have been reset to default values Всі налаштування були скинуті до значення "По замовчуванню" @@ -4394,7 +4497,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin Кеш профілю очищено - + Backup file is corrupted Backup файл пошкодженно @@ -4408,33 +4511,33 @@ This means that AmneziaWG keeps the fast performance of the original while addin Зберегти config AmneziaVPN - + Share Поділитись - + Copy Скопіювати - - + + Copied Скопійовано - + Copy config string Скопіювати стрічку конфігурації - + Show connection settings Показати налаштування підключення - + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" Для зчитування QR-коду в застосунку Amnezia виберіть "Додати сервер" → "У мене є дані підключенн" → "QR-код, ключ чи файл налаштувань" @@ -4457,27 +4560,27 @@ This means that AmneziaWG keeps the fast performance of the original while addin Сайт видалено %1 - + Can't open file: %1 Неможливо відкрити файл: %1 - + Failed to parse JSON data from file: %1 Не вдалося розібрати JSON-данні із файлу: %1 - + The JSON data is not an array in file: %1 Данні JSON не являються масивом в файлі: %1 - + Import completed Імпорт завершено - + Export completed Експорт завершено @@ -4518,7 +4621,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin TextFieldWithHeaderType - + The field can't be empty Поле не може бути пустим @@ -4526,7 +4629,7 @@ This means that AmneziaWG keeps the fast performance of the original while addin VpnConnection - + Mbps Mbps @@ -4624,12 +4727,12 @@ This means that AmneziaWG keeps the fast performance of the original while addin main2 - + Private key passphrase Пароль для особистого ключа - + Save Зберегти diff --git a/client/translations/amneziavpn_ur_PK.ts b/client/translations/amneziavpn_ur_PK.ts index cf445bfa..95419cba 100644 --- a/client/translations/amneziavpn_ur_PK.ts +++ b/client/translations/amneziavpn_ur_PK.ts @@ -1,55 +1,63 @@ + + AdLabel + + + Amnezia Premium - for access to any website + + + ApiServicesModel - + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s - + VPN to access blocked sites in regions with high levels of Internet censorship. - + <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> - + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. - + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship - + %1 MBit/s - + %1 days - + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> - + Free - + %1 $/month @@ -80,7 +88,7 @@ ConnectButton - + Unable to disconnect during configuration preparation تشکیل کی تیاری کے دوران منقطع ہونا ممکن نہیں ہے @@ -89,60 +97,60 @@ ConnectionController - - - + + + Connect جوڑنا - + The selected protocol is not supported on the current platform منتخب کردہ پروٹوکول موجودہ پلیٹ فارم پر تعاون یافتہ نہیں ہے - + VPN Protocols is not installed. Please install VPN container at first وی پی این پروٹوکول انسٹال نہیں ہے,براہ کرم پہلےوی پی این کنٹینر انسٹال کریں - + unable to create configuration تشکیل تیار کرنے میں ناکام - + Connecting... جوڑاجارھاھے.... - + Connected جوڑاجارھاھے - + Reconnecting... دوبارہ جوڑنےکی کوشش... - + Disconnecting... منقطع کرنا... - + Preparing... تیاری کیا جا رہا ہے... - + Settings updated successfully, reconnnection... ترتیب ک ھوگی،دوبارہ جوڑنےکی کوشش... - + Settings updated successfully دوبارہ ترتیب تاذہ کامیاب @@ -150,17 +158,17 @@ ConnectionTypeSelectionDrawer - + Add new connection نیا کنکشن کا اندراج کریں - + Configure your server اپنے سرور کو ترتیب دیں - + Open config file, key or QR code کھولو کنفیگ فاءیل،کی یا کور کوڈ @@ -198,7 +206,7 @@ HomeContainersListView - + Unable change protocol while there is an active connection موجودہ کنکشن ہونے کے دوران پروٹوکول کو تبدیل کرنے سے قاصر ہے @@ -206,45 +214,45 @@ HomeSplitTunnelingDrawer - + Split tunneling سپلٹ ٹنلنگ - + Allows you to connect to some sites or applications through a VPN connection and bypass others آپ کو VPN کنکشن کے ذریعے کچھ سائٹس یا ایپلیکیشنز سے جڑنے اور دوسروں کو نظرانداز کرنے کی اجازت دیتا ہے - + Split tunneling on the server سرور پر سپلٹ ٹنلنگ - + Enabled Can't be disabled for current server فعال کو موجودہ سرور کے لیے غیر فعال نہیں کیا جا سکتا - + Site-based split tunneling سائٹ پر مبنی ٹونلنگ - - + + Enabled فعال - - + + Disabled فعال نہیں - + App-based split tunneling ایپ پر مبنی سپلٹ ٹونلنگ @@ -252,23 +260,20 @@ Can't be disabled for current server ImportController - Unable to open file - فائل کو کھولنے سے قاصر ہے + فائل کو کھولنے سے قاصر ہے - - Invalid configuration file - غلط کنفیگریشن فائل + غلط کنفیگریشن فائل - + Scanned %1 of %2. سکین%1 کی%2. - + In the imported configuration, potentially dangerous lines were found: @@ -276,86 +281,86 @@ Can't be disabled for current server InstallController - + %1 installed successfully. %1 کامیابی سےنصب. - + %1 is already installed on the server. %1 پہلے ہی سرور پر انسٹال ہے. - + Added containers that were already installed on the server وہ کنٹینرز شامل کیے گئے جو پہلے سے سرور پر نصب تھے - + Already installed containers were found on the server. All installed containers have been added to the application سرور پر پہلے سے نصب کنٹینرز پائے گئے۔ تمام نصب کنٹینرز کو ایپلی کیشن میں شامل کر دیا گیا ہے - + Settings updated successfully ترتیب کامیابی کے ساتھ اپ ڈیٹ ہو گئی - + Server '%1' was rebooted سرور %1 دوبارہ چالو کیا گیا تھا - + Server '%1' was removed سرور %1 ہٹا دیا گیا تھا - + All containers from server '%1' have been removed سرور '%1' سے تمام کنٹینرز ہٹا دیے گئے ہیں - + %1 has been removed from the server '%2' سرور '%2' سے %1 ہٹا دیا گیا ہے - + Api config removed - + %1 cached profile cleared %1 کیش کردہ پروفائل ختم کر دی گئی - + Please login as the user براہ کرم صارف کے طور پر لاگ ان کریں - + Server added successfully سرور کامیابی سے شامل کیا گیا - + %1 installed successfully. - + API config reloaded - + Successfully changed the country of connection to %1 @@ -368,12 +373,12 @@ Already installed containers were found on the server. All installed containers ایپلیکیشن کو منتخب کریں - + application name ایپلیکیشن کا نام - + Add selected ایپلیکیشن کا نام @@ -443,12 +448,12 @@ Already installed containers were found on the server. All installed containers PageDevMenu - + Gateway endpoint - + Dev gateway environment @@ -456,85 +461,84 @@ Already installed containers were found on the server. All installed containers PageHome - + Logging enabled لاگنگ فعال ہے - + Split tunneling enabled سپلٹ ٹنلنگ فعال ہے - + Split tunneling disabled سپلٹ ٹنلنگ غیر فعال ہے - + VPN protocol وی پی این پروٹوکول - + Servers سرور - Unable change server while there is an active connection - فعال کنکشن موجود ہونے کی وجہ سے سرور تبدیل کرنے میں ناکام ہیں + فعال کنکشن موجود ہونے کی وجہ سے سرور تبدیل کرنے میں ناکام ہیں PageProtocolAwgClientSettings - + AmneziaWG settings امنیزیا وی جی کی ترتیبات - + MTU ام ٹی یو - + Server settings - + Port پورٹ - + Save - + Save settings? ترتیبات محفوظ کریں? - + Only the settings for this device will be changed - + Continue - + Cancel - + Unable change settings while there is an active connection جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا @@ -542,12 +546,12 @@ Already installed containers were found on the server. All installed containers PageProtocolAwgSettings - + AmneziaWG settings امنیزیا وی جی کی ترتیبات - + Port پورٹ @@ -556,87 +560,92 @@ Already installed containers were found on the server. All installed containers ام ٹی یو - + All users with whom you shared a connection with will no longer be able to connect to it. آپ جن لوگوں کے ساتھ آپ نے اس کنکشن کا اشتراک کیا تھا، وہ اس سے مزید جڑ نہیں سکیں گے۔ - + Save محفوظ کریں - + + VPN address subnet + وی پی این ایڈریس سب نیٹ + + + Jc - Junk packet count - + Jmin - Junk packet minimum size - + Jmax - Junk packet maximum size - + S1 - Init packet junk size - + S2 - Response packet junk size - + H1 - Init packet magic header - + H2 - Response packet magic header - + H4 - Transport packet magic header - + H3 - Underload packet magic header - + The values of the H1-H4 fields must be unique H1 تا H4 فیلڈز کی قیمتیں مخصوص ہونی چاہیے - + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) S1 + پیغام شروع کار (148) کے فیلڈ کی قیمت S2 + پیغام جواب (92) کے سائز کے برابر نہیں ہونی چاہئے - + Save settings? ترتیبات محفوظ کریں? - + Continue جاری رکھیں - + Cancel منسوخ کریں - + Unable change settings while there is an active connection جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا @@ -644,33 +653,33 @@ Already installed containers were found on the server. All installed containers PageProtocolCloakSettings - + Cloak settings پوشیدہ ترتیب - + Disguised as traffic from کے طور پر ٹریفک کی طرح - + Port پورٹ - - + + Cipher رمز - + Save محفوظ کریں - + Unable change settings while there is an active connection جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا @@ -678,175 +687,175 @@ Already installed containers were found on the server. All installed containers PageProtocolOpenVpnSettings - + OpenVPN settings اوپن وی پی این ترتیبات - + VPN address subnet وی پی این ایڈریس سب نیٹ - + Network protocol نیٹ ورک پروٹوکول - + Port پورٹ - + Auto-negotiate encryption خود کار مذاکرتی رمزنگاری - - + + Hash ہیش - + SHA512 - + SHA384 - + SHA256 - + SHA3-512 - + SHA3-384 - + SHA3-256 - + whirlpool بھنور - + BLAKE2b512 - + BLAKE2s256 - + SHA1 - - + + Cipher رمز - + AES-256-GCM - + AES-192-GCM - + AES-128-GCM - + AES-256-CBC - + AES-192-CBC - + AES-128-CBC - + ChaCha20-Poly1305 - + ARIA-256-CBC - + CAMELLIA-256-CBC - + none کوئی نہیں - + TLS auth TLS توثیق - + Block DNS requests outside of VPN وی پی این کے باہر DNS درخواستوں کو بلاک کریں - + Additional client configuration commands مزید کلائنٹ ترتیباتی احکامات - - + + Commands: احکامات: - + Additional server configuration commands اضافی سرور کنفیگریشن احکامات - + Unable change settings while there is an active connection جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا - + Save احفظ @@ -854,42 +863,42 @@ Already installed containers were found on the server. All installed containers PageProtocolRaw - + settings ترتیبات - + Show connection options کنکشن کے اختیارات دکھائیں - + Connection options %1 کنکشن کے اختیارات %1 - + Remove ہٹائیں - + Remove %1 from server? سرور سے %1 کو ہٹائیں؟ - + All users with whom you shared a connection with will no longer be able to connect to it. آپ جن لوگوں کے ساتھ آپ نے ایک کنکشن شیئر کیا تھا، وہ اب اس سے مزید جڑ نہیں سکیں گے. - + Continue جاری رکھیں - + Cancel منسوخ کریں @@ -897,28 +906,28 @@ Already installed containers were found on the server. All installed containers PageProtocolShadowSocksSettings - + Shadowsocks settings شیڈو ساکس ترتیبات - + Port پورٹ - - + + Cipher رمز - + Save محفوظ کریں - + Unable change settings while there is an active connection جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا @@ -926,52 +935,52 @@ Already installed containers were found on the server. All installed containers PageProtocolWireGuardClientSettings - + WG settings وائر گارڈ ترتیبات - + MTU ام ٹی یو - + Server settings - + Port پورٹ - + Save - + Save settings? ترتیبات محفوظ کریں? - + Only the settings for this device will be changed - + Continue - + Cancel - + Unable change settings while there is an active connection جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا @@ -979,32 +988,37 @@ Already installed containers were found on the server. All installed containers PageProtocolWireGuardSettings - + WG settings وائر گارڈ ترتیبات - + + VPN address subnet + وی پی این ایڈریس سب نیٹ + + + Port پورٹ - + Save settings? ترتیبات محفوظ کریں? - + All users with whom you shared a connection with will no longer be able to connect to it. - + Continue - + Cancel @@ -1013,12 +1027,12 @@ Already installed containers were found on the server. All installed containers ام ٹی یو - + Unable change settings while there is an active connection جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا - + Save محفوظ کریں @@ -1026,22 +1040,22 @@ Already installed containers were found on the server. All installed containers PageProtocolXraySettings - + XRay settings XRay کی ترتیبات - + Disguised as traffic from کے طور پر ٹریفک کی طرح - + Save محفوظ - + Unable change settings while there is an active connection جب ایک فعال کنکشن موجود ہو تو ترتیبات کو تبدیل نہیں کیا جا سکتا @@ -1049,39 +1063,39 @@ Already installed containers were found on the server. All installed containers PageServiceDnsSettings - + A DNS service is installed on your server, and it is only accessible via VPN. ایک DNS سروس آپ کے سرور پر انسٹال کی گئی ہے، اور صرف VPN کے ذریعے یہ قابل رسائی ہے. - + The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. ایڈریس وہی ہے جو آپ کے سرور کا پتہ ہے۔ آپ کنکشنز ٹیب کے نیچے سیٹنگز میں DNS کنفیگر کر سکتے ہیں. - + Remove ہٹائیں - + Remove %1 from server? سرور سے %1 کو ہٹا دیں? - + Cannot remove AmneziaDNS from running server آمنیزیا ڈی این ایس کو چل رہے سرور سے ہٹا نہیں سکتے - + Continue جاری رہے - + Cancel منسوخ کریں @@ -1089,69 +1103,69 @@ Already installed containers were found on the server. All installed containers PageServiceSftpSettings - + Settings updated successfully ترتیبات کامیابی سے اپ ڈیٹ ہوگئیں - + SFTP settings ایس ایف ٹی پی ترتیبات - + Host The term "Host" in the context of SFTP (Secure File Transfer Protocol) can be translated into Urdu as: میزبان - - - - + + + + Copied نقل کر دیا گیا - + Port پورٹ - + User name صارف کا نام - + Password پاس ورڈ - + Mount folder on device آلے پر فولڈر ماؤنٹ کریں - + In order to mount remote SFTP folder as local drive, perform following steps: <br> ریموٹ SFTP فولڈر کو لوکل ڈرائیو کے طور پر ماؤنٹ کرنے کے لیے، درج ذیل اقدامات کریں - - + + <br>1. Install the latest version of <br>1کا تازہ ترین ورژن انسٹال کریں - - + + <br>2. Install the latest version of .<br>2کا تازہ ترین ورژن انسٹال کریں ۔ - + Detailed instructions تفصیلی ہدایات @@ -1175,71 +1189,71 @@ Already installed containers were found on the server. All installed containers PageServiceSocksProxySettings - + Settings updated successfully - - + + SOCKS5 settings - + Host The term "Host" in the context of SFTP (Secure File Transfer Protocol) can be translated into Urdu as: میزبان - - - - + + + + Copied - - + + Port پورٹ - + User name صارف کا نام - - + + Password پاس ورڈ - + Username - - + + Change connection settings - + The port must be in the range of 1 to 65535 - + Password cannot be empty - + Username cannot be empty @@ -1247,37 +1261,37 @@ Already installed containers were found on the server. All installed containers PageServiceTorWebsiteSettings - + Settings updated successfully ترتیبات کامیابی سے اپ ڈیٹ ہوگئی ہیں - + Tor website settings ویب سائٹ کی ترتیبات - + Website address ویب سائٹ کا پتہ - + Copied نقل کر لیا گیا - + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. - + After creating your onion site, it takes a few minutes for the Tor network to make it available for use. اپنی اونین ویب سائٹ بنانے کے بعد، ٹور نیٹ ورک کو اسے دستیاب کرنے میں کچھ منٹ لگ سکتے ہیں۔ - + When configuring WordPress set the this onion address as domain. وورڈپریس کو ترتیب دیتے وقت، اس انیون ایڈریس کو ڈومین کے طور پر مقرر کریں. @@ -1301,42 +1315,42 @@ Already installed containers were found on the server. All installed containers PageSettings - + Settings ترتیبات - + Servers سرور - + Connection کنکشن - + Application ایپلیکیشن - + Backup بیک اپ - + About AmneziaVPN AmneziaVPN کے بارے میں - + Dev console - + Close application براہ کرم ایپلیکیشن بند کریں @@ -1344,32 +1358,32 @@ Already installed containers were found on the server. All installed containers PageSettingsAbout - + Support Amnezia Amnezia کی حمایت کریں - + Amnezia is a free and open-source application. You can support the developers if you like it. ایمنیزیا ایک مفت اور آزاد سورس ایپلیکیشن ہے۔ آپ اگر اسے پسند کریں تو ڈویلپرز کی حمایت کرسکتے ہیں. - + Contacts رابطے - + Telegram group ٹیلیگرام گروپ - + To discuss features "فیچرز" پر گفتگو کرنے کے لئے - + https://t.me/amnezia_vpn_en @@ -1378,122 +1392,145 @@ Already installed containers were found on the server. All installed containers میل - + support@amnezia.org - + For reviews and bug reports جائزہ اور بگ رپورٹس کے لئے - + Copied - + GitHub گِٹ ہَب - + + Discover the source code + + + + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website ویب سائٹ - + + Visit official website + + + + Software version: %1 سافٹ ویئر ورژن: %1 - + Check for updates اپ ڈیٹس چیک کریں - + Privacy Policy رازداری کی پالیسی + + PageSettingsApiLanguageList + + + Unable change server location while there is an active connection + + + PageSettingsApiServerInfo - + For the region - + Price - + Work period - + + Valid until + + + + Speed - + Support tag - + Copied - + Reload API config - + Reload API config? - - + + Continue - - + + Cancel - + Cannot reload API config during active connection - + Remove from application - + Remove from application? - + Cannot remove server during active connection چالو کنکشن کے دوران سرور کو ہٹایا نہیں جا سکتا @@ -1501,7 +1538,7 @@ Already installed containers were found on the server. All installed containers PageSettingsAppSplitTunneling - + Cannot change split tunneling settings during active connection فعال کنکشن کے دوران سپلٹ ٹنلنگ کی ترتیبات تبدیل نہیں کی جا سکتیں @@ -1514,7 +1551,7 @@ Already installed containers were found on the server. All installed containers فہرست میں شامل ایپ کو وی پی این کے ذریعے دسترس نہیں کیا جائے گا - + Only the apps from the list should have access via VPN @@ -1524,42 +1561,42 @@ Already installed containers were found on the server. All installed containers - + App split tunneling ایپ اسپلٹ ٹنلنگ - + Mode موڈ - + Remove ہٹائیں - + Continue جاری رکھیں - + Cancel منسوخ - + application name ایپ کا نام - + Open executable file قابل اجراء فائل کو کھولیں - + Executable files (*.*) قابل اجراء فائل (*.*) @@ -1567,102 +1604,102 @@ Already installed containers were found on the server. All installed containers PageSettingsApplication - + Application ایپلیکیشن - + Allow application screenshots ایپلیکیشن میں تصویریں لینے کی اجازت دیں - + Enable notifications - + Enable notifications to show the VPN state in the status bar - + Auto start خود کار شروع - + Launch the application every time the device is starts جب بھی آلہ چلائے، ایپلیکیشن کو لانچ کریں - + Auto connect خودکار منسلک - + Connect to VPN on app start ایپ شروع ہونے پر VPN سے جڑیں - + Start minimized کمینائز شروع کریں - + Launch application minimized ایپلیکیشن کو کمینائز کر کے لانچ کریں - + Language زبان - + Logging لاگنگ - + Enabled فعال - + Disabled غیر فعال - + Reset settings and remove all data from the application ترتیبات کو دوبارہ ترتیب کریں اور ایپلیکیشن سے تمام ڈیٹا کو ختم کریں - + Reset settings and remove all data from the application? ترتیبات کو دوبارہ ترتیب دیں اور ایپلیکیشن سے تمام ڈیٹا کو ہٹا دیں؟ - + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. تمام ترتیبات کو معمولی حالت پر لوٹایا جائے گا۔ سب انسٹال کیے گئے امنیزیا وی پی این سروسزسرورپرموجودرہیںگی. - + Continue جاری رکھیں - + Cancel منسوخ - + Cannot reset settings during active connection چالو کنکشن کے دوران ترتیبات کو دوبارہ ترتیب نہیں دی جا سکتی @@ -1670,78 +1707,78 @@ Already installed containers were found on the server. All installed containers PageSettingsBackup - + Settings restored from backup file ترتیبات بیک اپ فائل سے بحال کردی گئی ہیں - + Back up your configuration اپنی ترتیبات کا بیک اپ بنائیں - + You can save your settings to a backup file to restore them the next time you install the application. آپ اپنی ترتیبات کو بیک اپ فائل میں محفوظ کرکے اگلی دفعہ جب آپ ایپلیکیشن کو انسٹال کریں تو انہیں بحال کرنے کے لئے استعمال کرسکتے ہیں۔ - + The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. بیک اپ میں آپ کے تمام سرورز جنہیں AmneziaVPN میں شامل کیا گیا ہے، ان کے پاسورڈ اور نجی کلید شامل ہوں گے۔ اس معلومات کو ایک محفوظ جگہ میں محفوظ رکھیں. - + Make a backup ایک بیک اپ بنائیں - + Save backup file بیک اپ فائل کو محفوظ کریں - - + + Backup files (*.backup) بیک اپ فائلیں (*.backup) - + Backup file saved بیک اپ فائل محفوظ ہو گئی - + Restore from backup بیک اپ سے بحال کریں - + Open backup file بیک اپ فائل کو کھولیں - + Import settings from a backup file? بیک اپ فائل سے ترتیبات کو درآمد کریں؟ - + All current settings will be reset تمام موجودہ ترتیبات کو دوبارہ ترتیب دیں - + Continue جاری رکھیں - + Cancel منسوخ - + Cannot restore backup settings during active connection چالو کنکشن کے دوران بیک اپ ترتیبات کو دوبارہ قائم نہیں کیا جا سکتا @@ -1749,62 +1786,62 @@ Already installed containers were found on the server. All installed containers PageSettingsConnection - + Connection کنکشن - + When AmneziaDNS is not used or installed ایمنیزیا ڈی این ایس کو استعمال نہیں کیا گیا ہو یا اسے انسٹال نہیں کیا گیاہے - + Allows you to use the VPN only for certain Apps آپ کو صرف مخصوص ایپلیکیشنز کے لئے وی پی این استعمال کرنے کی اجازت دیتا ہے - + Use AmneziaDNS AmneziaDNS استعمال کریں - + If AmneziaDNS is installed on the server اگر سرور پر AmneziaDNS انسٹال کیا گیا ہو تو - + DNS servers ڈی این ایس سرور - + Site-based split tunneling سائٹ کے بنیادی سپلٹ ٹنلنگ - + Allows you to select which sites you want to access through the VPN آپ کو یہ امکان فراہم کرتا ہے کہ آپ وی پی این کے ذریعہ کس سائٹ کو دسترس دینا چاہتے ہیں وہ منتخب کریں - + App-based split tunneling ایپ کے بنیاد پر سپلٹ ٹنلنگ - + KillSwitch - + Disables your internet if your encrypted VPN connection drops out for any reason. - + Cannot change killSwitch settings during active connection @@ -1812,62 +1849,62 @@ Already installed containers were found on the server. All installed containers PageSettingsDns - + Default server does not support custom DNS افتراضی سرور کا مخصوص DNS کو سپورٹ نہیں کرتا ہے - + DNS servers ڈی این ایس سرور - + If AmneziaDNS is not used or installed اگر AmneziaDNS استعمال نہیں کیا گیا ہو یا انسٹال نہیں کیا گیا ہو تو - + Primary DNS اولین DNS - + Secondary DNS ثانوی DNS - + Restore default اصل حالت کو بحال کریں - + Restore default DNS settings? اصل DNS ترتیبات کو بحال کریں؟ - + Continue براہ کرم جاری رکھیں - + Cancel منسوخ - + Settings have been reset ترتیبات کو دوبارہ ترتیب دیا گیا ہے - + Save محفوظ - + Settings saved ترتیبات محفوظ ہوگئیں @@ -1879,12 +1916,12 @@ Already installed containers were found on the server. All installed containers لاگنگ فعال ہے۔ یاد رہے کہ لاگوں کو 14 دنوں کے بعد خود بخود غیر فعال کر دیا جائے گا، اور تمام لاگ فائلیں حذف کردی جائیں گی. - + Logging لاگنگ - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. اس فعل کو فعال کرنے سے، ایپلیکیشن کے لاگ خود بخود محفوظ ہوجائیں گے۔ پہلے سے، لاگنگ کی فعالیت غیر فعال ہوتی ہے۔ اگر ایپلیکیشن میں کوئی خرابی ہو، تو لاگ کو بچانا فعال کریں. @@ -1897,20 +1934,20 @@ Already installed containers were found on the server. All installed containers فائلوں کے فولڈر کو کھولیں - - + + Save محفوظ - - + + Logs files (*.log) لاگ فائلیں (*.log) - - + + Logs file saved لاگ فائل محفوظ ہوگئی @@ -1919,64 +1956,62 @@ Already installed containers were found on the server. All installed containers لاگوں کو فائل میں محفوظ کریں - + Enable logs - + Clear logs? کیا آپ لاگوں کو صاف کرنا چاہتے ہیں؟ - + Continue براہ کرم جاری رکھیں - + Cancel منسوخ - + Logs have been cleaned up تم مسح السجلاتلاگوں کو صاف کر دیا گیا ہے - + Client logs - + AmneziaVPN logs - - + Open logs folder - - + Export logs - + Service logs - + AmneziaVPN-service logs - + Clear logs لاگوں کو صاف کریں @@ -1994,12 +2029,12 @@ Already installed containers were found on the server. All installed containers کوئی نئے انسٹال شدہ کنٹینرز نہیں ملے - + Do you want to reboot the server? کیا آپ سرور کو دوبارہ چالو کرنا چاہتے ہیں؟ - + Do you want to clear server from Amnezia software? هل تريد حذف الخادم من Amnezia?کیا آپ سرور کو Amnezia سافٹ ویئر سے صاف کرنا چاہتے ہیں؟ @@ -2009,18 +2044,18 @@ Already installed containers were found on the server. All installed containers - - - - + + + + Continue براہ کرم جاری رکھیں - - - - + + + + Cancel منسوخ @@ -2035,67 +2070,67 @@ Already installed containers were found on the server. All installed containers اگر وہ دکھایا نہیں گیا تو انہیں ایپلیکیشن میں شامل کریں - + Reboot server سرور کو دوبارہ چالو کریں - + The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? ریبوٹ کا عمل تقریباً 30 سیکنڈ لے سکتا ہے۔ کیا آپ براہ کرم جاری رکھنا چاہتے ہیں؟ - + Cannot reboot server during active connection چالو کنکشن کے دوران سرور کو دوبارہ چالو نہیں کیا جا سکتا - + Remove server from application ایپلیکیشن سے سرور کو ہٹا دیں - + Do you want to remove the server from application? کیا آپ ایپلیکیشن سے سرور کو ہٹانا چاہتے ہیں؟ - + Cannot remove server during active connection چالو کنکشن کے دوران سرور کو ہٹایا نہیں جا سکتا - + All users whom you shared a connection with will no longer be able to connect to it. آپ نے اپنی کنکشن کا اشتراک دینے والے تمام صارفین کو اس سے جڑنے کی اجازت نہیں ہوگی. - + Cannot clear server from Amnezia software during active connection چالو کنکشن کے دوران سرور کو ایمنیزیا سافٹ ویئر سے صاف کرنا ممکن نہیں - + Reset API config API کونفیگ کو دوبارہ ترتیب دیں - + Do you want to reset API config? کیا آپ API کونفیگ کو دوبارہ ترتیب دینا چاہتے ہیں؟ - + Cannot reset API config during active connection چالو کنکشن کے دوران API ترتیبات کو دوبارہ ترتیب نہیں دی جا سکتی - + All installed AmneziaVPN services will still remain on the server. سرور پر تمام انسٹال شدہ AmneziaVPN سروسز محفوظ رہیں گے. - + Clear server from Amnezia software Amnezia سافٹ ویئر کو سرور سے صاف کریں @@ -2103,27 +2138,32 @@ Already installed containers were found on the server. All installed containers PageSettingsServerInfo - + + Subscription is valid until + + + + Server name سرور کا نام - + Save محفوظ - + Protocols پروٹوکولات - + Services خدمات - + Management مینجمنٹ @@ -2131,7 +2171,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServerProtocol - + settings ترتیبات @@ -2140,17 +2180,17 @@ Already installed containers were found on the server. All installed containers %1 پروفائل کو صاف کریں - + Clear %1 profile? کیا آپ واقعی %1 پروفائل کو صاف کرنا چاہتے ہیں؟ - + Unable to clear %1 profile while there is an active connection فعال کنکشن کے دوران %1 پروفائل کو صاف نہیں کیا جا سکتا - + Cannot remove active container فعال کنٹینر کو ہٹانا ممکن نہیں @@ -2160,54 +2200,54 @@ Already installed containers were found on the server. All installed containers - + Remove ہٹائیں - + All users with whom you shared a connection will no longer be able to connect to it. آپ نے جن کے ساتھ کنکشن شئیر کیا تھا، ان تمام صارفین کو اس سے جڑنے کی اجازت نہیں ہوگی. - + Remove %1 from server? کیا آپ سرور سے %1 کو ہٹانا چاہتے ہیں؟ - + connection settings - + Click the "connect" button to create a connection configuration - + server settings - + Clear profile - + The connection configuration will be deleted for this device only - - + + Continue براہ کرم جاری رکھیں - - + + Cancel منسوخ @@ -2215,7 +2255,7 @@ Already installed containers were found on the server. All installed containers PageSettingsServersList - + Servers سرور @@ -2223,100 +2263,100 @@ Already installed containers were found on the server. All installed containers PageSettingsSplitTunneling - + Default server does not support split tunneling function افتراضی سرور سپلٹ ٹنلنگ فعال نہیں کرتا - + Addresses from the list should not be accessed via VPN اس فہرست سے پتوں کا وی پی این کے ذریعے دسترس حاصل نہیں کیا جانا چاہئے - + Split tunneling اسپلٹ ٹنلنگ - + Mode موڈ - + Remove ہٹائیں - + Continue براہ کرم جاری رکھیں - + Cancel منسوخ - + Only the sites listed here will be accessed through the VPN صرف یہاں درج کردہ سائٹس وی پی این کے ذریعے دسترس حاصل کریں گی - + Cannot change split tunneling settings during active connection فعال کنکشن کے دوران سپلٹ ٹنلنگ کی ترتیبات تبدیل نہیں کی جا سکتیں - + website or IP ویب سائٹ یا آئی پی - + Import / Export Sites سائٹس درآمد / برآمد - + Import درآمد - + Save site list سائٹ کی فہرست کو محفوظ کریں - + Save sites سائٹس کو محفوظ کریں - - - + + + Sites files (*.json) سائٹس فائلیں (*.json) - + Import a list of sites ایک فہرست کو درآمد کریں - + Replace site list سائٹ کی فہرست کو بدلیں - - + + Open sites file سائٹس فائل کو کھولیں - + Add imported sites to existing ones آمدہ سائٹس کو موجودہ میں شامل کریں @@ -2324,32 +2364,32 @@ Already installed containers were found on the server. All installed containers PageSetupWizardApiServiceInfo - + For the region - + Price - + Work period - + Speed - + Features - + Connect @@ -2357,12 +2397,12 @@ Already installed containers were found on the server. All installed containers PageSetupWizardApiServicesList - + VPN by Amnezia - + Choose a VPN service that suits your needs. @@ -2386,7 +2426,7 @@ Already installed containers were found on the server. All installed containers کنکشن کی ترتیبات یا بیک اپ والی فائل - + Connection کنکشن @@ -2396,90 +2436,125 @@ Already installed containers were found on the server. All installed containers ترتیبات - + Enable logs - + + Support tag + + + + + Copied + + + + Insert the key, add a configuration file or scan the QR-code - + Insert key - + Insert داخل کریں - + Continue - + Other connection options - + + Site Amnezia + + + + VPN by Amnezia - + Connect to classic paid and free VPN services from Amnezia - + Self-hosted VPN - + Configure Amnezia VPN on your own server - + Restore from backup بیک اپ سے بحال کریں - + + + + + + Open backup file بیک اپ فائل کو کھولیں - + Backup files (*.backup) بیک اپ فائلیں (*.backup) - + File with connection settings کنکشن کی ترتیبات والی فائل - + + + + + + Open config file کنفیگ فائل کو کھولیں - + QR code QR کوڈ - + + + + + + I have nothing میرے پاس کچھ نہیں ہے + + + + + Key as text متن کے طور پر کلید @@ -2488,67 +2563,67 @@ Already installed containers were found on the server. All installed containers PageSetupWizardCredentials - + Configure your server اپنے سرور کو ترتیب دیں - + Server IP address [:port] سرور آئی پی پتہ [:پورٹ] - + Continue براہ کرم جاری رکھیں - + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties آپ جو ڈیٹا داخل کریں گے وہ بالکل خفیہ رہے گا اور نہ تو امنیزیا یا کسی تیسری شخصیت کے ساتھ اشتراک کیا جائے گا - + 255.255.255.255:22 - + SSH Username ایس ایس ایچ صارف نام - + Password or SSH private key پاس ورڈ یا SSH نجی کلید - + How to run your VPN server - + Where to get connection data, step-by-step instructions for buying a VPS - + Ip address cannot be empty آئی پی پتہ خالی نہیں ہو سکتا - + Enter the address in the format 255.255.255.255:88 ایڈریس درج کریں فارمیٹ 255.255.255.255:88 - + Login cannot be empty لاگ ان نام خالی نہیں ہو سکتا - + Password/private key cannot be empty پاس ورڈ یا نجی کلید خالی نہیں ہو سکتی @@ -2556,22 +2631,22 @@ Already installed containers were found on the server. All installed containers PageSetupWizardEasy - + What is the level of internet control in your region? آپ کے علاقے میں انٹرنیٹ کنٹرول کا سطح کیا ہے؟ - + Choose a VPN protocol ایک VPN پروٹوکول کا انتخاب کریں - + Skip setup ترتیب چھوڑیں - + Continue جاری رکھیں @@ -2618,37 +2693,37 @@ Already installed containers were found on the server. All installed containers PageSetupWizardProtocolSettings - + Installing %1 %1 کی انسٹالیشن - + More detailed مزید تفصیلات والا - + Close بند - + Network protocol نیٹ ورک پروٹوکول - + Port پورٹ - + Install انسٹال - + The port must be in the range of 1 to 65535 @@ -2656,12 +2731,12 @@ Already installed containers were found on the server. All installed containers PageSetupWizardProtocols - + VPN protocol وی پی این پروٹوکول - + Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP. آپ کے لئے سب سے زیادہ اہم پروٹوکول کا انتخاب کریں۔ بعد میں، آپ دوسرے پروٹوکول اور اضافی خدمات، جیسے DNS پراکسی اور SFTP، انسٹال کر سکتے ہیں۔ @@ -2697,7 +2772,7 @@ Already installed containers were found on the server. All installed containers میرے پاس کچھ نہیں ہے - + Let's get started @@ -2705,27 +2780,27 @@ Already installed containers were found on the server. All installed containers PageSetupWizardTextKey - + Connection key کنکشن کی کلید - + A line that starts with vpn://... ایک لائن جو vpn:// سے شروع ہوتی ہے... - + Key کلید - + Insert داخل کریں - + Continue جاری رہنے دیں @@ -2733,32 +2808,32 @@ Already installed containers were found on the server. All installed containers PageSetupWizardViewConfig - + New connection نیا کنکشن - + Collapse content مواد کو غیر فعال کریں - + Show content مواد دکھائیں - + Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. وائر گارڈ کی غلط شناخت کو بروئے کار لانے کے لئے وائر گارڈ غلط شناخت کو فعال کریں۔ آپ کے پرووائیڈر پر وائر گارڈ بند ہونے کی صورت میں یہ کار آمد ہو سکتی ہے۔ - + Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. صرف ان ماخذ سے کنکشن کوڈ استعمال کریں جن پر آپ کو اعتماد ہو۔ عوامی ماخذوں سے کوڈز آپ کے ڈیٹا کو منسلک کرنے کے لیے بنائے گئے ہو سکتے ہیں. - + Connect کنکٹ @@ -2766,37 +2841,37 @@ Already installed containers were found on the server. All installed containers PageShare - + Save OpenVPN config اوپن وی پی این کی ترتیبات کو محفوظ کریں - + Save WireGuard config وائر گارڈ کی ترتیبات کو محفوظ کریں - + Save AmneziaWG config ایمنیزیا ڈبلیو جی کی ترتیبات کو محفوظ کریں - + Save Shadowsocks config شیڈو ساکس کی ترتیبات کو محفوظ کریں - + Save Cloak config چادر کی ترتیبات کو محفوظ کریں - + Save XRay config "XRay کنفیگ کو محفوظ کریں - + For the AmneziaVPN app AmneziaVPN ایپ کے لئے @@ -2805,176 +2880,181 @@ Already installed containers were found on the server. All installed containers OpenVPN کا اصل فارمیٹ - + WireGuard native format وائر گارڈ کا اصل فارمیٹ - + AmneziaWG native format ایمنیزیا ڈبلیو جی کا اصل فارمیٹ - + Shadowsocks native format شیڈو ساکس کا اصل فارمیٹ - + Cloak native format Cloak کا اصل فارمیٹ - + XRay native format ایکس رے کا نیٹویٹ فارمیٹ - + Share VPN Access VPN دسترسی شیئر - + Share full access to the server and VPN سرور اور وی پی این کے لئے مکمل دسترسی کو شیئر کریں - + Use for your own devices, or share with those you trust to manage the server. اپنی خود کی ڈیوائسز کے لئے استعمال کریں، یا ان لوگوں کے ساتھ شیئر کریں جن پر آپ کا بھروسہ ہو کہ وہ سرور کو منظم کر سکیں. - - + + Users صارفین - + Share VPN access without the ability to manage the server سرور کو منظم کرنے کی صلاحیت کے بغیر وی پی این کی دسترسی شیئر - + Search تلاش - + Creation date: %1 - + Latest handshake: %1 - + Data received: %1 - + Data sent: %1 + + + Allowed IPs: %1 + + Creation date: تخلیق کی تاریخ: - + Rename نام تبدیل - + Client name کلائنٹ کا نام - + Save محفوظ - + Revoke واپس لین - + Revoke the config for a user - %1? کیا آپ مستعمل کے لئے کنفیگ کو واپس لینا چاہتے ہیں - %1؟ - + The user will no longer be able to connect to your server. صارف آپ کے سرور سے متصل ہونے کا اختیار نہیں رہے گا. - + Continue جاری رکھیں - + Cancel منسوخ - + Connection کنکشن - - + + Server سرور - + File with connection settings to کنکشن کی ترتیبات کی فائل - - + + Protocol پروٹوکول - + Connection to کنکشن کو - + Config revoked کنفیگ منسوخ - + OpenVPN native format - + User name صارف کا نام - - + + Connection format کنکشن فارمیٹ - - + + Share شیئر @@ -2982,55 +3062,55 @@ Already installed containers were found on the server. All installed containers PageShareFullAccess - + Full access to the server and VPN سرور اور وی پی این کی مکمل رسائی - + We recommend that you use full access to the server only for your own additional devices. ہم آپ کو سرور کا مکمل رسائی صرف اپنی اضافی ڈیوائسز کے لئے استعمال کرنے کی تجویز دیتے ہیں. - + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. "اگر آپ دوسروں کے ساتھ مکمل رسائی شیئر کریں تو وہ سرور پروٹوکول اور خدمات کو ہٹا سکتے ہیں اور شامل کر سکتے ہیں، جس سے وی پی این تمام صارفین کے لئے غلط کام کرے گا. - - + + Server سرور - + Accessing رسائی - + File with accessing settings to ترتیبات کے ساتھ دسترسی کی فائل - + Share شیئر - + Access error! رساءی ناممکن! - + Connection to کنکشن کو - + File with connection settings to کنکشن کی ترتیبات کی فائل @@ -3038,17 +3118,17 @@ Already installed containers were found on the server. All installed containers PageStart - + Logging was disabled after 14 days, log files were deleted لاگنگ کو 14 دنوں کے بعد غیر فعال کر دیا گیا، لاگ فائلوں کو حذف کر دیا گیا - + Settings restored from backup file ترتیبات بیک اپ فائل سے بحال کردی گئی ہیں - + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. @@ -3056,7 +3136,7 @@ Already installed containers were found on the server. All installed containers PopupType - + Close بند @@ -3387,22 +3467,22 @@ Already installed containers were found on the server. All installed containers سرور سے منسلک ہونے کا ٹائم آؤٹ - + VPN connection error VPN کنکشن کی خرابی - + Error when retrieving configuration from API آپی سے کنفیگریشن بازیافت کرتے وقت خرابی - + This config has already been added to the application یہ تشکیل پہلے ہی ایپلی کیشن میں شامل کی جا چکی ہے - + ErrorCode: %1. ایرر کوڈ: %1. @@ -3477,57 +3557,72 @@ Already installed containers were found on the server. All installed containers ترتیب میں سرور سے منسلک ہونے کے لیے کوئی کنٹینرز اور اسناد نہیں ہیں - - In the response from the server, an empty config was received + + Unable to open config file - SSL error occurred + In the response from the server, an empty config was received - Server response timeout on api request + SSL error occurred + Server response timeout on api request + + + + Missing AGW public key + + + Failed to decrypt response payload + + + Missing list of available services + + + + QFile error: The file could not be opened QFile کی خرابی: فائل کو نہیں کھولا جا سکا - + QFile error: An error occurred when reading from the file کیو فائل کی خرابی: فائل سے پڑھتے وقت ایک خرابی پیش آگئی - + QFile error: The file could not be accessed QFile کی خرابی: فائل تک رسائی نہیں ہو سکی - + QFile error: An unspecified error occurred کیو فائل میں خرابی: ایک غیر متعینہ خرابی پیش آگئی - + QFile error: A fatal error occurred کیو فائل میں خرابی: ایک مہلک خرابی پیش آگئی - + QFile error: The operation was aborted کیو فائل کی خرابی: آپریشن روک دیا گیا تھا - + Internal error داخلی خامی @@ -3909,11 +4004,19 @@ While it offers a blend of security, stability, and speed, it's essential t SelectLanguageDrawer - + Choose language زبان کا انتخاب کریں + + ServersListView + + + Unable change server while there is an active connection + فعال کنکشن موجود ہونے کی وجہ سے سرور تبدیل کرنے میں ناکام ہیں + + Settings @@ -3931,12 +4034,12 @@ While it offers a blend of security, stability, and speed, it's essential t SettingsController - + Backup file is corrupted بیک اپ فائل خراب ہو گئی ہے - + All settings have been reset to default values تمام ترتیبات کو ڈیفالٹ اقدار پر دوبارہ ترتیب دیا گیا ہے @@ -3950,33 +4053,33 @@ While it offers a blend of security, stability, and speed, it's essential t AmneziaVPN ترتیب کو محفوظ کریں - + Share بانٹیں - + Copy کاپی - - + + Copied کاپی - + Copy config string تشکیل سٹرنگ کو کاپی کریں - + Show connection settings کنکشن کی ترتیبات دکھائیں - + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" ایمنیزیا ایپ میں QR کوڈ پڑھنے کے لیے، "سرور شامل کریں" → "میرے پاس جوڑنے کے لیے ڈیٹا ہے" → "QR کوڈ، کلید یا سیٹنگ فائل" کو منتخب کریں @@ -3999,27 +4102,27 @@ While it offers a blend of security, stability, and speed, it's essential t سائٹ ہٹا دی گئی ہے: %1 - + Can't open file: %1 فائل نہیں کھول سکتا: %1 - + Failed to parse JSON data from file: %1 فائل سے JSON ڈیٹا پارس کرنے میں ناکامی: %1 - + The JSON data is not an array in file: %1 فائل میں JSON ڈیٹا ایک ایرے نہیں ہے: %1 - + Import completed واردات مکمل ہوگئی ہے - + Export completed ایکسپورٹ مکمل ہوگیا @@ -4060,7 +4163,7 @@ While it offers a blend of security, stability, and speed, it's essential t TextFieldWithHeaderType - + The field can't be empty یہ فیلڈ خالی نہیں ہو سکتا @@ -4068,7 +4171,7 @@ While it offers a blend of security, stability, and speed, it's essential t VpnConnection - + Mbps ایم بی پی ایس @@ -4154,12 +4257,12 @@ While it offers a blend of security, stability, and speed, it's essential t main2 - + Private key passphrase نجی کلید پاس فریز - + Save محفوظ کریں diff --git a/client/translations/amneziavpn_zh_CN.ts b/client/translations/amneziavpn_zh_CN.ts index 39b6bee0..cd39c2a6 100644 --- a/client/translations/amneziavpn_zh_CN.ts +++ b/client/translations/amneziavpn_zh_CN.ts @@ -1,55 +1,63 @@ + + AdLabel + + + Amnezia Premium - for access to any website + + + ApiServicesModel - + Classic VPN for comfortable work, downloading large files and watching videos. Works for any sites. Speed up to %1 MBit/s - + VPN to access blocked sites in regions with high levels of Internet censorship. - + <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> - + Amnezia Premium - A classic VPN for comfortable work, downloading large files, and watching videos in high resolution. It works for all websites, even in countries with the highest level of internet censorship. - + Amnezia Free is a free VPN to bypass blocking in countries with high levels of internet censorship - + %1 MBit/s - + %1 days - + VPN will open only popular sites blocked in your region, such as Instagram, Facebook, Twitter and others. Other sites will be opened from your real IP address, <a href="%1/free" style="color: #FBB26A;">more details on the website.</a> - + Free - + %1 $/month @@ -80,7 +88,7 @@ ConnectButton - + Unable to disconnect during configuration preparation @@ -89,60 +97,60 @@ ConnectionController - - - + + + Connect 连接 - + VPN Protocols is not installed. Please install VPN container at first 请先安装VPN协议 - + Connecting... 连接中 - + Connected 已连接 - + Reconnecting... 重连中 - + Disconnecting... 断开中 - + Preparing... - + Settings updated successfully, reconnnection... 配置已更新, 重连中... - + Settings updated successfully 配置更新成功 - + The selected protocol is not supported on the current platform 当前平台不支持所选协议 - + unable to create configuration @@ -150,17 +158,17 @@ ConnectionTypeSelectionDrawer - + Add new connection 添加新连接 - + Configure your server 配置您的服务器 - + Open config file, key or QR code 配置文件,授权码或二维码 @@ -198,7 +206,7 @@ HomeContainersListView - + Unable change protocol while there is an active connection 已建立连接时无法更改服务器配置 @@ -210,46 +218,46 @@ HomeSplitTunnelingDrawer - + Split tunneling 隧道分离 - + Allows you to connect to some sites or applications through a VPN connection and bypass others 允许您通过 VPN 连接连接到某些站点或应用程序,并绕过其他站点或应用程序 - + Split tunneling on the server 服务器上的分割隧道 - + Enabled Can't be disabled for current server 已启用 无法禁用当前服务器 - + Site-based split tunneling 基于网站的隧道分离 - - + + Enabled 开启 - - + + Disabled 禁用 - + App-based split tunneling 基于应用的隧道分离 @@ -257,23 +265,12 @@ Can't be disabled for current server ImportController - - Unable to open file - - - - - - Invalid configuration file - - - - + Scanned %1 of %2. 扫描 %1 of %2. - + In the imported configuration, potentially dangerous lines were found: @@ -289,75 +286,75 @@ Can't be disabled for current server 已安装在服务器上 - + %1 installed successfully. %1 安装成功。 - + %1 is already installed on the server. 服务器上已经安装 %1。 - + Added containers that were already installed on the server 添加已安装在服务器上的容器 - + Already installed containers were found on the server. All installed containers have been added to the application 在服务上发现已经安装协议并添加至应用 - + Settings updated successfully 配置更新成功 - + Server '%1' was rebooted 服务器 '%1' 已重新启动 - + Server '%1' was removed 已移除服务器 '%1' - + All containers from server '%1' have been removed 服务器 '%1' 的所有容器已移除 - + %1 has been removed from the server '%2' %1 已从服务器 '%2' 上移除 - + Api config removed - + %1 cached profile cleared - + %1 installed successfully. - + API config reloaded - + Successfully changed the country of connection to %1 @@ -378,12 +375,12 @@ Already installed containers were found on the server. All installed containers 协议已从 - + Please login as the user 请以用户身份登录 - + Server added successfully 增加服务器成功 @@ -396,12 +393,12 @@ Already installed containers were found on the server. All installed containers - + application name - + Add selected @@ -469,12 +466,12 @@ Already installed containers were found on the server. All installed containers PageDevMenu - + Gateway endpoint - + Dev gateway environment @@ -482,85 +479,84 @@ Already installed containers were found on the server. All installed containers PageHome - + Logging enabled - + Split tunneling enabled 用户分隔隧道已启用 - + Split tunneling disabled 分隔隧道已禁用 - + VPN protocol VPN协议 - + Servers 服务器 - Unable change server while there is an active connection - 已建立连接时无法更改服务器配置 + 已建立连接时无法更改服务器配置 PageProtocolAwgClientSettings - + AmneziaWG settings AmneziaWG 配置 - + MTU - + Server settings - + Port 端口 - + Save 保存 - + Save settings? 保存设置? - + Only the settings for this device will be changed - + Continue 继续 - + Cancel 取消 - + Unable change settings while there is an active connection @@ -568,12 +564,12 @@ Already installed containers were found on the server. All installed containers PageProtocolAwgSettings - + AmneziaWG settings AmneziaWG 配置 - + Port 端口 @@ -586,87 +582,92 @@ Already installed containers were found on the server. All installed containers 从服务上移除AmneziaWG? - + All users with whom you shared a connection with will no longer be able to connect to it. 与您共享连接的所有用户将无法再连接到该连接。 - + Save 保存 - + + VPN address subnet + VPN 地址子网 + + + Jc - Junk packet count - + Jmin - Junk packet minimum size - + Jmax - Junk packet maximum size - + S1 - Init packet junk size - + S2 - Response packet junk size - + H1 - Init packet magic header - + H2 - Response packet magic header - + H4 - Transport packet magic header - + H3 - Underload packet magic header - + The values of the H1-H4 fields must be unique - + The value of the field S1 + message initiation size (148) must not equal S2 + message response size (92) - + Save settings? 保存设置? - + Continue 继续 - + Cancel 取消 - + Unable change settings while there is an active connection @@ -674,33 +675,33 @@ Already installed containers were found on the server. All installed containers PageProtocolCloakSettings - + Cloak settings Cloak 配置 - + Disguised as traffic from 伪装流量为 - + Port 端口 - - + + Cipher 加密算法 - + Save 保存 - + Unable change settings while there is an active connection @@ -708,170 +709,170 @@ Already installed containers were found on the server. All installed containers PageProtocolOpenVpnSettings - + OpenVPN settings OpenVPN 配置 - + VPN address subnet VPN 地址子网 - + Network protocol 网络协议 - + Port 端口 - + Auto-negotiate encryption 自定义加密方式 - - + + Hash - + SHA512 - + SHA384 - + SHA256 - + SHA3-512 - + SHA3-384 - + SHA3-256 - + whirlpool - + BLAKE2b512 - + BLAKE2s256 - + SHA1 - - + + Cipher - + AES-256-GCM - + AES-192-GCM - + AES-128-GCM - + AES-256-CBC - + AES-192-CBC - + AES-128-CBC - + ChaCha20-Poly1305 - + ARIA-256-CBC - + CAMELLIA-256-CBC - + none - + TLS auth TLS认证 - + Block DNS requests outside of VPN 阻止VPN外的DNS请求 - + Additional client configuration commands 附加客户端配置命令 - - + + Commands: 命令: - + Additional server configuration commands 附加服务器端配置命令 - + Unable change settings while there is an active connection @@ -888,7 +889,7 @@ Already installed containers were found on the server. All installed containers 与您共享连接的所有用户将无法再连接到该连接。 - + Save 保存 @@ -908,12 +909,12 @@ Already installed containers were found on the server. All installed containers PageProtocolRaw - + settings 配置 - + Show connection options 显示连接选项 @@ -922,22 +923,22 @@ Already installed containers were found on the server. All installed containers 连接选项 - + Connection options %1 %1 连接选项 - + Remove 移除 - + Remove %1 from server? 从服务器移除 %1 ? - + All users with whom you shared a connection with will no longer be able to connect to it. 与您共享连接的所有用户将无法再连接到该连接。 @@ -950,12 +951,12 @@ Already installed containers were found on the server. All installed containers 与您共享连接的所有用户将无法再连接到此链接 - + Continue 继续 - + Cancel 取消 @@ -963,28 +964,28 @@ Already installed containers were found on the server. All installed containers PageProtocolShadowSocksSettings - + Shadowsocks settings Shadowsocks 配置 - + Port 端口 - - + + Cipher 加密算法 - + Save 保存 - + Unable change settings while there is an active connection @@ -992,52 +993,52 @@ Already installed containers were found on the server. All installed containers PageProtocolWireGuardClientSettings - + WG settings - + MTU - + Server settings - + Port 端口 - + Save 保存 - + Save settings? 保存设置? - + Only the settings for this device will be changed - + Continue 继续 - + Cancel 取消 - + Unable change settings while there is an active connection @@ -1045,27 +1046,32 @@ Already installed containers were found on the server. All installed containers PageProtocolWireGuardSettings - + WG settings - + + VPN address subnet + VPN 地址子网 + + + Port 端口 - + Save settings? 保存设置? - + All users with whom you shared a connection with will no longer be able to connect to it. 与您共享连接的所有用户将无法再连接到该连接。 - + Unable change settings while there is an active connection @@ -1074,17 +1080,17 @@ Already installed containers were found on the server. All installed containers 与您共享连接的所有用户将无法再连接到该连接。 - + Continue 继续 - + Cancel 取消 - + Save 保存 @@ -1092,22 +1098,22 @@ Already installed containers were found on the server. All installed containers PageProtocolXraySettings - + XRay settings - + Disguised as traffic from 伪装流量为 - + Save 保存 - + Unable change settings while there is an active connection @@ -1115,29 +1121,29 @@ Already installed containers were found on the server. All installed containers PageServiceDnsSettings - + A DNS service is installed on your server, and it is only accessible via VPN. 您的服务器已安装DNS服务,仅能通过VPN访问。 - + The DNS address is the same as the address of your server. You can configure DNS in the settings, under the connections tab. 其地址与您的服务器地址相同。您可以在 设置 连接 中进行配置。 - + Remove 移除 - + Remove %1 from server? 从服务器移除 %1 ? - + Cannot remove AmneziaDNS from running server @@ -1146,12 +1152,12 @@ Already installed containers were found on the server. All installed containers 从服务器 - + Continue 继续 - + Cancel 取消 @@ -1159,67 +1165,67 @@ Already installed containers were found on the server. All installed containers PageServiceSftpSettings - + Settings updated successfully 配置更新成功 - + SFTP settings SFTP 配置 - + Host 主机 - - - - + + + + Copied 拷贝 - + Port 端口 - + User name 用户名 - + Password 密码 - + Mount folder on device 挂载文件夹 - + In order to mount remote SFTP folder as local drive, perform following steps: <br> 为将远程 SFTP 文件夹挂载到本地,请执行以下步骤: <br> - - + + <br>1. Install the latest version of <br>1. 安装最新版的 - - + + <br>2. Install the latest version of <br>2. 安装最新版的 - + Detailed instructions 详细说明 @@ -1243,69 +1249,69 @@ Already installed containers were found on the server. All installed containers PageServiceSocksProxySettings - + Settings updated successfully 配置更新成功 - - + + SOCKS5 settings - + Host 主机 - - - - + + + + Copied - - + + Port 端口 - + User name 用户名 - - + + Password 密码 - + Username - - + + Change connection settings - + The port must be in the range of 1 to 65535 - + Password cannot be empty - + Username cannot be empty @@ -1313,37 +1319,37 @@ Already installed containers were found on the server. All installed containers PageServiceTorWebsiteSettings - + Settings updated successfully 配置更新成功 - + Tor website settings Tor网站配置 - + Website address 网址 - + Copied 已拷贝 - + Use <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor Browser</a> to open this URL. 用 <a href="https://www.torproject.org/download/" style="color: #FBB26A;">Tor 浏览器</a> 打开上面网址. - + After creating your onion site, it takes a few minutes for the Tor network to make it available for use. 创建您的洋葱网站后,需要几分钟时间,才能使其在Tor网络上可用 - + When configuring WordPress set the this onion address as domain. 配置 WordPress 时,将此洋葱地址设置为域。 @@ -1367,42 +1373,42 @@ Already installed containers were found on the server. All installed containers PageSettings - + Settings 设置 - + Servers 服务器 - + Connection 连接 - + Application 应用 - + Backup 备份 - + About AmneziaVPN 关于 - + Dev console - + Close application 关闭应用 @@ -1416,32 +1422,32 @@ And if you don't like the app, all the more support it - the donation will 如果您不喜欢,请捐助支持我们改进它。 - + Support Amnezia 支持Amnezia - + Amnezia is a free and open-source application. You can support the developers if you like it. Amnezia 是一款免费的开源应用程序。 如果您喜欢的话可以支持开发者。 - + Contacts 联系方式 - + Telegram group 电报群 - + To discuss features 用于功能讨论 - + https://t.me/amnezia_vpn_en @@ -1450,122 +1456,145 @@ And if you don't like the app, all the more support it - the donation will 邮件 - + support@amnezia.org - + For reviews and bug reports 用于评论和提交软件的缺陷 - + Copied - + GitHub GitHub - + + Discover the source code + + + + https://github.com/amnezia-vpn/amnezia-client https://github.com/amnezia-vpn/amnezia-client - + Website 官网 - + + Visit official website + + + + Software version: %1 软件版本: %1 - + Check for updates 检查更新 - + Privacy Policy 隐私政策 + + PageSettingsApiLanguageList + + + Unable change server location while there is an active connection + + + PageSettingsApiServerInfo - + For the region - + Price - + Work period - + + Valid until + + + + Speed - + Support tag - + Copied - + Reload API config - + Reload API config? - - + + Continue 继续 - - + + Cancel 取消 - + Cannot reload API config during active connection - + Remove from application - + Remove from application? - + Cannot remove server during active connection @@ -1573,12 +1602,12 @@ And if you don't like the app, all the more support it - the donation will PageSettingsAppSplitTunneling - + Cannot change split tunneling settings during active connection 无法在活动连接期间更改分割隧道设置 - + Only the apps from the list should have access via VPN @@ -1588,42 +1617,42 @@ And if you don't like the app, all the more support it - the donation will - + App split tunneling - + Mode 规则 - + Remove - + Continue 继续 - + Cancel 取消 - + application name - + Open executable file - + Executable files (*.*) @@ -1631,27 +1660,27 @@ And if you don't like the app, all the more support it - the donation will PageSettingsApplication - + Application 应用 - + Allow application screenshots 允许截屏 - + Enable notifications - + Enable notifications to show the VPN state in the status bar - + Auto start 自动运行 @@ -1664,77 +1693,77 @@ And if you don't like the app, all the more support it - the donation will 启动时自动运行运用程序 - + Launch the application every time the device is starts 每次设备启动时启动应用程序 - + Auto connect 自动连接 - + Connect to VPN on app start 应用开启时连接VPN - + Start minimized 最小化 - + Launch application minimized 开启应用软件时窗口最小化 - + Language 语言 - + Logging 日志 - + Enabled 开启 - + Disabled 禁用 - + Reset settings and remove all data from the application 重置并清理应用的所有数据 - + Reset settings and remove all data from the application? 重置并清理应用的所有数据? - + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. 所有配置恢复为默认值。服务器已安装的AmneziaVPN服务将被保留。 - + Continue 继续 - + Cancel 取消 - + Cannot reset settings during active connection @@ -1742,7 +1771,7 @@ And if you don't like the app, all the more support it - the donation will PageSettingsBackup - + Settings restored from backup file 从备份文件还原配置 @@ -1751,73 +1780,73 @@ And if you don't like the app, all the more support it - the donation will 帮助您在下次安装时立即恢复连接设置 - + Back up your configuration 备份您的配置 - + You can save your settings to a backup file to restore them the next time you install the application. 您可以将配置信息备份到文件中,以便在下次安装应用软件时恢复配置 - + The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. 备份将包含您添加到 AmneziaVPN 的所有服务器的密码和私钥。请将这些信息保存在安全的地方。 - + Make a backup 进行备份 - + Save backup file 保存备份 - - + + Backup files (*.backup) - + Backup file saved 备份文件已保存 - + Restore from backup 从备份还原 - + Open backup file 打开备份文件 - + Import settings from a backup file? 从备份文件导入设置? - + All current settings will be reset 当前所有设置将重置 - + Continue 继续 - + Cancel 取消 - + Cannot restore backup settings during active connection @@ -1825,32 +1854,32 @@ And if you don't like the app, all the more support it - the donation will PageSettingsConnection - + Connection 连接 - + When AmneziaDNS is not used or installed 当未使用或未安装AmneziaDNS时 - + Allows you to use the VPN only for certain Apps 只允许在某些应用程序中使用 VPN - + KillSwitch - + Disables your internet if your encrypted VPN connection drops out for any reason. - + Cannot change killSwitch settings during active connection @@ -1859,17 +1888,17 @@ And if you don't like the app, all the more support it - the donation will 使用AmneziaDNS,如其已安装在服务器上 - + Use AmneziaDNS 使用AmneziaDNS - + If AmneziaDNS is installed on the server 如果已在服务器安装AmneziaDNS - + DNS servers DNS服务器 @@ -1878,17 +1907,17 @@ And if you don't like the app, all the more support it - the donation will 如果未使用或未安装AmneziaDNS - + Site-based split tunneling 基于网站的隧道分离 - + Allows you to select which sites you want to access through the VPN 配置想要通过VPN访问网站 - + App-based split tunneling 基于应用的隧道分离 @@ -1912,62 +1941,62 @@ And if you don't like the app, all the more support it - the donation will PageSettingsDns - + Default server does not support custom DNS 默认服务器不支持自定义 DNS - + DNS servers DNS服务器 - + If AmneziaDNS is not used or installed 如果未使用或未安装AmneziaDNS - + Primary DNS 首选 DNS - + Secondary DNS 备用 DNS - + Restore default 恢复默认配置 - + Restore default DNS settings? 是否恢复默认DNS配置? - + Continue 继续 - + Cancel 取消 - + Settings have been reset 已重置 - + Save 保存 - + Settings saved 配置已保存 @@ -1975,12 +2004,12 @@ And if you don't like the app, all the more support it - the donation will PageSettingsLogging - + Logging 日志 - + Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. 默认情况下,日志功能是禁用的。如果应用程序出现故障,则启用日志保存功能。 @@ -1993,20 +2022,20 @@ And if you don't like the app, all the more support it - the donation will 打开日志文件夹 - - + + Save 保存 - - + + Logs files (*.log) - - + + Logs file saved 日志文件已保存 @@ -2015,64 +2044,62 @@ And if you don't like the app, all the more support it - the donation will 保存日志到文件 - + Enable logs - + Clear logs? 清理日志? - + Continue 继续 - + Cancel 取消 - + Logs have been cleaned up 日志已清理 - + Client logs - + AmneziaVPN logs - - + Open logs folder - - + Export logs - + Service logs - + AmneziaVPN-service logs - + Clear logs 清理日志 @@ -2102,12 +2129,12 @@ And if you don't like the app, all the more support it - the donation will 清除缓存? - + Do you want to reboot the server? 您想重新启动服务器吗? - + Do you want to clear server from Amnezia software? 您要清除服务器上的Amnezia软件吗? @@ -2117,18 +2144,18 @@ And if you don't like the app, all the more support it - the donation will - - - - + + + + Continue 继续 - - - - + + + + Cancel 取消 @@ -2143,57 +2170,57 @@ And if you don't like the app, all the more support it - the donation will 如果存在且未显示,则添加到应用软件 - + Reboot server 重新启动服务器 - + The reboot process may take approximately 30 seconds. Are you sure you wish to proceed? 重新启动过程可能需要大约30秒。您确定要继续吗? - + Cannot reboot server during active connection - + Remove server from application 移除本地服务器信息 - + Do you want to remove the server from application? 您想要从应用程序中移除服务器吗? - + Cannot remove server during active connection - + All users whom you shared a connection with will no longer be able to connect to it. 与您共享连接的所有用户将无法再连接到该连接。 - + Cannot clear server from Amnezia software during active connection - + Reset API config 重置 API 配置 - + Do you want to reset API config? 您想重置 API 配置吗? - + Cannot reset API config during active connection @@ -2202,12 +2229,12 @@ And if you don't like the app, all the more support it - the donation will 移除本地服务器信息? - + All installed AmneziaVPN services will still remain on the server. 所有已安装的 AmneziaVPN 服务仍将保留在服务器上。 - + Clear server from Amnezia software 清理Amnezia中服务器信息 @@ -2223,27 +2250,32 @@ And if you don't like the app, all the more support it - the donation will PageSettingsServerInfo - + + Subscription is valid until + + + + Server name 服务器名 - + Save 保存 - + Protocols 协议 - + Services 服务 - + Management 管理 @@ -2255,12 +2287,12 @@ And if you don't like the app, all the more support it - the donation will PageSettingsServerProtocol - + settings 配置 - + Clear %1 profile? @@ -2270,47 +2302,47 @@ And if you don't like the app, all the more support it - the donation will - + connection settings - + Click the "connect" button to create a connection configuration - + server settings - + Clear profile - + The connection configuration will be deleted for this device only - + Unable to clear %1 profile while there is an active connection - + Remove 移除 - + All users with whom you shared a connection will no longer be able to connect to it. 与您共享连接的所有用户将无法再连接到该连接。 - + Cannot remove active container @@ -2323,7 +2355,7 @@ And if you don't like the app, all the more support it - the donation will 从服务器 - + Remove %1 from server? 从服务器移除 %1 ? @@ -2332,14 +2364,14 @@ And if you don't like the app, all the more support it - the donation will 与您共享连接的所有用户将无法再连接到此链接 - - + + Continue 继续 - - + + Cancel 取消 @@ -2347,7 +2379,7 @@ And if you don't like the app, all the more support it - the donation will PageSettingsServersList - + Servers 服务器 @@ -2367,7 +2399,7 @@ And if you don't like the app, all the more support it - the donation will 网站级VPN分流 - + Default server does not support split tunneling function 默认服务器不支持分离隧道功能 @@ -2376,32 +2408,32 @@ And if you don't like the app, all the more support it - the donation will 仅使用VPN访问 - + Addresses from the list should not be accessed via VPN 不使用VPN访问 - + Split tunneling 隧道分离 - + Mode 规则 - + Remove 移除 - + Continue 继续 - + Cancel 取消 @@ -2414,65 +2446,65 @@ And if you don't like the app, all the more support it - the donation will 导入/导出网站 - + Cannot change split tunneling settings during active connection 无法在活动连接期间更改分割隧道设置 - + Only the sites listed here will be accessed through the VPN 只有这里列出的网站将通过VPN访问 - + website or IP 网站或IP - + Import / Export Sites 导入/导出网站 - + Import 导入 - + Save site list 保存网址 - + Save sites 保存网址 - - - + + + Sites files (*.json) - + Import a list of sites 导入网址列表 - + Replace site list 替换网址列表 - - + + Open sites file 打开网址文件 - + Add imported sites to existing ones 将导入的网址添加到现有网址中 @@ -2480,32 +2512,32 @@ And if you don't like the app, all the more support it - the donation will PageSetupWizardApiServiceInfo - + For the region - + Price - + Work period - + Speed - + Features - + Connect 连接 @@ -2513,12 +2545,12 @@ And if you don't like the app, all the more support it - the donation will PageSetupWizardApiServicesList - + VPN by Amnezia - + Choose a VPN service that suits your needs. @@ -2549,7 +2581,7 @@ It's okay as long as it's from someone you trust. 包含连接配置或备份的文件 - + Connection 连接 @@ -2559,90 +2591,125 @@ It's okay as long as it's from someone you trust. 设置 - + Enable logs - + + Support tag + + + + + Copied + + + + Insert the key, add a configuration file or scan the QR-code - + Insert key - + Insert 插入 - + Continue 继续 - + Other connection options - + + Site Amnezia + + + + VPN by Amnezia - + Connect to classic paid and free VPN services from Amnezia - + Self-hosted VPN - + Configure Amnezia VPN on your own server - + Restore from backup 从备份还原 - + + + + + + Open backup file 打开备份文件 - + Backup files (*.backup) - + File with connection settings 包含连接配置的文件 - + + + + + + Open config file 打开配置文件 - + QR code 二维码 - + + + + + + I have nothing 我没有 + + + + + Key as text 授权码文本 @@ -2655,12 +2722,12 @@ It's okay as long as it's from someone you trust. 连接服务器 - + Configure your server 配置服务器 - + Server IP address [:port] 服务器IP [:端口] @@ -2673,12 +2740,12 @@ It's okay as long as it's from someone you trust. 密码 或 私钥 - + Continue 继续 - + All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties 您输入的所有数据将严格保密,不会与 Amnezia 或任何第三方共享或披露 @@ -2689,47 +2756,47 @@ and will not be shared or disclosed to the Amnezia or any third parties 不会向 Amnezia 或任何第三方分享或披露 - + 255.255.255.255:22 - + SSH Username SSH 用户名 - + Password or SSH private key 密码或 SSH 私钥 - + How to run your VPN server - + Where to get connection data, step-by-step instructions for buying a VPS - + Ip address cannot be empty IP不能为空 - + Enter the address in the format 255.255.255.255:88 按照这种格式输入 255.255.255.255:88 - + Login cannot be empty 账号不能为空 - + Password/private key cannot be empty 密码或私钥不能为空 @@ -2737,17 +2804,17 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardEasy - + What is the level of internet control in your region? 您所在地区的互联网管控力度如何? - + Choose a VPN protocol 选择 VPN 协议 - + Skip setup 跳过设置 @@ -2760,7 +2827,7 @@ and will not be shared or disclosed to the Amnezia or any third parties 我想选择VPN协议 - + Continue 继续 @@ -2819,37 +2886,37 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardProtocolSettings - + Installing %1 正在安装 %1 - + More detailed 更多细节 - + Close 关闭 - + Network protocol 网络协议 - + Port 端口 - + Install 安装 - + The port must be in the range of 1 to 65535 @@ -2857,12 +2924,12 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardProtocols - + VPN protocol VPN 协议 - + Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP. 选择你认为优先级最高的一项。稍后,您可以安装其他协议和附加服务,例如 DNS 代理和 SFTP。 @@ -2898,7 +2965,7 @@ and will not be shared or disclosed to the Amnezia or any third parties 我没有 - + Let's get started @@ -2906,27 +2973,27 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardTextKey - + Connection key 连接授权码 - + A line that starts with vpn://... 以 vpn://... 开始的行 - + Key 授权码 - + Insert 插入 - + Continue 继续 @@ -2934,7 +3001,7 @@ and will not be shared or disclosed to the Amnezia or any third parties PageSetupWizardViewConfig - + New connection 新连接 @@ -2943,27 +3010,27 @@ and will not be shared or disclosed to the Amnezia or any third parties 请勿使用公共来源的连接码。它可以被创建来拦截您的数据。 - + Collapse content 折叠内容 - + Show content 显示内容 - + Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. - + Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. 只使用您信任的来源提供的连接代码。公共来源的代码可能是为了拦截您的数据而创建的。 - + Connect 连接 @@ -2971,162 +3038,167 @@ and will not be shared or disclosed to the Amnezia or any third parties PageShare - + Save OpenVPN config 保存OpenVPN配置 - + Save WireGuard config 保存WireGuard配置 - + Save AmneziaWG config 保存 AmneziaWG 配置 - + Save Shadowsocks config 保存 Shadowsocks 配置 - + Save Cloak config 保存斗篷配置 - + Save XRay config - + For the AmneziaVPN app AmneziaVPN 应用 - + OpenVPN native format OpenVPN原生格式 - + WireGuard native format WireGuard原生格式 - + AmneziaWG native format AmneziaWG 本地格式 - + Shadowsocks native format Shadowsocks原生格式 - + Cloak native format Cloak原生格式 - + XRay native format - + Share VPN Access 共享 VPN 访问 - + Share full access to the server and VPN 共享服务器和VPN的完全访问权限 - + Use for your own devices, or share with those you trust to manage the server. 用于您自己的设备,或与您信任的人共享以管理服务器. - - + + Users 用户 - + Share VPN access without the ability to manage the server 共享 VPN 访问,无需管理服务器 - + Search 搜索 - + Creation date: %1 - + Latest handshake: %1 - + Data received: %1 - + Data sent: %1 + + + Allowed IPs: %1 + + Creation date: 创建日期: - + Rename 重新命名 - + Client name 客户名称 - + Save 保存 - + Revoke 撤销 - + Revoke the config for a user - %1? 撤销用户的配置- %1? - + The user will no longer be able to connect to your server. 该用户将无法再连接到您的服务器. - + Continue 继续 - + Cancel 取消 @@ -3139,7 +3211,7 @@ and will not be shared or disclosed to the Amnezia or any third parties 访问VPN - + Connection 连接 @@ -3168,8 +3240,8 @@ and will not be shared or disclosed to the Amnezia or any third parties 服务器 - - + + Server 服务器 @@ -3182,7 +3254,7 @@ and will not be shared or disclosed to the Amnezia or any third parties 访问配置文件的内容为: - + File with connection settings to 连接配置文件的内容为: @@ -3191,35 +3263,35 @@ and will not be shared or disclosed to the Amnezia or any third parties 协议 - - + + Protocol 协议 - + Connection to 连接到 - + Config revoked 配置已撤销 - + User name 用户名 - - + + Connection format 连接格式 - - + + Share 共享 @@ -3227,55 +3299,55 @@ and will not be shared or disclosed to the Amnezia or any third parties PageShareFullAccess - + Full access to the server and VPN 对服务器和VPN的完全访问权限 - + We recommend that you use full access to the server only for your own additional devices. 我们建议您仅为自己的附加设备使用服务器的完全访问权限. - + If you share full access with other people, they can remove and add protocols and services to the server, which will cause the VPN to work incorrectly for all users. 如果您与其他人共享完全访问权限,他们可以从服务器中删除和添加协议和服务,这将导致VPN对所有用户的工作出现问题。 - - + + Server 服务器 - + Accessing 访问 - + File with accessing settings to 访问配置文件的内容为 - + Share 共享 - + Access error! 访问错误 - + Connection to 连接到 - + File with connection settings to 连接配置文件的内容为 @@ -3283,17 +3355,17 @@ and will not be shared or disclosed to the Amnezia or any third parties PageStart - + Logging was disabled after 14 days, log files were deleted - + Settings restored from backup file 从备份文件还原配置 - + Logging is enabled. Note that logs will be automaticallydisabled after 14 days, and all log files will be deleted. @@ -3301,7 +3373,7 @@ and will not be shared or disclosed to the Amnezia or any third parties PopupType - + Close 关闭 @@ -3646,6 +3718,11 @@ and will not be shared or disclosed to the Amnezia or any third parties SCP error: Generic failure + + + Unable to open config file + + Sftp error: End-of-file encountered Sftp错误: End-of-file encountered @@ -3699,72 +3776,82 @@ and will not be shared or disclosed to the Amnezia or any third parties Sftp 错误: 远程驱动器中没有媒介 - + VPN connection error VPN 连接错误 - + Error when retrieving configuration from API 从 API 检索配置时出错 - + This config has already been added to the application 该配置已添加到应用程序中 - + In the response from the server, an empty config was received - + SSL error occurred - + Server response timeout on api request - + Missing AGW public key + + + Failed to decrypt response payload + + - QFile error: The file could not be opened - - - - - QFile error: An error occurred when reading from the file - - - - - QFile error: The file could not be accessed + Missing list of available services - QFile error: An unspecified error occurred + QFile error: The file could not be opened - QFile error: A fatal error occurred + QFile error: An error occurred when reading from the file + QFile error: The file could not be accessed + + + + + QFile error: An unspecified error occurred + + + + + QFile error: A fatal error occurred + + + + QFile error: The operation was aborted - + ErrorCode: %1. 错误代码: %1. @@ -3832,7 +3919,7 @@ and will not be shared or disclosed to the Amnezia or any third parties 该配置不包含任何用于连接到服务器的容器和凭据。 - + Internal error @@ -4345,11 +4432,19 @@ While it offers a blend of security, stability, and speed, it's essential t SelectLanguageDrawer - + Choose language 选择语言 + + ServersListView + + + Unable change server while there is an active connection + 已建立连接时无法更改服务器配置 + + Settings @@ -4367,12 +4462,12 @@ While it offers a blend of security, stability, and speed, it's essential t SettingsController - + Backup file is corrupted 备份文件已损坏 - + All settings have been reset to default values 所配置恢复为默认值 @@ -4390,28 +4485,28 @@ While it offers a blend of security, stability, and speed, it's essential t 保存配置 - + Share 共享 - + Copy 拷贝 - - + + Copied 已拷贝 - + Copy config string 复制配置字符串 - + Show connection settings 显示连接配置 @@ -4420,7 +4515,7 @@ While it offers a blend of security, stability, and speed, it's essential t 展示内容 - + To read the QR code in the Amnezia app, select "Add server" → "I have data to connect" → "QR code, key or settings file" 要应用二维码到 Amnezia,请底部工具栏点击“+”→“连接方式”→“二维码、授权码或配置文件” @@ -4443,27 +4538,27 @@ While it offers a blend of security, stability, and speed, it's essential t 已移除网站: %1 - + Can't open file: %1 无法打开文件: %1 - + Failed to parse JSON data from file: %1 JSON解析失败,文件: %1 - + The JSON data is not an array in file: %1 文件中的JSON数据不是一个数组,文件: %1 - + Import completed 完成导入 - + Export completed 完成导出 @@ -4504,7 +4599,7 @@ While it offers a blend of security, stability, and speed, it's essential t TextFieldWithHeaderType - + The field can't be empty 输入不能为空 @@ -4512,7 +4607,7 @@ While it offers a blend of security, stability, and speed, it's essential t VpnConnection - + Mbps @@ -4610,12 +4705,12 @@ While it offers a blend of security, stability, and speed, it's essential t main2 - + Private key passphrase 私钥密码 - + Save 保存 diff --git a/client/ui/controllers/connectionController.cpp b/client/ui/controllers/connectionController.cpp index f8516f6e..f9491d4e 100644 --- a/client/ui/controllers/connectionController.cpp +++ b/client/ui/controllers/connectionController.cpp @@ -55,7 +55,7 @@ void ConnectionController::openConnection() && !m_serversModel->data(serverIndex, ServersModel::Roles::HasInstalledContainers).toBool()) { emit updateApiConfigFromGateway(); } else if (configVersion && m_serversModel->isApiKeyExpired(serverIndex)) { - qDebug() << "attempt to update api config by end_date event"; + qDebug() << "attempt to update api config by expires_at event"; if (configVersion == ApiConfigSources::Telegram) { emit updateApiConfigFromTelegram(); } else { diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index 2690b5b1..8681406e 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -121,9 +121,8 @@ ErrorCode ExportController::generateNativeConfig(const DockerContainer container jsonNativeConfig = QJsonDocument::fromJson(protocolConfigString.toUtf8()).object(); - if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg) { - auto clientId = jsonNativeConfig.value(config_key::clientId).toString(); - errorCode = m_clientManagementModel->appendClient(clientId, clientName, container, credentials, serverController); + if (protocol == Proto::OpenVpn || protocol == Proto::WireGuard || protocol == Proto::Awg || protocol == Proto::Xray) { + errorCode = m_clientManagementModel->appendClient(jsonNativeConfig, clientName, container, credentials, serverController); } return errorCode; } @@ -248,10 +247,10 @@ void ExportController::generateCloakConfig() emit exportConfigChanged(); } -void ExportController::generateXrayConfig() +void ExportController::generateXrayConfig(const QString &clientName) { QJsonObject nativeConfig; - ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, "", Proto::Xray, nativeConfig); + ErrorCode errorCode = generateNativeConfig(DockerContainer::Xray, clientName, Proto::Xray, nativeConfig); if (errorCode) { emit exportErrorOccurred(errorCode); return; diff --git a/client/ui/controllers/exportController.h b/client/ui/controllers/exportController.h index b031ea39..a2c9fcfa 100644 --- a/client/ui/controllers/exportController.h +++ b/client/ui/controllers/exportController.h @@ -28,7 +28,7 @@ public slots: void generateAwgConfig(const QString &clientName); void generateShadowSocksConfig(); void generateCloakConfig(); - void generateXrayConfig(); + void generateXrayConfig(const QString &clientName); QString getConfig(); QString getNativeConfigString(); diff --git a/client/ui/controllers/focusController.cpp b/client/ui/controllers/focusController.cpp new file mode 100644 index 00000000..54bdc7d8 --- /dev/null +++ b/client/ui/controllers/focusController.cpp @@ -0,0 +1,209 @@ +#include "focusController.h" +#include "utils/qmlUtils.h" + +#include +#include + +FocusController::FocusController(QQmlApplicationEngine *engine, QObject *parent) + : QObject { parent }, + m_engine { engine }, + m_focusChain {}, + m_focusedItem { nullptr }, + m_rootObjects {}, + m_defaultFocusItem { nullptr }, + m_lvfc { nullptr } +{ + QObject::connect(m_engine, &QQmlApplicationEngine::objectCreated, this, [this](QObject *object, const QUrl &url) { + QQuickItem *newDefaultFocusItem = object->findChild("defaultFocusItem"); + if (newDefaultFocusItem && m_defaultFocusItem != newDefaultFocusItem) { + m_defaultFocusItem = 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); +} + +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(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; + } +} diff --git a/client/ui/controllers/focusController.h b/client/ui/controllers/focusController.h new file mode 100644 index 00000000..11074dae --- /dev/null +++ b/client/ui/controllers/focusController.h @@ -0,0 +1,57 @@ +#ifndef FOCUSCONTROLLER_H +#define FOCUSCONTROLLER_H + +#include "ui/controllers/listViewFocusController.h" + +#include + +/*! + * \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(); + + QQmlApplicationEngine *m_engine; // Pointer to engine to get root object + QList m_focusChain; // List of current objects to be focused + QQuickItem *m_focusedItem; // Pointer to the active focus item + QStack m_rootObjects; // Pointer to stack of roots for focus chain + QQuickItem *m_defaultFocusItem; + + ListViewFocusController *m_lvfc; // ListView focus manager + +signals: + void focusedItemChanged(); +}; + +#endif // FOCUSCONTROLLER_H diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index f7e96bff..28bbc9f6 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -9,6 +9,7 @@ #include "core/errorstrings.h" #include "core/serialization/serialization.h" +#include "systemController.h" #include "utilities.h" #ifdef Q_OS_ANDROID @@ -76,17 +77,18 @@ ImportController::ImportController(const QSharedPointer &serversMo bool ImportController::extractConfigFromFile(const QString &fileName) { - QFile file(fileName); - - if (file.open(QIODevice::ReadOnly)) { - QString data = file.readAll(); - - m_configFileName = QFileInfo(file.fileName()).fileName(); - return extractConfigFromData(data); + QString data; + if (!SystemController::readFile(fileName, data)) { + emit importErrorOccurred(ErrorCode::ImportOpenConfigError, false); + return false; } - - emit importErrorOccurred(ErrorCode::ImportOpenConfigError, false); - return false; + m_configFileName = QFileInfo(QFile(fileName).fileName()).fileName(); +#ifdef Q_OS_ANDROID + if (m_configFileName.isEmpty()) { + m_configFileName = AndroidController::instance()->getFileName(fileName); + } +#endif + return extractConfigFromData(data); } bool ImportController::extractConfigFromData(QString data) diff --git a/client/ui/controllers/listViewFocusController.cpp b/client/ui/controllers/listViewFocusController.cpp new file mode 100644 index 00000000..9fa232ca --- /dev/null +++ b/client/ui/controllers/listViewFocusController.cpp @@ -0,0 +1,309 @@ +#include "listViewFocusController.h" +#include "utils/qmlUtils.h" + +#include + +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() ? headerItemProperty.value() : nullptr; + + QVariant footerItemProperty = m_listView->property("footerItem"); + m_footer = footerItemProperty.canConvert() ? footerItemProperty.value() : 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(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(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; +} diff --git a/client/ui/controllers/listViewFocusController.h b/client/ui/controllers/listViewFocusController.h new file mode 100644 index 00000000..f6405716 --- /dev/null +++ b/client/ui/controllers/listViewFocusController.h @@ -0,0 +1,70 @@ +#ifndef LISTVIEWFOCUSCONTROLLER_H +#define LISTVIEWFOCUSCONTROLLER_H + +#include +#include +#include +#include +#include + +/*! + * \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 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 m_currentSectionString; +}; + +#endif // LISTVIEWFOCUSCONTROLLER_H diff --git a/client/ui/controllers/pageController.cpp b/client/ui/controllers/pageController.cpp index bbcc55a1..d515df49 100644 --- a/client/ui/controllers/pageController.cpp +++ b/client/ui/controllers/pageController.cpp @@ -81,7 +81,7 @@ void PageController::keyPressEvent(Qt::Key key) case Qt::Key_Escape: { if (m_drawerDepth) { emit closeTopDrawer(); - setDrawerDepth(getDrawerDepth() - 1); + decrementDrawerDepth(); } else { emit escapePressed(); } @@ -142,11 +142,25 @@ void PageController::setDrawerDepth(const int depth) } } -int PageController::getDrawerDepth() +int PageController::getDrawerDepth() const { return m_drawerDepth; } +int PageController::incrementDrawerDepth() +{ + return ++m_drawerDepth; +} + +int PageController::decrementDrawerDepth() +{ + if (m_drawerDepth == 0) { + return m_drawerDepth; + } else { + return --m_drawerDepth; + } +} + void PageController::onShowErrorMessage(ErrorCode errorCode) { const auto fullErrorMessage = errorString(errorCode); diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index f89d39a1..ffbdd3a1 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -100,7 +100,9 @@ public slots: void closeApplication(); void setDrawerDepth(const int depth); - int getDrawerDepth(); + int getDrawerDepth() const; + int incrementDrawerDepth(); + int decrementDrawerDepth(); private slots: void onShowErrorMessage(amnezia::ErrorCode errorCode); @@ -135,9 +137,6 @@ signals: void escapePressed(); void closeTopDrawer(); - void forceTabBarActiveFocus(); - void forceStackActiveFocus(); - private: QSharedPointer m_serversModel; diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index c3945512..f4e3d83d 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -131,12 +131,8 @@ void SettingsController::backupAppConfig(const QString &fileName) void SettingsController::restoreAppConfig(const QString &fileName) { - QFile file(fileName); - - file.open(QIODevice::ReadOnly); - - QByteArray data = file.readAll(); - + QByteArray data; + SystemController::readFile(fileName, data); restoreAppConfigFromData(data); } @@ -324,4 +320,15 @@ bool SettingsController::isOnTv() #else return false; #endif -} \ No newline at end of file +} + +bool SettingsController::isHomeAdLabelVisible() +{ + return m_settings->isHomeAdLabelVisible(); +} + +void SettingsController::disableHomeAdLabel() +{ + m_settings->disableHomeAdLabel(); + emit isHomeAdLabelVisibleChanged(false); +} diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h index efc18a7d..7781f6c7 100644 --- a/client/ui/controllers/settingsController.h +++ b/client/ui/controllers/settingsController.h @@ -29,6 +29,8 @@ public: Q_PROPERTY(QString gatewayEndpoint READ getGatewayEndpoint WRITE setGatewayEndpoint NOTIFY gatewayEndpointChanged) Q_PROPERTY(bool isDevGatewayEnv READ isDevGatewayEnv WRITE toggleDevGatewayEnv NOTIFY devGatewayEnvChanged) + Q_PROPERTY(bool isHomeAdLabelVisible READ isHomeAdLabelVisible NOTIFY isHomeAdLabelVisibleChanged) + public slots: void toggleAmneziaDns(bool enable); bool isAmneziaDnsEnabled(); @@ -89,6 +91,9 @@ public slots: bool isOnTv(); + bool isHomeAdLabelVisible(); + void disableHomeAdLabel(); + signals: void primaryDnsChanged(); void secondaryDnsChanged(); @@ -112,6 +117,8 @@ signals: void gatewayEndpointChanged(const QString &endpoint); void devGatewayEnvChanged(bool enabled); + void isHomeAdLabelVisibleChanged(bool visible); + private: QSharedPointer m_serversModel; QSharedPointer m_containersModel; diff --git a/client/ui/controllers/sitesController.cpp b/client/ui/controllers/sitesController.cpp index d54dbdd2..24ae035f 100644 --- a/client/ui/controllers/sitesController.cpp +++ b/client/ui/controllers/sitesController.cpp @@ -82,14 +82,12 @@ void SitesController::removeSite(int index) void SitesController::importSites(const QString &fileName, bool replaceExisting) { - QFile file(fileName); - - if (!file.open(QIODevice::ReadOnly)) { + QByteArray jsonData; + if (!SystemController::readFile(fileName, jsonData)) { emit errorOccurred(tr("Can't open file: %1").arg(fileName)); return; } - QByteArray jsonData = file.readAll(); QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonData); if (jsonDocument.isNull()) { emit errorOccurred(tr("Failed to parse JSON data from file: %1").arg(fileName)); diff --git a/client/ui/controllers/systemController.cpp b/client/ui/controllers/systemController.cpp index 4598bff1..52ca1294 100644 --- a/client/ui/controllers/systemController.cpp +++ b/client/ui/controllers/systemController.cpp @@ -24,7 +24,7 @@ SystemController::SystemController(const std::shared_ptr &settings, QO { } -void SystemController::saveFile(QString fileName, const QString &data) +void SystemController::saveFile(const QString &fileName, const QString &data) { #if defined Q_OS_ANDROID AndroidController::instance()->saveFile(fileName, data); @@ -62,6 +62,31 @@ void SystemController::saveFile(QString fileName, const QString &data) #endif } +bool SystemController::readFile(const QString &fileName, QByteArray &data) +{ +#ifdef Q_OS_ANDROID + int fd = AndroidController::instance()->getFd(fileName); + if (fd == -1) return false; + QFile file; + if(!file.open(fd, QIODevice::ReadOnly)) return false; + data = file.readAll(); + AndroidController::instance()->closeFd(); +#else + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) return false; + data = file.readAll(); +#endif + return true; +} + +bool SystemController::readFile(const QString &fileName, QString &data) +{ + QByteArray byteArray; + if(!readFile(fileName, byteArray)) return false; + data = byteArray; + return true; +} + QString SystemController::getFileName(const QString &acceptLabel, const QString &nameFilter, const QString &selectedFile, const bool isSaveMode, const QString &defaultSuffix) { @@ -134,3 +159,10 @@ bool SystemController::isAuthenticated() return true; #endif } + +void SystemController::sendTouch(float x, float y) +{ +#ifdef Q_OS_ANDROID + AndroidController::instance()->sendTouch(x, y); +#endif +} diff --git a/client/ui/controllers/systemController.h b/client/ui/controllers/systemController.h index d2ee6f63..8cb3a0d1 100644 --- a/client/ui/controllers/systemController.h +++ b/client/ui/controllers/systemController.h @@ -11,7 +11,9 @@ class SystemController : public QObject public: explicit SystemController(const std::shared_ptr &setting, QObject *parent = nullptr); - static void saveFile(QString fileName, const QString &data); + static void saveFile(const QString &fileName, const QString &data); + static bool readFile(const QString &fileName, QByteArray &data); + static bool readFile(const QString &fileName, QString &data); public slots: QString getFileName(const QString &acceptLabel, const QString &nameFilter, const QString &selectedFile = "", @@ -20,6 +22,8 @@ public slots: void setQmlRoot(QObject *qmlRoot); bool isAuthenticated(); + void sendTouch(float x, float y); + signals: void fileDialogClosed(const bool isAccepted); diff --git a/client/ui/models/apiServicesModel.cpp b/client/ui/models/apiServicesModel.cpp index 2a87bde3..3e684195 100644 --- a/client/ui/models/apiServicesModel.cpp +++ b/client/ui/models/apiServicesModel.cpp @@ -27,6 +27,9 @@ namespace constexpr char storeEndpoint[] = "store_endpoint"; constexpr char isAvailable[] = "is_available"; + + constexpr char subscription[] = "subscription"; + constexpr char endDate[] = "end_date"; } namespace serviceType @@ -51,23 +54,23 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(rowCount())) return QVariant(); - QJsonObject service = m_services.at(index.row()).toObject(); - QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject(); - auto serviceType = service.value(configKey::serviceType).toString(); + auto apiServiceData = m_services.at(index.row()); + auto serviceType = apiServiceData.type; + auto isServiceAvailable = apiServiceData.isServiceAvailable; switch (role) { case NameRole: { - return serviceInfo.value(configKey::name).toString(); + return apiServiceData.serviceInfo.name; } case CardDescriptionRole: { - auto speed = serviceInfo.value(configKey::speed).toString(); + auto speed = apiServiceData.serviceInfo.speed; if (serviceType == serviceType::amneziaPremium) { return tr("Classic VPN for comfortable work, downloading large files and watching videos. " "Works for any sites. Speed up to %1 MBit/s") .arg(speed); } else if (serviceType == serviceType::amneziaFree){ QString description = tr("VPN to access blocked sites in regions with high levels of Internet censorship. "); - if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) { + if (!isServiceAvailable) { description += tr("

Not available in your region. If you have VPN enabled, disable it, return to the previous screen, and try again."); } return description; @@ -83,25 +86,24 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const } case IsServiceAvailableRole: { if (serviceType == serviceType::amneziaFree) { - if (service.value(configKey::isAvailable).isBool() && !service.value(configKey::isAvailable).toBool()) { + if (!isServiceAvailable) { return false; } } return true; } case SpeedRole: { - auto speed = serviceInfo.value(configKey::speed).toString(); - return tr("%1 MBit/s").arg(speed); + return tr("%1 MBit/s").arg(apiServiceData.serviceInfo.speed); } - case WorkPeriodRole: { - auto timelimit = serviceInfo.value(configKey::timelimit).toString(); - if (timelimit == "0") { + case TimeLimitRole: { + auto timeLimit = apiServiceData.serviceInfo.timeLimit; + if (timeLimit == "0") { return ""; } - return tr("%1 days").arg(timelimit); + return tr("%1 days").arg(timeLimit); } case RegionRole: { - return serviceInfo.value(configKey::region).toString(); + return apiServiceData.serviceInfo.region; } case FeaturesRole: { if (serviceType == serviceType::amneziaPremium) { @@ -113,12 +115,15 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const } } case PriceRole: { - auto price = serviceInfo.value(configKey::price).toString(); + auto price = apiServiceData.serviceInfo.price; if (price == "free") { return tr("Free"); } return tr("%1 $/month").arg(price); } + case EndDateRole: { + return QDateTime::fromString(apiServiceData.subscription.endDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy"); + } } return QVariant(); @@ -128,15 +133,18 @@ void ApiServicesModel::updateModel(const QJsonObject &data) { beginResetModel(); - m_countryCode = data.value(configKey::userCountryCode).toString(); - m_services = data.value(configKey::services).toArray(); - if (m_services.isEmpty()) { - QJsonObject service; - service.insert(configKey::serviceInfo, data.value(configKey::serviceInfo)); - service.insert(configKey::serviceType, data.value(configKey::serviceType)); + m_services.clear(); - m_services.push_back(service); + m_countryCode = data.value(configKey::userCountryCode).toString(); + auto services = data.value(configKey::services).toArray(); + + if (services.isEmpty()) { + m_services.push_back(getApiServicesData(data)); m_selectedServiceIndex = 0; + } else { + for (const auto &service : services) { + m_services.push_back(getApiServicesData(service.toObject())); + } } endResetModel(); @@ -149,32 +157,32 @@ void ApiServicesModel::setServiceIndex(const int index) QJsonObject ApiServicesModel::getSelectedServiceInfo() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::serviceInfo).toObject(); + auto service = m_services.at(m_selectedServiceIndex); + return service.serviceInfo.object; } QString ApiServicesModel::getSelectedServiceType() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::serviceType).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.type; } QString ApiServicesModel::getSelectedServiceProtocol() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::serviceProtocol).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.protocol; } QString ApiServicesModel::getSelectedServiceName() { - auto modelIndex = index(m_selectedServiceIndex, 0); - return data(modelIndex, ApiServicesModel::Roles::NameRole).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.serviceInfo.name; } QJsonArray ApiServicesModel::getSelectedServiceCountries() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::availableCountries).toArray(); + auto service = m_services.at(m_selectedServiceIndex); + return service.availableCountries; } QString ApiServicesModel::getCountryCode() @@ -184,8 +192,8 @@ QString ApiServicesModel::getCountryCode() QString ApiServicesModel::getStoreEndpoint() { - QJsonObject service = m_services.at(m_selectedServiceIndex).toObject(); - return service.value(configKey::storeEndpoint).toString(); + auto service = m_services.at(m_selectedServiceIndex); + return service.storeEndpoint; } QVariant ApiServicesModel::getSelectedServiceData(const QString roleString) @@ -209,10 +217,46 @@ QHash ApiServicesModel::roleNames() const roles[ServiceDescriptionRole] = "serviceDescription"; roles[IsServiceAvailableRole] = "isServiceAvailable"; roles[SpeedRole] = "speed"; - roles[WorkPeriodRole] = "workPeriod"; + roles[TimeLimitRole] = "timeLimit"; roles[RegionRole] = "region"; roles[FeaturesRole] = "features"; roles[PriceRole] = "price"; + roles[EndDateRole] = "endDate"; return roles; } + +ApiServicesModel::ApiServicesData ApiServicesModel::getApiServicesData(const QJsonObject &data) +{ + auto serviceInfo = data.value(configKey::serviceInfo).toObject(); + auto serviceType = data.value(configKey::serviceType).toString(); + auto serviceProtocol = data.value(configKey::serviceProtocol).toString(); + auto availableCountries = data.value(configKey::availableCountries).toArray(); + + auto subscriptionObject = data.value(configKey::subscription).toObject(); + + ApiServicesData serviceData; + serviceData.serviceInfo.name = serviceInfo.value(configKey::name).toString(); + serviceData.serviceInfo.price = serviceInfo.value(configKey::price).toString(); + serviceData.serviceInfo.region = serviceInfo.value(configKey::region).toString(); + serviceData.serviceInfo.speed = serviceInfo.value(configKey::speed).toString(); + serviceData.serviceInfo.timeLimit = serviceInfo.value(configKey::timelimit).toString(); + + serviceData.type = serviceType; + serviceData.protocol = serviceProtocol; + + serviceData.storeEndpoint = serviceInfo.value(configKey::storeEndpoint).toString(); + + if (serviceInfo.value(configKey::isAvailable).isBool()) { + serviceData.isServiceAvailable = data.value(configKey::isAvailable).toBool(); + } else { + serviceData.isServiceAvailable = true; + } + + serviceData.serviceInfo.object = serviceInfo; + serviceData.availableCountries = availableCountries; + + serviceData.subscription.endDate = subscriptionObject.value(configKey::endDate).toString(); + + return serviceData; +} diff --git a/client/ui/models/apiServicesModel.h b/client/ui/models/apiServicesModel.h index 49918940..c96a49ab 100644 --- a/client/ui/models/apiServicesModel.h +++ b/client/ui/models/apiServicesModel.h @@ -3,6 +3,7 @@ #include #include +#include class ApiServicesModel : public QAbstractListModel { @@ -15,10 +16,11 @@ public: ServiceDescriptionRole, IsServiceAvailableRole, SpeedRole, - WorkPeriodRole, + TimeLimitRole, RegionRole, FeaturesRole, - PriceRole + PriceRole, + EndDateRole }; explicit ApiServicesModel(QObject *parent = nullptr); @@ -48,8 +50,40 @@ protected: QHash roleNames() const override; private: + struct ServiceInfo + { + QString name; + QString speed; + QString timeLimit; + QString region; + QString price; + + QJsonObject object; + }; + + struct Subscription + { + QString endDate; + }; + + struct ApiServicesData + { + bool isServiceAvailable; + + QString type; + QString protocol; + QString storeEndpoint; + + ServiceInfo serviceInfo; + Subscription subscription; + + QJsonArray availableCountries; + }; + + ApiServicesData getApiServicesData(const QJsonObject &data); + QString m_countryCode; - QJsonArray m_services; + QVector m_services; int m_selectedServiceIndex; }; diff --git a/client/ui/models/clientManagementModel.cpp b/client/ui/models/clientManagementModel.cpp index 7445d60f..f07eae71 100644 --- a/client/ui/models/clientManagementModel.cpp +++ b/client/ui/models/clientManagementModel.cpp @@ -106,6 +106,8 @@ ErrorCode ClientManagementModel::updateModel(const DockerContainer container, co error = getOpenVpnClients(container, credentials, serverController, count); } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { error = getWireGuardClients(container, credentials, serverController, count); + } else if (container == DockerContainer::Xray) { + error = getXrayClients(container, credentials, serverController, count); } if (error != ErrorCode::NoError) { endResetModel(); @@ -239,6 +241,68 @@ ErrorCode ClientManagementModel::getWireGuardClients(const DockerContainer conta } return error; } +ErrorCode ClientManagementModel::getXrayClients(const DockerContainer container, const ServerCredentials& credentials, + const QSharedPointer &serverController, int &count) +{ + ErrorCode error = ErrorCode::NoError; + + const QString serverConfigPath = amnezia::protocols::xray::serverConfigPath; + const QString configString = serverController->getTextFileFromContainer(container, credentials, serverConfigPath, error); + if (error != ErrorCode::NoError) { + logger.error() << "Failed to get the xray server config file from the server"; + return error; + } + + QJsonDocument serverConfig = QJsonDocument::fromJson(configString.toUtf8()); + if (serverConfig.isNull()) { + logger.error() << "Failed to parse xray server config JSON"; + return ErrorCode::InternalError; + } + + if (!serverConfig.object().contains("inbounds") || serverConfig.object()["inbounds"].toArray().isEmpty()) { + logger.error() << "Invalid xray server config structure"; + return ErrorCode::InternalError; + } + + const QJsonObject inbound = serverConfig.object()["inbounds"].toArray()[0].toObject(); + if (!inbound.contains("settings")) { + logger.error() << "Missing settings in xray inbound config"; + return ErrorCode::InternalError; + } + + const QJsonObject settings = inbound["settings"].toObject(); + if (!settings.contains("clients")) { + logger.error() << "Missing clients in xray settings config"; + return ErrorCode::InternalError; + } + + const QJsonArray clients = settings["clients"].toArray(); + for (const auto &clientValue : clients) { + const QJsonObject clientObj = clientValue.toObject(); + if (!clientObj.contains("id")) { + logger.error() << "Missing id in xray client config"; + continue; + } + QString clientId = clientObj["id"].toString(); + + QString xrayDefaultUuid = serverController->getTextFileFromContainer(container, credentials, amnezia::protocols::xray::uuidPath, error); + xrayDefaultUuid.replace("\n", ""); + + if (!isClientExists(clientId) && clientId != xrayDefaultUuid) { + QJsonObject client; + client[configKey::clientId] = clientId; + + QJsonObject userData; + userData[configKey::clientName] = QString("Client %1").arg(count); + client[configKey::userData] = userData; + + m_clientsTable.push_back(client); + count++; + } + } + + return error; +} ErrorCode ClientManagementModel::wgShow(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, std::vector &data) @@ -326,17 +390,67 @@ ErrorCode ClientManagementModel::appendClient(const DockerContainer container, c const QSharedPointer &serverController) { Proto protocol; - if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - protocol = Proto::OpenVpn; - } else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - protocol = ContainerProps::defaultProtocol(container); - } else { - return ErrorCode::NoError; + switch (container) { + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: + protocol = Proto::OpenVpn; + break; + case DockerContainer::OpenVpn: + case DockerContainer::WireGuard: + case DockerContainer::Awg: + case DockerContainer::Xray: + protocol = ContainerProps::defaultProtocol(container); + break; + default: + return ErrorCode::NoError; } auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig); + return appendClient(protocolConfig, clientName, container, credentials, serverController); +} - return appendClient(protocolConfig.value(config_key::clientId).toString(), clientName, container, credentials, serverController); +ErrorCode ClientManagementModel::appendClient(QJsonObject &protocolConfig, const QString &clientName, const DockerContainer container, + const ServerCredentials &credentials, const QSharedPointer &serverController) +{ + 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, @@ -422,10 +536,27 @@ ErrorCode ClientManagementModel::revokeClient(const int row, const DockerContain auto client = m_clientsTable.at(row).toObject(); QString clientId = client.value(configKey::clientId).toString(); - if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); - } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - errorCode = revokeWireGuard(row, container, credentials, serverController); + switch(container) + { + case DockerContainer::OpenVpn: + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { + errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); + break; + } + case DockerContainer::WireGuard: + case DockerContainer::Awg: { + errorCode = revokeWireGuard(row, container, credentials, serverController); + break; + } + case DockerContainer::Xray: { + errorCode = revokeXray(row, container, credentials, serverController); + break; + } + default: { + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } } if (errorCode == ErrorCode::NoError) { @@ -463,19 +594,69 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig } Proto protocol; - if (container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { - protocol = Proto::OpenVpn; - } else if (container == DockerContainer::OpenVpn || container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - protocol = ContainerProps::defaultProtocol(container); - } else { - return ErrorCode::NoError; + + switch(container) + { + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { + protocol = Proto::OpenVpn; + break; + } + case DockerContainer::OpenVpn: + case DockerContainer::WireGuard: + case DockerContainer::Awg: + case DockerContainer::Xray: { + protocol = ContainerProps::defaultProtocol(container); + break; + } + default: { + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } } auto protocolConfig = ContainerProps::getProtocolConfigFromContainer(protocol, containerConfig); + QString clientId; + if (container == DockerContainer::Xray) { + if (!protocolConfig.contains("outbounds")) { + return ErrorCode::InternalError; + } + QJsonArray outbounds = protocolConfig.value("outbounds").toArray(); + if (outbounds.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject outbound = outbounds[0].toObject(); + if (!outbound.contains("settings")) { + return ErrorCode::InternalError; + } + QJsonObject settings = outbound["settings"].toObject(); + if (!settings.contains("vnext")) { + return ErrorCode::InternalError; + } + QJsonArray vnext = settings["vnext"].toArray(); + if (vnext.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject vnextObj = vnext[0].toObject(); + if (!vnextObj.contains("users")) { + return ErrorCode::InternalError; + } + QJsonArray users = vnextObj["users"].toArray(); + if (users.isEmpty()) { + return ErrorCode::InternalError; + } + QJsonObject user = users[0].toObject(); + if (!user.contains("id")) { + return ErrorCode::InternalError; + } + clientId = user["id"].toString(); + } else { + clientId = protocolConfig.value(config_key::clientId).toString(); + } + int row; bool clientExists = false; - QString clientId = protocolConfig.value(config_key::clientId).toString(); for (row = 0; row < rowCount(); row++) { auto client = m_clientsTable.at(row).toObject(); if (clientId == client.value(configKey::clientId).toString()) { @@ -487,11 +668,28 @@ ErrorCode ClientManagementModel::revokeClient(const QJsonObject &containerConfig return errorCode; } - if (container == DockerContainer::OpenVpn || container == DockerContainer::ShadowSocks || container == DockerContainer::Cloak) { + switch (container) + { + case DockerContainer::OpenVpn: + case DockerContainer::ShadowSocks: + case DockerContainer::Cloak: { errorCode = revokeOpenVpn(row, container, credentials, serverIndex, serverController); - } else if (container == DockerContainer::WireGuard || container == DockerContainer::Awg) { - errorCode = revokeWireGuard(row, container, credentials, serverController); + break; } + case DockerContainer::WireGuard: + case DockerContainer::Awg: { + errorCode = revokeWireGuard(row, container, credentials, serverController); + break; + } + case DockerContainer::Xray: { + errorCode = revokeXray(row, container, credentials, serverController); + break; + } + default: + logger.error() << "Internal error: received unexpected container type"; + return ErrorCode::InternalError; + } + return errorCode; } @@ -594,6 +792,117 @@ ErrorCode ClientManagementModel::revokeWireGuard(const int row, const DockerCont return ErrorCode::NoError; } +ErrorCode ClientManagementModel::revokeXray(const int row, + const DockerContainer container, + const ServerCredentials &credentials, + const QSharedPointer &serverController) +{ + 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 ClientManagementModel::roleNames() const { QHash roles; @@ -604,4 +913,4 @@ QHash ClientManagementModel::roleNames() const roles[DataSentRole] = "dataSent"; roles[AllowedIpsRole] = "allowedIps"; return roles; -} +} \ No newline at end of file diff --git a/client/ui/models/clientManagementModel.h b/client/ui/models/clientManagementModel.h index 60132abe..989120a9 100644 --- a/client/ui/models/clientManagementModel.h +++ b/client/ui/models/clientManagementModel.h @@ -40,6 +40,8 @@ public slots: const QSharedPointer &serverController); ErrorCode appendClient(const DockerContainer container, const ServerCredentials &credentials, const QJsonObject &containerConfig, const QString &clientName, const QSharedPointer &serverController); + ErrorCode appendClient(QJsonObject &protocolConfig, const QString &clientName,const DockerContainer container, + const ServerCredentials &credentials, const QSharedPointer &serverController); ErrorCode appendClient(const QString &clientId, const QString &clientName, const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController); ErrorCode renameClient(const int row, const QString &userName, const DockerContainer container, const ServerCredentials &credentials, @@ -64,11 +66,15 @@ private: const QSharedPointer &serverController); ErrorCode revokeWireGuard(const int row, const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController); + ErrorCode revokeXray(const int row, const DockerContainer container, const ServerCredentials &credentials, + const QSharedPointer &serverController); ErrorCode getOpenVpnClients(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, int &count); ErrorCode getWireGuardClients(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, int &count); + ErrorCode getXrayClients(const DockerContainer container, const ServerCredentials& credentials, + const QSharedPointer &serverController, int &count); ErrorCode wgShow(const DockerContainer container, const ServerCredentials &credentials, const QSharedPointer &serverController, std::vector &data); diff --git a/client/ui/models/protocols/awgConfigModel.cpp b/client/ui/models/protocols/awgConfigModel.cpp index 3a245ebe..860c8395 100644 --- a/client/ui/models/protocols/awgConfigModel.cpp +++ b/client/ui/models/protocols/awgConfigModel.cpp @@ -21,6 +21,7 @@ bool AwgConfigModel::setData(const QModelIndex &index, const QVariant &value, in } switch (role) { + case Roles::SubnetAddressRole: m_serverProtocolConfig.insert(config_key::subnet_address, value.toString()); break; case Roles::PortRole: m_serverProtocolConfig.insert(config_key::port, value.toString()); break; case Roles::ClientMtuRole: m_clientProtocolConfig.insert(config_key::mtu, value.toString()); break; @@ -58,6 +59,7 @@ QVariant AwgConfigModel::data(const QModelIndex &index, int role) const } switch (role) { + case Roles::SubnetAddressRole: return m_serverProtocolConfig.value(config_key::subnet_address).toString(); case Roles::PortRole: return m_serverProtocolConfig.value(config_key::port).toString(); case Roles::ClientMtuRole: return m_clientProtocolConfig.value(config_key::mtu); @@ -92,6 +94,7 @@ void AwgConfigModel::updateModel(const QJsonObject &config) m_serverProtocolConfig.insert(config_key::transport_proto, serverProtocolConfig.value(config_key::transport_proto).toString(defaultTransportProto)); m_serverProtocolConfig[config_key::last_config] = serverProtocolConfig.value(config_key::last_config); + m_serverProtocolConfig[config_key::subnet_address] = serverProtocolConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress); m_serverProtocolConfig[config_key::port] = serverProtocolConfig.value(config_key::port).toString(protocols::awg::defaultPort); m_serverProtocolConfig[config_key::junkPacketCount] = serverProtocolConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount); @@ -168,6 +171,7 @@ QHash AwgConfigModel::roleNames() const { QHash roles; + roles[SubnetAddressRole] = "subnetAddress"; roles[PortRole] = "port"; roles[ClientMtuRole] = "clientMtu"; @@ -197,6 +201,7 @@ AwgConfig::AwgConfig(const QJsonObject &serverProtocolConfig) clientJunkPacketMinSize = clientProtocolConfig.value(config_key::junkPacketMinSize).toString(protocols::awg::defaultJunkPacketMinSize); clientJunkPacketMaxSize = clientProtocolConfig.value(config_key::junkPacketMaxSize).toString(protocols::awg::defaultJunkPacketMaxSize); + subnetAddress = serverProtocolConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress); port = serverProtocolConfig.value(config_key::port).toString(protocols::awg::defaultPort); serverJunkPacketCount = serverProtocolConfig.value(config_key::junkPacketCount).toString(protocols::awg::defaultJunkPacketCount); serverJunkPacketMinSize = serverProtocolConfig.value(config_key::junkPacketMinSize).toString(protocols::awg::defaultJunkPacketMinSize); @@ -216,7 +221,7 @@ AwgConfig::AwgConfig(const QJsonObject &serverProtocolConfig) bool AwgConfig::hasEqualServerSettings(const AwgConfig &other) const { - if (port != other.port || serverJunkPacketCount != other.serverJunkPacketCount + if (subnetAddress != other.subnetAddress || port != other.port || serverJunkPacketCount != other.serverJunkPacketCount || serverJunkPacketMinSize != other.serverJunkPacketMinSize || serverJunkPacketMaxSize != other.serverJunkPacketMaxSize || serverInitPacketJunkSize != other.serverInitPacketJunkSize || serverResponsePacketJunkSize != other.serverResponsePacketJunkSize || serverInitPacketMagicHeader != other.serverInitPacketMagicHeader diff --git a/client/ui/models/protocols/awgConfigModel.h b/client/ui/models/protocols/awgConfigModel.h index 06475bf5..c1f8bb27 100644 --- a/client/ui/models/protocols/awgConfigModel.h +++ b/client/ui/models/protocols/awgConfigModel.h @@ -15,6 +15,7 @@ struct AwgConfig { AwgConfig(const QJsonObject &jsonConfig); + QString subnetAddress; QString port; QString clientMtu; @@ -43,7 +44,8 @@ class AwgConfigModel : public QAbstractListModel public: enum Roles { - PortRole = Qt::UserRole + 1, + SubnetAddressRole = Qt::UserRole + 1, + PortRole, ClientMtuRole, ClientJunkPacketCountRole, diff --git a/client/ui/models/protocols/wireguardConfigModel.cpp b/client/ui/models/protocols/wireguardConfigModel.cpp index 555915de..1c8e1341 100644 --- a/client/ui/models/protocols/wireguardConfigModel.cpp +++ b/client/ui/models/protocols/wireguardConfigModel.cpp @@ -21,6 +21,7 @@ bool WireGuardConfigModel::setData(const QModelIndex &index, const QVariant &val } switch (role) { + case Roles::SubnetAddressRole: m_serverProtocolConfig.insert(config_key::subnet_address, value.toString()); break; case Roles::PortRole: m_serverProtocolConfig.insert(config_key::port, value.toString()); break; case Roles::ClientMtuRole: m_clientProtocolConfig.insert(config_key::mtu, value.toString()); break; } @@ -36,6 +37,7 @@ QVariant WireGuardConfigModel::data(const QModelIndex &index, int role) const } switch (role) { + case Roles::SubnetAddressRole: return m_serverProtocolConfig.value(config_key::subnet_address).toString(); case Roles::PortRole: return m_serverProtocolConfig.value(config_key::port).toString(); case Roles::ClientMtuRole: return m_clientProtocolConfig.value(config_key::mtu); } @@ -56,6 +58,7 @@ void WireGuardConfigModel::updateModel(const QJsonObject &config) m_serverProtocolConfig.insert(config_key::transport_proto, serverProtocolConfig.value(config_key::transport_proto).toString(defaultTransportProto)); m_serverProtocolConfig[config_key::last_config] = serverProtocolConfig.value(config_key::last_config); + m_serverProtocolConfig[config_key::subnet_address] = serverProtocolConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress); m_serverProtocolConfig[config_key::port] = serverProtocolConfig.value(config_key::port).toString(protocols::wireguard::defaultPort); auto lastConfig = m_serverProtocolConfig.value(config_key::last_config).toString(); @@ -96,6 +99,7 @@ QHash WireGuardConfigModel::roleNames() const { QHash roles; + roles[SubnetAddressRole] = "subnetAddress"; roles[PortRole] = "port"; roles[ClientMtuRole] = "clientMtu"; @@ -108,12 +112,13 @@ WgConfig::WgConfig(const QJsonObject &serverProtocolConfig) QJsonObject clientProtocolConfig = QJsonDocument::fromJson(lastConfig.toUtf8()).object(); clientMtu = clientProtocolConfig[config_key::mtu].toString(protocols::wireguard::defaultMtu); + subnetAddress = serverProtocolConfig.value(config_key::subnet_address).toString(protocols::wireguard::defaultSubnetAddress); port = serverProtocolConfig.value(config_key::port).toString(protocols::wireguard::defaultPort); } bool WgConfig::hasEqualServerSettings(const WgConfig &other) const { - if (port != other.port) { + if (subnetAddress != other.subnetAddress || port != other.port) { return false; } return true; diff --git a/client/ui/models/protocols/wireguardConfigModel.h b/client/ui/models/protocols/wireguardConfigModel.h index a02bea5a..b1ce2d61 100644 --- a/client/ui/models/protocols/wireguardConfigModel.h +++ b/client/ui/models/protocols/wireguardConfigModel.h @@ -10,6 +10,7 @@ struct WgConfig { WgConfig(const QJsonObject &jsonConfig); + QString subnetAddress; QString port; QString clientMtu; @@ -24,7 +25,8 @@ class WireGuardConfigModel : public QAbstractListModel public: enum Roles { - PortRole = Qt::UserRole + 1, + SubnetAddressRole = Qt::UserRole + 1, + PortRole, ClientMtuRole }; diff --git a/client/ui/models/servers_model.cpp b/client/ui/models/servers_model.cpp index c87499a7..b72b10c3 100644 --- a/client/ui/models/servers_model.cpp +++ b/client/ui/models/servers_model.cpp @@ -22,7 +22,7 @@ namespace constexpr char serviceProtocol[] = "service_protocol"; constexpr char publicKeyInfo[] = "public_key"; - constexpr char endDate[] = "end_date"; + constexpr char expiresAt[] = "expires_at"; } } @@ -39,6 +39,9 @@ ServersModel::ServersModel(std::shared_ptr settings, QObject *parent) emit ServersModel::defaultServerNameChanged(); updateDefaultServerContainersModel(); }); + + connect(this, &ServersModel::processedServerIndexChanged, this, &ServersModel::processedServerChanged); + connect(this, &ServersModel::dataChanged, this, &ServersModel::processedServerChanged); } int ServersModel::rowCount(const QModelIndex &parent) const @@ -79,6 +82,12 @@ bool ServersModel::setData(const QModelIndex &index, const QVariant &value, int return true; } +bool ServersModel::setData(const int index, const QVariant &value, int role) +{ + QModelIndex modelIndex = this->index(index); + return setData(modelIndex, value, role); +} + QVariant ServersModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(m_servers.size())) { @@ -679,6 +688,18 @@ QVariant ServersModel::getProcessedServerData(const QString roleString) return {}; } +bool ServersModel::setProcessedServerData(const QString &roleString, const QVariant &value) +{ + const auto roles = roleNames(); + for (auto it = roles.begin(); it != roles.end(); it++) { + if (QString(it.value()) == roleString) { + return setData(m_processedServerIndex, value, it.key()); + } + } + + return false; +} + bool ServersModel::isDefaultServerDefaultContainerHasSplitTunneling() { auto server = m_servers.at(m_defaultServerIndex).toObject(); @@ -718,9 +739,9 @@ bool ServersModel::isApiKeyExpired(const int serverIndex) auto apiConfig = serverConfig.value(configKey::apiConfig).toObject(); auto publicKeyInfo = apiConfig.value(configKey::publicKeyInfo).toObject(); - const QString endDate = publicKeyInfo.value(configKey::endDate).toString(); - if (endDate.isEmpty()) { - publicKeyInfo.insert(configKey::endDate, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate)); + const QString expiresAt = publicKeyInfo.value(configKey::expiresAt).toString(); + if (expiresAt.isEmpty()) { + publicKeyInfo.insert(configKey::expiresAt, QDateTime::currentDateTimeUtc().addDays(1).toString(Qt::ISODate)); apiConfig.insert(configKey::publicKeyInfo, publicKeyInfo); serverConfig.insert(configKey::apiConfig, apiConfig); editServer(serverConfig, serverIndex); @@ -728,8 +749,8 @@ bool ServersModel::isApiKeyExpired(const int serverIndex) return false; } - auto endDateDateTime = QDateTime::fromString(endDate, Qt::ISODate).toUTC(); - if (endDateDateTime < QDateTime::currentDateTimeUtc()) { + auto expiresAtDateTime = QDateTime::fromString(expiresAt, Qt::ISODate).toUTC(); + if (expiresAtDateTime < QDateTime::currentDateTimeUtc()) { return true; } return false; diff --git a/client/ui/models/servers_model.h b/client/ui/models/servers_model.h index 0f18ea30..78bc22cc 100644 --- a/client/ui/models/servers_model.h +++ b/client/ui/models/servers_model.h @@ -46,6 +46,7 @@ public: int rowCount(const QModelIndex &parent = QModelIndex()) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + bool setData(const int index, const QVariant &value, int role = Qt::EditRole); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const int index, int role = Qt::DisplayRole) const; @@ -115,6 +116,7 @@ public slots: QVariant getDefaultServerData(const QString roleString); QVariant getProcessedServerData(const QString roleString); + bool setProcessedServerData(const QString &roleString, const QVariant &value); bool isDefaultServerDefaultContainerHasSplitTunneling(); @@ -127,6 +129,9 @@ protected: signals: void processedServerIndexChanged(const int index); + // emitted when the processed server index or processed server data is changed + void processedServerChanged(); + void defaultServerIndexChanged(const int index); void defaultServerNameChanged(); void defaultServerDescriptionChanged(); diff --git a/client/ui/qml/Components/AdLabel.qml b/client/ui/qml/Components/AdLabel.qml new file mode 100644 index 00000000..4133a01c --- /dev/null +++ b/client/ui/qml/Components/AdLabel.qml @@ -0,0 +1,72 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Shapes +import Qt5Compat.GraphicalEffects + +import Style 1.0 + +import "../Config" +import "../Controls2" +import "../Controls2/TextTypes" + +Rectangle { + id: root + + property real contentHeight: ad.implicitHeight + ad.anchors.topMargin + ad.anchors.bottomMargin + + border.width: 1 + border.color: AmneziaStyle.color.goldenApricot + color: AmneziaStyle.color.transparent + radius: 13 + + visible: GC.isDesktop() && ServersModel.isDefaultServerFromApi + && ServersModel.isDefaultServerDefaultContainerHasSplitTunneling && SettingsController.isHomeAdLabelVisible + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + + onClicked: function() { + Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl() + "/premium") + } + } + + RowLayout { + id: ad + anchors.fill: parent + anchors.margins: 16 + + Image { + source: "qrc:/images/controls/amnezia.svg" + sourceSize: Qt.size(36, 36) + + layer { + effect: ColorOverlay { + color: AmneziaStyle.color.paleGray + } + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.rightMargin: 10 + Layout.leftMargin: 10 + + text: qsTr("Amnezia Premium - for access to any website") + color: AmneziaStyle.color.pearlGray + + lineHeight: 18 + font.pixelSize: 15 + } + + ImageButtonType { + image: "qrc:/images/controls/close.svg" + imageColor: AmneziaStyle.color.paleGray + + onClicked: function() { + SettingsController.disableHomeAdLabel() + } + } + } +} diff --git a/client/ui/qml/Components/ConnectButton.qml b/client/ui/qml/Components/ConnectButton.qml index fa18703b..b90891a0 100644 --- a/client/ui/qml/Components/ConnectButton.qml +++ b/client/ui/qml/Components/ConnectButton.qml @@ -16,6 +16,32 @@ Button { property string connectedButtonColor: AmneziaStyle.color.goldenApricot property bool buttonActiveFocus: activeFocus && (Qt.platform.os !== "android" || SettingsController.isOnTv()) + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + implicitWidth: 190 implicitHeight: 190 diff --git a/client/ui/qml/Components/ConnectionTypeSelectionDrawer.qml b/client/ui/qml/Components/ConnectionTypeSelectionDrawer.qml index 23fe0d2a..c9124d81 100644 --- a/client/ui/qml/Components/ConnectionTypeSelectionDrawer.qml +++ b/client/ui/qml/Components/ConnectionTypeSelectionDrawer.qml @@ -14,7 +14,7 @@ DrawerType2 { width: parent.width height: parent.height - expandedContent: ColumnLayout { + expandedStateContent: ColumnLayout { id: content anchors.top: parent.top @@ -26,14 +26,6 @@ DrawerType2 { root.expandedHeight = content.implicitHeight + 32 } - Connections { - target: root - enabled: !GC.isMobile() - function onOpened() { - focusItem.forceActiveFocus() - } - } - Header2Type { Layout.fillWidth: true Layout.topMargin: 24 @@ -44,11 +36,6 @@ DrawerType2 { headerText: qsTr("Add new connection") } - Item { - id: focusItem - KeyNavigation.tab: ip.rightButton - } - LabelWithButtonType { id: ip Layout.fillWidth: true @@ -59,10 +46,8 @@ DrawerType2 { clickedFunction: function() { PageController.goToPage(PageEnum.PageSetupWizardCredentials) - root.close() + root.closeTriggered() } - - KeyNavigation.tab: qrCode.rightButton } DividerType {} @@ -76,10 +61,8 @@ DrawerType2 { clickedFunction: function() { PageController.goToPage(PageEnum.PageSetupWizardConfigSource) - root.close() + root.closeTriggered() } - - KeyNavigation.tab: focusItem } DividerType {} diff --git a/client/ui/qml/Components/HomeContainersListView.qml b/client/ui/qml/Components/HomeContainersListView.qml index b0e074d0..337918f1 100644 --- a/client/ui/qml/Components/HomeContainersListView.qml +++ b/client/ui/qml/Components/HomeContainersListView.qml @@ -17,55 +17,15 @@ ListView { property var rootWidth property var selectedText - property bool a: true - width: rootWidth - height: menuContent.contentItem.height + height: contentItem.height clip: true - interactive: false + snapMode: ListView.SnapToItem - property FlickableType parentFlickable - property var lastItemTabClicked + ScrollBar.vertical: ScrollBarType {} - property int currentFocusIndex: 0 - - activeFocusOnTab: true - onActiveFocusChanged: { - if (activeFocus) { - this.currentFocusIndex = 0 - this.itemAtIndex(currentFocusIndex).forceActiveFocus() - } - } - - Keys.onTabPressed: { - if (currentFocusIndex < this.count - 1) { - currentFocusIndex += 1 - this.itemAtIndex(currentFocusIndex).forceActiveFocus() - } else { - currentFocusIndex = 0 - if (lastItemTabClicked && typeof lastItemTabClicked === "function") { - lastItemTabClicked() - } - } - } - - onVisibleChanged: { - if (visible) { - currentFocusIndex = 0 - focusItem.forceActiveFocus() - } - } - - Item { - id: focusItem - } - - onCurrentFocusIndexChanged: { - if (parentFlickable) { - parentFlickable.ensureVisible(this.itemAtIndex(currentFocusIndex)) - } - } + property bool isFocusable: true ButtonGroup { id: containersRadioButtonGroup @@ -75,12 +35,6 @@ ListView { implicitWidth: rootWidth implicitHeight: content.implicitHeight - onActiveFocusChanged: { - if (activeFocus) { - containerRadioButton.forceActiveFocus() - } - } - ColumnLayout { id: content @@ -111,13 +65,13 @@ ListView { } if (checked) { - containersDropDown.close() + containersDropDown.closeTriggered() ServersModel.setDefaultContainer(ServersModel.defaultIndex, proxyDefaultServerContainersModel.mapToSource(index)) } else { ContainersModel.setProcessedContainerIndex(proxyDefaultServerContainersModel.mapToSource(index)) InstallController.setShouldCreateServer(false) PageController.goToPage(PageEnum.PageSetupWizardProtocolSettings) - containersDropDown.close() + containersDropDown.closeTriggered() } } diff --git a/client/ui/qml/Components/HomeSplitTunnelingDrawer.qml b/client/ui/qml/Components/HomeSplitTunnelingDrawer.qml index 405d4eda..097274a4 100644 --- a/client/ui/qml/Components/HomeSplitTunnelingDrawer.qml +++ b/client/ui/qml/Components/HomeSplitTunnelingDrawer.qml @@ -16,7 +16,7 @@ DrawerType2 { anchors.fill: parent expandedHeight: parent.height * 0.9 - expandedContent: ColumnLayout { + expandedStateContent: ColumnLayout { id: content anchors.top: parent.top @@ -24,14 +24,6 @@ DrawerType2 { anchors.right: parent.right spacing: 0 - Connections { - target: root - enabled: !GC.isMobile() - function onOpened() { - focusItem.forceActiveFocus() - } - } - Header2Type { Layout.fillWidth: true Layout.topMargin: 24 @@ -43,11 +35,6 @@ DrawerType2 { descriptionText: qsTr("Allows you to connect to some sites or applications through a VPN connection and bypass others") } - Item { - id: focusItem - KeyNavigation.tab: splitTunnelingSwitch.visible ? splitTunnelingSwitch : siteBasedSplitTunnelingSwitch.rightButton - } - LabelWithButtonType { id: splitTunnelingSwitch Layout.fillWidth: true @@ -59,11 +46,9 @@ DrawerType2 { descriptionText: qsTr("Enabled \nCan't be disabled for current server") rightImageSource: "qrc:/images/controls/chevron-right.svg" - KeyNavigation.tab: siteBasedSplitTunnelingSwitch.visible ? siteBasedSplitTunnelingSwitch.rightButton : focusItem - clickedFunction: function() { -// PageController.goToPage(PageEnum.PageSettingsSplitTunneling) -// root.close() + PageController.goToPage(PageEnum.PageSettingsSplitTunneling) + root.closeTriggered() } } @@ -80,13 +65,9 @@ DrawerType2 { descriptionText: enabled && SitesModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled") rightImageSource: "qrc:/images/controls/chevron-right.svg" - KeyNavigation.tab: appSplitTunnelingSwitch.visible ? - appSplitTunnelingSwitch.rightButton : - focusItem - clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsSplitTunneling) - root.close() + root.closeTriggered() } } @@ -103,11 +84,9 @@ DrawerType2 { descriptionText: AppSplitTunnelingModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled") rightImageSource: "qrc:/images/controls/chevron-right.svg" - KeyNavigation.tab: focusItem - clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsAppSplitTunneling) - root.close() + root.closeTriggered() } } diff --git a/client/ui/qml/Components/InstalledAppsDrawer.qml b/client/ui/qml/Components/InstalledAppsDrawer.qml index e5d10055..5835e1c6 100644 --- a/client/ui/qml/Components/InstalledAppsDrawer.qml +++ b/client/ui/qml/Components/InstalledAppsDrawer.qml @@ -26,7 +26,7 @@ DrawerType2 { id: installedAppsModel } - expandedContent: Item { + expandedStateContent: Item { id: container implicitHeight: expandedHeight @@ -43,7 +43,7 @@ DrawerType2 { BackButtonType { backButtonImage: "qrc:/images/controls/arrow-left.svg" backButtonFunction: function() { - root.close() + root.closeTriggered() } } @@ -69,6 +69,8 @@ DrawerType2 { clip: true interactive: true + property bool isFocusable: true + model: SortFilterProxyModel { id: proxyInstalledAppsModel sourceModel: installedAppsModel @@ -79,10 +81,7 @@ DrawerType2 { } } - ScrollBar.vertical: ScrollBar { - id: scrollBar - policy: ScrollBar.AlwaysOn - } + ScrollBar.vertical: ScrollBarType {} ButtonGroup { id: buttonGroup @@ -155,7 +154,7 @@ DrawerType2 { PageController.showBusyIndicator(true) AppSplitTunnelingController.addApps(installedAppsModel.getSelectedAppsInfo()) PageController.showBusyIndicator(false) - root.close() + root.closeTriggered() } } } diff --git a/client/ui/qml/Components/QuestionDrawer.qml b/client/ui/qml/Components/QuestionDrawer.qml index a0e86dbc..0c14e52d 100644 --- a/client/ui/qml/Components/QuestionDrawer.qml +++ b/client/ui/qml/Components/QuestionDrawer.qml @@ -20,7 +20,7 @@ DrawerType2 { property var yesButtonFunction property var noButtonFunction - expandedContent: ColumnLayout { + expandedStateContent: ColumnLayout { id: content anchors.top: parent.top @@ -33,14 +33,6 @@ DrawerType2 { root.expandedHeight = content.implicitHeight + 32 } - Connections { - target: root - enabled: !GC.isMobile() - function onOpened() { - focusItem.forceActiveFocus() - } - } - Header2TextType { Layout.fillWidth: true Layout.topMargin: 16 @@ -59,11 +51,6 @@ DrawerType2 { text: descriptionText } - Item { - id: focusItem - KeyNavigation.tab: yesButton - } - BasicButtonType { id: yesButton Layout.fillWidth: true @@ -78,8 +65,6 @@ DrawerType2 { yesButtonFunction() } } - - KeyNavigation.tab: noButton } BasicButtonType { @@ -102,8 +87,6 @@ DrawerType2 { noButtonFunction() } } - - KeyNavigation.tab: focusItem } } } diff --git a/client/ui/qml/Components/SelectLanguageDrawer.qml b/client/ui/qml/Components/SelectLanguageDrawer.qml index 4d9d7f0e..2c026848 100644 --- a/client/ui/qml/Components/SelectLanguageDrawer.qml +++ b/client/ui/qml/Components/SelectLanguageDrawer.qml @@ -11,7 +11,7 @@ import "../Config" DrawerType2 { id: root - expandedContent: Item { + expandedStateContent: Item { id: container implicitHeight: root.height * 0.9 @@ -20,19 +20,6 @@ DrawerType2 { root.expandedHeight = container.implicitHeight } - Connections { - target: root - enabled: !GC.isMobile() - function onOpened() { - focusItem.forceActiveFocus() - } - } - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -43,167 +30,148 @@ DrawerType2 { BackButtonType { id: backButton + + Layout.fillWidth: true + backButtonImage: "qrc:/images/controls/arrow-left.svg" - backButtonFunction: function() { root.close() } - KeyNavigation.tab: listView + backButtonFunction: function() { root.closeTriggered() } + } + + Header2Type { + id: header + + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + + headerText: qsTr("Choose language") } } - FlickableType { + ListView { + id: listView + anchors.top: backButtonLayout.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - contentHeight: content.implicitHeight - ColumnLayout { - id: content + property bool isFocusable: true + property int selectedIndex: LanguageModel.currentLanguageIndex - anchors.fill: parent + clip: true + reuseItems: true - Header2Type { - id: header - Layout.fillWidth: true - Layout.topMargin: 16 - Layout.rightMargin: 16 - Layout.leftMargin: 16 + ScrollBar.vertical: ScrollBarType {} - headerText: qsTr("Choose language") - } + model: LanguageModel - ListView { - id: listView + ButtonGroup { + id: buttonGroup + } - Layout.fillWidth: true - height: listView.contentItem.height + delegate: Item { + implicitWidth: root.width + implicitHeight: delegateContent.implicitHeight - clip: true - interactive: false + ColumnLayout { + id: delegateContent - model: LanguageModel - currentIndex: LanguageModel.currentLanguageIndex + anchors.fill: parent - ButtonGroup { - id: buttonGroup - } + RadioButton { + id: radioButton - property int currentFocusIndex: 0 + implicitWidth: parent.width + implicitHeight: radioButtonContent.implicitHeight - activeFocusOnTab: true - onActiveFocusChanged: { - if (activeFocus) { - this.currentFocusIndex = 0 - this.itemAtIndex(currentFocusIndex).forceActiveFocus() - } - } + hoverEnabled: true - Keys.onTabPressed: { - if (currentFocusIndex < this.count - 1) { - currentFocusIndex += 1 - this.itemAtIndex(currentFocusIndex).forceActiveFocus() - } else { - listViewFocusItem.forceActiveFocus() - focusItem.forceActiveFocus() - } - } + property bool isFocusable: true - Item { - id: listViewFocusItem Keys.onTabPressed: { - root.forceActiveFocus() + FocusController.nextKeyTabItem() } - } - onVisibleChanged: { - if (visible) { - listViewFocusItem.forceActiveFocus() - focusItem.forceActiveFocus() + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() } - } - delegate: Item { - implicitWidth: root.width - implicitHeight: delegateContent.implicitHeight + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } - onActiveFocusChanged: { - if (activeFocus) { - radioButton.forceActiveFocus() + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + + indicator: Rectangle { + width: parent.width - 1 + height: parent.height + color: radioButton.hovered ? AmneziaStyle.color.slateGray : AmneziaStyle.color.onyxBlack + border.color: radioButton.focus ? AmneziaStyle.color.paleGray : AmneziaStyle.color.transparent + border.width: radioButton.focus ? 1 : 0 + + Behavior on color { + PropertyAnimation { duration: 200 } + } + Behavior on border.color { + PropertyAnimation { duration: 200 } } } - ColumnLayout { - id: delegateContent - + RowLayout { + id: radioButtonContent anchors.fill: parent - RadioButton { - id: radioButton + anchors.rightMargin: 16 + anchors.leftMargin: 16 - implicitWidth: parent.width - implicitHeight: radioButtonContent.implicitHeight + spacing: 0 - hoverEnabled: true + z: 1 - indicator: Rectangle { - width: parent.width - 1 - height: parent.height - color: radioButton.hovered ? AmneziaStyle.color.slateGray : AmneziaStyle.color.onyxBlack - border.color: radioButton.focus ? AmneziaStyle.color.paleGray : AmneziaStyle.color.transparent - border.width: radioButton.focus ? 1 : 0 + ParagraphTextType { + Layout.fillWidth: true + Layout.topMargin: 20 + Layout.bottomMargin: 20 - Behavior on color { - PropertyAnimation { duration: 200 } - } - Behavior on border.color { - PropertyAnimation { duration: 200 } - } - } + text: languageName + } - RowLayout { - id: radioButtonContent - anchors.fill: parent + Image { + source: "qrc:/images/controls/check.svg" + visible: radioButton.checked - anchors.rightMargin: 16 - anchors.leftMargin: 16 + width: 24 + height: 24 - spacing: 0 - - z: 1 - - ParagraphTextType { - Layout.fillWidth: true - Layout.topMargin: 20 - Layout.bottomMargin: 20 - - text: languageName - } - - Image { - source: "qrc:/images/controls/check.svg" - visible: radioButton.checked - - width: 24 - height: 24 - - Layout.rightMargin: 8 - } - } - - ButtonGroup.group: buttonGroup - checked: listView.currentIndex === index - - onClicked: { - listView.currentIndex = index - LanguageModel.changeLanguage(languageIndex) - root.close() - } + Layout.rightMargin: 8 } } - Keys.onEnterPressed: radioButton.clicked() - Keys.onReturnPressed: radioButton.clicked() + ButtonGroup.group: buttonGroup + checked: listView.selectedIndex === index + + onClicked: { + listView.selectedIndex = index + LanguageModel.changeLanguage(languageIndex) + root.closeTriggered() + } } } + + Keys.onEnterPressed: radioButton.clicked() + Keys.onReturnPressed: radioButton.clicked() } } } diff --git a/client/ui/qml/Components/ServersListView.qml b/client/ui/qml/Components/ServersListView.qml new file mode 100644 index 00000000..dc5c5a33 --- /dev/null +++ b/client/ui/qml/Components/ServersListView.qml @@ -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 + } + } + } +} diff --git a/client/ui/qml/Components/SettingsContainersListView.qml b/client/ui/qml/Components/SettingsContainersListView.qml index 769e1abd..9e672130 100644 --- a/client/ui/qml/Components/SettingsContainersListView.qml +++ b/client/ui/qml/Components/SettingsContainersListView.qml @@ -20,47 +20,14 @@ ListView { height: root.contentItem.height clip: true - interactive: false + reuseItems: true - activeFocusOnTab: true - Keys.onTabPressed: { - if (currentIndex < this.count - 1) { - this.incrementCurrentIndex() - } else { - currentIndex = 0 - lastItemTabClickedSignal() - } - } - - onCurrentIndexChanged: { - if (visible) { - if (fl.contentHeight > fl.height) { - var item = this.currentItem - if (item.y < fl.height) { - fl.contentY = item.y - } else if (item.y + item.height > fl.contentY + fl.height) { - fl.contentY = item.y + item.height - fl.height - } - } - } - } - - onVisibleChanged: { - if (visible) { - this.currentIndex = 0 - } - } + property bool isFocusable: false delegate: Item { implicitWidth: root.width implicitHeight: delegateContent.implicitHeight - onActiveFocusChanged: { - if (activeFocus) { - containerRadioButton.rightButton.forceActiveFocus() - } - } - ColumnLayout { id: delegateContent diff --git a/client/ui/qml/Components/ShareConnectionDrawer.qml b/client/ui/qml/Components/ShareConnectionDrawer.qml index 3235ad0a..f98944f0 100644 --- a/client/ui/qml/Components/ShareConnectionDrawer.qml +++ b/client/ui/qml/Components/ShareConnectionDrawer.qml @@ -36,17 +36,9 @@ DrawerType2 { configFileName = "amnezia_config" } - expandedContent: Item { + expandedStateContent: Item { implicitHeight: root.expandedHeight - Connections { - target: root - enabled: !GC.isMobile() - function onOpened() { - header.forceActiveFocus() - } - } - Header2Type { id: header anchors.top: parent.top @@ -57,24 +49,27 @@ DrawerType2 { anchors.rightMargin: 16 headerText: root.headerText - - KeyNavigation.tab: shareButton } - FlickableType { + ListView { + id: listView + anchors.top: header.bottom anchors.bottom: parent.bottom - contentHeight: content.height + 32 + anchors.left: parent.left + anchors.right: parent.right - ColumnLayout { - id: content + property bool isFocusable: true - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + ScrollBar.vertical: ScrollBarType {} - anchors.leftMargin: 16 - anchors.rightMargin: 16 + model: 1 + + clip: true + reuseItems: true + + header: ColumnLayout { + width: listView.width visible: root.contentVisible @@ -82,11 +77,11 @@ DrawerType2 { id: shareButton Layout.fillWidth: true Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 text: qsTr("Share") - imageSource: "qrc:/images/controls/share-2.svg" - - KeyNavigation.tab: copyConfigTextButton + leftImageSource: "qrc:/images/controls/share-2.svg" clickedFunc: function() { var fileName = "" @@ -111,6 +106,8 @@ DrawerType2 { id: copyConfigTextButton Layout.fillWidth: true Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 defaultColor: AmneziaStyle.color.transparent hoveredColor: AmneziaStyle.color.translucentWhite @@ -120,18 +117,18 @@ DrawerType2 { borderWidth: 1 text: qsTr("Copy") - imageSource: "qrc:/images/controls/copy.svg" + leftImageSource: "qrc:/images/controls/copy.svg" Keys.onReturnPressed: { copyConfigTextButton.clicked() } Keys.onEnterPressed: { copyConfigTextButton.clicked() } - - KeyNavigation.tab: copyNativeConfigStringButton.visible ? copyNativeConfigStringButton : showSettingsButton } BasicButtonType { id: copyNativeConfigStringButton Layout.fillWidth: true Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 visible: false @@ -143,7 +140,7 @@ DrawerType2 { borderWidth: 1 text: qsTr("Copy config string") - imageSource: "qrc:/images/controls/copy.svg" + leftImageSource: "qrc:/images/controls/copy.svg" KeyNavigation.tab: showSettingsButton } @@ -153,6 +150,8 @@ DrawerType2 { Layout.fillWidth: true Layout.topMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 defaultColor: AmneziaStyle.color.transparent hoveredColor: AmneziaStyle.color.translucentWhite @@ -164,10 +163,8 @@ DrawerType2 { text: qsTr("Show connection settings") clickedFunc: function() { - configContentDrawer.open() + configContentDrawer.openTriggered() } - - KeyNavigation.tab: header } DrawerType2 { @@ -178,30 +175,11 @@ DrawerType2 { anchors.fill: parent expandedHeight: parent.height * 0.9 - onClosed: { - if (!GC.isMobile()) { - header.forceActiveFocus() - } - } - - expandedContent: Item { + expandedStateContent: Item { id: configContentContainer implicitHeight: configContentDrawer.expandedHeight - Connections { - target: configContentDrawer - enabled: !GC.isMobile() - function onOpened() { - focusItem.forceActiveFocus() - } - } - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - Connections { target: copyNativeConfigStringButton function onClicked() { @@ -231,9 +209,7 @@ DrawerType2 { anchors.right: parent.right anchors.topMargin: 16 - backButtonFunction: function() { configContentDrawer.close() } - - KeyNavigation.tab: focusItem + backButtonFunction: function() { configContentDrawer.closeTriggered() } } FlickableType { @@ -302,6 +278,10 @@ DrawerType2 { } } } + } + + delegate: ColumnLayout { + width: listView.width Rectangle { id: qrCodeContainer @@ -309,6 +289,8 @@ DrawerType2 { Layout.fillWidth: true Layout.preferredHeight: width Layout.topMargin: 20 + Layout.leftMargin: 16 + Layout.rightMargin: 16 visible: ExportController.qrCodesCount > 0 @@ -320,6 +302,32 @@ DrawerType2 { source: ExportController.qrCodesCount ? ExportController.qrCodes[0] : "" + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + Timer { property int index: 0 interval: 1000 @@ -346,6 +354,8 @@ DrawerType2 { Layout.fillWidth: true Layout.topMargin: 24 Layout.bottomMargin: 32 + Layout.leftMargin: 16 + Layout.rightMargin: 16 visible: ExportController.qrCodesCount > 0 diff --git a/client/ui/qml/Components/TransportProtoSelector.qml b/client/ui/qml/Components/TransportProtoSelector.qml index e40dd4bb..323892fa 100644 --- a/client/ui/qml/Components/TransportProtoSelector.qml +++ b/client/ui/qml/Components/TransportProtoSelector.qml @@ -39,8 +39,6 @@ Rectangle { implicitWidth: (rootWidth - 32) / 2 text: "UDP" - KeyNavigation.tab: tcpButton - onClicked: { root.currentIndex = 0 } diff --git a/client/ui/qml/Controls2/BackButtonType.qml b/client/ui/qml/Controls2/BackButtonType.qml index 86fc86a2..40136ad5 100644 --- a/client/ui/qml/Controls2/BackButtonType.qml +++ b/client/ui/qml/Controls2/BackButtonType.qml @@ -4,7 +4,7 @@ import Qt5Compat.GraphicalEffects import Style 1.0 -Item { +FocusScope { id: root property string backButtonImage: "qrc:/images/controls/arrow-left.svg" @@ -15,12 +15,6 @@ Item { visible: backButtonImage !== "" - onActiveFocusChanged: { - if (activeFocus) { - backButton.forceActiveFocus() - } - } - RowLayout { id: content diff --git a/client/ui/qml/Controls2/BasicButtonType.qml b/client/ui/qml/Controls2/BasicButtonType.qml index 5c599013..b60e96cf 100644 --- a/client/ui/qml/Controls2/BasicButtonType.qml +++ b/client/ui/qml/Controls2/BasicButtonType.qml @@ -22,9 +22,10 @@ Button { property int borderWidth: 0 property int borderFocusedWidth: 1 - property string imageSource + property string leftImageSource property string rightImageSource property string leftImageColor: textColor + property bool changeLeftImageSize: true property bool squareLeftSide: false @@ -34,10 +35,35 @@ Button { property alias buttonTextLabel: buttonText + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + implicitHeight: 56 hoverEnabled: true - focusPolicy: Qt.TabFocus onFocusChanged: { if (root.activeFocus) { @@ -127,24 +153,29 @@ Button { anchors.centerIn: parent Image { - Layout.preferredHeight: 20 - Layout.preferredWidth: 20 - - source: root.imageSource - visible: root.imageSource === "" ? false : true + id: leftImage + source: root.leftImageSource + visible: root.leftImageSource === "" ? false : true layer { - enabled: true + enabled: leftImageColor !== "" ? true : false effect: ColorOverlay { color: leftImageColor } } + + Component.onCompleted: { + if (root.changeLeftImageSize) { + leftImage.Layout.preferredHeight = 20 + leftImage.Layout.preferredWidth = 20 + } + } } ButtonTextType { id: buttonText - color: textColor + color: root.textColor text: root.text visible: root.text === "" ? false : true diff --git a/client/ui/qml/Controls2/CardType.qml b/client/ui/qml/Controls2/CardType.qml index f584a8fc..8e689541 100644 --- a/client/ui/qml/Controls2/CardType.qml +++ b/client/ui/qml/Controls2/CardType.qml @@ -22,6 +22,7 @@ RadioButton { property string pressedBorderColor: AmneziaStyle.color.softGoldenApricot property string selectedBorderColor: AmneziaStyle.color.goldenApricot property string defaultBodredColor: AmneziaStyle.color.transparent + property string focusBorderColor: AmneziaStyle.color.paleGray property int borderWidth: 0 implicitWidth: content.implicitWidth @@ -29,6 +30,32 @@ RadioButton { hoverEnabled: true + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + indicator: Rectangle { anchors.fill: parent radius: 16 @@ -52,6 +79,8 @@ RadioButton { return pressedBorderColor } else if (root.checked) { return selectedBorderColor + } else if (root.activeFocus) { + return focusBorderColor } } return defaultBodredColor @@ -59,7 +88,7 @@ RadioButton { border.width: { if (root.enabled) { - if(root.checked) { + if(root.checked || root.activeFocus) { return 1 } return root.pressed ? 1 : 0 diff --git a/client/ui/qml/Controls2/CardWithIconsType.qml b/client/ui/qml/Controls2/CardWithIconsType.qml index fea65116..4277d735 100644 --- a/client/ui/qml/Controls2/CardWithIconsType.qml +++ b/client/ui/qml/Controls2/CardWithIconsType.qml @@ -25,10 +25,15 @@ Button { property real textOpacity: 1.0 + property alias focusItem: rightImage + + property FlickableType parentFlickable + hoverEnabled: true background: Rectangle { id: backgroundRect + anchors.fill: parent 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 { anchors.left: parent.left anchors.right: parent.right implicitHeight: content.implicitHeight + RowLayout { id: content + anchors.fill: parent Image { @@ -61,6 +84,7 @@ Button { } ColumnLayout { + ListItemTitleType { text: root.headerText visible: text !== "" @@ -123,6 +147,7 @@ Button { Rectangle { id: rightImageBackground + anchors.fill: parent radius: 12 color: "transparent" @@ -131,10 +156,9 @@ Button { PropertyAnimation { duration: 200 } } } + onClicked: { - if (clickedFunction && typeof clickedFunction === "function") { - clickedFunction() - } + root.clicked() } } } @@ -145,6 +169,7 @@ Button { cursorShape: Qt.PointingHandCursor hoverEnabled: true + enabled: root.enabled onEntered: { backgroundRect.color = root.hoveredColor diff --git a/client/ui/qml/Controls2/DrawerType2.qml b/client/ui/qml/Controls2/DrawerType2.qml index c4b584c1..e67e36a1 100644 --- a/client/ui/qml/Controls2/DrawerType2.qml +++ b/client/ui/qml/Controls2/DrawerType2.qml @@ -9,17 +9,14 @@ import "TextTypes" Item { id: root - readonly property string drawerExpanded: "expanded" - readonly property string drawerCollapsed: "collapsed" + readonly property string drawerExpandedStateName: "expanded" + readonly property string drawerCollapsedStateName: "collapsed" - readonly property bool isOpened: drawerContent.state === root.drawerExpanded || (drawerContent.state === root.drawerCollapsed && dragArea.drag.active === true) - readonly property bool isClosed: drawerContent.state === root.drawerCollapsed && dragArea.drag.active === false + readonly property bool isOpened: isExpandedStateActive() || (isCollapsedStateActive() && (dragArea.drag.active === true)) + readonly property bool isClosed: isCollapsedStateActive() && (dragArea.drag.active === false) - readonly property bool isExpanded: drawerContent.state === root.drawerExpanded - readonly property bool isCollapsed: drawerContent.state === root.drawerCollapsed - - property Component collapsedContent - property Component expandedContent + property Component collapsedStateContent + property Component expandedStateContent property string defaultColor: AmneziaStyle.color.onyxBlack property string borderColor: AmneziaStyle.color.slateGray @@ -29,29 +26,41 @@ Item { property int depthIndex: 0 - signal entered - signal exited + signal cursorEntered + signal cursorExited signal pressed(bool pressed, bool entered) signal aboutToHide signal aboutToShow - signal close - signal open + signal closeTriggered + signal openTriggered signal closed signal opened + function isExpandedStateActive() { + return isStateActive(drawerExpandedStateName) + } + + function isCollapsedStateActive() { + return isStateActive(drawerCollapsedStateName) + } + + function isStateActive(stateName) { + return drawerContent.state === stateName + } + Connections { target: PageController function onCloseTopDrawer() { if (depthIndex === PageController.getDrawerDepth()) { - if (isCollapsed) { + if (isCollapsedStateActive()) { return } aboutToHide() - drawerContent.state = root.drawerCollapsed + drawerContent.state = root.drawerCollapsedStateName depthIndex = 0 closed() } @@ -61,30 +70,52 @@ Item { Connections { target: root - function onClose() { - if (isCollapsed) { + function onCloseTriggered() { + if (isCollapsedStateActive()) { return } aboutToHide() - drawerContent.state = root.drawerCollapsed - depthIndex = 0 - PageController.setDrawerDepth(PageController.getDrawerDepth() - 1) closed() } - function onOpen() { - if (isExpanded) { + function onClosed() { + 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 } - aboutToShow() + root.aboutToShow() - drawerContent.state = root.drawerExpanded - depthIndex = PageController.getDrawerDepth() + 1 - PageController.setDrawerDepth(depthIndex) - opened() + root.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 anchors.fill: parent - color: root.isCollapsed ? AmneziaStyle.color.transparent : AmneziaStyle.color.translucentMidnightBlack + color: root.isCollapsedStateActive() ? AmneziaStyle.color.transparent : AmneziaStyle.color.translucentMidnightBlack Behavior on color { PropertyAnimation { duration: 200 } @@ -102,18 +133,17 @@ Item { MouseArea { id: emptyArea anchors.fill: parent - enabled: root.isExpanded - visible: enabled + onClicked: { - root.close() + root.closeTriggered() } } MouseArea { id: dragArea + objectName: "dragArea" anchors.fill: drawerContentBackground - cursorShape: root.isCollapsed ? Qt.PointingHandCursor : Qt.ArrowCursor hoverEnabled: true 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 */ onReleased: { - if (root.isCollapsed && drawerContent.y < dragArea.drag.maximumY) { - root.open() + if (isCollapsedStateActive() && drawerContent.y < dragArea.drag.maximumY) { + root.openTriggered() return } - if (root.isExpanded && drawerContent.y > dragArea.drag.minimumY) { - root.close() + if (isExpandedStateActive() && drawerContent.y > dragArea.drag.minimumY) { + root.closeTriggered() return } } onEntered: { - root.entered() + root.cursorEntered() } onExited: { - root.exited() + root.cursorExited() } onPressedChanged: { root.pressed(pressed, entered) } onClicked: { - if (root.isCollapsed) { - root.open() + if (isCollapsedStateActive()) { + root.openTriggered() } } } Rectangle { id: drawerContentBackground + objectName: "drawerContentBackground" anchors { left: drawerContent.left; right: drawerContent.right; top: drawerContent.top } height: root.height @@ -174,53 +205,80 @@ Item { Item { id: drawerContent + objectName: "drawerContent" Drag.active: dragArea.drag.active anchors.right: root.right anchors.left: root.left - y: root.height - drawerContent.height - state: root.drawerCollapsed - implicitHeight: root.isCollapsed ? collapsedHeight : expandedHeight - - 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 - } - } + state: root.drawerCollapsedStateName states: [ State { - name: root.drawerCollapsed + name: root.drawerCollapsedStateName PropertyChanges { target: drawerContent + implicitHeight: 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 { - name: root.drawerExpanded + name: root.drawerExpandedStateName PropertyChanges { target: drawerContent + implicitHeight: expandedHeight 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: [ Transition { - from: root.drawerCollapsed - to: root.drawerExpanded + from: root.drawerCollapsedStateName + to: root.drawerExpandedStateName PropertyAnimation { target: drawerContent properties: "y" @@ -228,8 +286,8 @@ Item { } }, Transition { - from: root.drawerExpanded - to: root.drawerCollapsed + from: root.drawerExpandedStateName + to: root.drawerCollapsedStateName PropertyAnimation { target: drawerContent properties: "y" @@ -241,7 +299,7 @@ Item { Loader { id: collapsedLoader - sourceComponent: root.collapsedContent + sourceComponent: root.collapsedStateContent anchors.right: parent.right anchors.left: parent.left @@ -250,8 +308,7 @@ Item { Loader { id: expandedLoader - visible: root.isExpanded - sourceComponent: root.expandedContent + sourceComponent: root.expandedStateContent anchors.right: parent.right anchors.left: parent.left diff --git a/client/ui/qml/Controls2/DropDownType.qml b/client/ui/qml/Controls2/DropDownType.qml index 906cfffe..ae6dac85 100644 --- a/client/ui/qml/Controls2/DropDownType.qml +++ b/client/ui/qml/Controls2/DropDownType.qml @@ -45,33 +45,56 @@ Item { property Item drawerParent property Component listView - signal open - signal close + signal openTriggered + signal closeTriggered - function popupClosedFunc() { - if (!GC.isMobile()) { - this.forceActiveFocus() - } + readonly property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() } - property var parentFlickable - onFocusChanged: { - if (root.activeFocus) { - if (root.parentFlickable) { - root.parentFlickable.ensureVisible(root) - } - } + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() } implicitWidth: rootButtonContent.implicitWidth implicitHeight: rootButtonContent.implicitHeight - onOpen: { - menu.open() + onOpenTriggered: { + menu.openTriggered() } - onClose: { - menu.close() + onCloseTriggered: { + menu.closeTriggered() + } + + Keys.onEnterPressed: { + if (menu.isClosed) { + menu.openTriggered() + } + } + + Keys.onReturnPressed: { + if (menu.isClosed) { + menu.openTriggered() + } } Rectangle { @@ -173,7 +196,7 @@ Item { if (rootButtonClickedFunction && typeof rootButtonClickedFunction === "function") { rootButtonClickedFunction() } else { - menu.open() + menu.openTriggered() } } } @@ -186,93 +209,38 @@ Item { anchors.fill: parent expandedHeight: drawerParent.height * drawerHeight - onClosed: { - root.popupClosedFunc() - } - - expandedContent: Item { + expandedStateContent: Item { id: container implicitHeight: menu.expandedHeight - Connections { - target: menu - enabled: !GC.isMobile() - function onOpened() { - focusItem.forceActiveFocus() - } - } - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: header - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + anchors.fill: parent anchors.topMargin: 16 BackButtonType { id: backButton backButtonImage: root.headerBackButtonImage - backButtonFunction: function() { menu.close() } - KeyNavigation.tab: listViewLoader.item + backButtonFunction: function() { menu.closeTriggered() } + } + + Header2Type { + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + Layout.fillWidth: true + + headerText: root.headerText + } + + Loader { + id: listViewLoader + sourceComponent: root.listView + + Layout.fillHeight: true } } - - FlickableType { - id: flickable - anchors.top: header.bottom - anchors.topMargin: 16 - contentHeight: col.implicitHeight - - Column { - id: col - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - - spacing: 16 - - Header2Type { - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: 16 - anchors.rightMargin: 16 - - headerText: root.headerText - - width: parent.width - } - - 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() } } } diff --git a/client/ui/qml/Controls2/FlickableType.qml b/client/ui/qml/Controls2/FlickableType.qml index bcd14487..b3e5cabf 100644 --- a/client/ui/qml/Controls2/FlickableType.qml +++ b/client/ui/qml/Controls2/FlickableType.qml @@ -7,10 +7,11 @@ Flickable { function ensureVisible(item) { 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) { fl.contentY = item.y + item.height - fl.height + 40 // 40 is a bottom margin } + fl.returnToBounds() } clip: true @@ -24,7 +25,7 @@ Flickable { Keys.onUpPressed: scrollBar.decrease() Keys.onDownPressed: scrollBar.increase() - ScrollBar.vertical: ScrollBar { + ScrollBar.vertical: ScrollBarType { id: scrollBar policy: fl.height >= fl.contentHeight ? ScrollBar.AlwaysOff : ScrollBar.AlwaysOn } diff --git a/client/ui/qml/Controls2/HeaderType.qml b/client/ui/qml/Controls2/HeaderType.qml index f1cafbff..1366148d 100644 --- a/client/ui/qml/Controls2/HeaderType.qml +++ b/client/ui/qml/Controls2/HeaderType.qml @@ -19,8 +19,6 @@ Item { property string descriptionText - focus: true - implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight diff --git a/client/ui/qml/Controls2/HorizontalRadioButton.qml b/client/ui/qml/Controls2/HorizontalRadioButton.qml index 1ac1cd30..89cc1658 100644 --- a/client/ui/qml/Controls2/HorizontalRadioButton.qml +++ b/client/ui/qml/Controls2/HorizontalRadioButton.qml @@ -27,6 +27,32 @@ RadioButton { implicitWidth: content.implicitWidth 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 { anchors.fill: parent radius: 16 diff --git a/client/ui/qml/Controls2/ImageButtonType.qml b/client/ui/qml/Controls2/ImageButtonType.qml index fffb6d84..d5f646a7 100644 --- a/client/ui/qml/Controls2/ImageButtonType.qml +++ b/client/ui/qml/Controls2/ImageButtonType.qml @@ -24,22 +24,39 @@ Button { property int borderFocusedWidth: 1 hoverEnabled: true - focus: true - focusPolicy: Qt.TabFocus icon.source: image icon.color: root.enabled ? imageColor : disableImageColor - property Flickable parentFlickable + property bool isFocusable: true - onFocusChanged: { - if (root.activeFocus) { - if (root.parentFlickable) { - root.parentFlickable.ensureVisible(this) - } - } + Keys.onTabPressed: { + FocusController.nextKeyTabItem() } + 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 { PropertyAnimation { duration: 200 } } diff --git a/client/ui/qml/Controls2/LabelWithButtonType.qml b/client/ui/qml/Controls2/LabelWithButtonType.qml index 41faf108..087415f7 100644 --- a/client/ui/qml/Controls2/LabelWithButtonType.qml +++ b/client/ui/qml/Controls2/LabelWithButtonType.qml @@ -41,6 +41,32 @@ Item { property bool descriptionOnTop: false 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 implicitHeight: content.implicitHeight + content.anchors.leftMargin + content.anchors.rightMargin diff --git a/client/ui/qml/Controls2/ListViewWithLabelsType.qml b/client/ui/qml/Controls2/ListViewWithLabelsType.qml index 5b614c43..536bc25a 100644 --- a/client/ui/qml/Controls2/ListViewWithLabelsType.qml +++ b/client/ui/qml/Controls2/ListViewWithLabelsType.qml @@ -17,7 +17,7 @@ ListView { property bool dividerVisible: false - currentIndex: 0 + property int selectedIndex: 0 width: rootWidth height: menuContent.contentItem.height @@ -45,7 +45,7 @@ ListView { rightImageSource: imageSource clickedFunction: function() { - menuContent.currentIndex = index + menuContent.selectedIndex = index menuContent.selectedText = name if (menuContent.clickedFunction && typeof menuContent.clickedFunction === "function") { menuContent.clickedFunction() @@ -62,7 +62,7 @@ ListView { } Component.onCompleted: { - if (menuContent.currentIndex === index) { + if (menuContent.selectedIndex === index) { menuContent.selectedText = name } } diff --git a/client/ui/qml/Controls2/ListViewWithRadioButtonType.qml b/client/ui/qml/Controls2/ListViewWithRadioButtonType.qml index f7b777a7..bd7ca32e 100644 --- a/client/ui/qml/Controls2/ListViewWithRadioButtonType.qml +++ b/client/ui/qml/Controls2/ListViewWithRadioButtonType.qml @@ -20,155 +20,134 @@ ListView { property var clickedFunction - currentIndex: 0 + property int selectedIndex: 0 width: rootWidth height: root.contentItem.height clip: true - interactive: false + reuseItems: true - property FlickableType parentFlickable - property var lastItemTabClicked + property bool isFocusable: true - 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)) - } - } + ScrollBar.vertical: ScrollBarType {} ButtonGroup { id: buttonGroup } function triggerCurrentItem() { - var item = root.itemAtIndex(currentIndex) - var radioButton = item.children[0].children[0] - radioButton.clicked() + var item = root.itemAtIndex(selectedIndex) + item.selectable.clicked() } - delegate: Item { + delegate: ColumnLayout { + id: content + + property alias selectable: radioButton + implicitWidth: rootWidth - implicitHeight: content.implicitHeight - onActiveFocusChanged: { - if (activeFocus) { - radioButton.forceActiveFocus() + RadioButton { + id: radioButton + + implicitWidth: parent.width + implicitHeight: radioButtonContent.implicitHeight + + hoverEnabled: true + + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() } - } - ColumnLayout { - id: content + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } - anchors.fill: parent + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } - RadioButton { - id: radioButton + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } - implicitWidth: parent.width - implicitHeight: radioButtonContent.implicitHeight + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } - hoverEnabled: true + 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 + 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 } - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - enabled: false - } + Behavior on color { + PropertyAnimation { duration: 200 } + } + Behavior on border.color { + PropertyAnimation { duration: 200 } } - RowLayout { - id: radioButtonContent + MouseArea { anchors.fill: parent + cursorShape: Qt.PointingHandCursor + enabled: false + } + } - anchors.rightMargin: 16 - anchors.leftMargin: 16 + RowLayout { + id: radioButtonContent + anchors.fill: parent - z: 1 + anchors.rightMargin: 16 + anchors.leftMargin: 16 - ParagraphTextType { - Layout.fillWidth: true - Layout.topMargin: 20 - Layout.bottomMargin: 20 + z: 1 - text: name - maximumLineCount: root.textMaximumLineCount - elide: root.textElide + ParagraphTextType { + Layout.fillWidth: true + 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 - checked: root.currentIndex === index + Image { + source: imageSource + visible: radioButton.checked - onClicked: { - root.currentIndex = index - root.selectedText = name - if (clickedFunction && typeof clickedFunction === "function") { - clickedFunction() - } + width: 24 + height: 24 + + Layout.rightMargin: 8 + } + } + + ButtonGroup.group: buttonGroup + checked: root.selectedIndex === index + + onClicked: { + root.selectedIndex = index + root.selectedText = name + if (clickedFunction && typeof clickedFunction === "function") { + clickedFunction() } } } Component.onCompleted: { - if (root.currentIndex === index) { + if (root.selectedIndex === index) { root.selectedText = name } } diff --git a/client/ui/qml/Controls2/PageType.qml b/client/ui/qml/Controls2/PageType.qml index 0a2a2998..d7f3317f 100644 --- a/client/ui/qml/Controls2/PageType.qml +++ b/client/ui/qml/Controls2/PageType.qml @@ -9,53 +9,21 @@ Item { property StackView stackView: StackView.view - property var defaultActiveFocusItem: null - onVisibleChanged: { - if (visible && !GC.isMobile()) { + if (visible) { 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 Timer { id: timer - interval: 100 // Milliseconds + interval: 200 // Milliseconds onTriggered: { - if (defaultActiveFocusItem) { - defaultActiveFocusItem.forceActiveFocus() - } + FocusController.resetRootObject() + FocusController.setFocusOnDefaultItem() } repeat: false // Stop the timer after one trigger - running: !GC.isMobile() // Start the timer + running: true // Start the timer } } diff --git a/client/ui/qml/Controls2/PopupType.qml b/client/ui/qml/Controls2/PopupType.qml index 7a6a770e..dfb6f273 100644 --- a/client/ui/qml/Controls2/PopupType.qml +++ b/client/ui/qml/Controls2/PopupType.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts import Style 1.0 import "TextTypes" +import "../Config" Popup { id: root @@ -28,11 +29,11 @@ Popup { } onOpened: { - focusItem.forceActiveFocus() + timer.start() } onClosed: { - PageController.forceStackActiveFocus() + FocusController.dropRootObject(root) } background: Rectangle { @@ -42,6 +43,17 @@ Popup { 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 { implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight @@ -72,11 +84,6 @@ Popup { } } - Item { - id: focusItem - KeyNavigation.tab: closeButton - } - BasicButtonType { id: closeButton visible: closeButtonVisible @@ -92,7 +99,6 @@ Popup { borderWidth: 0 text: qsTr("Close") - KeyNavigation.tab: focusItem clickedFunc: function() { root.close() diff --git a/client/ui/qml/Controls2/ScrollBarType.qml b/client/ui/qml/Controls2/ScrollBarType.qml new file mode 100644 index 00000000..26e8edb6 --- /dev/null +++ b/client/ui/qml/Controls2/ScrollBarType.qml @@ -0,0 +1,11 @@ +import QtQuick +import QtQuick.Controls + +import "./" +import "../Controls2" + +ScrollBar { + id: root + + policy: parent.height >= parent.contentHeight ? ScrollBar.AlwaysOff : ScrollBar.AlwaysOn +} diff --git a/client/ui/qml/Controls2/SwitcherType.qml b/client/ui/qml/Controls2/SwitcherType.qml index 43c35778..0651390f 100644 --- a/client/ui/qml/Controls2/SwitcherType.qml +++ b/client/ui/qml/Controls2/SwitcherType.qml @@ -35,10 +35,37 @@ Switch { property string hoveredIndicatorBackgroundColor: AmneziaStyle.color.translucentWhite 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 focusPolicy: Qt.TabFocus property FlickableType parentFlickable: null + onFocusChanged: { if (root.activeFocus) { if (root.parentFlickable) { @@ -131,13 +158,15 @@ Switch { enabled: false } - Keys.onEnterPressed: { - root.checked = !root.checked - root.checkedChanged() - } + Keys.onEnterPressed: event => handleSwitch(event) + Keys.onReturnPressed: event => handleSwitch(event) + Keys.onSpacePressed: event => handleSwitch(event) - Keys.onReturnPressed: { - root.checked = !root.checked - root.checkedChanged() + function handleSwitch(event) { + if (!event.isAutoRepeat) { + root.checked = !root.checked + root.checkedChanged() + } + event.accepted = true } } diff --git a/client/ui/qml/Controls2/TabButtonType.qml b/client/ui/qml/Controls2/TabButtonType.qml index d57ff3a0..0e48d975 100644 --- a/client/ui/qml/Controls2/TabButtonType.qml +++ b/client/ui/qml/Controls2/TabButtonType.qml @@ -17,10 +17,35 @@ TabButton { 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 hoverEnabled: true - focusPolicy: Qt.TabFocus background: Rectangle { id: background diff --git a/client/ui/qml/Controls2/TabImageButtonType.qml b/client/ui/qml/Controls2/TabImageButtonType.qml index abe544aa..b49ad8eb 100644 --- a/client/ui/qml/Controls2/TabImageButtonType.qml +++ b/client/ui/qml/Controls2/TabImageButtonType.qml @@ -14,13 +14,38 @@ TabButton { 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 int borderFocusedWidth: 1 property var clickedFunc hoverEnabled: true - focusPolicy: Qt.TabFocus icon.source: image icon.color: isSelected ? selectedColor : defaultColor @@ -41,7 +66,7 @@ TabButton { cursorShape: Qt.PointingHandCursor enabled: false } - + Keys.onEnterPressed: { if (root.clickedFunc && typeof root.clickedFunc === "function") { root.clickedFunc() diff --git a/client/ui/qml/Controls2/TextAreaWithFooterType.qml b/client/ui/qml/Controls2/TextAreaWithFooterType.qml index 102929e2..cf7b9146 100644 --- a/client/ui/qml/Controls2/TextAreaWithFooterType.qml +++ b/client/ui/qml/Controls2/TextAreaWithFooterType.qml @@ -78,9 +78,6 @@ Rectangle { placeholderText: root.placeholderText text: root.text - - KeyNavigation.tab: firstButton - onCursorVisibleChanged: { if (textArea.cursorVisible) { fl.interactive = true diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index 4ec0976b..fbea618b 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -40,6 +40,7 @@ Item { implicitHeight: content.implicitHeight property FlickableType parentFlickable + Connections { target: textField function onFocusChanged() { @@ -84,7 +85,16 @@ Item { TextField { id: textField - activeFocusOnTab: false + + property bool isFocusable: true + + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } enabled: root.textFieldEditable color: root.enabled ? root.textFieldTextColor : root.textFieldTextDisabledColor @@ -183,7 +193,7 @@ Item { focusPolicy: Qt.NoFocus text: root.buttonText - imageSource: root.buttonImageSource + leftImageSource: root.buttonImageSource anchors.top: content.top anchors.bottom: content.bottom @@ -209,9 +219,9 @@ Item { clickedFunc() } - if (KeyNavigation.tab) { - KeyNavigation.tab.forceActiveFocus(); - } + // if (KeyNavigation.tab) { + // KeyNavigation.tab.forceActiveFocus(); + // } } Keys.onReturnPressed: { @@ -219,8 +229,8 @@ Item { clickedFunc() } - if (KeyNavigation.tab) { - KeyNavigation.tab.forceActiveFocus(); - } + // if (KeyNavigation.tab) { + // KeyNavigation.tab.forceActiveFocus(); + // } } } diff --git a/client/ui/qml/Controls2/VerticalRadioButton.qml b/client/ui/qml/Controls2/VerticalRadioButton.qml index 1a781f20..bee8ef7b 100644 --- a/client/ui/qml/Controls2/VerticalRadioButton.qml +++ b/client/ui/qml/Controls2/VerticalRadioButton.qml @@ -28,8 +28,33 @@ RadioButton { property string imageSource 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 - focusPolicy: Qt.TabFocus indicator: Rectangle { id: background diff --git a/client/ui/qml/Modules/Style/AmneziaStyle.qml b/client/ui/qml/Modules/Style/AmneziaStyle.qml index 1abfbe3a..f54fefce 100644 --- a/client/ui/qml/Modules/Style/AmneziaStyle.qml +++ b/client/ui/qml/Modules/Style/AmneziaStyle.qml @@ -26,5 +26,6 @@ QtObject { 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) + readonly property color pearlGray: '#EAEAEC' } } diff --git a/client/ui/qml/Pages2/PageDevMenu.qml b/client/ui/qml/Pages2/PageDevMenu.qml index 5da40eff..d93e5a38 100644 --- a/client/ui/qml/Pages2/PageDevMenu.qml +++ b/client/ui/qml/Pages2/PageDevMenu.qml @@ -16,40 +16,28 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - - ColumnLayout { - id: backButtonLayout + BackButtonType { + id: backButton anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: 20 - - BackButtonType { - id: backButton - // KeyNavigation.tab: removeButton - } } - FlickableType { - id: fl - anchors.top: backButtonLayout.bottom + ListView { + id: listView + anchors.top: backButton.bottom anchors.bottom: parent.bottom - contentHeight: content.implicitHeight + anchors.right: parent.right + anchors.left: parent.left - ColumnLayout { - id: content + property bool isFocusable: true - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + ScrollBar.vertical: ScrollBarType {} + + header: ColumnLayout { + width: listView.width HeaderType { id: header @@ -60,7 +48,14 @@ PageType { headerText: "Dev menu" } + } + + model: 1 + clip: true + spacing: 16 + delegate: ColumnLayout { + width: listView.width TextFieldWithHeaderType { id: passwordTextField @@ -69,7 +64,6 @@ PageType { Layout.topMargin: 16 Layout.rightMargin: 16 Layout.leftMargin: 16 - parentFlickable: fl headerText: qsTr("Gateway endpoint") textFieldText: SettingsController.gatewayEndpoint @@ -86,17 +80,19 @@ PageType { SettingsController.gatewayEndpoint = textFieldText } } - - // KeyNavigation.tab: saveButton } + } + + footer: ColumnLayout { + width: listView.width SwitcherType { id: switcher Layout.fillWidth: true + Layout.topMargin: 24 Layout.rightMargin: 16 Layout.leftMargin: 16 - Layout.topMargin: 16 text: qsTr("Dev gateway environment") checked: SettingsController.isDevGatewayEnv diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 5689e4d4..ae29b80c 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Qt5Compat.GraphicalEffects import SortFilterProxyModel 0.2 @@ -19,13 +20,13 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - Connections { + objectName: "pageControllerConnections" + target: PageController function onRestorePageHomeState(isContainerInstalled) { - drawer.open() + drawer.openTriggered() if (isContainerInstalled) { containersDropDown.rootButtonClickedFunction() } @@ -33,23 +34,32 @@ PageType { } Item { + objectName: "homeColumnItem" + anchors.fill: parent anchors.bottomMargin: drawer.collapsedHeight ColumnLayout { - anchors.fill: parent - anchors.topMargin: 34 - anchors.bottomMargin: 34 + objectName: "homeColumnLayout" - Item { - id: focusItem - KeyNavigation.tab: loggingButton.visible ? - loggingButton : - connectButton + anchors.fill: parent + anchors.topMargin: 12 + anchors.bottomMargin: 16 + + AdLabel { + id: adLabel + + Layout.fillWidth: true + Layout.preferredHeight: adLabel.contentHeight + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 22 } BasicButtonType { id: loggingButton + objectName: "loggingButton" + property bool isLoggingEnabled: SettingsController.isLoggingEnabled Layout.alignment: Qt.AlignHCenter @@ -69,8 +79,6 @@ PageType { Keys.onEnterPressed: loggingButton.clicked() Keys.onReturnPressed: loggingButton.clicked() - KeyNavigation.tab: connectButton - onClicked: { PageController.goToPage(PageEnum.PageSettingsLogging) } @@ -78,16 +86,17 @@ PageType { ConnectButton { id: connectButton + objectName: "connectButton" + Layout.fillHeight: true Layout.alignment: Qt.AlignCenter - KeyNavigation.tab: splitTunnelingButton } BasicButtonType { id: splitTunnelingButton + objectName: "splitTunnelingButton" Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom - Layout.bottomMargin: 34 leftPadding: 16 rightPadding: 16 @@ -98,7 +107,6 @@ PageType { pressedColor: AmneziaStyle.color.sheerWhite disabledColor: AmneziaStyle.color.mutedGray textColor: AmneziaStyle.color.mutedGray - leftImageColor: AmneziaStyle.color.transparent borderWidth: 0 buttonTextLabel.lineHeight: 20 @@ -110,62 +118,48 @@ PageType { 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" Keys.onEnterPressed: splitTunnelingButton.clicked() Keys.onReturnPressed: splitTunnelingButton.clicked() - KeyNavigation.tab: drawer - onClicked: { - homeSplitTunnelingDrawer.open() + homeSplitTunnelingDrawer.openTriggered() } HomeSplitTunnelingDrawer { id: homeSplitTunnelingDrawer - parent: root + objectName: "homeSplitTunnelingDrawer" - onClosed: { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } - } + parent: root } } } } - DrawerType2 { id: drawer + objectName: "drawerProtocol" + anchors.fill: parent - onClosed: { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } - } + collapsedStateContent: Item { + objectName: "ProtocolDrawerCollapsedContent" - collapsedContent: Item { implicitHeight: Qt.platform.os !== "ios" ? root.height * 0.9 : screen.height * 0.77 Component.onCompleted: { drawer.expandedHeight = implicitHeight } - Connections { - target: drawer - enabled: !GC.isMobile() - function onActiveFocusChanged() { - if (drawer.activeFocus && !drawer.isOpened) { - collapsedButtonChevron.forceActiveFocus() - } - } - } + ColumnLayout { id: collapsed + objectName: "collapsedColumnLayout" anchors.left: parent.left anchors.right: parent.right + spacing: 0 Component.onCompleted: { drawer.collapsedHeight = collapsed.implicitHeight @@ -180,6 +174,8 @@ PageType { } RowLayout { + objectName: "rowLayout" + Layout.topMargin: 14 Layout.leftMargin: 24 Layout.rightMargin: 24 @@ -188,9 +184,11 @@ PageType { spacing: 0 Connections { + objectName: "drawerConnections" + target: drawer - function onEntered() { - if (drawer.isCollapsed) { + function onCursorEntered() { + if (drawer.isCollapsedStateActive) { collapsedButtonChevron.backgroundColor = collapsedButtonChevron.hoveredColor collapsedButtonHeader.opacity = 0.8 } else { @@ -198,8 +196,8 @@ PageType { } } - function onExited() { - if (drawer.isCollapsed) { + function onCursorExited() { + if (drawer.isCollapsedStateActive) { collapsedButtonChevron.backgroundColor = collapsedButtonChevron.defaultColor collapsedButtonHeader.opacity = 1 } else { @@ -208,7 +206,7 @@ PageType { } function onPressed(pressed, entered) { - if (drawer.isCollapsed) { + if (drawer.isCollapsedStateActive) { collapsedButtonChevron.backgroundColor = pressed ? collapsedButtonChevron.pressedColor : entered ? collapsedButtonChevron.hoveredColor : collapsedButtonChevron.defaultColor collapsedButtonHeader.opacity = 0.7 } else { @@ -219,6 +217,8 @@ PageType { Header1TextType { id: collapsedButtonHeader + objectName: "collapsedButtonHeader" + Layout.maximumWidth: drawer.width - 48 - 18 - 12 maximumLineCount: 2 @@ -227,8 +227,6 @@ PageType { text: ServersModel.defaultServerName horizontalAlignment: Qt.AlignHCenter - KeyNavigation.tab: tabBar - Behavior on opacity { PropertyAnimation { duration: 200 } } @@ -236,10 +234,11 @@ PageType { ImageButtonType { id: collapsedButtonChevron + objectName: "collapsedButtonChevron" Layout.leftMargin: 8 - visible: drawer.isCollapsed + visible: drawer.isCollapsedStateActive() hoverEnabled: false image: "qrc:/images/controls/chevron-down.svg" @@ -254,47 +253,59 @@ PageType { Keys.onEnterPressed: collapsedButtonChevron.clicked() Keys.onReturnPressed: collapsedButtonChevron.clicked() - Keys.onTabPressed: lastItemTabClicked() - onClicked: { - if (drawer.isCollapsed) { - drawer.open() + if (drawer.isCollapsedStateActive()) { + drawer.openTriggered() } } } } RowLayout { + objectName: "rowLayoutLabel" Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - Layout.bottomMargin: drawer.isCollapsed ? 44 : ServersModel.isDefaultServerFromApi ? 89 : 44 + Layout.topMargin: 8 + Layout.bottomMargin: drawer.isCollapsedStateActive ? 44 : ServersModel.isDefaultServerFromApi ? 61 : 16 spacing: 0 - Image { - Layout.rightMargin: 8 - visible: source !== "" - source: ServersModel.defaultServerImagePathCollapsed - } + BasicButtonType { + enabled: (ServersModel.defaultServerImagePathCollapsed !== "") && drawer.isCollapsedStateActive + hoverEnabled: enabled - LabelTextType { - id: collapsedServerMenuDescription - text: drawer.isCollapsed ? ServersModel.defaultServerDescriptionCollapsed : ServersModel.defaultServerDescriptionExpanded - } - } - } + implicitHeight: 36 - Connections { - target: drawer - enabled: !GC.isMobile() - function onIsCollapsedChanged() { - if (!drawer.isCollapsed) { - focusItem1.forceActiveFocus() + 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 + + text: drawer.isCollapsedStateActive ? ServersModel.defaultServerDescriptionCollapsed : ServersModel.defaultServerDescriptionExpanded + leftImageSource: ServersModel.defaultServerImagePathCollapsed + leftImageColor: "" + changeLeftImageSize: false + + rightImageSource: hoverEnabled ? "qrc:/images/controls/chevron-down.svg" : "" + + onClicked: { + ServersModel.processedIndex = ServersModel.defaultIndex + PageController.goToPage(PageEnum.PageSettingsServerInfo) + } } } } ColumnLayout { id: serversMenuHeader + objectName: "serversMenuHeader" anchors.top: collapsed.bottom anchors.right: parent.right @@ -306,13 +317,9 @@ PageType { visible: !ServersModel.isDefaultServerFromApi - Item { - id: focusItem1 - KeyNavigation.tab: containersDropDown - } - DropDownType { id: containersDropDown + objectName: "containersDropDown" rootButtonImageColor: AmneziaStyle.color.midnightBlack rootButtonBackgroundColor: AmneziaStyle.color.paleGray @@ -323,28 +330,28 @@ PageType { rootButtonTextTopMargin: 8 rootButtonTextBottomMargin: 8 + enabled: drawer.isOpened + text: ServersModel.defaultServerDefaultContainerName textColor: AmneziaStyle.color.midnightBlack headerText: qsTr("VPN protocol") headerBackButtonImage: "qrc:/images/controls/arrow-left.svg" rootButtonClickedFunction: function() { - containersDropDown.open() + containersDropDown.openTriggered() } drawerParent: root - KeyNavigation.tab: serversMenuContent listView: HomeContainersListView { id: containersListView + objectName: "containersListView" + rootWidth: root.width - onVisibleChanged: { - if (containersDropDown.visible && !GC.isMobile()) { - focusItem1.forceActiveFocus() - } - } Connections { + objectName: "rowLayoutConnections" + target: ServersModel function onDefaultServerIndexChanged() { @@ -386,167 +393,21 @@ PageType { ButtonGroup { id: serversRadioButtonGroup + objectName: "serversRadioButtonGroup" } - ListView { + ServersListView { id: serversMenuContent + objectName: "serversMenuContent" - anchors.top: serversMenuHeader.bottom - 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() + isFocusable: false Connections { target: drawer - enabled: !GC.isMobile() - function onIsCollapsedChanged() { - if (drawer.isCollapsed) { - const item = serversMenuContent.itemAtIndex(serversMenuContent.focusItemIndex) - if (item) { item.serverRadioButtonProperty.focus = false } - } - } - } - Connections { - target: ServersModel - function onDefaultServerIndexChanged(serverIndex) { - 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 - } + // this item shouldn't be focused when drawer is closed + function onIsOpenedChanged() { + serversMenuContent.isFocusable = drawer.isOpened } } } diff --git a/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml b/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml index 2b912f18..d31f63e3 100644 --- a/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml @@ -16,18 +16,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: listview.currentItem.mtuTextField.textField - - Item { - id: focusItem - onFocusChanged: { - if (activeFocus) { - fl.ensureVisible(focusItem) - } - } - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -39,229 +27,235 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: listview.currentItem.mtuTextField.textField } } - FlickableType { - id: fl + ListView { + id: listview + anchors.top: backButtonLayout.bottom - anchors.bottom: parent.bottom - contentHeight: content.implicitHeight + saveButton.implicitHeight + saveButton.anchors.bottomMargin + saveButton.anchors.topMargin + anchors.bottom: saveButton.top - Column { - id: content + width: parent.width - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + clip: true - ListView { - id: listview + property bool isFocusable: true - width: parent.width - height: listview.contentItem.height + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } - clip: true - interactive: false + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } - model: AwgConfigModel + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } - delegate: Item { - id: delegateItem - implicitWidth: listview.width - implicitHeight: col.implicitHeight + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } - property alias mtuTextField: mtuTextField - property bool isSaveButtonEnabled: mtuTextField.errorText === "" && - junkPacketMaxSizeTextField.errorText === "" && - junkPacketMinSizeTextField.errorText === "" && - junkPacketCountTextField.errorText === "" + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } - ColumnLayout { - id: col + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + model: AwgConfigModel - anchors.leftMargin: 16 - anchors.rightMargin: 16 + delegate: Item { + 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 { - Layout.fillWidth: true + ColumnLayout { + id: col - headerText: qsTr("AmneziaWG settings") - } + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right - TextFieldWithHeaderType { - id: mtuTextField - Layout.fillWidth: true - Layout.topMargin: 40 + anchors.leftMargin: 16 + anchors.rightMargin: 16 - headerText: qsTr("MTU") - textFieldText: clientMtu - textField.validator: IntValidator { bottom: 576; top: 65535 } + spacing: 0 - textField.onEditingFinished: { - if (textFieldText !== clientMtu) { - clientMtu = textFieldText - } - } - checkEmptyText: true - KeyNavigation.tab: junkPacketCountTextField.textField - } + HeaderType { + Layout.fillWidth: true - TextFieldWithHeaderType { - id: junkPacketCountTextField - Layout.fillWidth: true - Layout.topMargin: 16 + headerText: qsTr("AmneziaWG settings") + } - headerText: "Jc - Junk packet count" - textFieldText: clientJunkPacketCount - textField.validator: IntValidator { bottom: 0 } - parentFlickable: fl + TextFieldWithHeaderType { + id: mtuTextField + Layout.fillWidth: true + Layout.topMargin: 40 - textField.onEditingFinished: { - if (textFieldText !== clientJunkPacketCount) { - clientJunkPacketCount = textFieldText - } - } + headerText: qsTr("MTU") + textFieldText: clientMtu + textField.validator: IntValidator { bottom: 576; top: 65535 } - 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 } - 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 + textField.onEditingFinished: { + if (textFieldText !== clientMtu) { + clientMtu = textFieldText } } + 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") - Keys.onTabPressed: lastItemTabClicked(focusItem) + onActiveFocusChanged: { + if(activeFocus) { + listview.positionViewAtEnd() + } + } clickedFunc: function() { forceActiveFocus() diff --git a/client/ui/qml/Pages2/PageProtocolAwgSettings.qml b/client/ui/qml/Pages2/PageProtocolAwgSettings.qml index 27ea66f9..3594cd9d 100644 --- a/client/ui/qml/Pages2/PageProtocolAwgSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolAwgSettings.qml @@ -2,6 +2,8 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import QtCore + import SortFilterProxyModel 0.2 import PageEnum 1.0 @@ -17,18 +19,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: listview.currentItem.portTextField.textField - - Item { - id: focusItem - onFocusChanged: { - if (activeFocus) { - fl.ensureVisible(focusItem) - } - } - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -40,341 +30,358 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: listview.currentItem.portTextField.textField } } - FlickableType { - id: fl + ListView { + id: listview + + property bool isFocusable: true + anchors.top: backButtonLayout.bottom anchors.bottom: parent.bottom - contentHeight: content.implicitHeight - Column { - id: content + width: parent.width - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } - ListView { - id: listview + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } - width: parent.width - height: listview.contentItem.height + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } - clip: true - interactive: false + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } - model: AwgConfigModel + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } - delegate: Item { - id: delegateItem - implicitWidth: listview.width - implicitHeight: col.implicitHeight + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } - property alias portTextField: portTextField - property bool isEnabled: ServersModel.isProcessedServerHasWriteAccess() + clip: true - ColumnLayout { - id: col + model: AwgConfigModel - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + delegate: Item { + id: delegateItem + implicitWidth: listview.width + implicitHeight: col.implicitHeight - anchors.leftMargin: 16 - anchors.rightMargin: 16 + property alias vpnAddressSubnetTextField: vpnAddressSubnetTextField + property bool isEnabled: ServersModel.isProcessedServerHasWriteAccess() - spacing: 0 + ColumnLayout { + id: col - HeaderType { - Layout.fillWidth: true + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right - headerText: qsTr("AmneziaWG settings") + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + spacing: 0 + + HeaderType { + Layout.fillWidth: true + + headerText: qsTr("AmneziaWG settings") + } + + TextFieldWithHeaderType { + id: vpnAddressSubnetTextField + + Layout.fillWidth: true + Layout.topMargin: 40 + + enabled: delegateItem.isEnabled + + headerText: qsTr("VPN address subnet") + textFieldText: subnetAddress + + textField.onEditingFinished: { + if (textFieldText !== subnetAddress) { + subnetAddress = textFieldText + } + } + + checkEmptyText: true + } + + TextFieldWithHeaderType { + id: portTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + 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: 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 { - id: portTextField - Layout.fillWidth: true - Layout.topMargin: 40 + if (textFieldText !== serverJunkPacketCount) { + serverJunkPacketCount = textFieldText + } + } - enabled: delegateItem.isEnabled + checkEmptyText: true + } - headerText: qsTr("Port") - textFieldText: port - textField.maximumLength: 5 - textField.validator: IntValidator { bottom: 1; top: 65535 } - parentFlickable: fl + TextFieldWithHeaderType { + id: junkPacketMinSizeTextField + Layout.fillWidth: true + Layout.topMargin: 16 - textField.onEditingFinished: { - if (textFieldText !== port) { - port = textFieldText - } + headerText: qsTr("Jmin - Junk packet minimum size") + textFieldText: serverJunkPacketMinSize + 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 === "" && + vpnAddressSubnetTextField.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 - - KeyNavigation.tab: junkPacketCountTextField.textField - } - - 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) + 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) } } } diff --git a/client/ui/qml/Pages2/PageProtocolCloakSettings.qml b/client/ui/qml/Pages2/PageProtocolCloakSettings.qml index 5089a764..e90325ba 100644 --- a/client/ui/qml/Pages2/PageProtocolCloakSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolCloakSettings.qml @@ -16,13 +16,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: listview.currentItem.trafficFromField.textField - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -34,7 +27,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: listview.currentItem.trafficFromField.textField } } @@ -56,11 +48,13 @@ PageType { ListView { id: listview + property int selectedIndex: 0 + width: parent.width height: listview.contentItem.height clip: true - interactive: false + reuseItems: true model: CloakConfigModel @@ -110,8 +104,6 @@ PageType { } } } - - KeyNavigation.tab: portTextField.textField } TextFieldWithHeaderType { @@ -130,8 +122,6 @@ PageType { port = textFieldText } } - - KeyNavigation.tab: cipherDropDown } DropDownType { @@ -143,7 +133,6 @@ PageType { headerText: qsTr("Cipher") drawerParent: root - KeyNavigation.tab: saveRestartButton listView: ListViewWithRadioButtonType { id: cipherListView @@ -161,7 +150,7 @@ PageType { clickedFunction: function() { cipherDropDown.text = selectedText cipher = cipherDropDown.text - cipherDropDown.close() + cipherDropDown.closeTriggered() } Component.onCompleted: { @@ -169,7 +158,7 @@ PageType { for (var i = 0; i < cipherListView.model.count; i++) { if (cipherListView.model.get(i).name === cipherDropDown.text) { - currentIndex = i + selectedIndex = i } } } @@ -184,7 +173,6 @@ PageType { Layout.bottomMargin: 24 text: qsTr("Save") - Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { forceActiveFocus() diff --git a/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml b/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml index 30540a93..6c5ad23f 100644 --- a/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml @@ -17,18 +17,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: listview.currentItem.vpnAddressSubnetTextField.textField - - Item { - id: focusItem - KeyNavigation.tab: backButton - onActiveFocusChanged: { - if (activeFocus) { - fl.ensureVisible(focusItem) - } - } - } - ColumnLayout { id: backButtonLayout @@ -40,7 +28,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: listview.currentItem.vpnAddressSubnetTextField.textField } } @@ -104,7 +91,6 @@ PageType { textFieldText: subnetAddress parentFlickable: fl - KeyNavigation.tab: transportProtoSelector textField.onEditingFinished: { if (textFieldText !== subnetAddress) { @@ -132,8 +118,6 @@ PageType { return transportProto === "tcp" ? 1 : 0 } - KeyNavigation.tab: portTextField.enabled ? portTextField.textField : autoNegotiateEncryprionSwitcher - onCurrentIndexChanged: { if (transportProto === "tcp" && currentIndex === 0) { transportProto = "udp" @@ -162,8 +146,6 @@ PageType { port = textFieldText } } - - KeyNavigation.tab: autoNegotiateEncryprionSwitcher } SwitcherType { @@ -181,10 +163,6 @@ PageType { autoNegotiateEncryprion = checked } } - - KeyNavigation.tab: hashDropDown.enabled ? - hashDropDown : - tlsAuthCheckBox } DropDownType { @@ -198,10 +176,6 @@ PageType { headerText: qsTr("Hash") drawerParent: root - parentFlickable: fl - KeyNavigation.tab: cipherDropDown.enabled ? - cipherDropDown : - tlsAuthCheckBox listView: ListViewWithRadioButtonType { id: hashListView @@ -224,7 +198,7 @@ PageType { clickedFunction: function() { hashDropDown.text = selectedText hash = hashDropDown.text - hashDropDown.close() + hashDropDown.closeTriggered() } Component.onCompleted: { @@ -250,9 +224,6 @@ PageType { headerText: qsTr("Cipher") drawerParent: root - parentFlickable: fl - - KeyNavigation.tab: tlsAuthCheckBox listView: ListViewWithRadioButtonType { id: cipherListView @@ -275,7 +246,7 @@ PageType { clickedFunction: function() { cipherDropDown.text = selectedText cipher = cipherDropDown.text - cipherDropDown.close() + cipherDropDown.closeTriggered() } Component.onCompleted: { @@ -320,8 +291,6 @@ PageType { text: qsTr("TLS auth") checked: tlsAuth - KeyNavigation.tab: blockDnsCheckBox - onCheckedChanged: { if (checked !== tlsAuth) { console.log("tlsAuth changed to: " + checked) @@ -339,8 +308,6 @@ PageType { text: qsTr("Block DNS requests outside of VPN") checked: blockDns - KeyNavigation.tab: additionalClientCommandsSwitcher - onCheckedChanged: { if (checked !== blockDns) { blockDns = checked @@ -355,9 +322,6 @@ PageType { Layout.fillWidth: true Layout.topMargin: 32 parentFlickable: fl - KeyNavigation.tab: additionalClientCommandsTextArea.visible ? - additionalClientCommandsTextArea.textArea : - additionalServerCommandsSwitcher checked: additionalClientCommands !== "" @@ -376,7 +340,7 @@ PageType { Layout.topMargin: 16 visible: additionalClientCommandsSwitcher.checked - KeyNavigation.tab: additionalServerCommandsSwitcher + parentFlickable: fl textAreaText: additionalClientCommands @@ -394,9 +358,6 @@ PageType { Layout.fillWidth: true Layout.topMargin: 16 parentFlickable: fl - KeyNavigation.tab: additionalServerCommandsTextArea.visible ? - additionalServerCommandsTextArea.textArea : - saveRestartButton checked: additionalServerCommands !== "" @@ -419,7 +380,6 @@ PageType { textAreaText: additionalServerCommands placeholderText: qsTr("Commands:") parentFlickable: fl - KeyNavigation.tab: saveRestartButton textArea.onEditingFinished: { if (additionalServerCommands !== textAreaText) { additionalServerCommands = textAreaText @@ -436,7 +396,6 @@ PageType { text: qsTr("Save") parentFlickable: fl - Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { forceActiveFocus() diff --git a/client/ui/qml/Pages2/PageProtocolRaw.qml b/client/ui/qml/Pages2/PageProtocolRaw.qml index 24853afd..03b4e297 100644 --- a/client/ui/qml/Pages2/PageProtocolRaw.qml +++ b/client/ui/qml/Pages2/PageProtocolRaw.qml @@ -19,13 +19,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: header @@ -37,7 +30,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: listView } HeaderType { @@ -75,13 +67,6 @@ PageType { activeFocusOnTab: true focus: true - onActiveFocusChanged: { - if (focus) { - listView.currentIndex = 0 - listView.currentItem.focusItem.forceActiveFocus() - } - } - delegate: Item { implicitWidth: parent.width implicitHeight: delegateContent.implicitHeight @@ -101,11 +86,9 @@ PageType { text: qsTr("Show connection options") clickedFunction: function() { - configContentDrawer.open() + configContentDrawer.openTriggered() } - KeyNavigation.tab: removeButton - MouseArea { anchors.fill: button cursorShape: Qt.PointingHandCursor @@ -120,31 +103,12 @@ PageType { expandedHeight: root.height * 0.9 - onClosed: { - if (!GC.isMobile()) { - defaultActiveFocusItem.forceActiveFocus() - } - } - parent: root anchors.fill: parent - expandedContent: Item { + expandedStateContent: Item { implicitHeight: configContentDrawer.expandedHeight - Connections { - target: configContentDrawer - enabled: !GC.isMobile() - function onOpened() { - focusItem1.forceActiveFocus() - } - } - - Item { - id: focusItem1 - KeyNavigation.tab: backButton1 - } - BackButtonType { id: backButton1 @@ -154,10 +118,8 @@ PageType { anchors.topMargin: 16 backButtonFunction: function() { - configContentDrawer.close() + configContentDrawer.closeTriggered() } - - KeyNavigation.tab: focusItem1 } FlickableType { @@ -226,7 +188,6 @@ PageType { text: qsTr("Remove ") + ContainersModel.getProcessedContainerName() textColor: AmneziaStyle.color.vibrantRed - Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunction: function() { 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.") diff --git a/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml b/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml index 4d3b2c4e..44cbd1ce 100644 --- a/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml @@ -16,15 +16,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: listview.currentItem.focusItemId.enabled ? - listview.currentItem.focusItemId.textField : - focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -36,9 +27,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: listview.currentItem.focusItemId.enabled ? - listview.currentItem.focusItemId.textField : - focusItem } } @@ -114,8 +102,6 @@ PageType { port = textFieldText } } - - KeyNavigation.tab: cipherDropDown } DropDownType { @@ -129,9 +115,9 @@ PageType { headerText: qsTr("Cipher") drawerParent: root - KeyNavigation.tab: saveRestartButton listView: ListViewWithRadioButtonType { + id: cipherListView rootWidth: root.width @@ -147,7 +133,7 @@ PageType { clickedFunction: function() { cipherDropDown.text = selectedText cipher = cipherDropDown.text - cipherDropDown.close() + cipherDropDown.closeTriggered() } Component.onCompleted: { @@ -172,7 +158,6 @@ PageType { enabled: isPortEditable | isCipherEditable text: qsTr("Save") - Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { forceActiveFocus() diff --git a/client/ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml b/client/ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml index 007de5ca..7413df38 100644 --- a/client/ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml @@ -16,8 +16,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: listview.currentItem.mtuTextField.textField - Item { id: focusItem onFocusChanged: { @@ -150,8 +148,6 @@ PageType { text: qsTr("Save") - Keys.onTabPressed: lastItemTabClicked(focusItem) - clickedFunc: function() { forceActiveFocus() var headerText = qsTr("Save settings?") diff --git a/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml b/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml index b5d08132..f83d97fd 100644 --- a/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml @@ -16,13 +16,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: listview - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -34,7 +27,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: listview } } @@ -64,17 +56,10 @@ PageType { model: WireGuardConfigModel - activeFocusOnTab: true - onActiveFocusChanged: { - if (activeFocus) { - listview.itemAtIndex(0)?.focusItemId.forceActiveFocus() - } - } - delegate: Item { id: delegateItem - property alias focusItemId: portTextField.textField + property alias focusItemId: vpnAddressSubnetTextField property bool isEnabled: ServersModel.isProcessedServerHasWriteAccess() implicitWidth: listview.width @@ -98,19 +83,36 @@ PageType { } TextFieldWithHeaderType { - id: portTextField + id: vpnAddressSubnetTextField Layout.fillWidth: true Layout.topMargin: 40 enabled: delegateItem.isEnabled + headerText: qsTr("VPN address subnet") + textFieldText: subnetAddress + + textField.onEditingFinished: { + if (textFieldText !== subnetAddress) { + subnetAddress = textFieldText + } + } + + checkEmptyText: true + } + + TextFieldWithHeaderType { + id: portTextField + Layout.fillWidth: true + Layout.topMargin: 16 + + enabled: delegateItem.isEnabled + headerText: qsTr("Port") textFieldText: port textField.maximumLength: 5 textField.validator: IntValidator { bottom: 1; top: 65535 } - KeyNavigation.tab: saveButton - textField.onEditingFinished: { if (textFieldText !== port) { port = textFieldText @@ -126,12 +128,11 @@ PageType { Layout.topMargin: 24 Layout.bottomMargin: 24 - enabled: portTextField.errorText === "" + enabled: portTextField.errorText === "" && + vpnAddressSubnetTextField.errorText === "" text: qsTr("Save") - Keys.onTabPressed: lastItemTabClicked(focusItem) - onClicked: function() { forceActiveFocus() diff --git a/client/ui/qml/Pages2/PageProtocolXraySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySettings.qml index 20ee1da6..6d2ad3d1 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySettings.qml @@ -17,13 +17,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: listview - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -35,7 +28,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: listview } } @@ -65,13 +57,6 @@ PageType { model: XrayConfigModel - activeFocusOnTab: true - onActiveFocusChanged: { - if (activeFocus) { - listview.itemAtIndex(0)?.focusItemId.forceActiveFocus() - } - } - delegate: Item { property alias focusItemId: textFieldWithHeaderType.textField @@ -103,8 +88,6 @@ PageType { headerText: qsTr("Disguised as traffic from") textFieldText: site - KeyNavigation.tab: basicButton - textField.onEditingFinished: { if (textFieldText !== site) { var tmpText = textFieldText @@ -128,8 +111,6 @@ PageType { text: qsTr("Save") - Keys.onTabPressed: lastItemTabClicked(focusItem) - onClicked: { forceActiveFocus() diff --git a/client/ui/qml/Pages2/PageServiceDnsSettings.qml b/client/ui/qml/Pages2/PageServiceDnsSettings.qml index bb3cbf96..cef29813 100644 --- a/client/ui/qml/Pages2/PageServiceDnsSettings.qml +++ b/client/ui/qml/Pages2/PageServiceDnsSettings.qml @@ -16,13 +16,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -34,7 +27,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: removeButton } } @@ -72,8 +64,6 @@ PageType { text: qsTr("Remove ") + ContainersModel.getProcessedContainerName() textColor: AmneziaStyle.color.vibrantRed - Keys.onTabPressed: root.lastItemTabClicked() - clickedFunction: function() { var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) var yesButtonText = qsTr("Continue") diff --git a/client/ui/qml/Pages2/PageServiceSftpSettings.qml b/client/ui/qml/Pages2/PageServiceSftpSettings.qml index 9bdbf2db..2deb315c 100644 --- a/client/ui/qml/Pages2/PageServiceSftpSettings.qml +++ b/client/ui/qml/Pages2/PageServiceSftpSettings.qml @@ -16,8 +16,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - Connections { target: InstallController @@ -26,11 +24,6 @@ PageType { } } - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -42,7 +35,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: listview } } @@ -107,7 +99,6 @@ PageType { Layout.topMargin: 32 parentFlickable: fl - KeyNavigation.tab: portLabel.rightButton text: qsTr("Host") descriptionText: ServersModel.getProcessedServerData("hostName") @@ -136,7 +127,6 @@ PageType { descriptionOnTop: true parentFlickable: fl - KeyNavigation.tab: usernameLabel.rightButton rightImageSource: "qrc:/images/controls/copy.svg" rightImageColor: AmneziaStyle.color.paleGray @@ -160,7 +150,6 @@ PageType { descriptionOnTop: true parentFlickable: fl - KeyNavigation.tab: passwordLabel.eyeButton rightImageSource: "qrc:/images/controls/copy.svg" rightImageColor: AmneziaStyle.color.paleGray @@ -184,14 +173,6 @@ PageType { descriptionOnTop: true parentFlickable: fl - eyeButton.KeyNavigation.tab: passwordLabel.rightButton - rightButton.Keys.onTabPressed: { - if (mountButton.visible) { - mountButton.forceActiveFocus() - } else { - detailedInstructionsButton.forceActiveFocus() - } - } rightImageSource: "qrc:/images/controls/copy.svg" rightImageColor: AmneziaStyle.color.paleGray @@ -225,7 +206,6 @@ PageType { borderWidth: 1 parentFlickable: fl - KeyNavigation.tab: detailedInstructionsButton text: qsTr("Mount folder on device") @@ -290,7 +270,6 @@ PageType { text: qsTr("Detailed instructions") parentFlickable: fl - Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { // Qt.openUrlExternally("https://github.com/amnezia-vpn/desktop-client/releases/latest") diff --git a/client/ui/qml/Pages2/PageServiceSocksProxySettings.qml b/client/ui/qml/Pages2/PageServiceSocksProxySettings.qml index 9d21963d..5eee9a1e 100644 --- a/client/ui/qml/Pages2/PageServiceSocksProxySettings.qml +++ b/client/ui/qml/Pages2/PageServiceSocksProxySettings.qml @@ -17,8 +17,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: listview - Connections { target: InstallController @@ -27,11 +25,6 @@ PageType { } } - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -43,7 +36,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: listview } } @@ -99,7 +91,6 @@ PageType { Layout.topMargin: 32 parentFlickable: fl - KeyNavigation.tab: portLabel.rightButton text: qsTr("Host") descriptionText: ServersModel.getProcessedServerData("hostName") @@ -128,7 +119,6 @@ PageType { descriptionOnTop: true parentFlickable: fl - KeyNavigation.tab: usernameLabel.rightButton rightImageSource: "qrc:/images/controls/copy.svg" rightImageColor: AmneziaStyle.color.paleGray @@ -152,7 +142,6 @@ PageType { descriptionOnTop: true parentFlickable: fl - KeyNavigation.tab: passwordLabel.eyeButton rightImageSource: "qrc:/images/controls/copy.svg" rightImageColor: AmneziaStyle.color.paleGray @@ -176,8 +165,6 @@ PageType { descriptionOnTop: true parentFlickable: fl - eyeButton.KeyNavigation.tab: passwordLabel.rightButton - rightButton.KeyNavigation.tab: changeSettingsButton rightImageSource: "qrc:/images/controls/copy.svg" rightImageColor: AmneziaStyle.color.paleGray @@ -200,13 +187,7 @@ PageType { anchors.fill: parent expandedHeight: root.height * 0.9 - onClosed: { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } - } - - expandedContent: ColumnLayout { + expandedStateContent: ColumnLayout { property string tempPort: port property string tempUsername: username property string tempPassword: password @@ -222,9 +203,6 @@ PageType { Connections { target: changeSettingsDrawer function onOpened() { - if (!GC.isMobile()) { - drawerFocusItem.forceActiveFocus() - } tempPort = port tempUsername = username tempPassword = password @@ -239,11 +217,6 @@ PageType { } } - Item { - id: drawerFocusItem - KeyNavigation.tab: portTextField.textField - } - HeaderType { Layout.fillWidth: true @@ -268,8 +241,6 @@ PageType { port = textFieldText } } - - KeyNavigation.tab: usernameTextField.textField } TextFieldWithHeaderType { @@ -290,8 +261,6 @@ PageType { username = textFieldText } } - - KeyNavigation.tab: passwordTextField.textField } TextFieldWithHeaderType { @@ -322,8 +291,6 @@ PageType { password = textFieldText } } - - KeyNavigation.tab: saveButton } BasicButtonType { @@ -334,7 +301,6 @@ PageType { Layout.bottomMargin: 24 text: qsTr("Change connection settings") - Keys.onTabPressed: lastItemTabClicked(drawerFocusItem) clickedFunc: function() { forceActiveFocus() @@ -356,7 +322,7 @@ PageType { tempPort = portTextField.textFieldText tempUsername = usernameTextField.textFieldText tempPassword = passwordTextField.textFieldText - changeSettingsDrawer.close() + changeSettingsDrawer.closeTriggered() } } } @@ -372,11 +338,10 @@ PageType { Layout.rightMargin: 16 text: qsTr("Change connection settings") - Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { forceActiveFocus() - changeSettingsDrawer.open() + changeSettingsDrawer.openTriggered() } } } diff --git a/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml b/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml index 946a77bb..249c70c7 100644 --- a/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml +++ b/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml @@ -17,8 +17,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - Connections { target: InstallController @@ -27,11 +25,6 @@ PageType { } } - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: backButtonLayout @@ -43,7 +36,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: websiteName.rightButton } } @@ -88,8 +80,6 @@ PageType { rightImageSource: "qrc:/images/controls/copy.svg" rightImageColor: AmneziaStyle.color.paleGray - Keys.onTabPressed: lastItemTabClicked(focusItem) - clickedFunction: function() { GC.copyToClipBoard(descriptionText) PageController.showNotificationMessage(qsTr("Copied")) diff --git a/client/ui/qml/Pages2/PageSettings.qml b/client/ui/qml/Pages2/PageSettings.qml index bb5ca766..c44e52ae 100644 --- a/client/ui/qml/Pages2/PageSettings.qml +++ b/client/ui/qml/Pages2/PageSettings.qml @@ -14,8 +14,6 @@ import "../Config" PageType { id: root - defaultActiveFocusItem: header - FlickableType { id: fl anchors.top: parent.top @@ -39,8 +37,6 @@ PageType { Layout.leftMargin: 16 headerText: qsTr("Settings") - - KeyNavigation.tab: account.rightButton } LabelWithButtonType { @@ -55,8 +51,6 @@ PageType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsServersList) } - - KeyNavigation.tab: connection.rightButton } DividerType {} @@ -72,8 +66,6 @@ PageType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsConnection) } - - KeyNavigation.tab: application.rightButton } DividerType {} @@ -89,14 +81,13 @@ PageType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsApplication) } - - KeyNavigation.tab: backup.rightButton } DividerType {} LabelWithButtonType { id: backup + visible: !SettingsController.isOnTv() Layout.fillWidth: true text: qsTr("Backup") @@ -106,11 +97,11 @@ PageType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsBackup) } - - KeyNavigation.tab: about.rightButton } - DividerType {} + DividerType { + visible: !SettingsController.isOnTv() + } LabelWithButtonType { id: about @@ -123,8 +114,6 @@ PageType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsAbout) } - KeyNavigation.tab: close - } DividerType {} @@ -138,8 +127,6 @@ PageType { rightImageSource: "qrc:/images/controls/chevron-right.svg" leftImageSource: "qrc:/images/controls/bug.svg" - // Keys.onTabPressed: lastItemTabClicked(header) - clickedFunction: function() { PageController.goToPage(PageEnum.PageDevMenu) } @@ -157,9 +144,7 @@ PageType { text: qsTr("Close application") leftImageSource: "qrc:/images/controls/x-circle.svg" - isLeftImageHoverEnabled: false - - Keys.onTabPressed: lastItemTabClicked(header) + isLeftImageHoverEnabled: false clickedFunction: function() { PageController.closeApplication() diff --git a/client/ui/qml/Pages2/PageSettingsAbout.qml b/client/ui/qml/Pages2/PageSettingsAbout.qml index cde9ee20..2160692b 100644 --- a/client/ui/qml/Pages2/PageSettingsAbout.qml +++ b/client/ui/qml/Pages2/PageSettingsAbout.qml @@ -14,19 +14,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - - onFocusChanged: { - if (focusItem.activeFocus) { - fl.contentY = 0 - } - } - } - BackButtonType { id: backButton @@ -35,21 +22,107 @@ PageType { anchors.right: parent.right anchors.topMargin: 20 - KeyNavigation.tab: telegramButton + onActiveFocusChanged: { + if(backButton.enabled && backButton.activeFocus) { + listView.positionViewAtBeginning() + } + } } - FlickableType { - id: fl + QtObject { + id: telegramGroup + + readonly property string title: qsTr("Telegram group") + readonly property string description: qsTr("To discuss features") + readonly property string imageSource: "qrc:/images/controls/telegram.svg" + readonly property var handler: function() { + Qt.openUrlExternally(qsTr("https://t.me/amnezia_vpn_en")) + } + } + + QtObject { + id: mail + + readonly property string title: qsTr("support@amnezia.org") + readonly property string description: qsTr("For reviews and bug reports") + readonly property string imageSource: "qrc:/images/controls/mail.svg" + readonly property var handler: function() { + GC.copyToClipBoard(title) + PageController.showNotificationMessage(qsTr("Copied")) + } + } + + QtObject { + id: github + + readonly property string title: qsTr("GitHub") + readonly property string description: qsTr("Discover the source code") + readonly property string imageSource: "qrc:/images/controls/github.svg" + readonly property var handler: function() { + Qt.openUrlExternally(qsTr("https://github.com/amnezia-vpn/amnezia-client")) + } + } + + QtObject { + id: website + + readonly property string title: qsTr("Website") + readonly property string description: qsTr("Visit official website") + readonly property string imageSource: "qrc:/images/controls/amnezia.svg" + readonly property var handler: function() { + Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl()) + } + } + + property list contacts: [ + telegramGroup, + mail, + github, + website + ] + + ListView { + id: listView + anchors.top: backButton.bottom anchors.bottom: parent.bottom - contentHeight: content.height + anchors.right: parent.right + anchors.left: parent.left - ColumnLayout { - id: content + property bool isFocusable: true - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } + + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + + ScrollBar.vertical: ScrollBarType {} + + model: contacts + + clip: true + + header: ColumnLayout { + width: listView.width Image { id: image @@ -96,81 +169,29 @@ PageType { text: qsTr("Contacts") } + } + + delegate: ColumnLayout { + width: listView.width LabelWithButtonType { id: telegramButton Layout.fillWidth: true - Layout.topMargin: 16 + Layout.topMargin: 6 - text: qsTr("Telegram group") - descriptionText: qsTr("To discuss features") - leftImageSource: "qrc:/images/controls/telegram.svg" + text: title + descriptionText: description + leftImageSource: imageSource - KeyNavigation.tab: mailButton - parentFlickable: fl - - clickedFunction: function() { - Qt.openUrlExternally(qsTr("https://t.me/amnezia_vpn_en")) - } + clickedFunction: handler } DividerType {} - LabelWithButtonType { - id: mailButton - Layout.fillWidth: true + } - text: qsTr("support@amnezia.org") - descriptionText: qsTr("For reviews and bug reports") - leftImageSource: "qrc:/images/controls/mail.svg" - - KeyNavigation.tab: githubButton - parentFlickable: fl - - clickedFunction: function() { - GC.copyToClipBoard(text) - PageController.showNotificationMessage(qsTr("Copied")) - } - - } - - DividerType {} - - LabelWithButtonType { - id: githubButton - Layout.fillWidth: true - - text: qsTr("GitHub") - leftImageSource: "qrc:/images/controls/github.svg" - - KeyNavigation.tab: websiteButton - parentFlickable: fl - - clickedFunction: function() { - Qt.openUrlExternally(qsTr("https://github.com/amnezia-vpn/amnezia-client")) - } - - } - - DividerType {} - - LabelWithButtonType { - id: websiteButton - Layout.fillWidth: true - - text: qsTr("Website") - leftImageSource: "qrc:/images/controls/amnezia.svg" - - KeyNavigation.tab: checkUpdatesButton - parentFlickable: fl - - clickedFunction: function() { - Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl()) - } - - } - - DividerType {} + footer: ColumnLayout { + width: listView.width CaptionTextType { Layout.fillWidth: true @@ -196,6 +217,7 @@ PageType { BasicButtonType { id: checkUpdatesButton + Layout.alignment: Qt.AlignHCenter Layout.topMargin: 8 Layout.bottomMargin: 16 @@ -209,35 +231,30 @@ PageType { text: qsTr("Check for updates") - KeyNavigation.tab: privacyPolicyButton - parentFlickable: fl - clickedFunc: function() { Qt.openUrlExternally("https://github.com/amnezia-vpn/desktop-client/releases/latest") } } BasicButtonType { - id: privacyPolicyButton - Layout.alignment: Qt.AlignHCenter - Layout.bottomMargin: 16 - Layout.topMargin: -15 - implicitHeight: 25 + id: privacyPolicyButton - defaultColor: AmneziaStyle.color.transparent - hoveredColor: AmneziaStyle.color.translucentWhite - pressedColor: AmneziaStyle.color.sheerWhite - disabledColor: AmneziaStyle.color.mutedGray - textColor: AmneziaStyle.color.goldenApricot + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: 16 + Layout.topMargin: -15 + implicitHeight: 25 - text: qsTr("Privacy Policy") + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.goldenApricot - Keys.onTabPressed: lastItemTabClicked() - parentFlickable: fl + text: qsTr("Privacy Policy") - clickedFunc: function() { - Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl() + "/policy") - } + clickedFunc: function() { + Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl() + "/policy") + } } } } diff --git a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml index 120313cd..30968b38 100644 --- a/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml +++ b/client/ui/qml/Pages2/PageSettingsApiLanguageList.qml @@ -18,85 +18,87 @@ PageType { ListView { id: menuContent - property var selectedText + property bool isFocusable: true width: parent.width - height: menuContent.contentItem.height + height: parent.height clip: true - interactive: false + interactive: true model: ApiCountryModel ButtonGroup { id: containersRadioButtonGroup } - delegate: Item { - implicitWidth: parent.width - implicitHeight: content.implicitHeight + delegate: ColumnLayout { + id: content - ColumnLayout { - id: content + width: menuContent.width + height: content.implicitHeight - anchors.fill: parent + RowLayout { + VerticalRadioButton { + id: containerRadioButton - RowLayout { - VerticalRadioButton { - id: containerRadioButton - - Layout.fillWidth: true - Layout.leftMargin: 16 - - text: countryName - - ButtonGroup.group: containersRadioButtonGroup - - imageSource: "qrc:/images/controls/download.svg" - - checked: index === ApiCountryModel.currentIndex - - onClicked: { - if (index !== ApiCountryModel.currentIndex) { - PageController.showBusyIndicator(true) - var prevIndex = ApiCountryModel.currentIndex - ApiCountryModel.currentIndex = index - if (!InstallController.updateServiceFromApi(ServersModel.defaultIndex, countryCode, countryName)) { - ApiCountryModel.currentIndex = prevIndex - } - } - } - - MouseArea { - anchors.fill: containerRadioButton - cursorShape: Qt.PointingHandCursor - enabled: false - } - - Keys.onEnterPressed: { - if (checkable) { - checked = true - } - containerRadioButton.clicked() - } - Keys.onReturnPressed: { - if (checkable) { - checked = true - } - containerRadioButton.clicked() - } - } - - Image { - Layout.rightMargin: 32 - Layout.alignment: Qt.AlignRight - - source: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg" - } - } - - DividerType { Layout.fillWidth: true + Layout.leftMargin: 16 + + text: countryName + + ButtonGroup.group: containersRadioButtonGroup + + imageSource: "qrc:/images/controls/download.svg" + + checked: index === ApiCountryModel.currentIndex + checkable: !ConnectionController.isConnected + + onClicked: { + if (ConnectionController.isConnected) { + PageController.showNotificationMessage(qsTr("Unable change server location while there is an active connection")) + return + } + + if (index !== ApiCountryModel.currentIndex) { + PageController.showBusyIndicator(true) + var prevIndex = ApiCountryModel.currentIndex + ApiCountryModel.currentIndex = index + if (!InstallController.updateServiceFromApi(ServersModel.defaultIndex, countryCode, countryName)) { + ApiCountryModel.currentIndex = prevIndex + } + } + } + + MouseArea { + anchors.fill: containerRadioButton + cursorShape: Qt.PointingHandCursor + enabled: false + } + + Keys.onEnterPressed: { + if (checkable) { + checked = true + } + containerRadioButton.clicked() + } + Keys.onReturnPressed: { + if (checkable) { + checked = true + } + containerRadioButton.clicked() + } } + + Image { + Layout.rightMargin: 32 + Layout.alignment: Qt.AlignRight + + source: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg" + } + } + + DividerType { + Layout.fillWidth: true } } } diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 2d6c1d9b..39207486 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -15,8 +15,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - FlickableType { id: fl anchors.top: parent.top @@ -32,11 +30,6 @@ PageType { spacing: 0 - Item { - id: focusItem -// KeyNavigation.tab: backButton - } - LabelWithImageType { Layout.fillWidth: true Layout.margins: 16 @@ -56,12 +49,15 @@ PageType { } LabelWithImageType { + property bool showSubscriptionEndDate: ServersModel.getProcessedServerData("isCountrySelectionAvailable") + Layout.fillWidth: true Layout.margins: 16 imageSource: "qrc:/images/controls/history.svg" - leftText: qsTr("Work period") - rightText: ApiServicesModel.getSelectedServiceData("workPeriod") + leftText: showSubscriptionEndDate ? qsTr("Valid until") : qsTr("Work period") + rightText: showSubscriptionEndDate ? ApiServicesModel.getSelectedServiceData("endDate") + : ApiServicesModel.getSelectedServiceData("workPeriod") visible: rightText !== "" } @@ -108,9 +104,6 @@ PageType { descriptionOnTop: true -// parentFlickable: fl -// KeyNavigation.tab: passwordLabel.eyeButton - rightImageSource: "qrc:/images/controls/copy.svg" rightImageColor: AmneziaStyle.color.paleGray @@ -138,8 +131,6 @@ PageType { text: qsTr("Reload API config") -// Keys.onTabPressed: lastItemTabClicked(focusItem) - clickedFunc: function() { var headerText = qsTr("Reload API config?") var yesButtonText = qsTr("Continue") @@ -178,8 +169,6 @@ PageType { text: qsTr("Remove from application") -// Keys.onTabPressed: lastItemTabClicked(focusItem) - clickedFunc: function() { var headerText = qsTr("Remove from application?") var yesButtonText = qsTr("Continue") diff --git a/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml index 2e8bda2f..a78ae446 100644 --- a/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml +++ b/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml @@ -21,8 +21,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - property bool pageEnabled Component.onCompleted: { @@ -48,13 +46,15 @@ PageType { QtObject { id: onlyForwardApps - property string name: qsTr("Only the apps from the list should have access via VPN") - property int type: routeMode.onlyForwardApps + + readonly property string name: qsTr("Only the apps from the list should have access via VPN") + readonly property int type: routeMode.onlyForwardApps } QtObject { id: allExceptApps - property string name: qsTr("Apps from the list should not have access via VPN") - property int type: routeMode.allExceptApps + + readonly property string name: qsTr("Apps from the list should not have access via VPN") + readonly property int type: routeMode.allExceptApps } function getRouteModesModelIndex() { @@ -66,11 +66,6 @@ PageType { } } - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: header @@ -82,7 +77,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: switcher } RowLayout { @@ -103,10 +97,6 @@ PageType { enabled: root.pageEnabled - KeyNavigation.tab: selector.enabled ? - selector : - searchField.textField - checked: AppSplitTunnelingModel.isTunnelingEnabled onToggled: { AppSplitTunnelingModel.toggleSplitTunneling(checked) @@ -130,25 +120,23 @@ PageType { enabled: Qt.platform.os === "android" && root.pageEnabled - KeyNavigation.tab: searchField.textField - listView: ListViewWithRadioButtonType { rootWidth: root.width model: root.routeModesModel - currentIndex: getRouteModesModelIndex() + selectedIndex: getRouteModesModelIndex() clickedFunction: function() { selector.text = selectedText - selector.close() - if (AppSplitTunnelingModel.routeMode !== root.routeModesModel[currentIndex].type) { - AppSplitTunnelingModel.routeMode = root.routeModesModel[currentIndex].type + selector.closeTriggered() + if (AppSplitTunnelingModel.routeMode !== root.routeModesModel[selectedIndex].type) { + AppSplitTunnelingModel.routeMode = root.routeModesModel[selectedIndex].type } } Component.onCompleted: { - if (root.routeModesModel[currentIndex].type === AppSplitTunnelingModel.routeMode) { + if (root.routeModesModel[selectedIndex].type === AppSplitTunnelingModel.routeMode) { selector.text = selectedText } else { selector.text = root.routeModesModel[0].name @@ -158,7 +146,7 @@ PageType { Connections { target: AppSplitTunnelingModel function onRouteModeChanged() { - currentIndex = getRouteModesModelIndex() + selectedIndex = getRouteModesModelIndex() } } } @@ -267,7 +255,6 @@ PageType { textFieldPlaceholderText: qsTr("application name") buttonImageSource: "qrc:/images/controls/plus.svg" - Keys.onTabPressed: lastItemTabClicked(focusItem) rightButtonClickedOnEnter: true clickedFunc: function() { @@ -281,7 +268,7 @@ PageType { AppSplitTunnelingController.addApp(fileName) } } else if (Qt.platform.os === "android"){ - installedAppDrawer.open() + installedAppDrawer.openTriggered() } PageController.showBusyIndicator(false) diff --git a/client/ui/qml/Pages2/PageSettingsApplication.qml b/client/ui/qml/Pages2/PageSettingsApplication.qml index 0f85ac30..6f77a521 100644 --- a/client/ui/qml/Pages2/PageSettingsApplication.qml +++ b/client/ui/qml/Pages2/PageSettingsApplication.qml @@ -14,19 +14,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - - onFocusChanged: { - if (focusItem.activeFocus) { - fl.contentY = 0 - } - } - } - BackButtonType { id: backButton @@ -34,8 +21,6 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 - - KeyNavigation.tab: GC.isMobile() ? switcher : switcherAutoStart } FlickableType { @@ -77,8 +62,8 @@ PageType { } } - KeyNavigation.tab: Qt.platform.os === "android" && !SettingsController.isNotificationPermissionGranted ? - labelWithButtonNotification.rightButton : labelWithButtonLanguage.rightButton + // KeyNavigation.tab: Qt.platform.os === "android" && !SettingsController.isNotificationPermissionGranted ? + // labelWithButtonNotification.rightButton : labelWithButtonLanguage.rightButton parentFlickable: fl } @@ -95,7 +80,6 @@ PageType { descriptionText: qsTr("Enable notifications to show the VPN state in the status bar") rightImageSource: "qrc:/images/controls/chevron-right.svg" - KeyNavigation.tab: labelWithButtonLanguage.rightButton parentFlickable: fl clickedFunction: function() { @@ -117,7 +101,6 @@ PageType { text: qsTr("Auto start") descriptionText: qsTr("Launch the application every time the device is starts") - KeyNavigation.tab: switcherAutoConnect parentFlickable: fl checked: SettingsController.isAutoStartEnabled() @@ -142,7 +125,6 @@ PageType { text: qsTr("Auto connect") descriptionText: qsTr("Connect to VPN on app start") - KeyNavigation.tab: switcherStartMinimized parentFlickable: fl checked: SettingsController.isAutoConnectEnabled() @@ -167,7 +149,6 @@ PageType { text: qsTr("Start minimized") descriptionText: qsTr("Launch application minimized") - KeyNavigation.tab: labelWithButtonLanguage.rightButton parentFlickable: fl checked: SettingsController.isStartMinimizedEnabled() @@ -190,11 +171,10 @@ PageType { descriptionText: LanguageModel.currentLanguageName rightImageSource: "qrc:/images/controls/chevron-right.svg" - KeyNavigation.tab: labelWithButtonLogging.rightButton parentFlickable: fl clickedFunction: function() { - selectLanguageDrawer.open() + selectLanguageDrawer.openTriggered() } } @@ -208,7 +188,6 @@ PageType { descriptionText: SettingsController.isLoggingEnabled ? qsTr("Enabled") : qsTr("Disabled") rightImageSource: "qrc:/images/controls/chevron-right.svg" - KeyNavigation.tab: labelWithButtonReset.rightButton parentFlickable: fl clickedFunction: function() { @@ -226,7 +205,6 @@ PageType { rightImageSource: "qrc:/images/controls/chevron-right.svg" textColor: AmneziaStyle.color.vibrantRed - Keys.onTabPressed: lastItemTabClicked() parentFlickable: fl clickedFunction: function() { @@ -243,15 +221,8 @@ PageType { SettingsController.clearSettings() PageController.goToPageHome() } - - if (!GC.isMobile()) { - root.defaultActiveFocusItem.forceActiveFocus() - } } var noButtonFunction = function() { - if (!GC.isMobile()) { - root.defaultActiveFocusItem.forceActiveFocus() - } } showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) @@ -267,11 +238,5 @@ PageType { width: root.width height: root.height - - onClosed: { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } - } } } diff --git a/client/ui/qml/Pages2/PageSettingsBackup.qml b/client/ui/qml/Pages2/PageSettingsBackup.qml index abede9b3..d2dd4f2a 100644 --- a/client/ui/qml/Pages2/PageSettingsBackup.qml +++ b/client/ui/qml/Pages2/PageSettingsBackup.qml @@ -17,8 +17,6 @@ import "../Controls2/TextTypes" PageType { id: root - defaultActiveFocusItem: focusItem - Connections { target: SettingsController @@ -36,11 +34,6 @@ PageType { } } - Item { - id: focusItem - KeyNavigation.tab: backButton - } - BackButtonType { id: backButton @@ -48,8 +41,6 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 - - KeyNavigation.tab: makeBackupButton } FlickableType { @@ -93,6 +84,8 @@ PageType { text: qsTr("Make a backup") + parentFlickable: fl + clickedFunc: function() { var fileName = "" if (GC.isMobile()) { @@ -111,8 +104,6 @@ PageType { PageController.showNotificationMessage(qsTr("Backup file saved")) } } - - KeyNavigation.tab: restoreBackupButton } BasicButtonType { @@ -129,6 +120,8 @@ PageType { text: qsTr("Restore from backup") + parentFlickable: fl + clickedFunc: function() { var filePath = SystemController.getFileName(qsTr("Open backup file"), qsTr("Backup files (*.backup)")) @@ -136,8 +129,6 @@ PageType { restoreBackup(filePath) } } - - Keys.onTabPressed: lastItemTabClicked() } } } diff --git a/client/ui/qml/Pages2/PageSettingsConnection.qml b/client/ui/qml/Pages2/PageSettingsConnection.qml index 31b1c764..d3743b96 100644 --- a/client/ui/qml/Pages2/PageSettingsConnection.qml +++ b/client/ui/qml/Pages2/PageSettingsConnection.qml @@ -12,15 +12,8 @@ import "../Config" PageType { id: root - defaultActiveFocusItem: focusItem - property bool isAppSplitTinnelingEnabled: Qt.platform.os === "windows" || Qt.platform.os === "android" - Item { - id: focusItem - KeyNavigation.tab: backButton - } - BackButtonType { id: backButton @@ -28,8 +21,6 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 - - KeyNavigation.tab: amneziaDnsSwitch } FlickableType { @@ -67,8 +58,6 @@ PageType { SettingsController.toggleAmneziaDns(checked) } } - - KeyNavigation.tab: dnsServersButton.rightButton } DividerType {} @@ -81,11 +70,11 @@ PageType { descriptionText: qsTr("When AmneziaDNS is not used or installed") rightImageSource: "qrc:/images/controls/chevron-right.svg" + parentFlickable: fl + clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsDns) } - - KeyNavigation.tab: splitTunnelingButton.rightButton } DividerType {} @@ -98,19 +87,11 @@ PageType { descriptionText: qsTr("Allows you to select which sites you want to access through the VPN") rightImageSource: "qrc:/images/controls/chevron-right.svg" + parentFlickable: fl + clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsSplitTunneling) } - - Keys.onTabPressed: { - if (splitTunnelingButton2.visible) { - return splitTunnelingButton2.rightButton.forceActiveFocus() - } else if (killSwitchSwitcher.visible) { - return killSwitchSwitcher.forceActiveFocus() - } else { - lastItemTabClicked() - } - } } DividerType { @@ -127,17 +108,11 @@ PageType { descriptionText: qsTr("Allows you to use the VPN only for certain Apps") rightImageSource: "qrc:/images/controls/chevron-right.svg" + parentFlickable: fl + clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsAppSplitTunneling) } - - Keys.onTabPressed: { - if (killSwitchSwitcher.visible) { - return killSwitchSwitcher.forceActiveFocus() - } else { - lastItemTabClicked() - } - } } DividerType { @@ -154,6 +129,8 @@ PageType { text: qsTr("KillSwitch") descriptionText: qsTr("Disables your internet if your encrypted VPN connection drops out for any reason.") + parentFlickable: fl + checked: SettingsController.isKillSwitchEnabled() checkable: !ConnectionController.isConnected onCheckedChanged: { @@ -166,8 +143,6 @@ PageType { PageController.showNotificationMessage(qsTr("Cannot change killSwitch settings during active connection")) } } - - Keys.onTabPressed: lastItemTabClicked() } DividerType { diff --git a/client/ui/qml/Pages2/PageSettingsDns.qml b/client/ui/qml/Pages2/PageSettingsDns.qml index 1d7517d9..11e49cf9 100644 --- a/client/ui/qml/Pages2/PageSettingsDns.qml +++ b/client/ui/qml/Pages2/PageSettingsDns.qml @@ -14,13 +14,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: primaryDns.textField - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - BackButtonType { id: backButton @@ -28,8 +21,6 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 - - KeyNavigation.tab: root.defaultActiveFocusItem } FlickableType { @@ -80,8 +71,6 @@ PageType { textField.validator: RegularExpressionValidator { regularExpression: InstallController.ipAddressRegExp() } - - KeyNavigation.tab: secondaryDns.textField } TextFieldWithHeaderType { @@ -94,8 +83,6 @@ PageType { textField.validator: RegularExpressionValidator { regularExpression: InstallController.ipAddressRegExp() } - - KeyNavigation.tab: restoreDefaultButton } BasicButtonType { @@ -122,21 +109,12 @@ PageType { SettingsController.secondaryDns = "1.0.0.1" secondaryDns.textFieldText = SettingsController.secondaryDns PageController.showNotificationMessage(qsTr("Settings have been reset")) - - if (!GC.isMobile()) { - defaultActiveFocusItem.forceActiveFocus() - } } var noButtonFunction = function() { - if (!GC.isMobile()) { - defaultActiveFocusItem.forceActiveFocus() - } } showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } - - KeyNavigation.tab: saveButton } BasicButtonType { @@ -155,8 +133,6 @@ PageType { } PageController.showNotificationMessage(qsTr("Settings saved")) } - - Keys.onTabPressed: lastItemTabClicked(focusItem) } } } diff --git a/client/ui/qml/Pages2/PageSettingsLogging.qml b/client/ui/qml/Pages2/PageSettingsLogging.qml index 9abfc453..7d85e2b3 100644 --- a/client/ui/qml/Pages2/PageSettingsLogging.qml +++ b/client/ui/qml/Pages2/PageSettingsLogging.qml @@ -16,13 +16,6 @@ import "../Controls2/TextTypes" PageType { id: root - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - BackButtonType { id: backButton @@ -30,23 +23,22 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 - - KeyNavigation.tab: switcher } - FlickableType { - id: fl + ListView { + id: listView + anchors.top: backButton.bottom anchors.bottom: parent.bottom - contentHeight: content.height + anchors.right: parent.right + anchors.left: parent.left - ColumnLayout { - id: content + property bool isFocusable: true - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - spacing: 0 + ScrollBar.vertical: ScrollBarType {} + + header: ColumnLayout { + width: listView.width HeaderType { Layout.fillWidth: true @@ -60,6 +52,7 @@ PageType { SwitcherType { id: switcher + Layout.fillWidth: true Layout.topMargin: 16 Layout.leftMargin: 16 @@ -68,7 +61,7 @@ PageType { text: qsTr("Enable logs") checked: SettingsController.isLoggingEnabled - //KeyNavigation.tab: openFolderButton + onCheckedChanged: { if (checked !== SettingsController.isLoggingEnabled) { SettingsController.isLoggingEnabled = checked @@ -79,7 +72,6 @@ PageType { DividerType {} LabelWithButtonType { - // id: labelWithButton2 Layout.fillWidth: true Layout.topMargin: -8 @@ -87,8 +79,6 @@ PageType { leftImageSource: "qrc:/images/controls/trash.svg" isSmallLeftImage: true - // KeyNavigation.tab: labelWithButton3 - clickedFunction: function() { var headerText = qsTr("Clear logs?") var yesButtonText = qsTr("Continue") @@ -99,19 +89,28 @@ PageType { SettingsController.clearLogs() PageController.showBusyIndicator(false) PageController.showNotificationMessage(qsTr("Logs have been cleaned up")) - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } } + var noButtonFunction = function() { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } + } showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } + } + + model: logTypes + clip: true + reuseItems: true + snapMode: ListView.SnapOneItem + + delegate: ColumnLayout { + id: delegateContent + + width: listView.width + + enabled: isVisible ListItemTitleType { Layout.fillWidth: true @@ -119,7 +118,7 @@ PageType { Layout.leftMargin: 16 Layout.rightMargin: 16 - text: qsTr("Client logs") + text: title } ParagraphTextType { @@ -129,11 +128,11 @@ PageType { Layout.rightMargin: 16 color: AmneziaStyle.color.mutedGray - text: qsTr("AmneziaVPN logs") + + text: description } LabelWithButtonType { - // id: labelWithButton2 Layout.fillWidth: true Layout.topMargin: -8 Layout.bottomMargin: -8 @@ -142,17 +141,12 @@ PageType { leftImageSource: "qrc:/images/controls/folder-open.svg" isSmallLeftImage: true - // KeyNavigation.tab: labelWithButton3 - - clickedFunction: function() { - SettingsController.openLogsFolder() - } + clickedFunction: openLogsHandler } DividerType {} LabelWithButtonType { - // id: labelWithButton2 Layout.fillWidth: true Layout.topMargin: -8 Layout.bottomMargin: -8 @@ -161,114 +155,72 @@ PageType { leftImageSource: "qrc:/images/controls/save.svg" isSmallLeftImage: true - // KeyNavigation.tab: labelWithButton3 - - clickedFunction: function() { - var fileName = "" - if (GC.isMobile()) { - fileName = "AmneziaVPN.log" - } else { - fileName = SystemController.getFileName(qsTr("Save"), - qsTr("Logs files (*.log)"), - StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/AmneziaVPN", - true, - ".log") - } - if (fileName !== "") { - PageController.showBusyIndicator(true) - SettingsController.exportLogsFile(fileName) - PageController.showBusyIndicator(false) - PageController.showNotificationMessage(qsTr("Logs file saved")) - } - } + clickedFunction: exportLogsHandler } DividerType {} + } + } - ListItemTitleType { - visible: !GC.isMobile() + property list logTypes: [ + clientLogs, + serviceLogs + ] - Layout.fillWidth: true - Layout.topMargin: 32 - Layout.leftMargin: 16 - Layout.rightMargin: 16 + QtObject { + id: clientLogs - text: qsTr("Service logs") + readonly property string title: qsTr("Client logs") + readonly property string description: qsTr("AmneziaVPN logs") + readonly property bool isVisible: true + readonly property var openLogsHandler: function() { + SettingsController.openLogsFolder() + } + readonly property var exportLogsHandler: function() { + var fileName = "" + if (GC.isMobile()) { + fileName = "AmneziaVPN.log" + } else { + fileName = SystemController.getFileName(qsTr("Save"), + qsTr("Logs files (*.log)"), + StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/AmneziaVPN", + true, + ".log") } - - ParagraphTextType { - visible: !GC.isMobile() - - Layout.fillWidth: true - Layout.topMargin: 8 - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - color: AmneziaStyle.color.mutedGray - text: qsTr("AmneziaVPN-service logs") + if (fileName !== "") { + PageController.showBusyIndicator(true) + SettingsController.exportLogsFile(fileName) + PageController.showBusyIndicator(false) + PageController.showNotificationMessage(qsTr("Logs file saved")) } + } + } - LabelWithButtonType { - // id: labelWithButton2 + QtObject { + id: serviceLogs - visible: !GC.isMobile() - - Layout.fillWidth: true - Layout.topMargin: -8 - Layout.bottomMargin: -8 - - text: qsTr("Open logs folder") - leftImageSource: "qrc:/images/controls/folder-open.svg" - isSmallLeftImage: true - - // KeyNavigation.tab: labelWithButton3 - - clickedFunction: function() { - SettingsController.openServiceLogsFolder() - } + readonly property string title: qsTr("Service logs") + readonly property string description: qsTr("AmneziaVPN-service logs") + readonly property bool isVisible: !GC.isMobile() + readonly property var openLogsHandler: function() { + SettingsController.openServiceLogsFolder() + } + readonly property var exportLogsHandler: function() { + var fileName = "" + if (GC.isMobile()) { + fileName = "AmneziaVPN-service.log" + } else { + fileName = SystemController.getFileName(qsTr("Save"), + qsTr("Logs files (*.log)"), + StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/AmneziaVPN-service", + true, + ".log") } - - DividerType { - visible: !GC.isMobile() - } - - LabelWithButtonType { - // id: labelWithButton2 - - visible: !GC.isMobile() - - Layout.fillWidth: true - Layout.topMargin: -8 - Layout.bottomMargin: -8 - - text: qsTr("Export logs") - leftImageSource: "qrc:/images/controls/save.svg" - isSmallLeftImage: true - - // KeyNavigation.tab: labelWithButton3 - - clickedFunction: function() { - var fileName = "" - if (GC.isMobile()) { - fileName = "AmneziaVPN-service.log" - } else { - fileName = SystemController.getFileName(qsTr("Save"), - qsTr("Logs files (*.log)"), - StandardPaths.standardLocations(StandardPaths.DocumentsLocation) + "/AmneziaVPN-service", - true, - ".log") - } - if (fileName !== "") { - PageController.showBusyIndicator(true) - SettingsController.exportServiceLogsFile(fileName) - PageController.showBusyIndicator(false) - PageController.showNotificationMessage(qsTr("Logs file saved")) - } - } - } - - DividerType { - visible: !GC.isMobile() + if (fileName !== "") { + PageController.showBusyIndicator(true) + SettingsController.exportServiceLogsFile(fileName) + PageController.showBusyIndicator(false) + PageController.showNotificationMessage(qsTr("Logs file saved")) } } } diff --git a/client/ui/qml/Pages2/PageSettingsServerData.qml b/client/ui/qml/Pages2/PageSettingsServerData.qml index e170a351..cd736d39 100644 --- a/client/ui/qml/Pages2/PageSettingsServerData.qml +++ b/client/ui/qml/Pages2/PageSettingsServerData.qml @@ -100,8 +100,6 @@ PageType { text: qsTr("Check the server for previously installed Amnezia services") descriptionText: qsTr("Add them to the application if they were not displayed") - KeyNavigation.tab: labelWithButton2 - clickedFunction: function() { PageController.showBusyIndicator(true) InstallController.scanServerForInstalledContainers() @@ -121,8 +119,6 @@ PageType { text: qsTr("Reboot server") textColor: AmneziaStyle.color.vibrantRed - KeyNavigation.tab: labelWithButton3 - clickedFunction: function() { var headerText = qsTr("Do you want to reboot the server?") var descriptionText = qsTr("The reboot process may take approximately 30 seconds. Are you sure you wish to proceed?") @@ -162,16 +158,6 @@ PageType { text: qsTr("Remove server from application") textColor: AmneziaStyle.color.vibrantRed - Keys.onTabPressed: { - if (content.isServerWithWriteAccess) { - labelWithButton4.forceActiveFocus() - } else { - labelWithButton5.visible ? - labelWithButton5.forceActiveFocus() : - lastItemTabClickedSignal() - } - } - clickedFunction: function() { var headerText = qsTr("Do you want to remove the server from application?") var descriptionText = qsTr("All installed AmneziaVPN services will still remain on the server.") @@ -210,10 +196,6 @@ PageType { text: qsTr("Clear server from Amnezia software") textColor: AmneziaStyle.color.vibrantRed - Keys.onTabPressed: labelWithButton5.visible ? - labelWithButton5.forceActiveFocus() : - root.lastItemTabClickedSignal() - clickedFunction: function() { var headerText = qsTr("Do you want to clear server from Amnezia software?") var descriptionText = qsTr("All users whom you shared a connection with will no longer be able to connect to it.") @@ -253,8 +235,6 @@ PageType { text: qsTr("Reset API config") textColor: AmneziaStyle.color.vibrantRed - Keys.onTabPressed: root.lastItemTabClickedSignal() - clickedFunction: function() { var headerText = qsTr("Do you want to reset API config?") var descriptionText = "" diff --git a/client/ui/qml/Pages2/PageSettingsServerInfo.qml b/client/ui/qml/Pages2/PageSettingsServerInfo.qml index 95ae5c8a..3172d31b 100644 --- a/client/ui/qml/Pages2/PageSettingsServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsServerInfo.qml @@ -19,24 +19,34 @@ import "../Components" PageType { id: root - property int pageSettingsServerProtocols: 0 - property int pageSettingsServerServices: 1 - property int pageSettingsServerData: 2 - property int pageSettingsApiServerInfo: 3 - property int pageSettingsApiLanguageList: 4 + readonly property int pageSettingsServerProtocols: 0 + readonly property int pageSettingsServerServices: 1 + readonly property int pageSettingsServerData: 2 + readonly property int pageSettingsApiServerInfo: 3 + readonly property int pageSettingsApiLanguageList: 4 - defaultActiveFocusItem: focusItem + property var processedServer Connections { target: PageController function onGoToPageSettingsServerServices() { - tabBar.currentIndex = root.pageSettingsServerServices + tabBar.setCurrentIndex(root.pageSettingsServerServices) + } + } + + Connections { + target: ServersModel + + function onProcessedServerChanged() { + root.processedServer = proxyServersModel.get(0) } } SortFilterProxyModel { id: proxyServersModel + objectName: "proxyServersModel" + sourceModel: ServersModel filters: [ ValueFilter { @@ -44,147 +54,115 @@ PageType { value: true } ] - } - Item { - id: focusItem - KeyNavigation.tab: header + Component.onCompleted: { + root.processedServer = proxyServersModel.get(0) + } } ColumnLayout { + objectName: "mainLayout" + anchors.fill: parent + anchors.topMargin: 20 - spacing: 16 + spacing: 4 - Repeater { - id: header - model: proxyServersModel + BackButtonType { + id: backButton + objectName: "backButton" - activeFocusOnTab: true - onFocusChanged: { - header.itemAt(0).focusItem.forceActiveFocus() + backButtonFunction: function() { + if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo && + root.processedServer.isCountrySelectionAvailable) { + nestedStackView.currentIndex = root.pageSettingsApiLanguageList + } else { + PageController.closePage() + } + } + } + + HeaderType { + id: headerContent + objectName: "headerContent" + + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 10 + + actionButtonImage: nestedStackView.currentIndex === root.pageSettingsApiLanguageList ? "qrc:/images/controls/settings.svg" + : "qrc:/images/controls/edit-3.svg" + + headerText: root.processedServer.name + descriptionText: { + if (root.processedServer.isServerFromGatewayApi) { + if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { + return qsTr("Subscription is valid until ") + ApiServicesModel.getSelectedServiceData("endDate") + } else { + return ApiServicesModel.getSelectedServiceData("serviceDescription") + } + } else if (root.processedServer.isServerFromTelegramApi) { + return root.processedServer.serverDescription + } else if (root.processedServer.hasWriteAccess) { + return root.processedServer.credentialsLogin + " · " + root.processedServer.hostName + } else { + return root.processedServer.hostName + } } - delegate: ColumnLayout { - - property alias focusItem: backButton - - id: content - - Layout.topMargin: 20 - - BackButtonType { - id: backButton - KeyNavigation.tab: headerContent.actionButton - - backButtonFunction: function() { - if (nestedStackView.currentIndex === root.pageSettingsApiServerInfo && - ServersModel.getProcessedServerData("isCountrySelectionAvailable")) { - nestedStackView.currentIndex = root.pageSettingsApiLanguageList - } else { - PageController.closePage() - } - } + actionButtonFunction: function() { + if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { + nestedStackView.currentIndex = root.pageSettingsApiServerInfo + } else { + serverNameEditDrawer.openTriggered() } + } + } + + DrawerType2 { + id: serverNameEditDrawer + objectName: "serverNameEditDrawer" + + parent: root + + anchors.fill: parent + expandedHeight: root.height * 0.35 + + expandedStateContent: ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 32 + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + TextFieldWithHeaderType { + id: serverName - HeaderType { - id: headerContent Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - actionButtonImage: nestedStackView.currentIndex === root.pageSettingsApiLanguageList ? "qrc:/images/controls/settings.svg" : "qrc:/images/controls/edit-3.svg" - - headerText: name - descriptionText: { - if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) { - return ApiServicesModel.getSelectedServiceData("serviceDescription") - } else if (ServersModel.getProcessedServerData("isServerFromTelegramApi")) { - return serverDescription - } else if (ServersModel.isProcessedServerHasWriteAccess()) { - return credentialsLogin + " · " + hostName - } else { - return hostName - } - } - - KeyNavigation.tab: tabBar - - actionButtonFunction: function() { - if (nestedStackView.currentIndex === root.pageSettingsApiLanguageList) { - nestedStackView.currentIndex = root.pageSettingsApiServerInfo - } else { - serverNameEditDrawer.open() - } - } + headerText: qsTr("Server name") + textFieldText: root.processedServer.name + textField.maximumLength: 30 + checkEmptyText: true } - DrawerType2 { - id: serverNameEditDrawer + BasicButtonType { + id: saveButton - parent: root + Layout.fillWidth: true - anchors.fill: parent - expandedHeight: root.height * 0.35 + text: qsTr("Save") - onClosed: { - if (!GC.isMobile()) { - headerContent.actionButton.forceActiveFocus() - } - } - - expandedContent: ColumnLayout { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 32 - anchors.leftMargin: 16 - anchors.rightMargin: 16 - - Connections { - target: serverNameEditDrawer - enabled: !GC.isMobile() - function onOpened() { - serverName.textField.forceActiveFocus() - } + clickedFunc: function() { + if (serverName.textFieldText === "") { + return } - Item { - id: focusItem1 - KeyNavigation.tab: serverName.textField - } - - TextFieldWithHeaderType { - id: serverName - - Layout.fillWidth: true - headerText: qsTr("Server name") - textFieldText: name - textField.maximumLength: 30 - checkEmptyText: true - - KeyNavigation.tab: saveButton - } - - BasicButtonType { - id: saveButton - - Layout.fillWidth: true - - text: qsTr("Save") - KeyNavigation.tab: focusItem1 - - clickedFunc: function() { - if (serverName.textFieldText === "") { - return - } - - if (serverName.textFieldText !== name) { - name = serverName.textFieldText - } - serverNameEditDrawer.close() - } + if (serverName.textFieldText !== root.processedServer.name) { + ServersModel.setProcessedServerData("name", serverName.textFieldText); } + serverNameEditDrawer.closeTriggered() } } } @@ -205,35 +183,27 @@ PageType { visible: !ServersModel.getProcessedServerData("isServerFromGatewayApi") - activeFocusOnTab: true - onFocusChanged: { - if (activeFocus) { - protocolsTab.forceActiveFocus() - } - } TabButtonType { id: protocolsTab visible: protocolsPage.installedProtocolsCount width: protocolsPage.installedProtocolsCount ? undefined : 0 - isSelected: tabBar.currentIndex === root.pageSettingsServerProtocols + isSelected: TabBar.tabBar.currentIndex === root.pageSettingsServerProtocols text: qsTr("Protocols") - KeyNavigation.tab: servicesTab - Keys.onReturnPressed: tabBar.currentIndex = root.pageSettingsServerProtocols - Keys.onEnterPressed: tabBar.currentIndex = root.pageSettingsServerProtocols + Keys.onReturnPressed: TabBar.tabBar.setCurrentIndex(root.pageSettingsServerProtocols) + Keys.onEnterPressed: TabBar.tabBar.setCurrentIndex(root.pageSettingsServerProtocols) } TabButtonType { id: servicesTab visible: servicesPage.installedServicesCount width: servicesPage.installedServicesCount ? undefined : 0 - isSelected: tabBar.currentIndex === root.pageSettingsServerServices + isSelected: TabBar.tabBar.currentIndex === root.pageSettingsServerServices text: qsTr("Services") - KeyNavigation.tab: dataTab - Keys.onReturnPressed: tabBar.currentIndex = root.pageSettingsServerServices - Keys.onEnterPressed: tabBar.currentIndex = root.pageSettingsServerServices + Keys.onReturnPressed: TabBar.tabBar.setCurrentIndex(root.pageSettingsServerServices) + Keys.onEnterPressed: TabBar.tabBar.setCurrentIndex(root.pageSettingsServerServices) } TabButtonType { @@ -241,24 +211,15 @@ PageType { isSelected: tabBar.currentIndex === root.pageSettingsServerData text: qsTr("Management") - Keys.onReturnPressed: tabBar.currentIndex = root.pageSettingsServerData - Keys.onEnterPressed: tabBar.currentIndex = root.pageSettingsServerData - Keys.onTabPressed: function() { - if (nestedStackView.currentIndex === root.pageSettingsServerProtocols) { - return protocolsPage - } else if (nestedStackView.currentIndex === root.pageSettingsServerProtocols) { - return servicesPage - } else { - return dataPage - } - } + Keys.onReturnPressed: TabBar.tabBar.setCurrentIndex(root.pageSettingsServerData) + Keys.onEnterPressed: TabBar.tabBar.setCurrentIndex(root.pageSettingsServerData) } } StackLayout { id: nestedStackView - Layout.preferredWidth: root.width - Layout.preferredHeight: root.height - tabBar.implicitHeight - header.implicitHeight + + Layout.fillWidth: true currentIndex: ServersModel.getProcessedServerData("isServerFromGatewayApi") ? (ServersModel.getProcessedServerData("isCountrySelectionAvailable") ? @@ -267,38 +228,27 @@ PageType { PageSettingsServerProtocols { id: protocolsPage stackView: root.stackView - - onLastItemTabClickedSignal: lastItemTabClicked(focusItem) } PageSettingsServerServices { id: servicesPage stackView: root.stackView - - onLastItemTabClickedSignal: lastItemTabClicked(focusItem) } PageSettingsServerData { id: dataPage stackView: root.stackView - - onLastItemTabClickedSignal: lastItemTabClicked(focusItem) } PageSettingsApiServerInfo { id: apiInfoPage stackView: root.stackView - -// onLastItemTabClickedSignal: lastItemTabClicked(focusItem) } PageSettingsApiLanguageList { id: apiLanguageListPage stackView: root.stackView - -// onLastItemTabClickedSignal: lastItemTabClicked(focusItem) } } - } } diff --git a/client/ui/qml/Pages2/PageSettingsServerProtocol.qml b/client/ui/qml/Pages2/PageSettingsServerProtocol.qml index dcdf01af..ade94ebb 100644 --- a/client/ui/qml/Pages2/PageSettingsServerProtocol.qml +++ b/client/ui/qml/Pages2/PageSettingsServerProtocol.qml @@ -21,13 +21,6 @@ PageType { property bool isClearCacheVisible: ServersModel.isProcessedServerHasWriteAccess() && !ContainersModel.isServiceContainer(ContainersModel.getProcessedContainerIndex()) - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: header @@ -39,7 +32,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: protocols } HeaderType { @@ -57,30 +49,36 @@ PageType { height: protocols.contentItem.height clip: true interactive: true - model: ProtocolsModel - property int currentFocusIndex: 0 - - activeFocusOnTab: true - onActiveFocusChanged: { - if (activeFocus) { - this.currentFocusIndex = 0 - protocols.itemAtIndex(currentFocusIndex).focusItem.forceActiveFocus() - } - } + property bool isFocusable: true Keys.onTabPressed: { - if (currentFocusIndex < this.count - 1) { - currentFocusIndex += 1 - protocols.itemAtIndex(currentFocusIndex).focusItem.forceActiveFocus() - } else { - clearCacheButton.forceActiveFocus() - } + FocusController.nextKeyTabItem() } - delegate: Item { - property var focusItem: clientSettings.rightButton + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + + model: ProtocolsModel + + delegate: Item { implicitWidth: protocols.width implicitHeight: delegateContent.implicitHeight @@ -160,109 +158,112 @@ PageType { } } } - } - LabelWithButtonType { - id: clearCacheButton + footer: ColumnLayout { + width: header.width - Layout.fillWidth: true + LabelWithButtonType { + id: clearCacheButton - visible: root.isClearCacheVisible - KeyNavigation.tab: removeButton + Layout.fillWidth: true - text: qsTr("Clear profile") + visible: root.isClearCacheVisible - clickedFunction: function() { - var headerText = qsTr("Clear %1 profile?").arg(ContainersModel.getProcessedContainerName()) - var descriptionText = qsTr("The connection configuration will be deleted for this device only") - var yesButtonText = qsTr("Continue") - var noButtonText = qsTr("Cancel") + text: qsTr("Clear profile") - var yesButtonFunction = function() { - if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) { - var message = qsTr("Unable to clear %1 profile while there is an active connection").arg(ContainersModel.getProcessedContainerName()) - PageController.showNotificationMessage(message) - return + clickedFunction: function() { + var headerText = qsTr("Clear %1 profile?").arg(ContainersModel.getProcessedContainerName()) + var descriptionText = qsTr("The connection configuration will be deleted for this device only") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function() { + if (ConnectionController.isConnected && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) { + var message = qsTr("Unable to clear %1 profile while there is an active connection").arg(ContainersModel.getProcessedContainerName()) + PageController.showNotificationMessage(message) + return + } + + PageController.showBusyIndicator(true) + InstallController.clearCachedProfile() + PageController.showBusyIndicator(false) + } + var noButtonFunction = function() { + // if (!GC.isMobile()) { + // focusItem.forceActiveFocus() + // } + } + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } - PageController.showBusyIndicator(true) - InstallController.clearCachedProfile() - PageController.showBusyIndicator(false) - } - var noButtonFunction = function() { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() + MouseArea { + anchors.fill: clearCacheButton + cursorShape: Qt.PointingHandCursor + enabled: false } } - showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) - } + DividerType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 - MouseArea { - anchors.fill: clearCacheButton - cursorShape: Qt.PointingHandCursor - enabled: false - } - } - - DividerType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - visible: root.isClearCacheVisible - } - - LabelWithButtonType { - id: removeButton - - Layout.fillWidth: true - - visible: ServersModel.isProcessedServerHasWriteAccess() - Keys.onTabPressed: lastItemTabClicked(focusItem) - - text: qsTr("Remove ") - textColor: AmneziaStyle.color.vibrantRed - - clickedFunction: function() { - var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) - var descriptionText = qsTr("All users with whom you shared a connection will no longer be able to connect to it.") - var yesButtonText = qsTr("Continue") - var noButtonText = qsTr("Cancel") - - var yesButtonFunction = function() { - if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected - && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) { - PageController.showNotificationMessage(qsTr("Cannot remove active container")) - } else - { - PageController.goToPage(PageEnum.PageDeinstalling) - InstallController.removeProcessedContainer() - } + visible: root.isClearCacheVisible } - var noButtonFunction = function() { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() + + LabelWithButtonType { + id: removeButton + + Layout.fillWidth: true + + visible: ServersModel.isProcessedServerHasWriteAccess() + + text: qsTr("Remove ") + textColor: AmneziaStyle.color.vibrantRed + + clickedFunction: function() { + var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) + var descriptionText = qsTr("All users with whom you shared a connection will no longer be able to connect to it.") + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function() { + if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected + && ServersModel.getDefaultServerData("defaultContainer") === ContainersModel.getProcessedContainerIndex()) { + PageController.showNotificationMessage(qsTr("Cannot remove active container")) + } else + { + PageController.goToPage(PageEnum.PageDeinstalling) + InstallController.removeProcessedContainer() + } + } + var noButtonFunction = function() { + if (!GC.isMobile()) { + focusItem.forceActiveFocus() + } + } + + showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) + } + + MouseArea { + anchors.fill: removeButton + cursorShape: Qt.PointingHandCursor + enabled: false } } - showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) - } + DividerType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 - MouseArea { - anchors.fill: removeButton - cursorShape: Qt.PointingHandCursor - enabled: false + visible: ServersModel.isProcessedServerHasWriteAccess() + } } } - DividerType { - Layout.fillWidth: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - - visible: ServersModel.isProcessedServerHasWriteAccess() - } } } diff --git a/client/ui/qml/Pages2/PageSettingsServerProtocols.qml b/client/ui/qml/Pages2/PageSettingsServerProtocols.qml index 0bc1fc7d..ba72957e 100644 --- a/client/ui/qml/Pages2/PageSettingsServerProtocols.qml +++ b/client/ui/qml/Pages2/PageSettingsServerProtocols.qml @@ -21,53 +21,45 @@ PageType { property var installedProtocolsCount - onFocusChanged: settingsContainersListView.forceActiveFocus() - signal lastItemTabClickedSignal() + function resetView() { + settingsContainersListView.positionViewAtBeginning() + } - FlickableType { - id: fl - anchors.top: parent.top - anchors.bottom: parent.bottom - contentHeight: content.implicitHeight + SettingsContainersListView { + id: settingsContainersListView - Column { - id: content + anchors.fill: parent - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + Connections { + target: ServersModel - SettingsContainersListView { - id: settingsContainersListView - - Connections { - target: ServersModel - - function onProcessedServerIndexChanged() { - settingsContainersListView.updateContainersModelFilters() - } - } - - function updateContainersModelFilters() { - if (ServersModel.isProcessedServerHasWriteAccess()) { - proxyContainersModel.filters = ContainersModelFilters.getWriteAccessProtocolsListFilters() - } else { - proxyContainersModel.filters = ContainersModelFilters.getReadAccessProtocolsListFilters() - } - root.installedProtocolsCount = proxyContainersModel.count - } - - model: SortFilterProxyModel { - id: proxyContainersModel - sourceModel: ContainersModel - sorters: [ - RoleSorter { roleName: "isInstalled"; sortOrder: Qt.DescendingOrder }, - RoleSorter { roleName: "installPageOrder"; sortOrder: Qt.AscendingOrder } - ] - } - - Component.onCompleted: updateContainersModelFilters() + function onProcessedServerIndexChanged() { + settingsContainersListView.updateContainersModelFilters() } } + + function updateContainersModelFilters() { + if (ServersModel.isProcessedServerHasWriteAccess()) { + proxyContainersModel.filters = ContainersModelFilters.getWriteAccessProtocolsListFilters() + } else { + proxyContainersModel.filters = ContainersModelFilters.getReadAccessProtocolsListFilters() + } + root.installedProtocolsCount = proxyContainersModel.count + } + + model: SortFilterProxyModel { + id: proxyContainersModel + sourceModel: ContainersModel + sorters: [ + RoleSorter { roleName: "isInstalled"; sortOrder: Qt.DescendingOrder }, + RoleSorter { roleName: "installPageOrder"; sortOrder: Qt.AscendingOrder } + ] + } + + Component.onCompleted: { + settingsContainersListView.isFocusable = true + settingsContainersListView.interactive = true + updateContainersModelFilters() + } } } diff --git a/client/ui/qml/Pages2/PageSettingsServerServices.qml b/client/ui/qml/Pages2/PageSettingsServerServices.qml index 440fd8db..a46d4051 100644 --- a/client/ui/qml/Pages2/PageSettingsServerServices.qml +++ b/client/ui/qml/Pages2/PageSettingsServerServices.qml @@ -21,52 +21,40 @@ PageType { property var installedServicesCount - onFocusChanged: settingsContainersListView.forceActiveFocus() - signal lastItemTabClickedSignal() + SettingsContainersListView { + id: settingsContainersListView - FlickableType { - id: fl - anchors.top: parent.top - anchors.bottom: parent.bottom - contentHeight: content.implicitHeight + anchors.fill: parent - Column { - id: content + Connections { + target: ServersModel - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - - SettingsContainersListView { - id: settingsContainersListView - - Connections { - target: ServersModel - - function onProcessedServerIndexChanged() { - settingsContainersListView.updateContainersModelFilters() - } - } - - function updateContainersModelFilters() { - if (ServersModel.isProcessedServerHasWriteAccess()) { - proxyContainersModel.filters = ContainersModelFilters.getWriteAccessServicesListFilters() - } else { - proxyContainersModel.filters = ContainersModelFilters.getReadAccessServicesListFilters() - } - root.installedServicesCount = proxyContainersModel.count - } - - model: SortFilterProxyModel { - id: proxyContainersModel - sourceModel: ContainersModel - sorters: [ - RoleSorter { roleName: "isInstalled"; sortOrder: Qt.DescendingOrder } - ] - } - - Component.onCompleted: updateContainersModelFilters() + function onProcessedServerIndexChanged() { + settingsContainersListView.updateContainersModelFilters() } } + + function updateContainersModelFilters() { + if (ServersModel.isProcessedServerHasWriteAccess()) { + proxyContainersModel.filters = ContainersModelFilters.getWriteAccessServicesListFilters() + } else { + proxyContainersModel.filters = ContainersModelFilters.getReadAccessServicesListFilters() + } + root.installedServicesCount = proxyContainersModel.count + } + + model: SortFilterProxyModel { + id: proxyContainersModel + sourceModel: ContainersModel + sorters: [ + RoleSorter { roleName: "isInstalled"; sortOrder: Qt.DescendingOrder } + ] + } + + Component.onCompleted: { + settingsContainersListView.isFocusable = true + settingsContainersListView.interactive = true + updateContainersModelFilters() + } } } diff --git a/client/ui/qml/Pages2/PageSettingsServersList.qml b/client/ui/qml/Pages2/PageSettingsServersList.qml index 102dd46f..17337a48 100644 --- a/client/ui/qml/Pages2/PageSettingsServersList.qml +++ b/client/ui/qml/Pages2/PageSettingsServersList.qml @@ -18,13 +18,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - ColumnLayout { id: header @@ -36,7 +29,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: servers } HeaderType { @@ -48,95 +40,64 @@ PageType { } } - FlickableType { - id: fl + ListView { + id: servers + objectName: "servers" + + width: parent.width anchors.top: header.bottom anchors.topMargin: 16 - contentHeight: col.implicitHeight + anchors.left: parent.left + anchors.right: parent.right - Column { - id: col - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + height: 500 - ListView { - id: servers - width: parent.width - height: servers.contentItem.height + property bool isFocusable: true - model: ServersModel + model: ServersModel - clip: true - interactive: false + clip: true + reuseItems: true - activeFocusOnTab: true - focus: true - Keys.onTabPressed: { - if (currentIndex < servers.count - 1) { - servers.incrementCurrentIndex() - } else { - servers.currentIndex = 0 - focusItem.forceActiveFocus() - root.lastItemTabClicked() - } + delegate: Item { + implicitWidth: servers.width + implicitHeight: delegateContent.implicitHeight - fl.ensureVisible(this.currentItem) - } + ColumnLayout { + id: delegateContent - onVisibleChanged: { - if (visible) { - currentIndex = 0 - } - } + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right - delegate: Item { - implicitWidth: servers.width - implicitHeight: delegateContent.implicitHeight + LabelWithButtonType { + id: server + Layout.fillWidth: true - onFocusChanged: { - if (focus) { - server.rightButton.forceActiveFocus() - } - } + text: name - ColumnLayout { - id: delegateContent - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - - LabelWithButtonType { - id: server - Layout.fillWidth: true - - text: name - parentFlickable: fl - descriptionText: { - var servicesNameString = "" - var servicesName = ServersModel.getAllInstalledServicesName(index) - for (var i = 0; i < servicesName.length; i++) { - servicesNameString += servicesName[i] + " · " - } - - if (ServersModel.isServerFromApi(index)) { - return servicesNameString + serverDescription - } else { - return servicesNameString + hostName - } - } - rightImageSource: "qrc:/images/controls/chevron-right.svg" - - clickedFunction: function() { - ServersModel.processedIndex = index - PageController.goToPage(PageEnum.PageSettingsServerInfo) - } + descriptionText: { + var servicesNameString = "" + var servicesName = ServersModel.getAllInstalledServicesName(index) + for (var i = 0; i < servicesName.length; i++) { + servicesNameString += servicesName[i] + " · " } - DividerType {} + if (ServersModel.isServerFromApi(index)) { + return servicesNameString + serverDescription + } else { + return servicesNameString + hostName + } + } + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + ServersModel.processedIndex = index + PageController.goToPage(PageEnum.PageSettingsServerInfo) } } + + DividerType {} } } } diff --git a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml index f5fe285a..759ad9cd 100644 --- a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml +++ b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml @@ -23,13 +23,6 @@ PageType { property var isServerFromTelegramApi: ServersModel.getDefaultServerData("isServerFromTelegramApi") - defaultActiveFocusItem: searchField.textField - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - property bool pageEnabled Component.onCompleted: { @@ -99,7 +92,6 @@ PageType { BackButtonType { id: backButton - KeyNavigation.tab: switcher } RowLayout { @@ -129,8 +121,6 @@ PageType { onToggled: { onToggledFunc() } Keys.onEnterPressed: { onToggledFunc() } Keys.onReturnPressed: { onToggledFunc() } - - KeyNavigation.tab: selector } } @@ -154,18 +144,18 @@ PageType { model: root.routeModesModel - currentIndex: getRouteModesModelIndex() + selectedIndex: getRouteModesModelIndex() clickedFunction: function() { selector.text = selectedText - selector.close() - if (SitesModel.routeMode !== root.routeModesModel[currentIndex].type) { - SitesModel.routeMode = root.routeModesModel[currentIndex].type + selector.closeTriggered() + if (SitesModel.routeMode !== root.routeModesModel[selectedIndex].type) { + SitesModel.routeMode = root.routeModesModel[selectedIndex].type } } Component.onCompleted: { - if (root.routeModesModel[currentIndex].type === SitesModel.routeMode) { + if (root.routeModesModel[selectedIndex].type === SitesModel.routeMode) { selector.text = selectedText } else { selector.text = root.routeModesModel[0].name @@ -175,128 +165,89 @@ PageType { Connections { target: SitesModel function onRouteModeChanged() { - currentIndex = getRouteModesModelIndex() + selectedIndex = getRouteModesModelIndex() } } } - - KeyNavigation.tab: { - return sites.count > 0 ? - sites : - searchField.textField - } } } - FlickableType { - id: fl + ListView { + id: listView + anchors.top: header.bottom anchors.topMargin: 16 - contentHeight: col.implicitHeight + addSiteButton.implicitHeight + addSiteButton.anchors.bottomMargin + addSiteButton.anchors.topMargin + anchors.bottom: addSiteButton.top + + width: parent.width enabled: root.pageEnabled - Column { - id: col - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + property bool isFocusable: true - ListView { - id: sites - width: parent.width - height: sites.contentItem.height - - model: SortFilterProxyModel { - id: proxySitesModel - sourceModel: SitesModel - filters: [ - AnyOf { - RegExpFilter { - roleName: "url" - pattern: ".*" + searchField.textField.text + ".*" - caseSensitivity: Qt.CaseInsensitive - } - RegExpFilter { - roleName: "ip" - pattern: ".*" + searchField.textField.text + ".*" - caseSensitivity: Qt.CaseInsensitive - } - } - ] - } - - clip: true - interactive: false - - activeFocusOnTab: true - focus: true - Keys.onTabPressed: { - if (currentIndex < this.count - 1) { - this.incrementCurrentIndex() - } else { - currentIndex = 0 - searchField.textField.forceActiveFocus() + model: SortFilterProxyModel { + id: proxySitesModel + sourceModel: SitesModel + filters: [ + AnyOf { + RegExpFilter { + roleName: "url" + pattern: ".*" + searchField.textField.text + ".*" + caseSensitivity: Qt.CaseInsensitive + } + RegExpFilter { + roleName: "ip" + pattern: ".*" + searchField.textField.text + ".*" + caseSensitivity: Qt.CaseInsensitive } - - fl.ensureVisible(currentItem) } + ] + } - delegate: Item { - implicitWidth: sites.width - implicitHeight: delegateContent.implicitHeight + clip: true - onActiveFocusChanged: { - if (activeFocus) { + reuseItems: true + + delegate: ColumnLayout { + id: delegateContent + + width: listView.width + + LabelWithButtonType { + id: site + Layout.fillWidth: true + + text: url + descriptionText: ip + rightImageSource: "qrc:/images/controls/trash.svg" + rightImageColor: AmneziaStyle.color.paleGray + + clickedFunction: function() { + var headerText = qsTr("Remove ") + url + "?" + var yesButtonText = qsTr("Continue") + var noButtonText = qsTr("Cancel") + + var yesButtonFunction = function() { + SitesController.removeSite(proxySitesModel.mapToSource(index)) + if (!GC.isMobile()) { + site.rightButton.forceActiveFocus() + } + } + var noButtonFunction = function() { + if (!GC.isMobile()) { site.rightButton.forceActiveFocus() } } - ColumnLayout { - id: delegateContent - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - - LabelWithButtonType { - id: site - Layout.fillWidth: true - - text: url - descriptionText: ip - rightImageSource: "qrc:/images/controls/trash.svg" - rightImageColor: AmneziaStyle.color.paleGray - - clickedFunction: function() { - var headerText = qsTr("Remove ") + url + "?" - var yesButtonText = qsTr("Continue") - var noButtonText = qsTr("Cancel") - - var yesButtonFunction = function() { - SitesController.removeSite(proxySitesModel.mapToSource(index)) - if (!GC.isMobile()) { - site.rightButton.forceActiveFocus() - } - } - var noButtonFunction = function() { - if (!GC.isMobile()) { - site.rightButton.forceActiveFocus() - } - } - - showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) - } - } - - DividerType {} - } + showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } } + DividerType {} } } + Rectangle { anchors.fill: addSiteButton anchors.bottomMargin: -24 @@ -325,7 +276,6 @@ PageType { textFieldPlaceholderText: qsTr("website or IP") buttonImageSource: "qrc:/images/controls/plus.svg" - KeyNavigation.tab: GC.isMobile() ? focusItem : addSiteButtonImage clickedFunc: function() { PageController.showBusyIndicator(true) @@ -344,13 +294,11 @@ PageType { imageColor: AmneziaStyle.color.paleGray onClicked: function () { - moreActionsDrawer.open() + moreActionsDrawer.openTriggered() } Keys.onReturnPressed: addSiteButtonImage.clicked() Keys.onEnterPressed: addSiteButtonImage.clicked() - - Keys.onTabPressed: lastItemTabClicked(focusItem) } } @@ -360,38 +308,13 @@ PageType { anchors.fill: parent expandedHeight: parent.height * 0.4375 - onClosed: { - if (root.defaultActiveFocusItem && !GC.isMobile()) { - root.defaultActiveFocusItem.forceActiveFocus() - } - } - - expandedContent: ColumnLayout { + expandedStateContent: ColumnLayout { id: moreActionsDrawerContent anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - Connections { - target: moreActionsDrawer - - function onOpened() { - focusItem1.forceActiveFocus() - } - - function onActiveFocusChanged() { - if (!GC.isMobile()) { - focusItem1.forceActiveFocus() - } - } - } - - Item { - id: focusItem1 - KeyNavigation.tab: importSitesButton.rightButton - } - Header2Type { Layout.fillWidth: true Layout.margins: 16 @@ -407,21 +330,18 @@ PageType { rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { - importSitesDrawer.open() + importSitesDrawer.openTriggered() } - - KeyNavigation.tab: exportSitesButton } DividerType {} LabelWithButtonType { id: exportSitesButton + enabled: !SettingsController.isOnTv() Layout.fillWidth: true text: qsTr("Save site list") - KeyNavigation.tab: focusItem1 - clickedFunction: function() { var fileName = "" if (GC.isMobile()) { @@ -436,13 +356,15 @@ PageType { if (fileName !== "") { PageController.showBusyIndicator(true) SitesController.exportSites(fileName) - moreActionsDrawer.close() + moreActionsDrawer.closeTriggered() PageController.showBusyIndicator(false) } } } - DividerType {} + DividerType { + enabled: !SettingsController.isOnTv() + } } } @@ -452,28 +374,9 @@ PageType { anchors.fill: parent expandedHeight: parent.height * 0.4375 - onClosed: { - if (!GC.isMobile()) { - moreActionsDrawer.forceActiveFocus() - } - } - - expandedContent: Item { + expandedStateContent: Item { implicitHeight: importSitesDrawer.expandedHeight - Connections { - target: importSitesDrawer - enabled: !GC.isMobile() - function onOpened() { - focusItem2.forceActiveFocus() - } - } - - Item { - id: focusItem2 - KeyNavigation.tab: importSitesDrawerBackButton - } - BackButtonType { id: importSitesDrawerBackButton @@ -482,10 +385,8 @@ PageType { anchors.right: parent.right anchors.topMargin: 16 - KeyNavigation.tab: importSitesButton2 - backButtonFunction: function() { - importSitesDrawer.close() + importSitesDrawer.closeTriggered() } } @@ -516,7 +417,6 @@ PageType { Layout.fillWidth: true text: qsTr("Replace site list") - KeyNavigation.tab: importSitesButton3 clickedFunction: function() { var fileName = SystemController.getFileName(qsTr("Open sites file"), @@ -533,7 +433,6 @@ PageType { id: importSitesButton3 Layout.fillWidth: true text: qsTr("Add imported sites to existing ones") - KeyNavigation.tab: focusItem2 clickedFunction: function() { var fileName = SystemController.getFileName(qsTr("Open sites file"), @@ -548,8 +447,8 @@ PageType { PageController.showBusyIndicator(true) SitesController.importSites(fileName, replaceExistingSites) PageController.showBusyIndicator(false) - importSitesDrawer.close() - moreActionsDrawer.close() + importSitesDrawer.closeTriggered() + moreActionsDrawer.closeTriggered() } DividerType {} diff --git a/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml b/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml index 75fd3d47..8c02195e 100644 --- a/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml +++ b/client/ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml @@ -15,8 +15,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - FlickableType { id: fl anchors.top: parent.top @@ -32,15 +30,9 @@ PageType { spacing: 0 - Item { - id: focusItem - KeyNavigation.tab: backButton - } - BackButtonType { id: backButton Layout.topMargin: 20 -// KeyNavigation.tab: fileButton.rightButton } HeaderType { @@ -78,7 +70,7 @@ PageType { imageSource: "qrc:/images/controls/history.svg" leftText: qsTr("Work period") - rightText: ApiServicesModel.getSelectedServiceData("workPeriod") + rightText: ApiServicesModel.getSelectedServiceData("timeLimit") visible: rightText !== "" } diff --git a/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml b/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml index 85a50393..c3e3edbc 100644 --- a/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml +++ b/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml @@ -14,87 +14,85 @@ import "../Config" PageType { id: root - defaultActiveFocusItem: focusItem + ColumnLayout { + id: header - FlickableType { - id: fl anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + spacing: 0 + + BackButtonType { + id: backButton + Layout.topMargin: 20 + } + + HeaderType { + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 16 + + headerText: qsTr("VPN by Amnezia") + descriptionText: qsTr("Choose a VPN service that suits your needs.") + } + } + + ListView { + id: servicesListView + + anchors.top: header.bottom + anchors.right: parent.right + anchors.left: parent.left anchors.bottom: parent.bottom - contentHeight: content.height + anchors.topMargin: 16 + spacing: 0 - ColumnLayout { - id: content + property bool isFocusable: true + + clip: true + reuseItems: true - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + model: ApiServicesModel - spacing: 0 + ScrollBar.vertical: ScrollBarType {} - Item { - id: focusItem - KeyNavigation.tab: backButton - } + delegate: Item { + implicitWidth: servicesListView.width + implicitHeight: delegateContent.implicitHeight - BackButtonType { - id: backButton - Layout.topMargin: 20 -// KeyNavigation.tab: fileButton.rightButton - } + enabled: isServiceAvailable - HeaderType { - Layout.fillWidth: true - Layout.topMargin: 8 - Layout.rightMargin: 16 - Layout.leftMargin: 16 - Layout.bottomMargin: 32 + ColumnLayout { + id: delegateContent - headerText: qsTr("VPN by Amnezia") - descriptionText: qsTr("Choose a VPN service that suits your needs.") - } + anchors.fill: parent - ListView { - id: containers - width: parent.width - height: containers.contentItem.height - spacing: 16 + CardWithIconsType { + id: card - currentIndex: 1 - interactive: false - model: ApiServicesModel + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 16 - delegate: Item { - implicitWidth: containers.width - implicitHeight: delegateContent.implicitHeight + headerText: name + bodyText: cardDescription + footerText: price - ColumnLayout { - id: delegateContent + rightImageSource: "qrc:/images/controls/chevron-right.svg" - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - - CardWithIconsType { - id: card - - Layout.fillWidth: true - Layout.rightMargin: 16 - Layout.leftMargin: 16 - - headerText: name - bodyText: cardDescription - footerText: price - - rightImageSource: "qrc:/images/controls/chevron-right.svg" - - onClicked: { - if (isServiceAvailable) { - ApiServicesModel.setServiceIndex(index) - PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo) - } - } + onClicked: { + if (isServiceAvailable) { + ApiServicesModel.setServiceIndex(index) + PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo) } } + + Keys.onEnterPressed: clicked() + Keys.onReturnPressed: clicked() } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index 7c031997..17d733d8 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -25,32 +25,29 @@ PageType { } } - defaultActiveFocusItem: focusItem + ListView { + id: listView - FlickableType { - id: fl - anchors.top: parent.top - anchors.bottom: parent.bottom - contentHeight: content.height + anchors.fill: parent - ColumnLayout { - id: content + property bool isFocusable: true - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right + ScrollBar.vertical: ScrollBarType {} - spacing: 0 + model: variants - Item { - id: focusItem - KeyNavigation.tab: textKey.textField - } + clip: true + reuseItems: true + + header: ColumnLayout { + width: listView.width HeaderType { - property bool isVisible: SettingsController.getInstallationUuid() !== "" || PageController.isStartPageVisible() + id: moreButton + property bool isVisible: SettingsController.getInstallationUuid() !== "" || PageController.isStartPageVisible() + Layout.fillWidth: true Layout.topMargin: 24 Layout.rightMargin: 16 @@ -60,7 +57,7 @@ PageType { actionButtonImage: isVisible ? "qrc:/images/controls/more-vertical.svg" : "" actionButtonFunction: function() { - moreActionsDrawer.open() + moreActionsDrawer.openTriggered() } DrawerType2 { @@ -71,7 +68,7 @@ PageType { anchors.fill: parent expandedHeight: root.height * 0.5 - expandedContent: ColumnLayout { + expandedStateContent: ColumnLayout { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right @@ -131,6 +128,8 @@ PageType { } ParagraphTextType { + objectName: "insertKeyLabel" + Layout.fillWidth: true Layout.topMargin: 32 Layout.rightMargin: 16 @@ -154,8 +153,6 @@ PageType { textField.text = "" textField.paste() } - - KeyNavigation.tab: continueButton } BasicButtonType { @@ -169,7 +166,6 @@ PageType { visible: textKey.textFieldText !== "" text: qsTr("Continue") - Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { if (ImportController.extractConfigFromData(textKey.textFieldText)) { @@ -188,143 +184,155 @@ PageType { color: AmneziaStyle.color.charcoalGray text: qsTr("Other connection options") } + } + + delegate: ColumnLayout { + width: listView.width CardWithIconsType { - id: apiInstalling - Layout.fillWidth: true Layout.rightMargin: 16 Layout.leftMargin: 16 Layout.bottomMargin: 16 - headerText: qsTr("VPN by Amnezia") - bodyText: qsTr("Connect to classic paid and free VPN services from Amnezia") + visible: isVisible + + headerText: title + bodyText: description rightImageSource: "qrc:/images/controls/chevron-right.svg" - leftImageSource: "qrc:/images/controls/amnezia.svg" + leftImageSource: imageSource - onClicked: function() { - PageController.showBusyIndicator(true) - var result = InstallController.fillAvailableServices() - PageController.showBusyIndicator(false) - if (result) { - PageController.goToPage(PageEnum.PageSetupWizardApiServicesList) - } - } + onClicked: { handler() } } + } - CardWithIconsType { - id: manualInstalling + footer: ColumnLayout { + width: listView.width - Layout.fillWidth: true - Layout.rightMargin: 16 - Layout.leftMargin: 16 + BasicButtonType { + id: siteLink2 + Layout.topMargin: 24 Layout.bottomMargin: 16 + Layout.alignment: Qt.AlignHCenter + implicitHeight: 32 - headerText: qsTr("Self-hosted VPN") - bodyText: qsTr("Configure Amnezia VPN on your own server") + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.goldenApricot - rightImageSource: "qrc:/images/controls/chevron-right.svg" - leftImageSource: "qrc:/images/controls/server.svg" + text: qsTr("Site Amnezia") - onClicked: { - PageController.goToPage(PageEnum.PageSetupWizardCredentials) - } - } + rightImageSource: "qrc:/images/controls/external-link.svg" - CardWithIconsType { - id: backupRestore - - Layout.fillWidth: true - Layout.rightMargin: 16 - Layout.leftMargin: 16 - Layout.bottomMargin: 16 - - visible: PageController.isStartPageVisible() - - headerText: qsTr("Restore from backup") - - rightImageSource: "qrc:/images/controls/chevron-right.svg" - leftImageSource: "qrc:/images/controls/archive-restore.svg" - - onClicked: { - var filePath = SystemController.getFileName(qsTr("Open backup file"), - qsTr("Backup files (*.backup)")) - if (filePath !== "") { - PageController.showBusyIndicator(true) - SettingsController.restoreAppConfig(filePath) - PageController.showBusyIndicator(false) - } - } - } - - CardWithIconsType { - id: openFile - - Layout.fillWidth: true - Layout.rightMargin: 16 - Layout.leftMargin: 16 - Layout.bottomMargin: 16 - - headerText: qsTr("File with connection settings") - - rightImageSource: "qrc:/images/controls/chevron-right.svg" - leftImageSource: "qrc:/images/controls/folder-search-2.svg" - - onClicked: { - var nameFilter = !ServersModel.getServersCount() ? "Config or backup files (*.vpn *.ovpn *.conf *.json *.backup)" : - "Config files (*.vpn *.ovpn *.conf *.json)" - var fileName = SystemController.getFileName(qsTr("Open config file"), nameFilter) - if (fileName !== "") { - if (ImportController.extractConfigFromFile(fileName)) { - PageController.goToPage(PageEnum.PageSetupWizardViewConfig) - } - } - } - } - - CardWithIconsType { - id: scanQr - - Layout.fillWidth: true - Layout.rightMargin: 16 - Layout.leftMargin: 16 - Layout.bottomMargin: 16 - - visible: SettingsController.isCameraPresent() - - headerText: qsTr("QR code") - - rightImageSource: "qrc:/images/controls/chevron-right.svg" - leftImageSource: "qrc:/images/controls/scan-line.svg" - - onClicked: { - ImportController.startDecodingQr() - if (Qt.platform.os === "ios") { - PageController.goToPage(PageEnum.PageSetupWizardQrReader) - } - } - } - - CardWithIconsType { - id: siteLink - - Layout.fillWidth: true - Layout.rightMargin: 16 - Layout.leftMargin: 16 - Layout.bottomMargin: 16 - - visible: PageController.isStartPageVisible() - - headerText: qsTr("I have nothing") - - rightImageSource: "qrc:/images/controls/chevron-right.svg" - leftImageSource: "qrc:/images/controls/help-circle.svg" - - onClicked: { + clickedFunc: function() { Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl()) } } } } + + property list variants: [ + amneziaVpn, + selfHostVpn, + backupRestore, + fileOpen, + qrScan, + siteLink + ] + + QtObject { + id: amneziaVpn + + property string title: qsTr("VPN by Amnezia") + property string description: qsTr("Connect to classic paid and free VPN services from Amnezia") + property string imageSource: "qrc:/images/controls/amnezia.svg" + property bool isVisible: true + property var handler: function() { + PageController.showBusyIndicator(true) + var result = InstallController.fillAvailableServices() + PageController.showBusyIndicator(false) + if (result) { + PageController.goToPage(PageEnum.PageSetupWizardApiServicesList) + } + } + } + + QtObject { + id: selfHostVpn + + property string title: qsTr("Self-hosted VPN") + property string description: qsTr("Configure Amnezia VPN on your own server") + property string imageSource: "qrc:/images/controls/server.svg" + property bool isVisible: true + property var handler: function() { + PageController.goToPage(PageEnum.PageSetupWizardCredentials) + } + } + + QtObject { + id: backupRestore + + property string title: qsTr("Restore from backup") + property string description: qsTr("") + property string imageSource: "qrc:/images/controls/archive-restore.svg" + property bool isVisible: PageController.isStartPageVisible() + property var handler: function() { + var filePath = SystemController.getFileName(qsTr("Open backup file"), + qsTr("Backup files (*.backup)")) + if (filePath !== "") { + PageController.showBusyIndicator(true) + SettingsController.restoreAppConfig(filePath) + PageController.showBusyIndicator(false) + } + } + } + + QtObject { + id: fileOpen + + property string title: qsTr("File with connection settings") + property string description: qsTr("") + property string imageSource: "qrc:/images/controls/folder-search-2.svg" + property bool isVisible: true + property var handler: function() { + var nameFilter = !ServersModel.getServersCount() ? "Config or backup files (*.vpn *.ovpn *.conf *.json *.backup)" : + "Config files (*.vpn *.ovpn *.conf *.json)" + var fileName = SystemController.getFileName(qsTr("Open config file"), nameFilter) + if (fileName !== "") { + if (ImportController.extractConfigFromFile(fileName)) { + PageController.goToPage(PageEnum.PageSetupWizardViewConfig) + } + } + } + } + + QtObject { + id: qrScan + + property string title: qsTr("QR code") + property string description: qsTr("") + property string imageSource: "qrc:/images/controls/scan-line.svg" + property bool isVisible: SettingsController.isCameraPresent() + property var handler: function() { + ImportController.startDecodingQr() + if (Qt.platform.os === "ios") { + PageController.goToPage(PageEnum.PageSetupWizardQrReader) + } + } + } + + QtObject { + id: siteLink + + property string title: qsTr("I have nothing") + property string description: qsTr("") + property string imageSource: "qrc:/images/controls/help-circle.svg" + property bool isVisible: PageController.isStartPageVisible() + property var handler: function() { + Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl()) + } + } } diff --git a/client/ui/qml/Pages2/PageSetupWizardCredentials.qml b/client/ui/qml/Pages2/PageSetupWizardCredentials.qml index aced12b1..20b85550 100644 --- a/client/ui/qml/Pages2/PageSetupWizardCredentials.qml +++ b/client/ui/qml/Pages2/PageSetupWizardCredentials.qml @@ -13,13 +13,6 @@ import "../Controls2/TextTypes" PageType { id: root - defaultActiveFocusItem: hostname.textField - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - BackButtonType { id: backButton @@ -28,100 +21,133 @@ PageType { anchors.right: parent.right anchors.topMargin: 20 - KeyNavigation.tab: hostname.textField + onFocusChanged: { + if (this.activeFocus) { + listView.positionViewAtBeginning() + } + } } - FlickableType { - id: fl + ListView { + id: listView anchors.top: backButton.bottom anchors.bottom: parent.bottom - contentHeight: content.height + anchors.right: parent.right + anchors.left: parent.left - ColumnLayout { - id: content + property bool isFocusable: true - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.rightMargin: 16 - anchors.leftMargin: 16 + Keys.onTabPressed: { + FocusController.nextKeyTabItem() + } - spacing: 16 + Keys.onBacktabPressed: { + FocusController.previousKeyTabItem() + } + + Keys.onUpPressed: { + FocusController.nextKeyUpItem() + } + + Keys.onDownPressed: { + FocusController.nextKeyDownItem() + } + + Keys.onLeftPressed: { + FocusController.nextKeyLeftItem() + } + + Keys.onRightPressed: { + FocusController.nextKeyRightItem() + } + + ScrollBar.vertical: ScrollBarType {} + + header: ColumnLayout { + width: listView.width HeaderType { Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 headerText: qsTr("Configure your server") } + } + + model: inputFields + spacing: 16 + clip: true + reuseItems: true + + delegate: ColumnLayout { + property alias textField: _textField.textField + + width: listView.width TextFieldWithHeaderType { - id: hostname + id: _textField Layout.fillWidth: true - headerText: qsTr("Server IP address [:port]") - textFieldPlaceholderText: qsTr("255.255.255.255:22") + Layout.leftMargin: 16 + Layout.rightMargin: 16 - textField.onFocusChanged: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - } + property bool hidePassword: hideText - KeyNavigation.tab: username.textField - } + headerText: title + textField.echoMode: hideText ? TextInput.Password : TextInput.Normal + buttonImageSource: imageSource + textFieldPlaceholderText: placeholderText + textField.text: textFieldText - TextFieldWithHeaderType { - id: username + rightButtonClickedOnEnter: true - Layout.fillWidth: true - headerText: qsTr("SSH Username") - textFieldPlaceholderText: "root" - - textField.onFocusChanged: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') - } - - KeyNavigation.tab: secretData.textField - } - - TextFieldWithHeaderType { - id: secretData - - property bool hidePassword: true - - Layout.fillWidth: true - headerText: qsTr("Password or SSH private key") - textField.echoMode: hidePassword ? TextInput.Password : TextInput.Normal - buttonImageSource: textFieldText !== "" ? (hidePassword ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg") - : "" - - clickedFunc: function() { - hidePassword = !hidePassword + clickedFunc: function () { + clickedHandler() } textField.onFocusChanged: { - textField.text = textField.text.replace(/^\s+|\s+$/g, '') + var _currentIndex = listView.currentIndex + var _currentItem = listView.itemAtIndex(_currentIndex).children[0] + listView.model[_currentIndex].textFieldText = _currentItem.textFieldText.replace(/^\s+|\s+$/g, '') } - KeyNavigation.tab: continueButton + textField.onTextChanged: { + var _currentIndex = listView.currentIndex + textFieldText = textField.text + + if (_currentIndex === vars.secretDataIndex) { + buttonImageSource = textFieldText !== "" ? (hideText ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg") : "" + } + } } + } + + footer: ColumnLayout { + width: listView.width BasicButtonType { id: continueButton Layout.fillWidth: true - Layout.topMargin: 24 + Layout.topMargin: 32 + Layout.leftMargin: 16 + Layout.rightMargin: 16 text: qsTr("Continue") - Keys.onTabPressed: lastItemTabClicked(focusItem) - clickedFunc: function() { - forceActiveFocus() - if (!isCredentialsFilled()) { + if (!root.isCredentialsFilled()) { return } InstallController.setShouldCreateServer(true) - InstallController.setProcessedServerCredentials(hostname.textField.text, username.textField.text, secretData.textField.text) + var _hostname = listView.itemAtIndex(vars.hostnameIndex).children[0].textFieldText + var _username = listView.itemAtIndex(vars.usernameIndex).children[0].textFieldText + var _secretData = listView.itemAtIndex(vars.secretDataIndex).children[0].textFieldText + + InstallController.setProcessedServerCredentials(_hostname, _username, _secretData) PageController.showBusyIndicator(true) var isConnectionOpened = InstallController.checkSshConnection() @@ -136,7 +162,10 @@ PageType { LabelTextType { Layout.fillWidth: true - Layout.topMargin: 12 + Layout.topMargin: 24 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 text: qsTr("All data you enter will remain strictly confidential and will not be shared or disclosed to the Amnezia or any third parties") } @@ -145,6 +174,8 @@ PageType { id: siteLink Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 Layout.bottomMargin: 16 headerText: qsTr("How to run your VPN server") @@ -163,21 +194,78 @@ PageType { function isCredentialsFilled() { var hasEmptyField = false - if (hostname.textFieldText === "") { - hostname.errorText = qsTr("Ip address cannot be empty") + var _hostname = listView.itemAtIndex(vars.hostnameIndex).children[0] + if (_hostname.textFieldText === "") { + _hostname.errorText = qsTr("Ip address cannot be empty") hasEmptyField = true - } else if (!hostname.textField.acceptableInput) { - hostname.errorText = qsTr("Enter the address in the format 255.255.255.255:88") + } else if (!_hostname.textField.acceptableInput) { + _hostname.errorText = qsTr("Enter the address in the format 255.255.255.255:88") } - if (username.textFieldText === "") { - username.errorText = qsTr("Login cannot be empty") + var _username = listView.itemAtIndex(vars.usernameIndex).children[0] + if (_username.textFieldText === "") { + _username.errorText = qsTr("Login cannot be empty") hasEmptyField = true } - if (secretData.textFieldText === "") { - secretData.errorText = qsTr("Password/private key cannot be empty") + + var _secretData = listView.itemAtIndex(vars.secretDataIndex).children[0] + if (_secretData.textFieldText === "") { + _secretData.errorText = qsTr("Password/private key cannot be empty") hasEmptyField = true } + return !hasEmptyField } + + property list inputFields: [ + hostname, + username, + secretData + ] + + QtObject { + id: hostname + + property string title: qsTr("Server IP address [:port]") + readonly property string placeholderText: qsTr("255.255.255.255:22") + property string textFieldText: "" + property bool hideText: false + property string imageSource: "" + readonly property var clickedHandler: function() { + console.debug(">>> Server IP address text field was clicked!!!") + clicked() + } + } + + QtObject { + id: username + + property string title: qsTr("SSH Username") + readonly property string placeholderText: "root" + property string textFieldText: "" + property bool hideText: false + property string imageSource: "" + readonly property var clickedHandler: undefined + } + + QtObject { + id: secretData + + property string title: qsTr("Password or SSH private key") + readonly property string placeholderText: "" + property string textFieldText: "" + property bool hideText: true + property string imageSource: textFieldText !== "" ? (hideText ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg") : "" + readonly property var clickedHandler: function() { + hideText = !hideText + } + } + + QtObject { + id: vars + + readonly property int hostnameIndex: 0 + readonly property int usernameIndex: 1 + readonly property int secretDataIndex: 2 + } } diff --git a/client/ui/qml/Pages2/PageSetupWizardEasy.qml b/client/ui/qml/Pages2/PageSetupWizardEasy.qml index 02a7c928..eb6000c2 100644 --- a/client/ui/qml/Pages2/PageSetupWizardEasy.qml +++ b/client/ui/qml/Pages2/PageSetupWizardEasy.qml @@ -17,7 +17,6 @@ PageType { id: root property bool isEasySetup: true - defaultActiveFocusItem: focusItem SortFilterProxyModel { id: proxyContainersModel @@ -34,14 +33,6 @@ PageType { } } - Item { - id: focusItem - implicitWidth: 1 - implicitHeight: 54 - - KeyNavigation.tab: backButton - } - BackButtonType { id: backButton @@ -49,8 +40,6 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 - - KeyNavigation.tab: continueButton } FlickableType { @@ -98,6 +87,8 @@ PageType { property int containerDefaultPort property int containerDefaultTransportProto + property bool isFocusable: true + delegate: Item { implicitWidth: containers.width implicitHeight: delegateContent.implicitHeight @@ -163,7 +154,7 @@ PageType { implicitWidth: parent.width text: qsTr("Continue") - KeyNavigation.tab: setupLaterButton + parentFlickable: fl clickedFunc: function() { diff --git a/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml b/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml index 6b4c0a1c..0de4da6b 100644 --- a/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml +++ b/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml @@ -49,6 +49,32 @@ PageType { interactive: false model: proxyContainersModel + 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() + } + delegate: Item { implicitWidth: processedContainerListView.width implicitHeight: (delegateContent.implicitHeight > root.height) ? delegateContent.implicitHeight : root.height @@ -62,19 +88,12 @@ PageType { anchors.rightMargin: 16 anchors.leftMargin: 16 - Item { - id: focusItem - KeyNavigation.tab: backButton - } - BackButtonType { id: backButton Layout.topMargin: 20 Layout.rightMargin: -16 Layout.leftMargin: -16 - - KeyNavigation.tab: showDetailsButton } HeaderType { @@ -104,42 +123,19 @@ PageType { KeyNavigation.tab: transportProtoSelector clickedFunc: function() { - showDetailsDrawer.open() + showDetailsDrawer.openTriggered() } } DrawerType2 { id: showDetailsDrawer parent: root - onClosed: { - if (!GC.isMobile()) { - defaultActiveFocusItem.forceActiveFocus() - } - } anchors.fill: parent expandedHeight: parent.height * 0.9 - expandedContent: Item { - Connections { - target: showDetailsDrawer - enabled: !GC.isMobile() - function onOpened() { - focusItem2.forceActiveFocus() - } - } - + expandedStateContent: Item { implicitHeight: showDetailsDrawer.expandedHeight - Item { - id: focusItem2 - KeyNavigation.tab: showDetailsBackButton - onFocusChanged: { - if (focusItem2.activeFocus) { - fl.contentY = 0 - } - } - } - BackButtonType { id: showDetailsBackButton @@ -148,10 +144,8 @@ PageType { anchors.right: parent.right anchors.topMargin: 16 - KeyNavigation.tab: showDetailsCloseButton - backButtonFunction: function() { - showDetailsDrawer.close() + showDetailsDrawer.closeTriggered() } } @@ -205,10 +199,9 @@ PageType { parentFlickable: fl text: qsTr("Close") - Keys.onTabPressed: lastItemTabClicked(focusItem2) clickedFunc: function() { - showDetailsDrawer.close() + showDetailsDrawer.closeTriggered() } } } @@ -229,8 +222,6 @@ PageType { Layout.fillWidth: true rootWidth: root.width - - KeyNavigation.tab: (port.visible && port.enabled) ? port.textField : installButton } TextFieldWithHeaderType { @@ -242,8 +233,6 @@ PageType { headerText: qsTr("Port") textField.maximumLength: 5 textField.validator: IntValidator { bottom: 1; top: 65535 } - - KeyNavigation.tab: installButton } Rectangle { @@ -259,8 +248,6 @@ PageType { text: qsTr("Install") - Keys.onTabPressed: lastItemTabClicked(focusItem) - clickedFunc: function() { if (!port.textField.acceptableInput && ContainerProps.containerTypeToString(dockerContainer) !== "torwebsite" && @@ -288,11 +275,6 @@ PageType { var protocolSelectorVisible = ProtocolProps.defaultTransportProtoChangeable(defaultContainerProto) transportProtoSelector.visible = protocolSelectorVisible transportProtoHeader.visible = protocolSelectorVisible - - if (port.visible && port.enabled) - defaultActiveFocusItem = port.textField - else - defaultActiveFocusItem = focusItem } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardProtocols.qml b/client/ui/qml/Pages2/PageSetupWizardProtocols.qml index 48265f66..6b6b6038 100644 --- a/client/ui/qml/Pages2/PageSetupWizardProtocols.qml +++ b/client/ui/qml/Pages2/PageSetupWizardProtocols.qml @@ -15,13 +15,6 @@ import "../Config" PageType { id: root - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - SortFilterProxyModel { id: proxyContainersModel sourceModel: ContainersModel @@ -41,126 +34,66 @@ PageType { } } - ColumnLayout { - id: backButtonLayout + BackButtonType { + id: backButton anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 - - BackButtonType { - id: backButton - KeyNavigation.tab: containers - } } - FlickableType { - id: fl - anchors.top: backButtonLayout.bottom + ListView { + id: listView + anchors.top: backButton.bottom anchors.bottom: parent.bottom - contentHeight: content.implicitHeight + content.anchors.topMargin + content.anchors.bottomMargin + anchors.right: parent.right + anchors.left: parent.left - Column { - id: content + property bool isFocusable: true - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.bottomMargin: 20 + ScrollBar.vertical: ScrollBarType {} - Item { - width: parent.width - height: header.implicitHeight + header: ColumnLayout { + width: listView.width - HeaderType { - id: header + HeaderType { + id: header - anchors.fill: parent + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 - anchors.leftMargin: 16 - anchors.rightMargin: 16 + headerText: qsTr("VPN protocol") + descriptionText: qsTr("Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP.") + } + } - width: parent.width + model: proxyContainersModel + clip: true + spacing: 0 + reuseItems: true + snapMode: ListView.SnapToItem - headerText: qsTr("VPN protocol") - descriptionText: qsTr("Choose the one with the highest priority for you. Later, you can install other protocols and additional services, such as DNS proxy and SFTP.") + delegate: ColumnLayout { + width: listView.width + + LabelWithButtonType { + Layout.fillWidth: true + + text: name + descriptionText: description + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + ContainersModel.setProcessedContainerIndex(proxyContainersModel.mapToSource(index)) + PageController.goToPage(PageEnum.PageSetupWizardProtocolSettings) } } - ListView { - id: containers - width: parent.width - height: containers.contentItem.height - // currentIndex: -1 - clip: true - interactive: false - model: proxyContainersModel - - function ensureCurrentItemVisible() { - if (currentIndex >= 0) { - if (currentItem.y < fl.contentY) { - fl.contentY = currentItem.y - } else if (currentItem.y + currentItem.height + header.height > fl.contentY + fl.height) { - fl.contentY = currentItem.y + currentItem.height + header.height - fl.height + 40 // 40 is a bottom margin - } - } - } - - activeFocusOnTab: true - Keys.onTabPressed: { - if (currentIndex < this.count - 1) { - this.incrementCurrentIndex() - } else { - this.currentIndex = 0 - focusItem.forceActiveFocus() - } - - ensureCurrentItemVisible() - } - - onVisibleChanged: { - if (visible) { - currentIndex = 0 - } - } - - delegate: Item { - implicitWidth: containers.width - implicitHeight: delegateContent.implicitHeight - - onActiveFocusChanged: { - if (activeFocus) { - container.rightButton.forceActiveFocus() - } - } - - ColumnLayout { - id: delegateContent - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - - LabelWithButtonType { - id: container - Layout.fillWidth: true - - text: name - descriptionText: description - rightImageSource: "qrc:/images/controls/chevron-right.svg" - - clickedFunction: function() { - ContainersModel.setProcessedContainerIndex(proxyContainersModel.mapToSource(index)) - PageController.goToPage(PageEnum.PageSetupWizardProtocolSettings) - } - } - - DividerType {} - } - } - } + DividerType {} } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardStart.qml b/client/ui/qml/Pages2/PageSetupWizardStart.qml index b12c7830..2d6790ba 100644 --- a/client/ui/qml/Pages2/PageSetupWizardStart.qml +++ b/client/ui/qml/Pages2/PageSetupWizardStart.qml @@ -14,8 +14,6 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: focusItem - ColumnLayout { id: content @@ -32,11 +30,6 @@ PageType { Layout.preferredHeight: 287 } - Item { - id: focusItem - KeyNavigation.tab: startButton - } - BasicButtonType { id: startButton Layout.fillWidth: true @@ -50,8 +43,6 @@ PageType { clickedFunc: function() { PageController.goToPage(PageEnum.PageSetupWizardConfigSource) } - - Keys.onTabPressed: lastItemTabClicked(focusItem) } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardTextKey.qml b/client/ui/qml/Pages2/PageSetupWizardTextKey.qml index c4227df1..126a7c91 100644 --- a/client/ui/qml/Pages2/PageSetupWizardTextKey.qml +++ b/client/ui/qml/Pages2/PageSetupWizardTextKey.qml @@ -13,14 +13,6 @@ import "../Config" PageType { id: root - defaultActiveFocusItem: textKey.textField - - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - FlickableType { id: fl anchors.top: parent.top @@ -39,7 +31,6 @@ PageType { BackButtonType { id: backButton Layout.topMargin: 20 - KeyNavigation.tab: textKey.textField } HeaderType { @@ -67,8 +58,6 @@ PageType { textField.text = "" textField.paste() } - - KeyNavigation.tab: continueButton } } } @@ -84,7 +73,6 @@ PageType { anchors.bottomMargin: 32 text: qsTr("Continue") - Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { if (ImportController.extractConfigFromData(textKey.textFieldText)) { diff --git a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml index 92048f36..14096742 100644 --- a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml +++ b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml @@ -16,13 +16,6 @@ PageType { property bool showContent: false - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - BackButtonType { id: backButton @@ -30,8 +23,6 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 - - KeyNavigation.tab: showContentButton } Connections { @@ -107,7 +98,8 @@ PageType { textColor: AmneziaStyle.color.goldenApricot text: showContent ? qsTr("Collapse content") : qsTr("Show content") - KeyNavigation.tab: connectButton + + parentFlickable: fl clickedFunc: function() { showContent = !showContent @@ -186,8 +178,6 @@ PageType { anchors.rightMargin: 16 anchors.leftMargin: 16 - Keys.onTabPressed: lastItemTabClicked(focusItem) - BasicButtonType { id: connectButton Layout.fillWidth: true diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 617b1091..f5c85a5f 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -18,8 +18,6 @@ import "../Config" PageType { id: root - defaultActiveFocusItem: clientNameTextField.textField - enum ConfigType { AmneziaConnection, OpenVpn, @@ -47,7 +45,7 @@ PageType { shareConnectionDrawer.headerText = qsTr("Connection to ") + serverSelector.text shareConnectionDrawer.configContentHeaderText = qsTr("File with connection settings to ") + serverSelector.text - shareConnectionDrawer.open() + shareConnectionDrawer.openTriggered() shareConnectionDrawer.contentVisible = false PageController.showBusyIndicator(true) @@ -92,7 +90,7 @@ PageType { break } case PageShare.ConfigType.Xray: { - ExportController.generateXrayConfig() + ExportController.generateXrayConfig(clientNameTextField.textFieldText) shareConnectionDrawer.configCaption = qsTr("Save XRay config") shareConnectionDrawer.configExtension = ".json" shareConnectionDrawer.configFileName = "amnezia_for_xray" @@ -104,7 +102,7 @@ PageType { } function onExportErrorOccurred(error) { - shareConnectionDrawer.close() + shareConnectionDrawer.closeTriggered() PageController.showErrorMessage(error) } @@ -119,38 +117,38 @@ PageType { QtObject { id: amneziaConnectionFormat - property string name: qsTr("For the AmneziaVPN app") - property var type: PageShare.ConfigType.AmneziaConnection + readonly property string name: qsTr("For the AmneziaVPN app") + readonly property int type: PageShare.ConfigType.AmneziaConnection } QtObject { id: openVpnConnectionFormat - property string name: qsTr("OpenVPN native format") - property var type: PageShare.ConfigType.OpenVpn + readonly property string name: qsTr("OpenVPN native format") + readonly property int type: PageShare.ConfigType.OpenVpn } QtObject { id: wireGuardConnectionFormat - property string name: qsTr("WireGuard native format") - property var type: PageShare.ConfigType.WireGuard + readonly property string name: qsTr("WireGuard native format") + readonly property int type: PageShare.ConfigType.WireGuard } QtObject { id: awgConnectionFormat - property string name: qsTr("AmneziaWG native format") - property var type: PageShare.ConfigType.Awg + readonly property string name: qsTr("AmneziaWG native format") + readonly property int type: PageShare.ConfigType.Awg } QtObject { id: shadowSocksConnectionFormat - property string name: qsTr("Shadowsocks native format") - property var type: PageShare.ConfigType.ShadowSocks + readonly property string name: qsTr("Shadowsocks native format") + readonly property int type: PageShare.ConfigType.ShadowSocks } QtObject { id: cloakConnectionFormat - property string name: qsTr("Cloak native format") - property var type: PageShare.ConfigType.Cloak + readonly property string name: qsTr("Cloak native format") + readonly property int type: PageShare.ConfigType.Cloak } QtObject { id: xrayConnectionFormat - property string name: qsTr("XRay native format") - property var type: PageShare.ConfigType.Xray + readonly property string name: qsTr("XRay native format") + readonly property int type: PageShare.ConfigType.Xray } FlickableType { @@ -172,16 +170,6 @@ PageType { spacing: 0 - Item { - id: focusItem - KeyNavigation.tab: header.actionButton - onFocusChanged: { - if (focusItem.activeFocus) { - a.contentY = 0 - } - } - } - HeaderType { id: header Layout.fillWidth: true @@ -191,11 +179,9 @@ PageType { actionButtonImage: "qrc:/images/controls/more-vertical.svg" actionButtonFunction: function() { - shareFullAccessDrawer.open() + shareFullAccessDrawer.openTriggered() } - KeyNavigation.tab: connectionRadioButton - DrawerType2 { id: shareFullAccessDrawer @@ -203,13 +189,8 @@ PageType { anchors.fill: parent expandedHeight: root.height - onClosed: { - if (!GC.isMobile()) { - clientNameTextField.textField.forceActiveFocus() - } - } - expandedContent: ColumnLayout { + expandedStateContent: ColumnLayout { id: shareFullAccessDrawerContent anchors.top: parent.top anchors.left: parent.left @@ -222,14 +203,6 @@ PageType { shareFullAccessDrawer.expandedHeight = shareFullAccessDrawerContent.implicitHeight + 32 } - Connections { - target: shareFullAccessDrawer - enabled: !GC.isMobile() - function onOpened() { - focusItem.forceActiveFocus() - } - } - Header2Type { Layout.fillWidth: true Layout.bottomMargin: 16 @@ -240,24 +213,17 @@ PageType { descriptionText: qsTr("Use for your own devices, or share with those you trust to manage the server.") } - Item { - id: focusItem - KeyNavigation.tab: shareFullAccessButton.rightButton - } - LabelWithButtonType { id: shareFullAccessButton Layout.fillWidth: true text: qsTr("Share") rightImageSource: "qrc:/images/controls/chevron-right.svg" - KeyNavigation.tab: focusItem clickedFunction: function() { PageController.goToPage(PageEnum.PageShareFullAccess) - shareFullAccessDrawer.close() + shareFullAccessDrawer.closeTriggered() } - } } } @@ -288,13 +254,8 @@ PageType { implicitWidth: (root.width - 32) / 2 text: qsTr("Connection") - KeyNavigation.tab: usersRadioButton - onClicked: { accessTypeSelector.currentIndex = 0 - if (!GC.isMobile()) { - clientNameTextField.textField.forceActiveFocus() - } } } @@ -305,15 +266,12 @@ PageType { implicitWidth: (root.width - 32) / 2 text: qsTr("Users") - KeyNavigation.tab: accessTypeSelector.currentIndex === 0 ? clientNameTextField.textField : serverSelector - onClicked: { accessTypeSelector.currentIndex = 1 PageController.showBusyIndicator(true) ExportController.updateClientManagementModel(ContainersModel.getProcessedContainerIndex(), ServersModel.getProcessedServerCredentials()) PageController.showBusyIndicator(false) - focusItem.forceActiveFocus() } } } @@ -342,9 +300,6 @@ PageType { textField.maximumLength: 20 checkEmptyText: true - - KeyNavigation.tab: serverSelector - } DropDownType { @@ -385,31 +340,30 @@ PageType { clickedFunction: function() { handler() - if (serverSelector.currentIndex !== serverSelectorListView.currentIndex) { - serverSelector.currentIndex = serverSelectorListView.currentIndex + if (serverSelector.currentIndex !== serverSelectorListView.selectedIndex) { + serverSelector.currentIndex = serverSelectorListView.selectedIndex serverSelector.severSelectorIndexChanged() } - serverSelector.close() + serverSelector.closeTriggered() } Component.onCompleted: { if (ServersModel.isDefaultServerHasWriteAccess() && ServersModel.getDefaultServerData("hasInstalledContainers")) { - serverSelectorListView.currentIndex = proxyServersModel.mapFromSource(ServersModel.defaultIndex) + serverSelectorListView.selectedIndex = proxyServersModel.mapFromSource(ServersModel.defaultIndex) } else { - serverSelectorListView.currentIndex = 0 + serverSelectorListView.selectedIndex = 0 } + serverSelectorListView.positionViewAtIndex(selectedIndex, ListView.Beginning) serverSelectorListView.triggerCurrentItem() } function handler() { serverSelector.text = selectedText - ServersModel.processedIndex = proxyServersModel.mapToSource(currentIndex) + ServersModel.processedIndex = proxyServersModel.mapToSource(selectedIndex) } } - - KeyNavigation.tab: protocolSelector } DropDownType { @@ -445,12 +399,10 @@ PageType { ] } - currentIndex: 0 - clickedFunction: function() { handler() - protocolSelector.close() + protocolSelector.closeTriggered() } Connections { @@ -458,7 +410,8 @@ PageType { function onSeverSelectorIndexChanged() { var defaultContainer = proxyContainersModel.mapFromSource(ServersModel.getProcessedServerData("defaultContainer")) - protocolSelectorListView.currentIndex = defaultContainer + protocolSelectorListView.selectedIndex = defaultContainer + protocolSelectorListView.positionViewAtIndex(selectedIndex, ListView.Beginning) protocolSelectorListView.triggerCurrentItem() } } @@ -473,7 +426,7 @@ PageType { protocolSelector.text = selectedText - ContainersModel.setProcessedContainerIndex(proxyContainersModel.mapToSource(currentIndex)) + ContainersModel.setProcessedContainerIndex(proxyContainersModel.mapToSource(selectedIndex)) fillConnectionTypeModel() @@ -488,7 +441,7 @@ PageType { function fillConnectionTypeModel() { root.connectionTypesModel = [amneziaConnectionFormat] - var index = proxyContainersModel.mapToSource(currentIndex) + var index = proxyContainersModel.mapToSource(selectedIndex) if (index === ContainerProps.containerFromString("amnezia-openvpn")) { root.connectionTypesModel.push(openVpnConnectionFormat) @@ -508,12 +461,6 @@ PageType { } } } - - KeyNavigation.tab: accessTypeSelector.currentIndex === 0 ? - exportTypeSelector : - isSearchBarVisible ? - searchTextField.textField : - usersHeader.actionButton } DropDownType { @@ -549,7 +496,7 @@ PageType { clickedFunction: function() { exportTypeSelector.text = selectedText exportTypeSelector.currentIndex = currentIndex - exportTypeSelector.close() + exportTypeSelector.closeTriggered() } Component.onCompleted: { @@ -557,9 +504,6 @@ PageType { exportTypeSelector.currentIndex = currentIndex } } - - KeyNavigation.tab: shareButton - } BasicButtonType { @@ -573,9 +517,8 @@ PageType { visible: accessTypeSelector.currentIndex === 0 text: qsTr("Share") - imageSource: "qrc:/images/controls/share-2.svg" + leftImageSource: "qrc:/images/controls/share-2.svg" - Keys.onTabPressed: lastItemTabClicked(focusItem) parentFlickable: a @@ -584,7 +527,6 @@ PageType { ExportController.generateConfig(root.connectionTypesModel[exportTypeSelector.currentIndex].type) } } - } Header2Type { @@ -600,11 +542,6 @@ PageType { actionButtonFunction: function() { root.isSearchBarVisible = true } - - Keys.onTabPressed: clientsListView.model.count > 0 ? - clientsListView.forceActiveFocus() : - lastItemTabClicked(focusItem) - } RowLayout { @@ -618,35 +555,13 @@ PageType { textFieldPlaceholderText: qsTr("Search") - Connections { - target: root - function onIsSearchBarVisibleChanged() { - if (root.isSearchBarVisible) { - searchTextField.textField.forceActiveFocus() - } else { - searchTextField.textFieldText = "" - if (!GC.isMobile()) { - usersHeader.actionButton.forceActiveFocus() - } - } - } - } - Keys.onEscapePressed: { root.isSearchBarVisible = false } function navigateTo() { - if (GC.isMobile()) { - focusItem.forceActiveFocus() - return; - } - if (searchTextField.textFieldText === "") { root.isSearchBarVisible = false - usersHeader.actionButton.forceActiveFocus() - } else { - closeSearchButton.forceActiveFocus() } } @@ -660,16 +575,6 @@ PageType { image: "qrc:/images/controls/close.svg" imageColor: AmneziaStyle.color.paleGray - Keys.onTabPressed: { - if (!GC.isMobile()) { - if (clientsListView.model.count > 0) { - clientsListView.forceActiveFocus() - } else { - lastItemTabClicked(focusItem) - } - } - } - function clickedFunc() { root.isSearchBarVisible = false } @@ -687,6 +592,8 @@ PageType { visible: accessTypeSelector.currentIndex === 1 + property bool isFocusable: true + model: SortFilterProxyModel { id: proxyClientManagementModel sourceModel: ClientManagementModel @@ -699,44 +606,12 @@ PageType { clip: true interactive: false - - activeFocusOnTab: true - focus: true - Keys.onTabPressed: { - if (!GC.isMobile()) { - if (currentIndex < this.count - 1) { - this.incrementCurrentIndex() - currentItem.focusItem.forceActiveFocus() - } else { - this.currentIndex = 0 - lastItemTabClicked(focusItem) - } - } - } - - onActiveFocusChanged: { - if (focus && !GC.isMobile()) { - currentIndex = 0 - currentItem.focusItem.forceActiveFocus() - } - } - - onCurrentIndexChanged: { - if (currentItem) { - if (currentItem.y < a.contentY) { - a.contentY = currentItem.y - } else if (currentItem.y + currentItem.height + clientsListView.y > a.contentY + a.height) { - a.contentY = currentItem.y + clientsListView.y + currentItem.height - a.height - } - } - } + reuseItems: true delegate: Item { implicitWidth: clientsListView.width implicitHeight: delegateContent.implicitHeight - property alias focusItem: clientFocusItem.rightButton - ColumnLayout { id: delegateContent @@ -755,7 +630,7 @@ PageType { rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { - clientInfoDrawer.open() + clientInfoDrawer.openTriggered() } } @@ -766,17 +641,11 @@ PageType { parent: root - onClosed: { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } - } - width: root.width height: root.height - expandedContent: ColumnLayout { - id: expandedContent + expandedStateContent: ColumnLayout { + id: expandedStateContent anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right @@ -785,15 +654,7 @@ PageType { anchors.rightMargin: 16 onImplicitHeightChanged: { - clientInfoDrawer.expandedHeight = expandedContent.implicitHeight + 32 - } - - Connections { - target: clientInfoDrawer - enabled: !GC.isMobile() - function onOpened() { - focusItem1.forceActiveFocus() - } + clientInfoDrawer.expandedHeight = expandedStateContent.implicitHeight + 32 } Header2TextType { @@ -809,7 +670,11 @@ PageType { ParagraphTextType { color: AmneziaStyle.color.mutedGray visible: creationDate - Layout.fillWidth: true + Layout.maximumWidth: parent.width + + maximumLineCount: 2 + wrapMode: Text.Wrap + elide: Qt.ElideRight text: qsTr("Creation date: %1").arg(creationDate) } @@ -817,7 +682,11 @@ PageType { ParagraphTextType { color: AmneziaStyle.color.mutedGray visible: latestHandshake - Layout.fillWidth: true + Layout.maximumWidth: parent.width + + maximumLineCount: 2 + wrapMode: Text.Wrap + elide: Qt.ElideRight text: qsTr("Latest handshake: %1").arg(latestHandshake) } @@ -825,7 +694,11 @@ PageType { ParagraphTextType { color: AmneziaStyle.color.mutedGray visible: dataReceived - Layout.fillWidth: true + Layout.maximumWidth: parent.width + + maximumLineCount: 2 + wrapMode: Text.Wrap + elide: Qt.ElideRight text: qsTr("Data received: %1").arg(dataReceived) } @@ -833,7 +706,11 @@ PageType { ParagraphTextType { color: AmneziaStyle.color.mutedGray visible: dataSent - Layout.fillWidth: true + Layout.maximumWidth: parent.width + + maximumLineCount: 2 + wrapMode: Text.Wrap + elide: Qt.ElideRight text: qsTr("Data sent: %1").arg(dataSent) } @@ -841,16 +718,13 @@ PageType { ParagraphTextType { color: AmneziaStyle.color.mutedGray visible: allowedIps - Layout.fillWidth: true + Layout.maximumWidth: parent.width + + wrapMode: Text.Wrap text: qsTr("Allowed IPs: %1").arg(allowedIps) } - Item { - id: focusItem1 - KeyNavigation.tab: renameButton - } - BasicButtonType { id: renameButton Layout.fillWidth: true @@ -865,10 +739,8 @@ PageType { text: qsTr("Rename") - KeyNavigation.tab: revokeButton - clickedFunc: function() { - clientNameEditDrawer.open() + clientNameEditDrawer.openTriggered() } DrawerType2 { @@ -879,13 +751,7 @@ PageType { anchors.fill: parent expandedHeight: root.height * 0.35 - onClosed: { - if (!GC.isMobile()) { - focusItem1.forceActiveFocus() - } - } - - expandedContent: ColumnLayout { + expandedStateContent: ColumnLayout { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right @@ -893,19 +759,6 @@ PageType { anchors.leftMargin: 16 anchors.rightMargin: 16 - Connections { - target: clientNameEditDrawer - enabled: !GC.isMobile() - function onOpened() { - clientNameEditor.textField.forceActiveFocus() - } - } - - Item { - id: focusItem2 - KeyNavigation.tab: clientNameEditor.textField - } - TextFieldWithHeaderType { id: clientNameEditor Layout.fillWidth: true @@ -913,8 +766,6 @@ PageType { textFieldText: clientName textField.maximumLength: 20 checkEmptyText: true - - KeyNavigation.tab: saveButton } BasicButtonType { @@ -923,7 +774,6 @@ PageType { Layout.fillWidth: true text: qsTr("Save") - KeyNavigation.tab: focusItem2 clickedFunc: function() { if (clientNameEditor.textFieldText === "") { @@ -937,7 +787,7 @@ PageType { ContainersModel.getProcessedContainerIndex(), ServersModel.getProcessedServerCredentials()) PageController.showBusyIndicator(false) - clientNameEditDrawer.close() + clientNameEditDrawer.closeTriggered() } } } @@ -958,7 +808,6 @@ PageType { borderWidth: 1 text: qsTr("Revoke") - KeyNavigation.tab: focusItem1 clickedFunc: function() { var headerText = qsTr("Revoke the config for a user - %1?").arg(clientName) @@ -967,12 +816,12 @@ PageType { var noButtonText = qsTr("Cancel") var yesButtonFunction = function() { - clientInfoDrawer.close() + clientInfoDrawer.closeTriggered() root.revokeConfig(index) } var noButtonFunction = function() { if (!GC.isMobile()) { - focusItem1.forceActiveFocus() + // focusItem1.forceActiveFocus() } } @@ -991,18 +840,5 @@ PageType { id: shareConnectionDrawer anchors.fill: parent - onClosed: { - if (!GC.isMobile()) { - clientNameTextField.textField.forceActiveFocus() - } - } - } - - MouseArea { - anchors.fill: parent - onPressed: function(mouse) { - forceActiveFocus() - mouse.accepted = false - } } } diff --git a/client/ui/qml/Pages2/PageShareFullAccess.qml b/client/ui/qml/Pages2/PageShareFullAccess.qml index 2a565230..af409d72 100644 --- a/client/ui/qml/Pages2/PageShareFullAccess.qml +++ b/client/ui/qml/Pages2/PageShareFullAccess.qml @@ -18,13 +18,6 @@ import "../Config" PageType { id: root - defaultActiveFocusItem: focusItem - - Item { - id: focusItem - KeyNavigation.tab: backButton - } - BackButtonType { id: backButton @@ -32,8 +25,6 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 - - KeyNavigation.tab: serverSelector } FlickableType { @@ -85,8 +76,6 @@ PageType { descriptionText: qsTr("Server") headerText: qsTr("Server") - KeyNavigation.tab: shareButton - listView: ListViewWithRadioButtonType { id: serverSelectorListView @@ -113,7 +102,7 @@ PageType { shareConnectionDrawer.headerText = qsTr("Accessing ") + serverSelector.text shareConnectionDrawer.configContentHeaderText = qsTr("File with accessing settings to ") + serverSelector.text - serverSelector.close() + serverSelector.closeTriggered() } Component.onCompleted: { @@ -135,9 +124,7 @@ PageType { Layout.topMargin: 40 text: qsTr("Share") - imageSource: "qrc:/images/controls/share-2.svg" - - Keys.onTabPressed: lastItemTabClicked(focusItem) + leftImageSource: "qrc:/images/controls/share-2.svg" clickedFunc: function() { PageController.showBusyIndicator(true) @@ -153,7 +140,7 @@ PageType { shareConnectionDrawer.headerText = qsTr("Connection to ") + serverSelector.text shareConnectionDrawer.configContentHeaderText = qsTr("File with connection settings to ") + serverSelector.text - shareConnectionDrawer.open() + shareConnectionDrawer.openTriggered() shareConnectionDrawer.contentVisible = true PageController.showBusyIndicator(false) @@ -166,10 +153,5 @@ PageType { id: shareConnectionDrawer anchors.fill: parent - onClosed: { - if (!GC.isMobile()) { - focusItem.forceActiveFocus() - } - } } } diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index 640c61ef..71009e28 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -15,12 +15,12 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: homeTabButton - property bool isControlsDisabled: false property bool isTabBarDisabled: false Connections { + objectName: "pageControllerConnection" + target: PageController function onGoToPageHome() { @@ -90,19 +90,11 @@ PageType { PageController.closePage() } } - - function onForceTabBarActiveFocus() { - homeTabButton.focus = true - tabBar.forceActiveFocus() - } - - function onForceStackActiveFocus() { - homeTabButton.focus = true - tabBarStackView.forceActiveFocus() - } } Connections { + objectName: "installControllerConnections" + target: InstallController function onInstallationErrorOccurred(error) { @@ -165,6 +157,8 @@ PageType { } Connections { + objectName: "connectionControllerConnections" + target: ConnectionController function onReconnectWithUpdatedContainer(message) { @@ -182,6 +176,8 @@ PageType { } Connections { + objectName: "importControllerConnections" + target: ImportController function onImportErrorOccurred(error, goToPageHome) { @@ -196,6 +192,8 @@ PageType { } Connections { + objectName: "settingsControllerConnections" + target: SettingsController function onLoggingDisableByWatcher() { @@ -218,6 +216,7 @@ PageType { StackViewType { id: tabBarStackView + objectName: "tabBarStackView" anchors.top: parent.top anchors.right: parent.right @@ -247,13 +246,28 @@ PageType { } Keys.onPressed: function(event) { - PageController.keyPressEvent(event.key) - event.accepted = true + console.debug(">>>> ", event.key, " Event is caught by StartPage") + switch (event.key) { + case Qt.Key_Tab: + case Qt.Key_Down: + case Qt.Key_Right: + FocusController.nextKeyTabItem() + break + case Qt.Key_Backtab: + case Qt.Key_Up: + case Qt.Key_Left: + FocusController.previousKeyTabItem() + break + default: + PageController.keyPressEvent(event.key) + event.accepted = true + } } } TabBar { id: tabBar + objectName: "tabBar" anchors.right: parent.right anchors.left: parent.left @@ -269,6 +283,8 @@ PageType { enabled: !root.isControlsDisabled && !root.isTabBarDisabled background: Shape { + objectName: "backgroundShape" + width: parent.width height: parent.height @@ -289,6 +305,8 @@ PageType { TabImageButtonType { id: homeTabButton + objectName: "homeTabButton" + isSelected: tabBar.currentIndex === 0 image: "qrc:/images/controls/home.svg" clickedFunc: function () { @@ -296,27 +314,26 @@ PageType { ServersModel.processedIndex = ServersModel.defaultIndex tabBar.currentIndex = 0 } - - KeyNavigation.tab: shareTabButton - Keys.onEnterPressed: this.clicked() - Keys.onReturnPressed: this.clicked() } TabImageButtonType { id: shareTabButton + objectName: "shareTabButton" Connections { target: ServersModel function onModelReset() { - var hasServerWithWriteAccess = ServersModel.hasServerWithWriteAccess() - shareTabButton.visible = hasServerWithWriteAccess - shareTabButton.width = hasServerWithWriteAccess ? undefined : 0 + if (!SettingsController.isOnTv()) { + var hasServerWithWriteAccess = ServersModel.hasServerWithWriteAccess() + shareTabButton.visible = hasServerWithWriteAccess + shareTabButton.width = hasServerWithWriteAccess ? undefined : 0 + } } } - visible: ServersModel.hasServerWithWriteAccess() - width: ServersModel.hasServerWithWriteAccess() ? undefined : 0 + visible: !SettingsController.isOnTv() && ServersModel.hasServerWithWriteAccess() + width: !SettingsController.isOnTv() && ServersModel.hasServerWithWriteAccess() ? undefined : 0 isSelected: tabBar.currentIndex === 1 image: "qrc:/images/controls/share-2.svg" @@ -324,32 +341,30 @@ PageType { tabBarStackView.goToTabBarPage(PageEnum.PageShare) tabBar.currentIndex = 1 } - - KeyNavigation.tab: settingsTabButton } TabImageButtonType { id: settingsTabButton + objectName: "settingsTabButton" + isSelected: tabBar.currentIndex === 2 - image: "qrc:/images/controls/settings-2.svg" + image: "qrc:/images/controls/settings.svg" clickedFunc: function () { tabBarStackView.goToTabBarPage(PageEnum.PageSettings) tabBar.currentIndex = 2 } - - KeyNavigation.tab: plusTabButton } TabImageButtonType { id: plusTabButton + objectName: "plusTabButton" + isSelected: tabBar.currentIndex === 3 image: "qrc:/images/controls/plus.svg" clickedFunc: function () { tabBarStackView.goToTabBarPage(PageEnum.PageSetupWizardConfigSource) tabBar.currentIndex = 3 } - - Keys.onTabPressed: PageController.forceStackActiveFocus() } } } diff --git a/client/ui/qml/main2.qml b/client/ui/qml/main2.qml index fb99559f..8b73e62d 100644 --- a/client/ui/qml/main2.qml +++ b/client/ui/qml/main2.qml @@ -15,6 +15,7 @@ import "Pages2" Window { id: root objectName: "mainWindow" + visible: true width: GC.screenWidth height: GC.screenHeight @@ -26,13 +27,39 @@ Window { color: AmneziaStyle.color.midnightBlack onClosing: function() { - console.debug("QML onClosing signal") PageController.closeWindow() } title: "AmneziaVPN" + Item { // This item is needed for focus handling + id: defaultFocusItem + objectName: "defaultFocusItem" + + focus: true + + Keys.onPressed: function(event) { + switch (event.key) { + case Qt.Key_Tab: + case Qt.Key_Down: + case Qt.Key_Right: + FocusController.nextKeyTabItem() + break + case Qt.Key_Backtab: + case Qt.Key_Up: + case Qt.Key_Left: + FocusController.previousKeyTabItem() + break + default: + PageController.keyPressEvent(event.key) + event.accepted = true + } + } + } + Connections { + objectName: "pageControllerConnections" + target: PageController function onRaiseMainWindow() { @@ -58,7 +85,7 @@ Window { } function onShowPassphraseRequestDrawer() { - privateKeyPassphraseDrawer.open() + privateKeyPassphraseDrawer.openTriggered() } function onGoToPageSettingsBackup() { @@ -72,6 +99,8 @@ Window { } Connections { + objectName: "settingsControllerConnections" + target: SettingsController function onChangeSettingsFinished(finishedMessage) { @@ -80,11 +109,15 @@ Window { } PageStart { + objectName: "pageStart" + width: root.width height: root.height } Item { + objectName: "popupNotificationItem" + anchors.right: parent.right anchors.left: parent.left anchors.bottom: parent.bottom @@ -108,6 +141,8 @@ Window { } Item { + objectName: "popupErrorMessageItem" + anchors.right: parent.right anchors.left: parent.left anchors.bottom: parent.bottom @@ -120,6 +155,8 @@ Window { } Item { + objectName: "privateKeyPassphraseDrawerItem" + anchors.fill: parent DrawerType2 { @@ -128,7 +165,7 @@ Window { anchors.fill: parent expandedHeight: root.height * 0.35 - expandedContent: ColumnLayout { + expandedStateContent: ColumnLayout { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right @@ -167,8 +204,6 @@ Window { clickedFunc: function() { hidePassword = !hidePassword } - - KeyNavigation.tab: saveButton } BasicButtonType { @@ -186,7 +221,7 @@ Window { text: qsTr("Save") clickedFunc: function() { - privateKeyPassphraseDrawer.close() + privateKeyPassphraseDrawer.closeTriggered() PageController.passphraseRequestDrawerClosed(passphrase.textFieldText) } } @@ -195,6 +230,8 @@ Window { } Item { + objectName: "questionDrawerItem" + anchors.fill: parent QuestionDrawer { @@ -205,6 +242,8 @@ Window { } Item { + objectName: "busyIndicatorItem" + anchors.fill: parent BusyIndicatorType { @@ -221,26 +260,26 @@ Window { questionDrawer.noButtonText = noButtonText questionDrawer.yesButtonFunction = function() { - questionDrawer.close() + questionDrawer.closeTriggered() if (yesButtonFunction && typeof yesButtonFunction === "function") { yesButtonFunction() } } questionDrawer.noButtonFunction = function() { - questionDrawer.close() + questionDrawer.closeTriggered() if (noButtonFunction && typeof noButtonFunction === "function") { noButtonFunction() } } - questionDrawer.open() + questionDrawer.openTriggered() } FileDialog { id: mainFileDialog + objectName: "mainFileDialog" property bool isSaveMode: false - objectName: "mainFileDialog" fileMode: isSaveMode ? FileDialog.SaveFile : FileDialog.OpenFile onAccepted: SystemController.fileDialogClosed(true) diff --git a/client/utilities.cpp b/client/utilities.cpp index 1cc69aeb..61944e51 100755 --- a/client/utilities.cpp +++ b/client/utilities.cpp @@ -194,8 +194,13 @@ bool Utils::processIsRunning(const QString &fileName, const bool fullFlag) return false; #else QProcess process; + QStringList arguments; + if (fullFlag) { + arguments << "-f"; + } + arguments << fileName; process.setProcessChannelMode(QProcess::MergedChannels); - process.start("pgrep", QStringList({ fullFlag ? "-f" : "", fileName })); + process.start("pgrep", arguments); process.waitForFinished(); if (process.exitStatus() == QProcess::NormalExit) { if (fullFlag) { @@ -248,7 +253,7 @@ bool Utils::killProcessByName(const QString &name) #elif defined Q_OS_IOS || defined(Q_OS_ANDROID) return false; #else - QProcess::execute(QString("pkill %1").arg(name)); + return QProcess::execute("pkill", { name }) == 0; #endif } diff --git a/client/utils/qmlUtils.cpp b/client/utils/qmlUtils.cpp new file mode 100644 index 00000000..b9557b14 --- /dev/null +++ b/client/utils/qmlUtils.cpp @@ -0,0 +1,128 @@ +#include "qmlUtils.h" + +#include +#include +#include + +namespace FocusControl +{ + QPointF getItemCenterPointOnScene(QQuickItem *item) + { + const auto x0 = item->x() + (item->width() / 2); + const auto y0 = item->y() + (item->height() / 2); + return item->parentItem()->mapToScene(QPointF { x0, y0 }); + } + + bool isEnabled(QObject *obj) + { + const auto item = qobject_cast(obj); + return item && item->isEnabled(); + } + + bool isVisible(QObject *item) + { + const auto res = item->property("visible").toBool(); + return res; + } + + bool isFocusable(QObject *item) + { + const auto res = item->property("isFocusable").toBool(); + return res; + } + + bool isListView(QObject *item) + { + return item->inherits("QQuickListView"); + } + + bool isOnTheScene(QObject *object) + { + QQuickItem *item = qobject_cast(object); + if (!item) { + qWarning() << "Couldn't recognize object as item"; + return false; + } + + if (!item->isVisible()) { + return false; + } + + QRectF itemRect = item->mapRectToScene(item->childrenRect()); + + QQuickWindow *window = item->window(); + if (!window) { + qWarning() << "Couldn't get the window on the Scene check"; + return false; + } + + const auto contentItem = window->contentItem(); + if (!contentItem) { + qWarning() << "Couldn't get the content item on the Scene check"; + return false; + } + QRectF windowRect = contentItem->childrenRect(); + const auto res = (windowRect.contains(itemRect) || isListView(item)); + return res; + } + + bool isMore(QObject *item1, QObject *item2) + { + return !isLess(item1, item2); + } + + bool isLess(QObject *item1, QObject *item2) + { + const auto p1 = getItemCenterPointOnScene(qobject_cast(item1)); + const auto p2 = getItemCenterPointOnScene(qobject_cast(item2)); + return (p1.y() == p2.y()) ? (p1.x() < p2.x()) : (p1.y() < p2.y()); + } + + QList getSubChain(QObject *object) + { + QList res; + if (!object) { + return res; + } + + const auto children = object->children(); + + for (const auto child : children) { + if (child && isFocusable(child) && isOnTheScene(child) && isEnabled(child)) { + res.append(child); + } else { + res.append(getSubChain(child)); + } + } + return res; + } + + QList getItemsChain(QObject *object) + { + QList res; + if (!object) { + return res; + } + + const auto children = object->children(); + + for (const auto child : children) { + if (child && isFocusable(child) && isEnabled(child) && isVisible(child)) { + res.append(child); + } else { + res.append(getItemsChain(child)); + } + } + return res; + } + + void printItems(const QList &items, QObject *current_item) + { + for (const auto &item : items) { + QQuickItem *i = qobject_cast(item); + QPointF coords { getItemCenterPointOnScene(i) }; + QString prefix = current_item == i ? "==>" : " "; + qDebug() << prefix << " Item: " << i << " with coords: " << coords; + } + } +} // namespace FocusControl \ No newline at end of file diff --git a/client/utils/qmlUtils.h b/client/utils/qmlUtils.h new file mode 100644 index 00000000..535377c4 --- /dev/null +++ b/client/utils/qmlUtils.h @@ -0,0 +1,30 @@ +#ifndef FOCUSCONTROL_H +#define FOCUSCONTROL_H + +#include +#include + +namespace FocusControl +{ + bool isEnabled(QObject *item); + bool isVisible(QObject *item); + bool isFocusable(QObject *item); + bool isListView(QObject *item); + bool isOnTheScene(QObject *object); + bool isMore(QObject *item1, QObject *item2); + bool isLess(QObject *item1, QObject *item2); + + /*! + * \brief Make focus chain of elements which are on the scene + */ + QList getSubChain(QObject *object); + + /*! + * \brief Make focus chain of elements which could be not on the scene + */ + QList getItemsChain(QObject *object); + + void printItems(const QList &items, QObject *current_item); +} // namespace FocusControl + +#endif // FOCUSCONTROL_H diff --git a/deploy/data/linux/post_install.sh b/deploy/data/linux/post_install.sh index b3345bac..324462d9 100755 --- a/deploy/data/linux/post_install.sh +++ b/deploy/data/linux/post_install.sh @@ -19,6 +19,11 @@ date > $LOG_FILE echo "Script started" >> $LOG_FILE sudo killall -9 $APP_NAME 2>> $LOG_FILE +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly disable >> $LOG_FILE + echo "steamos-readonly disabled" >> $LOG_FILE +fi + if sudo systemctl is-active --quiet $APP_NAME; then sudo systemctl stop $APP_NAME >> $LOG_FILE sudo systemctl disable $APP_NAME >> $LOG_FILE @@ -42,6 +47,11 @@ sudo chmod 555 /usr/share/applications/$APP_NAME.desktop >> $LOG_FILE echo "user desktop creation loop ended" >> $LOG_FILE +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly enable >> $LOG_FILE + echo "steamos-readonly enabled" >> $LOG_FILE +fi + date >> $LOG_FILE echo "Service status:" >> $LOG_FILE sudo systemctl status $APP_NAME >> $LOG_FILE diff --git a/deploy/data/linux/post_uninstall.sh b/deploy/data/linux/post_uninstall.sh index 5849a90e..98090d20 100755 --- a/deploy/data/linux/post_uninstall.sh +++ b/deploy/data/linux/post_uninstall.sh @@ -13,6 +13,11 @@ date >> $LOG_FILE echo "Uninstall Script started" >> $LOG_FILE sudo killall -9 $APP_NAME 2>> $LOG_FILE +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly disable >> $LOG_FILE + echo "steamos-readonly disabled" >> $LOG_FILE +fi + ls /opt/AmneziaVPN/client/lib/* | while IFS=: read -r dir; do sudo unlink $dir >> $LOG_FILE done @@ -59,6 +64,11 @@ if test -f /usr/share/pixmaps/$APP_NAME.png; then fi +if command -v steamos-readonly &> /dev/null; then + sudo steamos-readonly enable >> $LOG_FILE + echo "steamos-readonly enabled" >> $LOG_FILE +fi + date >> $LOG_FILE echo "Service after uninstall status:" >> $LOG_FILE sudo systemctl status $APP_NAME >> $LOG_FILE diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index bb8a4182..17f34499 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -35,10 +35,6 @@ int IpcServer::createPrivilegedProcess() qDebug() << "IpcServer::createPrivilegedProcess"; #endif -#ifdef Q_OS_WIN - WindowsFirewall::instance()->init(); -#endif - m_localpid++; ProcessDescriptor pd(this); @@ -195,7 +191,9 @@ void IpcServer::setLogsEnabled(bool enabled) bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterIndex) { #ifdef Q_OS_WIN - return WindowsFirewall::instance()->enableKillSwitch(vpnAdapterIndex); + auto firewallManager = WindowsFirewall::create(this); + Q_ASSERT(firewallManager != nullptr); + return firewallManager->enableInterface(vpnAdapterIndex); #endif #if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) @@ -228,6 +226,8 @@ bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterInd #ifdef Q_OS_LINUX // double-check + ensure our firewall is installed and enabled + if (!LinuxFirewall::isInstalled()) + LinuxFirewall::install(); LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("000.allowLoopback"), true); LinuxFirewall::setAnchorEnabled(LinuxFirewall::Both, QStringLiteral("100.blockAll"), blockAll); LinuxFirewall::setAnchorEnabled(LinuxFirewall::IPv4, QStringLiteral("110.allowNets"), allowNets); @@ -282,7 +282,9 @@ bool IpcServer::enableKillSwitch(const QJsonObject &configStr, int vpnAdapterInd bool IpcServer::disableKillSwitch() { #ifdef Q_OS_WIN - return WindowsFirewall::instance()->disableKillSwitch(); + auto firewallManager = WindowsFirewall::create(this); + Q_ASSERT(firewallManager != nullptr); + return firewallManager->disableKillSwitch(); #endif #ifdef Q_OS_LINUX @@ -347,7 +349,9 @@ bool IpcServer::enablePeerTraffic(const QJsonObject &configStr) // killSwitch toggle if (QVariant(configStr.value(amnezia::config_key::killSwitchOption).toString()).toBool()) { - WindowsFirewall::instance()->enablePeerTraffic(config); + auto firewallManager = WindowsFirewall::create(this); + Q_ASSERT(firewallManager != nullptr); + firewallManager->enablePeerTraffic(config); } WindowsDaemon::instance()->prepareActivation(config, inetAdapterIndex); diff --git a/ipc/ipctun2socksprocess.cpp b/ipc/ipctun2socksprocess.cpp index ffcb1bcd..2125f6ab 100644 --- a/ipc/ipctun2socksprocess.cpp +++ b/ipc/ipctun2socksprocess.cpp @@ -11,7 +11,6 @@ IpcProcessTun2Socks::IpcProcessTun2Socks(QObject *parent) : IpcProcessTun2SocksSource(parent), m_t2sProcess(QSharedPointer(new QProcess())) { - connect(m_t2sProcess.data(), &QProcess::stateChanged, this, &IpcProcessTun2Socks::stateChanged); qDebug() << "IpcProcessTun2Socks::IpcProcessTun2Socks()"; } @@ -23,8 +22,10 @@ IpcProcessTun2Socks::~IpcProcessTun2Socks() void IpcProcessTun2Socks::start() { + connect(m_t2sProcess.data(), &QProcess::stateChanged, this, &IpcProcessTun2Socks::stateChanged); qDebug() << "IpcProcessTun2Socks::start()"; m_t2sProcess->setProgram(amnezia::permittedProcessPath(static_cast(amnezia::PermittedProcess::Tun2Socks))); + QString XrayConStr = "socks5://127.0.0.1:10808"; #ifdef Q_OS_WIN @@ -41,7 +42,11 @@ void IpcProcessTun2Socks::start() m_t2sProcess->setArguments(arguments); - Utils::killProcessByName(m_t2sProcess->program()); + if (Utils::processIsRunning(Utils::executable("tun2socks", false))) { + qDebug().noquote() << "kill previos tun2socks"; + Utils::killProcessByName(Utils::executable("tun2socks", false)); + } + m_t2sProcess->start(); connect(m_t2sProcess.data(), &QProcess::readyReadStandardOutput, this, [this]() { @@ -54,12 +59,10 @@ void IpcProcessTun2Socks::start() connect(m_t2sProcess.data(), QOverload::of(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) { qDebug().noquote() << "tun2socks finished, exitCode, exiStatus" << exitCode << exitStatus; emit setConnectionState(Vpn::ConnectionState::Disconnected); - if (exitStatus != QProcess::NormalExit){ - stop(); - } - if (exitCode !=0 ){ - stop(); + if ((exitStatus != QProcess::NormalExit) || (exitCode != 0)) { + emit setConnectionState(Vpn::ConnectionState::Error); } + }); m_t2sProcess->start(); @@ -69,6 +72,8 @@ void IpcProcessTun2Socks::start() void IpcProcessTun2Socks::stop() { qDebug() << "IpcProcessTun2Socks::stop()"; - m_t2sProcess->close(); + m_t2sProcess->disconnect(); + m_t2sProcess->kill(); + m_t2sProcess->waitForFinished(3000); } #endif diff --git a/metadata/img-readme/apl.png b/metadata/img-readme/apl.png deleted file mode 100644 index 6dedfa12..00000000 Binary files a/metadata/img-readme/apl.png and /dev/null differ diff --git a/metadata/img-readme/download-alt.svg b/metadata/img-readme/download-alt.svg new file mode 100644 index 00000000..f97c9c3d --- /dev/null +++ b/metadata/img-readme/download-alt.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/metadata/img-readme/download-website-ru.svg b/metadata/img-readme/download-website-ru.svg new file mode 100644 index 00000000..386ae4fe --- /dev/null +++ b/metadata/img-readme/download-website-ru.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/metadata/img-readme/download-website.svg b/metadata/img-readme/download-website.svg new file mode 100644 index 00000000..d0cf8375 --- /dev/null +++ b/metadata/img-readme/download-website.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/metadata/img-readme/download.png b/metadata/img-readme/download.png deleted file mode 100644 index 0e6a8850..00000000 Binary files a/metadata/img-readme/download.png and /dev/null differ diff --git a/metadata/img-readme/play.png b/metadata/img-readme/play.png deleted file mode 100644 index 2fb316c8..00000000 Binary files a/metadata/img-readme/play.png and /dev/null differ diff --git a/service/server/CMakeLists.txt b/service/server/CMakeLists.txt index 0f101087..28174774 100644 --- a/service/server/CMakeLists.txt +++ b/service/server/CMakeLists.txt @@ -127,6 +127,7 @@ if(WIN32) ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/windowsutils.h ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/windowspingsender.h ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/windows/windowsnetworkwatcher.h + ${CMAKE_CURRENT_LIST_DIR}/../../client/daemon/daemonerrors.h ) set(SOURCES ${SOURCES} diff --git a/service/server/router_linux.cpp b/service/server/router_linux.cpp index fb7a108c..852c878f 100644 --- a/service/server/router_linux.cpp +++ b/service/server/router_linux.cpp @@ -152,21 +152,29 @@ bool RouterLinux::routeDeleteList(const QString &gw, const QStringList &ips) return cnt; } +bool RouterLinux::isServiceActive(const QString &serviceName) { + QProcess process; + process.start("systemctl", { "is-active", "--quiet", serviceName }); + process.waitForFinished(); + + return process.exitCode() == 0; +} + void RouterLinux::flushDns() { QProcess p; p.setProcessChannelMode(QProcess::MergedChannels); //check what the dns manager use - if (QFileInfo::exists("/usr/bin/nscd") - || QFileInfo::exists("/usr/sbin/nscd") - || QFileInfo::exists("/usr/lib/systemd/system/nscd.service")) - { - p.start("systemctl restart nscd"); - } - else - { - p.start("systemctl restart systemd-resolved"); + if (isServiceActive("nscd.service")) { + qDebug() << "Restarting nscd.service"; + p.start("systemctl", { "restart", "nscd" }); + } else if (isServiceActive("systemd-resolved.service")) { + qDebug() << "Restarting systemd-resolved.service"; + p.start("systemctl", { "restart", "systemd-resolved" }); + } else { + qDebug() << "No suitable DNS manager found."; + return; } p.waitForFinished(); diff --git a/service/server/router_linux.h b/service/server/router_linux.h index 04a26fd2..2094f596 100644 --- a/service/server/router_linux.h +++ b/service/server/router_linux.h @@ -43,6 +43,7 @@ private: RouterLinux(RouterLinux const &) = delete; RouterLinux& operator= (RouterLinux const&) = delete; + bool isServiceActive(const QString &serviceName); QList m_addedRoutes; DnsUtilsLinux *m_dnsUtil; };