Compare commits

..
Author SHA1 Message Date
makkaor 3121775a6f fix: preserve imported MTU (#3113) 2026-10-26 11:17:42 +09:00
Yakimov Grigory 3977447d4a feat: add Indonesian (id_ID) localization (#3325) 2026-10-26 00:44:16 +09:00
yp bf95867a7c fix: restore favorite locations (#3309)
* fix: restore favorite locations

* fix per subscription favorites
2026-10-26 00:33:54 +09:00
NickVs2015 a55ef420e8 fix: rotate local proxy port on restart (#3320) 2026-10-26 00:33:21 +09:00
vkamn 3b5a93889f feat: new design system (#3299)
* fix: use text button for local proxy learn more

* feat: pin page header and collapse large title on scroll

* fix: enable page scrolling only when content overflows

* refactor: drop qml focus hacks and simplify list selectors

- remove unused objectNames, dead functions and focus timers
- handle back/escape in FocusController instead of PageStart keys
- declare static menus as items instead of list<QtObject> models
- make radio/segment selection binding-driven, reuseItems opt-in
- fix full access share default server preselection

* refactor: convert config models to Q_PROPERTY and merge MtProto pages
- config models: QObject with properties instead of single-row list models
- protocol/service pages: Flickable layout, write-through edits, no save focus hack
- MtProxy/Telemt settings share PageServiceMtProtoSettingsBase
- question drawer requested via Dialogs singleton

* chore: bump version

* refactor: create rarely shown controls on demand

- focus ring and selection glow create MultiEffect only while visible
- text fields create the context menu, action buttons and attached button on demand
- setup page loads the details sheet content when it opens

* feat: macOS and iOS 26 app icon from Icon Composer

- add images/AppIcon.icon with colour and mono marks
- build the app icon from it on macOS, macOS NE and iOS, require Xcode 26
- drop app.icns and the AppIcon asset catalogs

* feat: update design system changes
- header: centered 18 semibold title in a 56 px row with 48 px buttons, no large title
- hide the back arrow on stack root pages
- subtitle as the first content block, 16 under the header and 24 to the next block
- main toggles as the first content row, desktop only
- logs: share and a more menu instead of three header icons
- countries: 48 px gear, search first under the header, chips to list 24
- sheets: 32 px from title to the first block
- tab bar: 44 px buttons centered with 18 px gaps
- connection key field: Insert while empty, clear button after paste
- error toast: default text color, accent error code link
- 24 px between the last block and sticky buttons

* chore: minor ui fixes

* fix: restore trailing buttons in country rows

* chore: bump version

* chore: build macOS and iOS on macos-26 runners for the Icon Composer app icon

* fix: load only one Go runtime in the UI process
libAmneziaVPN linked libwg-go and libovpn3 (which pulls in the Go-based
libck-ovpn-plugin) without using any of their symbols. With libagw that
made three Go runtimes in the UI process. They conflict over signal
handlers, and the app crashed at startup with SIGSEGV and no tombstone.
Stop linking awg-android and openvpn-pt-android into the app library.
Package their .so files through QT_ANDROID_EXTRA_LIBS instead; the VPN
service processes still load them from Kotlin.

* chore: sync with figma and minor ui fixes

* chore: bump version

* fix: disable Go async preemption on Apple platforms

Build libagw with -X=runtime.godebugDefault=asyncpreemptoff=1 on macOS
and iOS to work around the Go runtime crash on macOS
(golang/go#75887).
2026-10-26 00:32:49 +09:00
NickVs2015 02838a94d5 fix: add new dep to test (#3301) 2026-10-20 10:22:09 +08:00
NickVs2015 9a1200d6f3 fix: build libagw with go 1.27.1 (#3303) 2026-10-20 10:21:53 +08:00
NickVs2015andvkamn ddde43635e feat: local proxy mode (#3166)
* fix: share double-stop guard via VpnProtocol base class

* feat: add local proxy core module

* feat: local proxy settings api

* feat: wire local proxy into app controllers

* feat: local proxy settings ui

* feat: arbitrate shared xray engine ownership via ipc tokens

* fix: remove connection type

* chore: add qthttpserver to desktop ci qt modules

* fix: local proxy port fallback and active port reporting

* fix: keep text field button aligned when error is shown

* fix: hide local proxy for free servers and guard device unlink

* fix: show local proxy state per server

* fix: anchor text field button to content

* fix: adapt local proxy pages to design

* fix: copy logic and port logic

* refactor: move local proxy files to controllers and utils

* refactor: merge local proxy into LocalProxyController

* refactor: reuse gateway config request and keep local proxy state in memory

---------

Co-authored-by: vkamn <vk@amnezia.org>
2026-10-18 21:25:04 +08:00
vkamn ffba1cb2a4 chore: minor ui fixes (#3295)
* fix: sync ds component sizes with updated figma

* fix: hover only on action buttons in list rows, standalone country row
2026-10-17 23:51:22 +08:00
vkamn 30ff3451c7 fix: prepare gateway request (#3291)
* fix: prepare the gateway request once instead of a blocking callback per attempt

* chore: libagw 1.0.8, gateway and failover timeouts 6s, health check 1s
2026-10-16 00:10:29 +08:00
vkamn db951e9928 chore: minor ui fixes (#3290)
* chore: change amnezia hosting description

* fix: ios safe areas and ds component alignment

- extend window into ios safe areas, pass insets to PageController
- use dark interface style and portrait-only orientation on iphone
- drop bottom inset while keyboard is open
- align controls with figma: title-only cards, caption height,
  selectable list items, nav bar spacing, chip hover, switch rows

* fix: news settings icon unread badge

* fix: align ds effects with figma

- three-layer selection glow for switch, radio and connect ring
- connect button disk with glow in all states
- snackbar elevate shadow spread, dropdown panel shadow
- backdrop blur for loading overlay container
- pressed state layer 0.1 for icon buttons, segments, list items, cards

* refactor: remove Qt5Compat.GraphicalEffects
2026-10-15 23:50:51 +08:00
321 changed files with 37892 additions and 27555 deletions

No files matched your search

+101 -43
View File
@@ -88,7 +88,7 @@ jobs:
host: 'linux'
target: 'desktop'
arch: 'linux_gcc_64'
modules: 'qtremoteobjects qt5compat qtshadertools'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
setup-python: 'true'
tools: 'tools_ifw'
@@ -209,7 +209,7 @@ jobs:
host: 'windows'
target: 'desktop'
arch: 'win64_msvc2022_64'
modules: 'qtremoteobjects qt5compat qtshadertools'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
setup-python: 'true'
tools: 'tools_ifw'
@@ -324,7 +324,7 @@ jobs:
# ------------------------------------------------------
Build-iOS:
runs-on: macos-latest
runs-on: macos-26
needs: Bake-Prebuilts-iOS
if: ${{ always() }}
@@ -472,7 +472,7 @@ jobs:
# ------------------------------------------------------
Build-MacOS:
runs-on: macos-15
runs-on: macos-26
needs: Bake-Prebuilts-MacOS
if: ${{ always() }}
@@ -528,7 +528,7 @@ jobs:
host: 'mac'
target: 'desktop'
arch: 'clang_64'
modules: 'qtremoteobjects qt5compat qtshadertools'
modules: 'qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets'
dir: ${{ runner.temp }}
setup-python: 'true'
set-env: 'true'
@@ -671,7 +671,7 @@ jobs:
version: ${{ env.QT_VERSION }}
host: 'mac'
target: 'desktop'
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia'
modules: 'qtremoteobjects qt5compat qtshadertools qtmultimedia qthttpserver qtwebsockets'
arch: 'clang_64'
dir: ${{ runner.temp }}
set-env: 'true'
@@ -1054,17 +1054,10 @@ jobs:
- UiAppSTModelAndControllerTest
- UiAllowedDnsModelAndControllerTest
- BackupExportImportTest
- FavoriteLocationsRestoreTest
- ConnectionTest
- OpenSSLLibraryTest
- GatewayServiceImportTest
- UiNewsModelAndControllerTest
- UiServicesModelAndControllerTest
- UiSubscriptionControllerTest
- UiApiAccountInfoModelTest
- ApiBenefitsModelTest
- ApiCountryModelTest
- ApiCountryListModelTest
- ApiDevicesModelTest
runs-on: ubuntu-22.04
@@ -1085,7 +1078,6 @@ jobs:
THIRD_PARTY_VLESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VLESS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}
AMNEZIA_TEST_API_KEY: ${{ secrets.AMNEZIA_TEST_API_KEY }}
steps:
- name: Checkout
@@ -1114,7 +1106,7 @@ jobs:
host: linux
target: desktop
arch: linux_gcc_64
modules: qtremoteobjects qt5compat qtshadertools
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
dir: ${{ runner.temp }}
set-env: true
aqtversion: '==3.3.0'
@@ -1190,7 +1182,7 @@ jobs:
host: linux
target: desktop
arch: linux_gcc_64
modules: qtremoteobjects qt5compat qtshadertools
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
dir: ${{ runner.temp }}
set-env: true
aqtversion: '==3.3.0'
@@ -1253,17 +1245,10 @@ jobs:
- UiAppSTModelAndControllerTest
- UiAllowedDnsModelAndControllerTest
- BackupExportImportTest
- FavoriteLocationsRestoreTest
- ConnectionTest
- OpenSSLLibraryTest
- GatewayServiceImportTest
- UiNewsModelAndControllerTest
- UiServicesModelAndControllerTest
- UiSubscriptionControllerTest
- UiApiAccountInfoModelTest
- ApiBenefitsModelTest
- ApiCountryModelTest
- ApiCountryListModelTest
- ApiDevicesModelTest
runs-on: windows-latest
@@ -1283,8 +1268,7 @@ jobs:
THIRD_PARTY_TROJAN_IMPORT_DATA: ${{ secrets.THIRD_PARTY_TROJAN_IMPORT_DATA }}
THIRD_PARTY_VLESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VLESS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}|
AMNEZIA_TEST_API_KEY: ${{ secrets.AMNEZIA_TEST_API_KEY }}
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}
steps:
- name: Checkout
@@ -1321,7 +1305,7 @@ jobs:
host: windows
target: desktop
arch: win64_msvc2022_64
modules: qtremoteobjects qt5compat qtshadertools
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
dir: ${{ runner.temp }}
set-env: true
@@ -1369,17 +1353,10 @@ jobs:
- UiAppSTModelAndControllerTest
- UiAllowedDnsModelAndControllerTest
- BackupExportImportTest
- FavoriteLocationsRestoreTest
- ConnectionTest
- OpenSSLLibraryTest
- GatewayServiceImportTest
- UiNewsModelAndControllerTest
- UiServicesModelAndControllerTest
- UiSubscriptionControllerTest
- UiApiAccountInfoModelTest
- ApiBenefitsModelTest
- ApiCountryModelTest
- ApiCountryListModelTest
- ApiDevicesModelTest
runs-on: macos-15
@@ -1400,7 +1377,6 @@ jobs:
THIRD_PARTY_VLESS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_VLESS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_IMPORT_DATA }}
THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA: ${{ secrets.THIRD_PARTY_SHADOWSOCKS_SUBSCRIPTION_IMPORT_DATA }}
AMNEZIA_TEST_API_KEY: ${{ secrets.AMNEZIA_TEST_API_KEY }}
steps:
- name: Setup Xcode
@@ -1440,7 +1416,7 @@ jobs:
host: mac
target: desktop
arch: clang_64
modules: qtremoteobjects qt5compat qtshadertools
modules: qtremoteobjects qt5compat qtshadertools qthttpserver qtwebsockets
dir: ${{ runner.temp }}
set-env: true
@@ -1535,16 +1511,98 @@ jobs:
# KEYSTORE_BASE64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_BASE64 }}
# run: echo $KEYSTORE_BASE64 | base64 --decode > android.keystore
- name: 'Configure (Android x86_64)'
env:
QT_ANDROID: ${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/android_x86_64
QT_HOST: ${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64
CMAKE_ANDROID_NDK: ${{ env.ANDROID_HOME }}/ndk/${{ env.NDK_VERSION }}
run: |
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_TOOLCHAIN_FILE=$QT_ANDROID/lib/cmake/Qt6/qt.toolchain.cmake \
-DANDROID_PLATFORM=${{ env.ANDROID_PLATFORM }} \
-DCMAKE_ANDROID_NDK=$CMAKE_ANDROID_NDK \
-DQT_HOST_PATH=$QT_HOST \
-DANDROID_SDK_ROOT=$ANDROID_HOME \
-DAMNEZIA_BUILD_TESTS=ON
- name: 'Build Android tests'
# env:
# QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/android.keystore
# QT_ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
# QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
run: |
cmake --build build --target test_android_openssl_make_apk
cmake --build build --target test_android_restore_backup_make_apk
- name: 'Write Android test runner script'
run: |
cat > ci_run_android_tests.sh << 'EOF'
#!/usr/bin/env bash
set -euo pipefail
ADB=$(which adb)
echo ">>> Waiting for package manager to be ready..."
"$ADB" shell 'while ! pm list packages > /dev/null 2>&1; do sleep 2; done'
echo ">>> Package manager ready."
run_test() {
local target="$1"
local apk="build/client/tests/android-build/${target}.apk"
local pkg_name="org.qtproject.example.${target}"
local activity="${pkg_name}/org.qtproject.qt.android.bindings.QtActivity"
echo ">>> Installing ${target} APK..."
timeout 120 "$ADB" install -r "$apk"
"$ADB" shell "run-as $pkg_name sh -c 'mkdir -p files && : > files/stdout.txt'" 2>/dev/null || true
"$ADB" shell "run-as $pkg_name sh -c 'rm -f files/qtest_last_exit_code'" 2>/dev/null || true
echo ">>> Launching ${target}..."
"$ADB" shell am start -n "$activity"
echo ">>> Waiting for test to finish (up to 120s)..."
local elapsed=0 exit_code=""
while [[ $elapsed -lt 120 ]]; do
sleep 2
elapsed=$((elapsed + 2))
exit_code=$("$ADB" shell \
"run-as $pkg_name cat files/qtest_last_exit_code 2>/dev/null" \
| tr -d '[:space:]' || true)
[[ -n "$exit_code" ]] && break
done
local stdout_content
stdout_content=$("$ADB" shell \
"run-as $pkg_name cat files/stdout.txt 2>/dev/null" || true)
if [[ -n "$stdout_content" ]]; then
echo ">>> Test output:"
echo "$stdout_content"
fi
if [[ -z "$exit_code" ]]; then
echo ">>> TIMEOUT: ${target} did not complete within 120s."
exit 1
elif [[ "$exit_code" != "0" ]]; then
echo ">>> ${target} FAILED (exit code: $exit_code)"
exit 1
else
echo ">>> ${target} PASSED"
fi
}
run_test test_android_openssl
run_test test_android_restore_backup
EOF
chmod +x ci_run_android_tests.sh
- name: 'Run Android tests on emulator'
uses: reactivecircus/android-emulator-runner@v2
env:
QT_ROOT: ${{ runner.temp }}/Qt/${{ env.QT_VERSION }}
NDK_VERSION: ${{ env.NDK_VERSION }}
ANDROID_PLATFORM: ${{ env.ANDROID_PLATFORM }}
with:
api-level: 28
arch: x86_64
emulator-options: -no-snapshot -no-window -no-audio -no-boot-anim -accel on
disable-animations: true
script: bash client/tests/run_android_tests.sh --wait-boot
script: bash ci_run_android_tests.sh
+2 -2
View File
@@ -5,7 +5,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PROJECT AmneziaVPN)
if(NOT AMNEZIAVPN_VERSION)
set(AMNEZIAVPN_VERSION 5.1.0.0 CACHE STRING "Client app version")
set(AMNEZIAVPN_VERSION 5.1.0.3 CACHE STRING "Client app version")
endif()
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
@@ -34,7 +34,7 @@ set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
# bump by 2 on every release, because we're releasing two versions on the play store
set(APP_ANDROID_VERSION_CODE 2173)
set(APP_ANDROID_VERSION_CODE 2184)
if(DEFINED APP_ANDROID_VERSION_CODE_OFFSET)
math(EXPR APP_ANDROID_VERSION_CODE "${APP_ANDROID_VERSION_CODE} + ${APP_ANDROID_VERSION_CODE_OFFSET}")
endif()
+7 -2
View File
@@ -37,7 +37,7 @@ add_definitions(-DFREE_V2_ENDPOINT="$ENV{FREE_V2_ENDPOINT}")
add_definitions(-DPREM_V1_ENDPOINT="$ENV{PREM_V1_ENDPOINT}")
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
set(PACKAGES ${PACKAGES} Widgets)
set(PACKAGES ${PACKAGES} Widgets HttpServer)
endif()
find_package(Qt6 REQUIRED COMPONENTS ${PACKAGES})
@@ -60,7 +60,7 @@ if(TARGET Qt6::CorePrivate)
endif()
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
set(LIBS ${LIBS} Qt6::Widgets)
set(LIBS ${LIBS} Qt6::Widgets Qt6::HttpServer)
endif()
qt_standard_project_setup()
@@ -104,6 +104,7 @@ else()
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_ko_KR.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_fr_FR.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_th_TH.ts
${CMAKE_CURRENT_LIST_DIR}/translations/${CLIENT_TS_PREFIX}_id_ID.ts
)
endif()
@@ -226,6 +227,10 @@ if(ANDROID)
include(cmake/android.cmake)
endif()
if(APPLE)
include(cmake/appleAppIcon.cmake)
endif()
if(IOS)
include(cmake/ios.cmake)
include(cmake/ios-arch-fixup.cmake)
+5
View File
@@ -129,6 +129,11 @@ void AmneziaApplication::init()
win->setPersistentSceneGraph(true);
win->setPersistentGraphics(true);
#endif
#ifdef Q_OS_IOS
auto pageController = m_coreController->pageController();
pageController->setSafeAreaMargins(win->safeAreaMargins());
QObject::connect(win, &QWindow::safeAreaMarginsChanged, pageController, &PageController::setSafeAreaMargins);
#endif
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
win->show();
#else
+12 -4
View File
@@ -49,15 +49,23 @@ set(SOURCES ${SOURCES}
find_package(awg-android REQUIRED)
set(LIBS ${LIBS} amnezia::awg-android)
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${AMNEZIA_ANDROID_LIBWG_PATH} ${AMNEZIA_ANDROID_LIBWG_QUICK_PATH})
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS
$<TARGET_FILE:amnezia::awg-android>
${AMNEZIA_ANDROID_LIBWG_PATH}
${AMNEZIA_ANDROID_LIBWG_QUICK_PATH}
)
find_package(amnezia-libxray REQUIRED)
file(COPY ${AMNEZIA_LIBXRAY_PATH} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/android/xray/libXray)
find_package(openvpn-pt-android REQUIRED)
set(LIBS ${LIBS} amnezia::openvpn-pt-android)
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH})
get_filename_component(OPENVPN_PT_ANDROID_LIB_DIR ${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH} DIRECTORY)
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS
${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH}
${OPENVPN_PT_ANDROID_LIB_DIR}/libovpn3.so
${OPENVPN_PT_ANDROID_LIB_DIR}/libovpnutil.so
${OPENVPN_PT_ANDROID_LIB_DIR}/librsapss.so
)
set(APP_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android)
+25
View File
@@ -0,0 +1,25 @@
function(check_apple_app_icon ICON_PATH)
if(NOT EXISTS "${ICON_PATH}/icon.json")
message(FATAL_ERROR "App icon not found: ${ICON_PATH} (expected an Icon Composer .icon bundle)")
endif()
execute_process(
COMMAND xcrun actool --version --output-format human-readable-text
OUTPUT_VARIABLE ACTOOL_VERSION_OUTPUT
ERROR_QUIET
)
string(REGEX MATCH "short-bundle-version: ([0-9]+)" ACTOOL_VERSION_MATCH "${ACTOOL_VERSION_OUTPUT}")
if(NOT ACTOOL_VERSION_MATCH OR CMAKE_MATCH_1 LESS 26)
message(FATAL_ERROR "Xcode 26 or later is required to build the app icon ${ICON_PATH}")
endif()
execute_process(
COMMAND sw_vers -productVersion
OUTPUT_VARIABLE HOST_MACOS_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if(HOST_MACOS_VERSION VERSION_LESS 26)
message(FATAL_ERROR "macOS 26 or later is required to build the app icon ${ICON_PATH} (found macOS ${HOST_MACOS_VERSION})")
endif()
endfunction()
+2 -2
View File
@@ -19,8 +19,8 @@ endif()
if(NOT CLIENT_IOS_LAUNCHSCREEN_PATH)
set(CLIENT_IOS_LAUNCHSCREEN_PATH "${_CLIENT_SRC_DIR}/ios/app/${CLIENT_IOS_LAUNCHSCREEN_FILE}" CACHE FILEPATH "iOS launch screen storyboard path")
endif()
if(NOT CLIENT_IOS_MEDIA_ASSETS_PATH)
set(CLIENT_IOS_MEDIA_ASSETS_PATH "${_CLIENT_SRC_DIR}/ios/app/Media.xcassets" CACHE FILEPATH "iOS app Media.xcassets path")
if(NOT CLIENT_IOS_APP_ICON_PATH)
set(CLIENT_IOS_APP_ICON_PATH "${_CLIENT_SRC_DIR}/images/AppIcon.icon" CACHE PATH "iOS app icon (Icon Composer .icon)")
endif()
if(NOT CLIENT_IOS_BUNDLE_ID)
+2 -5
View File
@@ -11,11 +11,8 @@ endif()
if(NOT CLIENT_MACOS_NE_ENTITLEMENTS_PATH)
set(CLIENT_MACOS_NE_ENTITLEMENTS_PATH "${_CLIENT_SRC_DIR}/macos/networkextension/${CLIENT_MACOS_NE_ENTITLEMENTS_FILE}" CACHE FILEPATH "macOS network extension entitlements file path")
endif()
if(NOT CLIENT_MACOS_MEDIA_ASSETS_PATH)
set(CLIENT_MACOS_MEDIA_ASSETS_PATH "${_CLIENT_SRC_DIR}/macos/app/Images.xcassets" CACHE FILEPATH "macOS app Images.xcassets path")
endif()
if(NOT CLIENT_MACOS_APP_ICNS_PATH)
set(CLIENT_MACOS_APP_ICNS_PATH "${_CLIENT_SRC_DIR}/images/app.icns" CACHE FILEPATH "macOS app icon (.icns) used by non-App Store builds")
if(NOT CLIENT_MACOS_APP_ICON_PATH)
set(CLIENT_MACOS_APP_ICON_PATH "${_CLIENT_SRC_DIR}/images/AppIcon.icon" CACHE PATH "macOS app icon (Icon Composer .icon)")
endif()
if(NOT CLIENT_MACOS_EXTRA_PLIST_ENTRIES)
+7 -3
View File
@@ -127,14 +127,18 @@ target_sources(${PROJECT} PRIVATE
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
)
check_apple_app_icon(${CLIENT_IOS_APP_ICON_PATH})
set_source_files_properties(
${CLIENT_IOS_MEDIA_ASSETS_PATH}
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
${CLIENT_IOS_APP_ICON_PATH}
PROPERTIES
MACOSX_PACKAGE_LOCATION Resources
XCODE_LAST_KNOWN_FILE_TYPE folder.iconcomposer.icon
)
target_sources(${PROJECT} PRIVATE
${CLIENT_IOS_LAUNCHSCREEN_PATH}
${CLIENT_IOS_MEDIA_ASSETS_PATH}
${CLIENT_IOS_APP_ICON_PATH}
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
)
+29 -4
View File
@@ -34,10 +34,35 @@ set(SOURCES ${SOURCES}
set(ICON_FILE ${CLIENT_MACOS_APP_ICNS_PATH})
get_filename_component(MACOSX_BUNDLE_ICON_FILE "${CLIENT_MACOS_APP_ICNS_PATH}" NAME)
set_source_files_properties(${ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
set(SOURCES ${SOURCES} ${ICON_FILE})
check_apple_app_icon(${CLIENT_MACOS_APP_ICON_PATH})
set(APP_ICON_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/appicon)
set(APP_ICON_FILES ${APP_ICON_OUTPUT_DIR}/Assets.car ${APP_ICON_OUTPUT_DIR}/AppIcon.icns)
file(GLOB_RECURSE APP_ICON_INPUTS CONFIGURE_DEPENDS "${CLIENT_MACOS_APP_ICON_PATH}/*")
add_custom_command(
OUTPUT ${APP_ICON_FILES}
COMMAND ${CMAKE_COMMAND} -E make_directory ${APP_ICON_OUTPUT_DIR}
COMMAND xcrun actool --output-format human-readable-text --warnings --errors
--platform macosx
--minimum-deployment-target ${CMAKE_OSX_DEPLOYMENT_TARGET}
--app-icon AppIcon
--output-partial-info-plist ${APP_ICON_OUTPUT_DIR}/partial.plist
--compile ${APP_ICON_OUTPUT_DIR}
${CLIENT_MACOS_APP_ICON_PATH}
DEPENDS ${APP_ICON_INPUTS}
COMMENT "Compiling macOS app icon"
VERBATIM
)
set(MACOSX_BUNDLE_ICON_FILE AppIcon)
set_source_files_properties(${APP_ICON_FILES} PROPERTIES GENERATED TRUE MACOSX_PACKAGE_LOCATION Resources)
set(SOURCES ${SOURCES} ${APP_ICON_FILES})
add_custom_command(TARGET ${PROJECT} POST_BUILD
COMMAND plutil -replace CFBundleIconName -string AppIcon $<TARGET_BUNDLE_CONTENT_DIR:${PROJECT}>/Info.plist
VERBATIM
)
target_compile_options(${PROJECT} PRIVATE
-DGROUP_ID=\"${BUILD_IOS_GROUP_IDENTIFIER}\"
+7 -3
View File
@@ -122,13 +122,17 @@ target_sources(${PROJECT} PRIVATE
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
)
check_apple_app_icon(${CLIENT_MACOS_APP_ICON_PATH})
set_source_files_properties(
${CLIENT_MACOS_MEDIA_ASSETS_PATH}
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
${CLIENT_MACOS_APP_ICON_PATH}
PROPERTIES
MACOSX_PACKAGE_LOCATION Resources
XCODE_LAST_KNOWN_FILE_TYPE folder.iconcomposer.icon
)
target_sources(${PROJECT} PRIVATE
${CLIENT_MACOS_MEDIA_ASSETS_PATH}
${CLIENT_MACOS_APP_ICON_PATH}
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
)
-58
View File
@@ -40,64 +40,6 @@ function(osx_bundle_files TARGET)
endforeach()
endfunction(osx_bundle_files)
## A helper to bundle an asset catalog.
function(osx_bundle_assetcatalog TARGET)
cmake_parse_arguments(XCASSETS
""
"CATALOG;PLATFORM"
"DEVICES"
${ARGN})
if(XCASSETS_PLATFORM)
set(XCASSETS_TARGET_ARGS --platform ${XCASSETS_PLATFORM})
elseif(IOS)
set(XCASSETS_TARGET_ARGS --platform iphoneos)
else()
set(XCASSETS_TARGET_ARGS --platform macosx)
endif()
foreach(DEVNAME ${XCASSETS_DEVICES})
list(APPEND XCASSETS_TARGET_ARGS --target-device ${DEVNAME})
endforeach()
list(APPEND XCASSETS_TARGET_ARGS --minimum-deployment-target ${CMAKE_OSX_DEPLOYMENT_TARGET})
## Compile the asset catalog
set(XCASSETS_GEN_PLIST ${CMAKE_CURRENT_BINARY_DIR}/xcassets_generated_info.plist)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/xcassets/Assets.car ${XCASSETS_GEN_PLIST}
MAIN_DEPENDENCY ${XCASSETS_CATALOG}/Contents.json
COMMENT "Building asset catalog"
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/xcassets
COMMAND actool --output-format human-readable-text --notices --warnings
${XCASSETS_TARGET_ARGS}
--app-icon AppIcon
--output-partial-info-plist ${XCASSETS_GEN_PLIST}
--development-region en --enable-on-demand-resources NO
--compile ${CMAKE_CURRENT_BINARY_DIR}/xcassets ${XCASSETS_CATALOG}
)
## Patch the asset catalog into the target bundle.
if(NOT IOS)
set(XCASSETS_RESOURCE_DIR "Resources")
endif()
add_custom_command(TARGET ${TARGET} POST_BUILD
COMMENT "Bundling asset catalog"
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_BINARY_DIR}/xcassets $<TARGET_BUNDLE_CONTENT_DIR:${TARGET}>/${XCASSETS_RESOURCE_DIR}
COMMAND ${CLIENT_ROOT_DIR}/scripts/macos/merge_plist.py ${XCASSETS_GEN_PLIST} -o $<TARGET_BUNDLE_CONTENT_DIR:${TARGET}>/Info.plist
)
target_sources(${TARGET} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/xcassets/Assets.car)
set_source_files_properties(
${CMAKE_CURRENT_BINARY_DIR}/xcassets/Assets.car
${XCASSETS_GEN_PLIST}
PROPERTIES
GENERATED TRUE
HEADER_FILE_ONLY TRUE
)
target_sources(${TARGET} PRIVATE ${XCASSETS_GEN_PLIST})
set_source_files_properties(${XCASSETS_GEN_PLIST} PROPERTIES GENERATED TRUE)
endfunction()
## A helper to code-sign an executable.
function(osx_codesign_target TARGET)
## Xcode should perform automatic code-signing for us.
+7
View File
@@ -44,6 +44,7 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/controllers/selfhosted/exportController.h
${CLIENT_ROOT_DIR}/core/controllers/connectionController.h
${CLIENT_ROOT_DIR}/core/controllers/settingsController.h
${CLIENT_ROOT_DIR}/core/controllers/localProxyController.h
${CLIENT_ROOT_DIR}/core/controllers/logsController.h
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.h
${CLIENT_ROOT_DIR}/core/controllers/api/storePurchaseController.h
@@ -69,6 +70,9 @@ set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.h
${CLIENT_ROOT_DIR}/core/utils/api/countryCatalog.h
${CLIENT_ROOT_DIR}/core/utils/api/gatewayPayloadBuilder.h
${CLIENT_ROOT_DIR}/core/utils/localProxy/localProxyConstants.h
${CLIENT_ROOT_DIR}/core/utils/localProxy/portUtils.h
${CLIENT_ROOT_DIR}/core/utils/localProxy/xrayConfigUtils.h
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.h
${CLIENT_ROOT_DIR}/core/utils/swiftBridge.h
${CLIENT_ROOT_DIR}/core/utils/utilities.h
@@ -130,6 +134,7 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/controllers/selfhosted/exportController.cpp
${CLIENT_ROOT_DIR}/core/controllers/connectionController.cpp
${CLIENT_ROOT_DIR}/core/controllers/settingsController.cpp
${CLIENT_ROOT_DIR}/core/controllers/localProxyController.cpp
${CLIENT_ROOT_DIR}/core/controllers/logsController.cpp
${CLIENT_ROOT_DIR}/core/controllers/api/servicesCatalogController.cpp
${CLIENT_ROOT_DIR}/core/controllers/api/storePurchaseController.cpp
@@ -157,6 +162,8 @@ set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/api/countryCatalog.cpp
${CLIENT_ROOT_DIR}/core/utils/api/gatewayPayloadBuilder.cpp
${CLIENT_ROOT_DIR}/core/utils/localProxy/portUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/localProxy/xrayConfigUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/serverConfigUtils.cpp
${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.cpp
${CLIENT_ROOT_DIR}/core/utils/utilities.cpp
@@ -362,6 +362,33 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server
return applyUpdatedServiceConfig(serverId, serviceProtocol, protocolData, responseBody);
}
ErrorCode SubscriptionController::fetchServiceConfig(const QString &serverId, const QString &serviceProtocol,
QJsonObject &serverConfigJson)
{
auto apiV2 = m_serversRepository->apiV2Config(serverId);
if (!apiV2.has_value()) {
return ErrorCode::InternalError;
}
ProtocolData protocolData = generateProtocolData(serviceProtocol);
QJsonObject apiPayload = GatewayPayloadBuilder(m_appSettingsRepository)
.addField(apiDefs::key::userCountryCode, apiV2->apiConfig.userCountryCode)
.addField(apiDefs::key::serviceType, apiV2->serviceType())
.addField(apiDefs::key::serviceProtocol, serviceProtocol)
.addField(apiDefs::key::publicKey, publicKeyForProtocol(serviceProtocol, protocolData))
.addField(apiDefs::key::authData, apiV2->authData.toJson())
.build();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, apiV2->apiConfig.isTestPurchase);
if (errorCode != ErrorCode::NoError) {
return errorCode;
}
return extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
}
void SubscriptionController::markSubscriptionExpiredByServer(const QString &serverId, const ApiV2ServerConfig &apiV2)
{
ApiV2ServerConfig expiredApiV2 = apiV2;
@@ -45,6 +45,8 @@ public:
ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
const QString &serviceProtocol, const QString &email);
ErrorCode fetchServiceConfig(const QString &serverId, const QString &serviceProtocol, QJsonObject &serverConfigJson);
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
CaptchaInfo *captchaInfoOut = nullptr, ProtocolData *usedProtocolDataOut = nullptr);
@@ -179,6 +179,7 @@ void CoreController::initCoreControllers()
m_importCoreController = new ImportController(m_serversRepository, m_appSettingsRepository, this);
m_connectionController = new ConnectionController(m_serversRepository, m_appSettingsRepository, m_vpnConnection.get(), this);
m_settingsController = new SettingsController(m_serversRepository, m_appSettingsRepository, this);
m_localProxyController = new LocalProxyController(m_serversRepository, m_appSettingsRepository, m_subscriptionController, this);
}
void CoreController::initControllers()
@@ -212,6 +213,9 @@ void CoreController::initControllers()
m_settingsUiController = new SettingsUiController(m_settingsController, m_serversController, this);
setQmlContextProperty("SettingsController", m_settingsUiController);
m_localProxyUiController = new LocalProxyUiController(m_localProxyController, this);
setQmlContextProperty("LocalProxyController", m_localProxyUiController);
m_logsUiController = new LogsUiController(m_settingsController, this);
setQmlContextProperty("LogsController", m_logsUiController);
+8 -1
View File
@@ -24,6 +24,7 @@
#include "ui/controllers/selfhosted/installUiController.h"
#include "ui/controllers/qml/pageController.h"
#include "ui/controllers/settingsUiController.h"
#include "ui/controllers/localProxyUiController.h"
#include "ui/controllers/logsUiController.h"
#include "ui/controllers/serversUiController.h"
#include "ui/controllers/ipSplitTunnelingUiController.h"
@@ -44,6 +45,7 @@
#include "core/controllers/api/newsController.h"
#include "core/controllers/selfhosted/installController.h"
#include "core/controllers/settingsController.h"
#include "core/controllers/localProxyController.h"
#include "core/controllers/connectionController.h"
#include "core/controllers/updateController.h"
@@ -141,6 +143,9 @@ protected:
ServicesCatalogUiController* servicesCatalogUiControllerProtected() const { return m_servicesCatalogUiController; }
ApiNewsUiController* apiNewsUiControllerProtected() const { return m_apiNewsUiController; }
SubscriptionController* subscriptionControllerProtected() const { return m_subscriptionController; }
ApiCountryModel* apiCountryModelProtected() const { return m_apiCountryModel; }
ApiCountryListModel* apiCountryListModelProtected() const { return m_apiCountryListModel; }
ApiCountryListModel* apiConfigsCountryListModelProtected() const { return m_apiConfigsCountryListModel; }
private:
void initRepositories();
@@ -168,13 +173,14 @@ private:
QMetaObject::Connection m_reloadConfigErrorOccurredConnection;
ConnectionUiController* m_connectionUiController;
FocusController* m_focusController;
FocusController* m_focusController = nullptr;
PageController* m_pageController;
InstallUiController* m_installUiController;
ImportUiController* m_importController;
ImportController* m_importCoreController;
ExportUiController* m_exportUiController;
SettingsUiController* m_settingsUiController;
LocalProxyUiController* m_localProxyUiController;
LogsUiController* m_logsUiController;
ServersUiController* m_serversUiController;
IpSplitTunnelingUiController* m_ipSplitTunnelingUiController;
@@ -204,6 +210,7 @@ private:
ExportController* m_exportController;
ConnectionController* m_connectionController;
SettingsController* m_settingsController;
LocalProxyController* m_localProxyController;
ContainersModel* m_containersModel;
ContainersModel* m_defaultServerContainersModel;
+73 -4
View File
@@ -1,5 +1,6 @@
#include "coreSignalHandlers.h"
#include <QCoreApplication>
#include <QTimer>
#include <QtConcurrent>
@@ -11,6 +12,7 @@
#include "core/utils/serverConfigUtils.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "vpnConnection.h"
#include "ui/controllers/qml/focusController.h"
#include "ui/controllers/qml/pageController.h"
#include "ui/controllers/connectionUiController.h"
#include "ui/controllers/settingsUiController.h"
@@ -32,6 +34,7 @@
#include "core/controllers/selfhosted/installController.h"
#include "core/controllers/selfhosted/exportController.h"
#include "core/controllers/connectionController.h"
#include "core/controllers/localProxyController.h"
#include "ui/models/clientManagementModel.h"
#include "ui/controllers/api/apiNewsUiController.h"
#include "ui/models/containersModel.h"
@@ -81,6 +84,7 @@ void CoreSignalHandlers::initAllHandlers()
initAppSplitTunnelingModelUpdateHandler();
initPrepareConfigHandler();
initUnsupportedConnectDrawerHandler();
initBackNavigationHandler();
initStrictKillSwitchHandler();
initAndroidSettingsHandler();
initAndroidConnectionHandler();
@@ -88,6 +92,7 @@ void CoreSignalHandlers::initAllHandlers()
initIosSettingsHandler();
initNotificationHandler();
initUpdateFoundHandler();
initLocalProxyHandler();
}
void CoreSignalHandlers::initErrorMessagesHandler()
@@ -182,12 +187,21 @@ void CoreSignalHandlers::initCountryListSortModeHandler()
const QVector<ApiCountryListModel *> models { m_coreController->m_apiCountryListModel,
m_coreController->m_apiConfigsCountryListModel };
const auto reloadSortModes = [repository, models]() {
for (ApiCountryListModel *model : models) {
model->setSortMode(repository->countryListSortMode(model->listId()));
}
};
reloadSortModes();
for (ApiCountryListModel *model : models) {
model->setSortMode(repository->countryListSortMode(model->listId()));
connect(model, &ApiCountryListModel::sortModeChanged, this, [repository, model]() {
repository->setCountryListSortMode(model->listId(), model->sortMode());
});
}
connect(m_coreController->m_settingsUiController, &SettingsUiController::restoreBackupFinished, this, reloadSortModes);
connect(repository, &SecureAppSettingsRepository::settingsCleared, this, reloadSortModes);
}
void CoreSignalHandlers::initCountryListStateHandler()
@@ -197,10 +211,45 @@ void CoreSignalHandlers::initCountryListStateHandler()
const QVector<ApiCountryListModel *> models { m_coreController->m_apiCountryListModel,
m_coreController->m_apiConfigsCountryListModel };
ApiCountryModel *countryModel = m_coreController->m_apiCountryModel;
SecureServersRepository *serversRepository = m_coreController->m_serversRepository;
const auto reloadFavorites = [repository, models, countryModel]() {
const QStringList codes = repository->favoriteLocations(countryModel->serverId());
for (ApiCountryListModel *model : models) {
model->setFavorites(codes);
}
};
const auto migrateLegacyFavorites = [repository, serversRepository]() {
QStringList apiServerIds;
for (const QString &serverId : serversRepository->orderedServerIds()) {
if (serversRepository->apiV2Config(serverId).has_value()) {
apiServerIds.append(serverId);
}
}
repository->migrateLegacyFavoriteLocations(apiServerIds);
};
migrateLegacyFavorites();
reloadFavorites();
connect(countryModel, &ApiCountryModel::serverIdChanged, this, reloadFavorites);
connect(m_coreController->m_settingsUiController, &SettingsUiController::restoreBackupFinished, this,
[migrateLegacyFavorites, reloadFavorites]() {
migrateLegacyFavorites();
reloadFavorites();
});
connect(repository, &SecureAppSettingsRepository::settingsCleared, this, reloadFavorites);
connect(serversRepository, &SecureServersRepository::serverRemoved, this,
[repository, countryModel](const QString &serverId) {
repository->removeFavoriteLocations(serverId);
if (countryModel->serverId() == serverId) {
countryModel->setServerId(QString());
}
});
for (ApiCountryListModel *model : models) {
model->setFavorites(repository->favoriteLocations());
connect(model, &ApiCountryListModel::favoritesChanged, this, [repository, models, model](const QStringList &codes) {
repository->setFavoriteLocations(codes);
connect(model, &ApiCountryListModel::favoritesChanged, this, [repository, models, model, countryModel](const QStringList &codes) {
repository->setFavoriteLocations(countryModel->serverId(), codes);
for (ApiCountryListModel *other : models) {
if (other != model) {
other->setFavorites(codes);
@@ -223,6 +272,7 @@ void CoreSignalHandlers::initApiCountryModelUpdateHandler()
return;
}
m_coreController->m_apiCountryModel->setServerId(processedServerId);
m_coreController->m_apiCountryModel->updateModel(apiV2->apiConfig.availableCountries,
apiV2->apiConfig.serverCountryCode,
apiV2->apiConfig.userCountryCode);
@@ -409,6 +459,16 @@ void CoreSignalHandlers::initUnsupportedConnectDrawerHandler()
m_coreController->m_pageController, &PageController::unsupportedConnectDrawerRequested);
}
void CoreSignalHandlers::initBackNavigationHandler()
{
if (!m_coreController->m_focusController) {
return;
}
connect(m_coreController->m_focusController, &FocusController::backRequested,
m_coreController->m_pageController, &PageController::escapePressed);
}
void CoreSignalHandlers::initStrictKillSwitchHandler()
{
connect(m_coreController->m_settingsUiController, &SettingsUiController::strictKillSwitchEnabledChanged, m_coreController->m_connectionController,
@@ -494,3 +554,12 @@ void CoreSignalHandlers::initUpdateFoundHandler()
});
}
void CoreSignalHandlers::initLocalProxyHandler()
{
connect(m_coreController->m_connectionController, &ConnectionController::openConnectionRequested, m_coreController->m_localProxyController,
&LocalProxyController::onVpnConnectionRequested);
connect(m_coreController->m_localProxyController, &LocalProxyController::stoppedByVpn, m_coreController->m_pageController,
&PageController::showNotificationMessage);
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, m_coreController->m_localProxyController,
&LocalProxyController::disable);
}
@@ -36,6 +36,7 @@ private:
void initAppSplitTunnelingModelUpdateHandler();
void initPrepareConfigHandler();
void initUnsupportedConnectDrawerHandler();
void initBackNavigationHandler();
void initStrictKillSwitchHandler();
void initAndroidSettingsHandler();
void initAndroidConnectionHandler();
@@ -43,6 +44,7 @@ private:
void initIosSettingsHandler();
void initNotificationHandler();
void initUpdateFoundHandler();
void initLocalProxyHandler();
CoreController* m_coreController;
};
+52 -31
View File
@@ -5,11 +5,13 @@
#include <QFutureWatcher>
#include <QJsonArray>
#include <QJsonDocument>
#include <QMetaObject>
#include <QJsonObject>
#include <QThread>
#include <QUrl>
#include <QtConcurrent/QtConcurrentRun>
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/utils/constants/apiConstants.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/api/apiUtils.h"
@@ -32,6 +34,9 @@ namespace
constexpr QLatin1String s3FallbackEndpoints("s3_fallback_endpoints");
constexpr QLatin1String isDevEnvironment("is_dev_environment");
constexpr QLatin1String requestTimeoutMsecs("request_timeout_msecs");
constexpr QLatin1String proxyRequestTimeoutMsecs("proxy_request_timeout_msecs");
constexpr QLatin1String proxyStorageTimeoutMsecs("proxy_storage_timeout_msecs");
constexpr QLatin1String proxyHealthTimeoutMsecs("proxy_health_timeout_msecs");
}
namespace agwOptionsKey
@@ -40,6 +45,11 @@ namespace
constexpr QLatin1String userCountryCode("user_country_code");
}
namespace agwStateKey
{
constexpr QLatin1String workingProxy("working_proxy");
}
constexpr QLatin1String agwStateCacheKey("agw_state");
constexpr QLatin1String agwTracePrefix("agw trace:");
constexpr QLatin1String agwTraceFailoverMarker("suspicious");
@@ -71,6 +81,7 @@ GatewayController::GatewayController(const QString &gatewayEndpoint, const bool
const bool isStrictKillSwitchEnabled, SecureAppSettingsRepository *appSettingsRepository,
QObject *parent)
: QObject(parent),
m_gatewayEndpoint(gatewayEndpoint),
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled),
m_appSettingsRepository(appSettingsRepository)
{
@@ -93,13 +104,11 @@ GatewayController::GatewayController(const QString &gatewayEndpoint, const bool
config[agwConfigKey::s3FallbackEndpoints] = QJsonArray::fromStringList(fallbackEndpoints);
config[agwConfigKey::isDevEnvironment] = isDevEnvironment;
config[agwConfigKey::requestTimeoutMsecs] = requestTimeoutMsecs;
config[agwConfigKey::proxyRequestTimeoutMsecs] = apiDefs::proxyRequestTimeoutMsecs;
config[agwConfigKey::proxyStorageTimeoutMsecs] = apiDefs::proxyStorageTimeoutMsecs;
config[agwConfigKey::proxyHealthTimeoutMsecs] = apiDefs::proxyHealthTimeoutMsecs;
agw_callbacks callbacks {};
callbacks.struct_size = sizeof(agw_callbacks);
callbacks.on_before_request = &GatewayController::onBeforeRequest;
callbacks.on_before_request_user_data = this;
m_client = agw_client_create(QJsonDocument(config).toJson(QJsonDocument::Compact).constData(), &callbacks);
m_client = agw_client_create(QJsonDocument(config).toJson(QJsonDocument::Compact).constData(), nullptr);
if (m_client == 0) {
qCritical() << "GatewayController: failed to create gateway client (missing key or endpoint?)";
return;
@@ -137,6 +146,7 @@ amnezia::ErrorCode GatewayController::post(const QString &endpoint, const QJsonO
QFuture<QPair<amnezia::ErrorCode, QByteArray>> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload)
{
prepareRequest();
return QtConcurrent::run(
[this, endpoint, apiPayload]() -> QPair<amnezia::ErrorCode, QByteArray> { return executePost(endpoint, apiPayload); });
}
@@ -190,44 +200,55 @@ amnezia::ErrorCode GatewayController::mapResultCode(const int code, const QByteA
}
}
void GatewayController::onBeforeRequest(const char *host, void *userData)
{
auto *controller = static_cast<GatewayController *>(userData);
const QString hostString = QString::fromUtf8(host);
if (QThread::currentThread() == controller->thread()) {
controller->handleBeforeRequest(hostString);
} else {
QMetaObject::invokeMethod(
controller, [controller, hostString]() { controller->handleBeforeRequest(hostString); },
Qt::BlockingQueuedConnection);
}
}
void GatewayController::handleBeforeRequest(const QString &host)
void GatewayController::prepareRequest()
{
#ifdef Q_OS_IOS
Q_UNUSED(host);
IosController::Instance()->requestInetAccess();
QThread::msleep(10);
#endif
#ifdef AMNEZIA_DESKTOP
if (m_isStrictKillSwitchEnabled) {
if (!m_isStrictKillSwitchEnabled) {
return;
}
QStringList ips;
for (const QString &host : requestHosts()) {
const QString ip = NetworkUtilities::getIPAddress(host);
if (!ip.isEmpty()) {
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(QStringList { ip });
if (!reply.waitForFinished(1000) || !reply.returnValue()) {
qWarning() << "GatewayController::handleBeforeRequest(): failed to add killswitch exception for" << host;
}
});
ips.append(ip);
}
}
#else
Q_UNUSED(host);
if (ips.isEmpty()) {
return;
}
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
QRemoteObjectPendingReply<bool> reply = iface->addKillSwitchAllowedRange(ips);
if (!reply.waitForFinished(1000) || !reply.returnValue()) {
qWarning() << "GatewayController::prepareRequest(): Failed to execute remote addKillSwitchAllowedRange call";
}
});
#endif
}
QStringList GatewayController::requestHosts() const
{
QStringList hosts;
if (m_client != 0) {
char *state = agw_export_state(m_client);
if (state != nullptr) {
const QString workingProxy = QJsonDocument::fromJson(QByteArray(state)).object().value(agwStateKey::workingProxy).toString();
agw_string_free(state);
if (!workingProxy.isEmpty()) {
hosts.append(QUrl(workingProxy).host());
}
}
}
hosts.append(QUrl(m_gatewayEndpoint).host());
hosts.removeAll(QString());
hosts.removeDuplicates();
return hosts;
}
void GatewayController::persistState()
{
if (m_client == 0 || m_appSettingsRepository == nullptr) {
+4 -2
View File
@@ -7,6 +7,7 @@
#include <QObject>
#include <QPair>
#include <QString>
#include <QStringList>
#include "core/utils/errorCodes.h"
@@ -28,13 +29,14 @@ public:
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
private:
static void onBeforeRequest(const char *host, void *userData);
void handleBeforeRequest(const QString &host);
void prepareRequest();
QStringList requestHosts() const;
QPair<amnezia::ErrorCode, QByteArray> executePost(const QString &endpoint, const QJsonObject &apiPayload);
static amnezia::ErrorCode mapResultCode(const int code, const QByteArray &responseBody);
void persistState();
QString m_gatewayEndpoint;
bool m_isStrictKillSwitchEnabled = false;
SecureAppSettingsRepository *m_appSettingsRepository = nullptr;
@@ -0,0 +1,528 @@
#include "localProxyController.h"
#include "core/controllers/api/subscriptionController.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include "core/repositories/secureServersRepository.h"
#include "core/utils/api/apiUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/localProxy/localProxyConstants.h"
#include "core/utils/localProxy/portUtils.h"
#include "core/utils/localProxy/xrayConfigUtils.h"
#include <QDebug>
#include <QJsonDocument>
#include <QJsonParseError>
#ifdef AMNEZIA_DESKTOP
#include "core/utils/ipcClient.h"
#include <QHostAddress>
#include <QHttpServer>
#include <QHttpServerResponse>
#include <QTcpServer>
#endif
using namespace amnezia;
LocalProxyController::LocalProxyController(SecureServersRepository *serversRepository,
SecureAppSettingsRepository *appSettingsRepository,
SubscriptionController *subscriptionController, QObject *parent)
: QObject(parent),
m_serversRepository(serversRepository),
m_appSettingsRepository(appSettingsRepository),
m_subscriptionController(subscriptionController)
{
connect(m_serversRepository, &SecureServersRepository::serverEdited, this, &LocalProxyController::onServerEdited);
connect(m_serversRepository, &SecureServersRepository::serverRemoved, this,
[this](const QString &serverId, int) { onServerRemoved(serverId); });
}
LocalProxyController::~LocalProxyController()
{
stop();
}
bool LocalProxyController::isEnabled() const
{
return m_isEnabled;
}
int LocalProxyController::port() const
{
return m_appSettingsRepository->localProxyPort();
}
QString LocalProxyController::ownerId() const
{
return m_appSettingsRepository->localProxyOwnerId();
}
bool LocalProxyController::isPortUserDefined() const
{
return m_appSettingsRepository->isLocalProxyPortUserDefined();
}
int LocalProxyController::activePort() const
{
return m_activePort;
}
bool LocalProxyController::setPort(int port)
{
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
return false;
}
const bool isPortChanged = m_appSettingsRepository->localProxyPort() != static_cast<quint16>(port);
if (isPortChanged) {
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(port));
m_lastProxyPort = 0;
}
m_appSettingsRepository->setLocalProxyPortUserDefined(true);
emit stateChanged();
if (isPortChanged && m_isEnabled) {
start();
}
return true;
}
bool LocalProxyController::isPortBusy(int port) const
{
return !portUtils::isPortAvailable(port);
}
bool LocalProxyController::enable(const QString &ownerId, int port)
{
if (!isSupported() || ownerId.isEmpty() || port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
return false;
}
if (m_isEnabled && m_appSettingsRepository->localProxyOwnerId() != ownerId) {
return false;
}
if (!portUtils::isPortAvailable(port) && m_appSettingsRepository->isLocalProxyPortUserDefined()) {
return false;
}
if (m_appSettingsRepository->localProxyPort() != static_cast<quint16>(port)) {
m_appSettingsRepository->setLocalProxyPort(static_cast<quint16>(port));
}
m_appSettingsRepository->setLocalProxyOwnerId(ownerId);
m_isEnabled = true;
emit stateChanged();
start();
return true;
}
void LocalProxyController::disable()
{
if (!m_isEnabled) {
return;
}
qDebug() << "Local proxy is disabled";
m_isEnabled = false;
stop();
emit stateChanged();
}
void LocalProxyController::start()
{
if (!startApi(localProxy::apiPort)) {
disableWithError(tr("Local proxy failed to start. Check if the port is available"));
return;
}
if (!syncXray()) {
disableWithError(tr("Couldn’t start the proxy due to an internal error. Try restarting the app"));
return;
}
qDebug() << "Local proxy is running on 127.0.0.1:" << m_apiPort;
}
void LocalProxyController::onServerEdited(const QString &serverId)
{
if (m_apiPort == 0 || m_appSettingsRepository->localProxyOwnerId() != serverId) {
return;
}
qDebug() << "Owner server edited, restarting Xray";
if (restartXray()) {
m_appliedProxyPort = m_appSettingsRepository->localProxyPort();
} else {
disableWithError(tr("Couldn’t start the proxy due to an internal error. Try restarting the app"));
}
}
void LocalProxyController::onServerRemoved(const QString &serverId)
{
if (m_appSettingsRepository->localProxyOwnerId() != serverId) {
return;
}
m_appSettingsRepository->setLocalProxyOwnerId(QString());
if (m_isEnabled) {
m_isEnabled = false;
stop();
}
emit stateChanged();
}
void LocalProxyController::onVpnConnectionRequested()
{
if (!m_isEnabled) {
return;
}
disable();
emit stoppedByVpn(tr("Local proxy stopped because VPN was turned on"));
}
void LocalProxyController::stop()
{
stopXray();
stopApi();
m_appliedProxyPort = 0;
}
bool LocalProxyController::syncXray()
{
const quint16 proxyPort = m_appSettingsRepository->localProxyPort();
bool synced;
if (!isXrayEngineRunning()) {
synced = startXray();
} else if (m_appliedProxyPort != proxyPort) {
qDebug() << "Proxy port changed from" << m_appliedProxyPort << "to" << proxyPort;
synced = restartXray();
} else {
return true;
}
if (synced) {
m_appliedProxyPort = proxyPort;
}
return synced;
}
bool LocalProxyController::startXray()
{
if (isXrayEngineRunning()) {
return true;
}
const auto config = buildXrayConfig();
if (!config) {
return false;
}
if (!startXrayEngine(config->serializedConfig)) {
return false;
}
qDebug() << "Xray started on port" << config->proxyPort;
m_lastProxyPort = config->proxyPort;
setActivePort(config->proxyPort);
return true;
}
bool LocalProxyController::stopXray()
{
if (!stopXrayEngine()) {
return false;
}
setActivePort(0);
return true;
}
bool LocalProxyController::restartXray()
{
qDebug() << "Restarting Xray with updated config";
if (!stopXray()) {
return false;
}
return startXray();
}
void LocalProxyController::disableWithError(const QString &message)
{
qWarning() << message;
m_isEnabled = false;
stop();
emit stateChanged();
emit startFailed(message);
}
void LocalProxyController::setActivePort(int port)
{
if (m_activePort == port) {
return;
}
m_activePort = port;
emit stateChanged();
}
int LocalProxyController::runningProxyPort() const
{
return isXrayEngineRunning() ? m_activePort : 0;
}
std::optional<LocalProxyController::XrayConfig> LocalProxyController::buildXrayConfig() const
{
const QString ownerId = m_appSettingsRepository->localProxyOwnerId();
if (ownerId.isEmpty()) {
qWarning() << "Local proxy owner server id is not configured";
return std::nullopt;
}
const auto ownerServer = m_serversRepository->serverJsonById(ownerId);
if (!ownerServer) {
qWarning() << "Owner server with id" << ownerId << "not found";
return std::nullopt;
}
if (!apiUtils::isPremiumServer(*ownerServer)) {
qWarning() << "Server" << ownerServer->value(configKey::name).toString() << "is not premium, local proxy is unavailable";
return std::nullopt;
}
auto serializedConfig = xrayConfigUtils::extractLastConfig(*ownerServer);
if (!serializedConfig) {
serializedConfig = fetchXrayConfigFromGateway(ownerId);
if (!serializedConfig) {
return std::nullopt;
}
}
QJsonParseError parseError;
const QJsonDocument doc = QJsonDocument::fromJson(serializedConfig->toUtf8(), &parseError);
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
qWarning() << "Failed to parse Xray config JSON:" << parseError.errorString();
return std::nullopt;
}
const auto proxyPort = selectProxyPort();
if (!proxyPort) {
return std::nullopt;
}
QJsonObject xrayConfig = doc.object();
if (!xrayConfigUtils::setInboundPort(xrayConfig, *proxyPort)) {
qWarning() << "Failed to override local proxy inbound port, using original config";
return XrayConfig { *serializedConfig, 0 };
}
return XrayConfig { QString::fromUtf8(QJsonDocument(xrayConfig).toJson(QJsonDocument::Compact)), *proxyPort };
}
std::optional<int> LocalProxyController::selectProxyPort() const
{
int basePort = m_appSettingsRepository->localProxyPort();
if (basePort < localProxy::proxyPortMin || basePort > localProxy::proxyPortMax) {
basePort = localProxy::defaultProxyPort;
}
if (m_lastProxyPort == 0) {
if (portUtils::waitForPort(basePort, localProxy::portReleaseWaitMs)) {
return basePort;
}
if (m_appSettingsRepository->isLocalProxyPortUserDefined()) {
qWarning() << "Local proxy port" << basePort << "is already in use";
return std::nullopt;
}
}
// Every restart moves to a new port so that clients notice the change and reconnect.
const int from = (m_lastProxyPort > 0 ? m_lastProxyPort : basePort) + 1;
auto freePort = portUtils::findFirstAvailablePort(from, localProxy::proxyPortMax);
if (!freePort && from > basePort + 1) {
freePort = portUtils::findFirstAvailablePort(basePort, from - 2);
}
if (!freePort) {
qWarning() << "No available local proxy port in range" << basePort << "-" << localProxy::proxyPortMax;
return std::nullopt;
}
qDebug() << "Local proxy port selected:" << *freePort << "(previous" << m_lastProxyPort << ")";
return freePort;
}
std::optional<QString> LocalProxyController::fetchXrayConfigFromGateway(const QString &serverId) const
{
QJsonObject serverConfig;
const ErrorCode errorCode = m_subscriptionController->fetchServiceConfig(serverId, configKey::vless, serverConfig);
if (errorCode != ErrorCode::NoError) {
qWarning() << "Gateway request failed with error code" << static_cast<int>(errorCode);
return std::nullopt;
}
const auto serializedConfig = xrayConfigUtils::extractLastConfig(serverConfig);
if (!serializedConfig) {
qWarning() << "Gateway response lacks Xray last_config payload";
return std::nullopt;
}
qDebug() << "Fetched Xray config from gateway";
return serializedConfig;
}
#ifdef AMNEZIA_DESKTOP
bool LocalProxyController::isSupported() const
{
return true;
}
bool LocalProxyController::startApi(quint16 port)
{
if (m_httpServer && m_apiPort == port) {
return true;
}
stopApi();
const auto proxyPortValue = [this]() {
const int port = runningProxyPort();
return port > 0 ? QJsonValue(port) : QJsonValue::Null;
};
auto httpServer = new QHttpServer(this);
httpServer->route("/api/v1/up", QHttpServerRequest::Method::Post, [this, proxyPortValue] {
const bool started = startXray();
return QHttpServerResponse(QJsonObject { { "status", started ? "ok" : "error" }, { "proxyPort", proxyPortValue() } });
});
httpServer->route("/api/v1/down", QHttpServerRequest::Method::Post, [this] {
const bool stopped = stopXray();
return QHttpServerResponse(QJsonObject { { "status", stopped ? "ok" : "error" } });
});
httpServer->route("/api/v1/ping", QHttpServerRequest::Method::Get, [proxyPortValue] {
return QHttpServerResponse(QJsonObject { { "status", "ok" }, { "proxyPort", proxyPortValue() } });
});
auto tcpServer = new QTcpServer(httpServer);
if (!tcpServer->listen(QHostAddress::LocalHost, port) || !httpServer->bind(tcpServer)) {
qWarning() << "Failed to start HTTP API server on port" << port;
delete httpServer;
return false;
}
m_httpServer = httpServer;
m_apiPort = port;
qDebug() << "HTTP API server is running on localhost:" << port;
return true;
}
void LocalProxyController::stopApi()
{
delete m_httpServer;
m_httpServer = nullptr;
m_apiPort = 0;
}
bool LocalProxyController::startXrayEngine(const QString &config)
{
if (config.trimmed().isEmpty()) {
qWarning() << "Xray config content is empty";
return false;
}
const qint64 token = IpcClient::withInterface(
[&](QSharedPointer<IpcInterfaceReplica> iface) -> qint64 {
auto xrayStart = iface->xrayStartOwned(config);
if (!xrayStart.waitForFinished()) {
return 0;
}
return xrayStart.returnValue();
},
[]() -> qint64 { return 0; });
if (token == 0) {
qWarning() << "Failed to start Xray via IPC";
return false;
}
m_xrayToken = token;
return true;
}
bool LocalProxyController::stopXrayEngine()
{
if (m_xrayToken == 0) {
return true;
}
const bool stopped = IpcClient::withInterface(
[this](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStop = iface->xrayStopOwned(m_xrayToken);
return xrayStop.waitForFinished() && xrayStop.returnValue();
},
[]() { return false; });
if (!stopped) {
qWarning() << "Failed to stop Xray via IPC";
return false;
}
m_xrayToken = 0;
qDebug() << "Xray stopped";
return true;
}
bool LocalProxyController::isXrayEngineRunning() const
{
if (m_xrayToken == 0) {
return false;
}
return IpcClient::withInterface(
[this](QSharedPointer<IpcInterfaceReplica> iface) {
auto currentToken = iface->xrayCurrentToken();
return currentToken.waitForFinished() && currentToken.returnValue() == m_xrayToken;
},
[]() { return false; });
}
#else
bool LocalProxyController::isSupported() const
{
return false;
}
bool LocalProxyController::startApi(quint16)
{
return false;
}
void LocalProxyController::stopApi()
{
}
bool LocalProxyController::startXrayEngine(const QString &)
{
return false;
}
bool LocalProxyController::stopXrayEngine()
{
return true;
}
bool LocalProxyController::isXrayEngineRunning() const
{
return false;
}
#endif
@@ -0,0 +1,85 @@
#ifndef LOCALPROXYCONTROLLER_H
#define LOCALPROXYCONTROLLER_H
#include <optional>
#include <QObject>
#include <QString>
class QHttpServer;
class SecureServersRepository;
class SecureAppSettingsRepository;
class SubscriptionController;
class LocalProxyController : public QObject
{
Q_OBJECT
public:
LocalProxyController(SecureServersRepository *serversRepository, SecureAppSettingsRepository *appSettingsRepository,
SubscriptionController *subscriptionController, QObject *parent = nullptr);
~LocalProxyController();
bool isSupported() const;
bool isEnabled() const;
int port() const;
QString ownerId() const;
bool isPortUserDefined() const;
int activePort() const;
bool setPort(int port);
bool isPortBusy(int port) const;
bool enable(const QString &ownerId, int port);
void disable();
public slots:
void onServerEdited(const QString &serverId);
void onServerRemoved(const QString &serverId);
void onVpnConnectionRequested();
signals:
void stateChanged();
void startFailed(const QString &message);
void stoppedByVpn(const QString &message);
private:
struct XrayConfig
{
QString serializedConfig;
int proxyPort = 0;
};
void start();
void stop();
bool syncXray();
bool startXray();
bool stopXray();
bool restartXray();
void disableWithError(const QString &message);
void setActivePort(int port);
int runningProxyPort() const;
std::optional<XrayConfig> buildXrayConfig() const;
std::optional<int> selectProxyPort() const;
std::optional<QString> fetchXrayConfigFromGateway(const QString &serverId) const;
bool startApi(quint16 port);
void stopApi();
bool startXrayEngine(const QString &config);
bool stopXrayEngine();
bool isXrayEngineRunning() const;
SecureServersRepository *m_serversRepository;
SecureAppSettingsRepository *m_appSettingsRepository;
SubscriptionController *m_subscriptionController;
bool m_isEnabled = false;
QHttpServer *m_httpServer = nullptr;
quint16 m_apiPort = 0;
qint64 m_xrayToken = 0;
quint16 m_appliedProxyPort = 0;
int m_activePort = 0;
int m_lastProxyPort = 0;
};
#endif // LOCALPROXYCONTROLLER_H
@@ -779,8 +779,11 @@ void ImportController::processAmneziaConfig(QJsonObject &config) const
}
QJsonObject jsonConfig = QJsonDocument::fromJson(protocolConfig.toUtf8()).object();
jsonConfig[configKey::mtu] =
ContainerUtils::isAwgContainer(dockerContainer) ? protocols::awg::defaultMtu : protocols::wireguard::defaultMtu;
if (jsonConfig.value(configKey::mtu).toString().trimmed().isEmpty()) {
jsonConfig[configKey::mtu] = ContainerUtils::isAwgContainer(dockerContainer)
? protocols::awg::defaultMtu
: protocols::wireguard::defaultMtu;
}
containerConfig[configKey::lastConfig] = QString(QJsonDocument(jsonConfig).toJson());
@@ -790,4 +793,3 @@ void ImportController::processAmneziaConfig(QJsonObject &config) const
}
}
}
+2 -1
View File
@@ -13,6 +13,7 @@
#include "core/controllers/gatewayController.h"
#include "core/utils/api/gatewayPayloadBuilder.h"
#include "core/utils/appUiConfig.h"
#include "core/utils/constants/apiConstants.h"
#include "core/utils/constants/apiKeys.h"
#include "core/utils/selfhosted/scriptsRegistry.h"
@@ -140,7 +141,7 @@ void UpdateController::checkForUpdates()
auto gatewayController = QSharedPointer<GatewayController>::create(m_appSettingsRepository->getGatewayEndpoint(),
m_appSettingsRepository->isDevGatewayEnv(),
7000,
apiDefs::requestTimeoutMsecs,
m_appSettingsRepository->isStrictKillSwitchEnabled(),
m_appSettingsRepository);
@@ -61,6 +61,10 @@ void OpenVpnProtocol::cleanupResources()
void OpenVpnProtocol::stop()
{
if (guardStop()) {
return;
}
qDebug() << "OpenVpnProtocol::stop()";
const bool wasActive = m_connectionState == Vpn::ConnectionState::Preparing
@@ -179,6 +183,8 @@ void OpenVpnProtocol::updateRouteGateway(QString line)
ErrorCode OpenVpnProtocol::start()
{
resetStopGuard();
cleanupResources();
if (!QFileInfo::exists(configPath())) {
+14
View File
@@ -27,6 +27,20 @@ VpnProtocol::VpnProtocol(const QJsonObject &configuration, QObject *parent)
connect(m_timeoutTimer, &QTimer::timeout, this, &VpnProtocol::onTimeout);
}
bool VpnProtocol::guardStop()
{
if (m_stopped) {
return true;
}
m_stopped = true;
return false;
}
void VpnProtocol::resetStopGuard()
{
m_stopped = false;
}
void VpnProtocol::setLastError(ErrorCode lastError)
{
m_lastError = lastError;
+5
View File
@@ -89,6 +89,11 @@ protected:
void startTimeoutTimer();
void stopTimeoutTimer();
bool guardStop();
void resetStopGuard();
bool m_stopped = false;
Vpn::ConnectionState m_connectionState;
QString m_routeGateway;
+4 -1
View File
@@ -50,8 +50,10 @@ WireguardProtocol::~WireguardProtocol()
void WireguardProtocol::stop()
{
if (guardStop()) {
return;
}
stopMzImpl();
return;
}
ErrorCode WireguardProtocol::startMzImpl()
@@ -75,5 +77,6 @@ ErrorCode WireguardProtocol::stopMzImpl()
ErrorCode WireguardProtocol::start()
{
resetStopGuard();
return startMzImpl();
}
+12 -4
View File
@@ -67,6 +67,8 @@ XrayProtocol::~XrayProtocol()
ErrorCode XrayProtocol::start()
{
resetStopGuard();
qDebug() << "XrayProtocol::start()";
// Inject SOCKS5 auth into the inbound before starting xray.
@@ -92,11 +94,12 @@ ErrorCode XrayProtocol::start()
return IpcClient::withInterface(
[&](QSharedPointer<IpcInterfaceReplica> iface) {
auto xrayStart = iface->xrayStart(xrayConfigStr);
if (!xrayStart.waitForFinished() || !xrayStart.returnValue()) {
auto xrayStart = iface->xrayStartOwned(xrayConfigStr);
if (!xrayStart.waitForFinished() || xrayStart.returnValue() == 0) {
qCritical() << "Failed to start xray";
return ErrorCode::XrayExecutableCrashed;
}
m_xrayToken = xrayStart.returnValue();
return startTun2Socks();
},
[]() { return ErrorCode::AmneziaServiceConnectionFailed; });
@@ -104,9 +107,13 @@ ErrorCode XrayProtocol::start()
void XrayProtocol::stop()
{
if (guardStop()) {
return;
}
qDebug() << "XrayProtocol::stop()";
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> iface) {
auto disableKillSwitch = iface->disableKillSwitch();
if (!disableKillSwitch.waitForFinished() || !disableKillSwitch.returnValue())
qWarning() << "Failed to disable killswitch";
@@ -123,9 +130,10 @@ void XrayProtocol::stop()
if (!deleteTun.waitForFinished() || !deleteTun.returnValue())
qWarning() << "Failed to delete tun";
auto xrayStop = iface->xrayStop();
auto xrayStop = iface->xrayStopOwned(m_xrayToken);
if (!xrayStop.waitForFinished() || !xrayStop.returnValue())
qWarning() << "Failed to stop xray";
m_xrayToken = 0;
});
if (m_tun2socksProcess) {
+4
View File
@@ -38,6 +38,10 @@ private:
int m_tun2socksRetryCount = 0;
static constexpr int maxTun2SocksRetries = 5;
static constexpr int tun2socksRetryDelayMs = 400;
// Owner token of the shared xray engine; a late stop with a stale token
// won't kill an engine restarted by another consumer (e.g. the local proxy).
qint64 m_xrayToken = 0;
};
#endif // XRAYPROTOCOL_H
@@ -385,14 +385,57 @@ void SecureAppSettingsRepository::setCountryListSortMode(const QString &listId,
setValue("Conf/countryListSortMode/" + listId, mode);
}
QStringList SecureAppSettingsRepository::favoriteLocations() const
QStringList SecureAppSettingsRepository::favoriteLocations(const QString &serverId) const
{
return value("Conf/favoriteLocations").toStringList();
QStringList codes = value("Conf/favoriteLocations").toMap().value(serverId).toStringList();
codes.removeAll(QString());
codes.removeDuplicates();
codes.sort();
return codes;
}
void SecureAppSettingsRepository::setFavoriteLocations(const QStringList &codes)
void SecureAppSettingsRepository::setFavoriteLocations(const QString &serverId, const QStringList &codes)
{
setValue("Conf/favoriteLocations", codes);
if (serverId.isEmpty()) {
return;
}
QVariantMap favorites = value("Conf/favoriteLocations").toMap();
if (codes.isEmpty()) {
favorites.remove(serverId);
} else {
favorites.insert(serverId, codes);
}
setValue("Conf/favoriteLocations", favorites);
}
void SecureAppSettingsRepository::removeFavoriteLocations(const QString &serverId)
{
QVariantMap favorites = value("Conf/favoriteLocations").toMap();
if (favorites.remove(serverId) > 0) {
setValue("Conf/favoriteLocations", favorites);
}
}
void SecureAppSettingsRepository::migrateLegacyFavoriteLocations(const QStringList &serverIds)
{
// Before per-subscription favorites the key held one list shared by all servers
const QVariant stored = value("Conf/favoriteLocations");
const int type = stored.typeId();
if (type != QMetaType::QStringList && type != QMetaType::QVariantList && type != QMetaType::QString) {
return;
}
QStringList codes = stored.toStringList();
codes.removeAll(QString());
codes.removeDuplicates();
QVariantMap favorites;
if (!codes.isEmpty()) {
for (const QString &serverId : serverIds) {
favorites.insert(serverId, codes);
}
}
setValue("Conf/favoriteLocations", favorites);
}
bool SecureAppSettingsRepository::isNewsNotifications() const
@@ -504,3 +547,33 @@ void SecureAppSettingsRepository::setXraySavedConfigs(const QByteArray &data)
{
setValue("Xray/savedConfigs", data);
}
QString SecureAppSettingsRepository::localProxyOwnerId() const
{
return value("Conf/localProxyOwnerId", "").toString();
}
void SecureAppSettingsRepository::setLocalProxyOwnerId(const QString &serverId)
{
setValue("Conf/localProxyOwnerId", serverId);
}
quint16 SecureAppSettingsRepository::localProxyPort() const
{
return static_cast<quint16>(value("Conf/localProxyPort", 10808).toUInt());
}
void SecureAppSettingsRepository::setLocalProxyPort(quint16 port)
{
setValue("Conf/localProxyPort", port);
}
bool SecureAppSettingsRepository::isLocalProxyPortUserDefined() const
{
return value("Conf/localProxyPortUserDefined", false).toBool();
}
void SecureAppSettingsRepository::setLocalProxyPortUserDefined(bool userDefined)
{
setValue("Conf/localProxyPortUserDefined", userDefined);
}
@@ -80,8 +80,10 @@ public:
int countryListSortMode(const QString &listId) const;
void setCountryListSortMode(const QString &listId, int mode);
QStringList favoriteLocations() const;
void setFavoriteLocations(const QStringList &codes);
QStringList favoriteLocations(const QString &serverId) const;
void setFavoriteLocations(const QString &serverId, const QStringList &codes);
void removeFavoriteLocations(const QString &serverId);
void migrateLegacyFavoriteLocations(const QStringList &serverIds);
bool isNewsNotifications() const;
void setNewsNotifications(bool enabled);
@@ -105,6 +107,13 @@ public:
QByteArray xraySavedConfigs() const;
void setXraySavedConfigs(const QByteArray &data);
QString localProxyOwnerId() const;
void setLocalProxyOwnerId(const QString &serverId);
quint16 localProxyPort() const;
void setLocalProxyPort(quint16 port);
bool isLocalProxyPortUserDefined() const;
void setLocalProxyPortUserDefined(bool userDefined);
signals:
void appLanguageChanged(QLocale locale);
void allowedDnsServersChanged(const QStringList &servers);
@@ -149,6 +149,9 @@ void SecureServersRepository::loadFromStorage()
QJsonDocument::fromJson(value(QStringLiteral("Servers/serversList"), QByteArray()).toByteArray())
.array();
QJsonArray persistedArray;
bool hasGeneratedIds = false;
for (int i = 0; i < serversArray.size(); ++i) {
const QJsonObject json = serversArray.at(i).toObject();
const QString candidateId = readStorageServerId(json);
@@ -158,15 +161,25 @@ void SecureServersRepository::loadFromStorage()
qWarning() << "Skipping stored server config with unsupported format version"
<< serverConfigUtils::configFormatVersion(strippedJson);
++m_unsupportedFormatConfigsCount;
persistedArray.append(json);
continue;
}
const serverConfigUtils::ConfigType kind = serverConfigUtils::configTypeFromJson(strippedJson);
if (m_serverJsonById.contains(serverId) || kind == serverConfigUtils::ConfigType::Invalid) {
persistedArray.append(json);
continue;
}
m_serverJsonById.insert(serverId, embedStorageServerId(serverId, strippedJson));
m_orderedServerIds.append(serverId);
persistedArray.append(m_serverJsonById.value(serverId));
hasGeneratedIds = hasGeneratedIds || serverId != candidateId;
}
// Per-server settings are keyed by id, so a generated one must survive restarts.
// Unlike syncToStorage(), this keeps entries that were skipped above
if (hasGeneratedIds) {
setValue(QStringLiteral("Servers/serversList"), QJsonDocument(persistedArray).toJson());
}
updateDefaultServerFromStorage();
@@ -323,6 +336,15 @@ serverConfigUtils::ConfigType SecureServersRepository::serverKind(const QString
return serverConfigUtils::configTypeFromJson(withoutStorageServerId(it.value()));
}
std::optional<QJsonObject> SecureServersRepository::serverJsonById(const QString &serverId) const
{
const auto it = m_serverJsonById.constFind(serverId);
if (it == m_serverJsonById.constEnd()) {
return std::nullopt;
}
return it.value();
}
std::optional<SelfHostedAdminServerConfig> SecureServersRepository::selfHostedAdminConfig(const QString &serverId) const
{
const auto it = m_serverJsonById.constFind(serverId);
@@ -31,6 +31,8 @@ public:
void removeServer(const QString &serverId);
serverConfigUtils::ConfigType serverKind(const QString &serverId) const;
std::optional<QJsonObject> serverJsonById(const QString &serverId) const;
std::optional<SelfHostedAdminServerConfig> selfHostedAdminConfig(const QString &serverId) const;
std::optional<SelfHostedUserServerConfig> selfHostedUserConfig(const QString &serverId) const;
std::optional<NativeServerConfig> nativeConfig(const QString &serverId) const;
+4 -1
View File
@@ -4,7 +4,10 @@
namespace apiDefs
{
constexpr int requestTimeoutMsecs = 12 * 1000; // 12 secs
constexpr int requestTimeoutMsecs = 6 * 1000;
constexpr int proxyRequestTimeoutMsecs = 6 * 1000;
constexpr int proxyStorageTimeoutMsecs = 6 * 1000;
constexpr int proxyHealthTimeoutMsecs = 1000;
} // namespace apiDefs
@@ -0,0 +1,15 @@
#ifndef LOCALPROXYDEFS_H
#define LOCALPROXYDEFS_H
#include <QtGlobal>
namespace amnezia::localProxy
{
constexpr int defaultProxyPort = 10808;
constexpr int proxyPortMin = 1024;
constexpr int proxyPortMax = 65535;
constexpr quint16 apiPort = 49490;
constexpr int portReleaseWaitMs = 800;
}
#endif // LOCALPROXYDEFS_H
@@ -0,0 +1,51 @@
#include "portUtils.h"
#include "localProxyConstants.h"
#include <QElapsedTimer>
#include <QHostAddress>
#include <QTcpServer>
#include <QThread>
using namespace amnezia;
bool portUtils::isPortAvailable(int port)
{
if (port < localProxy::proxyPortMin || port > localProxy::proxyPortMax) {
return false;
}
QTcpServer server;
const bool success = server.listen(QHostAddress::LocalHost, static_cast<quint16>(port));
server.close();
return success;
}
bool portUtils::waitForPort(int port, int timeoutMs)
{
QElapsedTimer timer;
timer.start();
while (!isPortAvailable(port)) {
if (timer.elapsed() >= timeoutMs) {
return false;
}
QThread::msleep(200);
}
return true;
}
std::optional<int> portUtils::findFirstAvailablePort(int startPort, int endPort)
{
startPort = qMax(startPort, localProxy::proxyPortMin);
endPort = qMin(endPort, localProxy::proxyPortMax);
for (int port = startPort; port <= endPort; ++port) {
if (isPortAvailable(port)) {
return port;
}
}
return std::nullopt;
}
+13
View File
@@ -0,0 +1,13 @@
#ifndef PORTUTILS_H
#define PORTUTILS_H
#include <optional>
namespace portUtils
{
bool isPortAvailable(int port);
bool waitForPort(int port, int timeoutMs);
std::optional<int> findFirstAvailablePort(int startPort, int endPort);
}
#endif // PORTUTILS_H
@@ -0,0 +1,47 @@
#include "xrayConfigUtils.h"
#include "core/utils/constants/configKeys.h"
#include "core/utils/containers/containerUtils.h"
#include <QJsonArray>
using namespace amnezia;
std::optional<QString> xrayConfigUtils::extractLastConfig(const QJsonObject &serverConfig)
{
const QJsonArray containers = serverConfig.value(configKey::containers).toArray();
const QString targetContainer = ContainerUtils::containerToString(DockerContainer::Xray);
for (const QJsonValue &value : containers) {
const QJsonObject container = value.toObject();
if (container.value(configKey::container).toString() != targetContainer) {
continue;
}
const QJsonObject proto = container.value(QString(configKey::xray)).toObject();
const QString serialized = proto.value(configKey::lastConfig).toString();
if (!serialized.isEmpty()) {
return serialized;
}
}
return std::nullopt;
}
bool xrayConfigUtils::setInboundPort(QJsonObject &xrayConfig, int port)
{
if (!xrayConfig.value(QLatin1String("inbounds")).isArray()) {
return false;
}
QJsonArray inbounds = xrayConfig.value(QLatin1String("inbounds")).toArray();
if (inbounds.isEmpty() || !inbounds.at(0).isObject()) {
return false;
}
QJsonObject firstInbound = inbounds.at(0).toObject();
firstInbound.insert(QLatin1String("port"), port);
inbounds[0] = firstInbound;
xrayConfig.insert(QLatin1String("inbounds"), inbounds);
return true;
}
@@ -0,0 +1,15 @@
#ifndef XRAYCONFIGUTILS_H
#define XRAYCONFIGUTILS_H
#include <optional>
#include <QJsonObject>
#include <QString>
namespace xrayConfigUtils
{
std::optional<QString> extractLastConfig(const QJsonObject &serverConfig);
bool setInboundPort(QJsonObject &xrayConfig, int port);
}
#endif // XRAYCONFIGUTILS_H
@@ -0,0 +1,45 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="AppIcon / macOS / source 1024" clip-path="url(#clip0_5470_4251)">
<g id="01-mark">
<mask id="mask0_5470_4251" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="177" y="175" width="670" height="646">
<path id="Vector" d="M846.215 406.917C846.56 407.138 846.531 407.651 846.164 407.833C843.297 409.261 840.463 410.762 837.528 412.063C814.995 422.068 793.433 433.719 775.111 450.46C768.848 456.182 763.185 462.678 757.881 469.326C752.851 475.626 751.17 483.342 751.247 491.353C751.263 492.749 751.49 494.145 751.726 495.714C751.887 496.788 752.805 497.598 753.891 497.598H779.404C780.594 497.598 781.564 498.565 781.51 499.754C781.058 509.761 777.584 518.371 771.532 525.565C763.501 535.101 760.227 546.436 756.441 557.815C750.919 574.403 750.548 591.503 751.858 608.473C753.222 626.162 746.675 640.265 736.112 653.42C727.819 663.753 719.787 674.303 711.538 684.679C703.081 695.316 699.927 707.327 700.833 720.83C702.001 738.138 706.06 754.824 709.846 771.674C712.935 785.428 713.949 799.641 715.815 813.657C715.901 814.287 715.864 814.929 715.787 815.796C715.653 817.301 714.047 818.261 712.646 817.695C709.606 816.465 706.843 815.459 704.532 813.809C694.002 806.3 687.52 795.597 682.217 784.099C679.625 778.473 677.112 772.808 674.593 767.216C674.36 766.699 673.932 766.297 673.405 766.089C665.906 763.141 663.858 761.337 661.752 756.183C661.42 755.369 660.646 754.81 659.766 754.798C648.525 754.652 637.34 755.748 626.577 759.478C607.524 766.072 588.439 772.6 569.659 779.892C548.73 788.022 527.07 789.668 505.267 787.041C484.393 784.524 463.758 779.99 443.003 776.469C436.041 775.292 428.992 774.595 421.997 773.538C420.262 773.276 418.538 772.6 416.945 771.826C404.374 765.701 391.957 759.249 379.266 753.385C365.506 747.031 354.877 751.565 349.781 765.669C347.686 771.467 346.442 777.559 344.413 783.379C341.314 792.262 338.073 801.123 334.505 809.82C331.963 816.025 327.118 819.188 319.961 820.095C318.656 820.26 317.583 819.138 317.694 817.827C318.185 812.007 318.478 806.392 319.424 800.894C321.366 789.635 324.64 778.562 325.829 767.238C327.008 755.925 326.43 744.416 326.299 733.004C326.277 731.348 324.749 729.43 323.451 728.1C314.143 718.64 304.835 709.179 295.298 699.959C285.204 690.193 275.394 680.101 268.65 667.763C262.518 656.548 252.937 654.946 242.32 657.017C225.231 660.352 208.35 664.69 191.382 668.635C187.141 669.623 182.929 670.725 178.71 671.781C178.187 671.912 177.642 671.667 177.39 671.19C177.157 670.746 177.232 670.203 177.576 669.838C179.6 667.69 181.492 665.667 183.765 663.774C196.609 653.093 209.747 642.75 222.438 631.895C230.011 625.41 237.071 618.304 244.076 611.198C245.484 609.77 246.368 607.034 246.226 604.985C242.712 554.218 254.519 506.579 275.623 460.858C284.55 441.511 293.821 422.322 303.182 402.563C303.555 401.776 304.371 401.31 305.241 401.26C313.219 400.796 314.444 392.717 317.602 386.832C319.765 382.799 321.34 378.454 323.132 373.9C323.685 372.496 325.478 372.112 326.573 373.151C327.95 374.459 329.253 375.696 330.668 377.043C331.511 377.845 332.838 377.833 333.693 377.044C336.456 374.492 339.332 372.158 341.368 369.241C349.269 357.95 357.038 346.549 364.229 334.811C367.546 329.395 371.235 325.994 377.585 324.218C398.823 318.277 419.859 311.645 440.808 305.309C441.322 305.154 441.778 304.825 442.074 304.377C442.175 304.223 442.246 304.087 442.261 303.945C443.811 291.455 444.825 290.409 457.462 288.338C470.916 286.137 479.395 278.943 481.664 265.341C483.923 251.772 484.589 237.931 485.909 224.198C487.011 212.634 487.83 201.038 489.194 189.507C490.038 182.358 492.002 178.954 496.78 176.066C497.814 175.441 499.14 175.897 499.687 176.974C502.111 181.747 504.846 186.414 506.468 191.436C510.865 205.07 514.357 218.988 518.635 232.666C522.312 244.415 526.186 256.132 530.638 267.608C536.596 282.987 548.785 289.504 564.782 289.777C570.969 289.886 577.156 290.104 583.332 289.831C590.894 289.504 597.922 291.161 603.978 295.499C620.357 307.248 636.888 318.812 652.645 331.356C664.54 340.833 675.405 351.606 687.249 362.36C687.798 362.858 688.563 363.024 689.29 362.885C695.263 361.736 700.529 364.865 705.861 368.54C706.732 369.141 707.028 370.29 706.565 371.241C704.823 374.815 703.1 378.343 701.283 382.079C700.73 383.215 701.275 384.578 702.433 385.084C714.46 390.345 722.85 399.811 732.183 408.063C735.446 410.94 738.949 413.567 742.408 416.216C748.922 421.207 756.135 422.243 764.057 420.248C779.378 416.39 794.688 412.477 810.117 409.131C822.084 406.534 833.414 405.098 846.004 406.839C846.079 406.85 846.151 406.876 846.215 406.917ZM522.481 739.756C522.484 739.753 522.489 739.751 522.494 739.751C535.03 739.751 547.565 739.893 560.1 739.664C563.45 739.598 567.139 739.108 570.074 737.626C585.787 729.702 601.599 721.931 616.799 713.103C632.48 704.002 633.8 699.479 629.152 681.747C624.962 665.78 617.225 651.339 609.041 637.214C599.362 620.495 591.811 603.372 591.331 583.58C591.08 573.193 588.09 563.232 583.147 553.913C578.771 545.662 571.994 541.761 562.861 541.641C537.6 541.314 512.862 544.333 488.801 552.474C426.602 573.542 388.716 624.702 387.668 688.58C387.417 703.991 394.095 714.454 407.572 720.558C416.967 724.819 426.755 728.362 436.62 731.391C464.607 739.99 493.499 740.786 522.468 739.762C522.473 739.762 522.477 739.Line truncated
</mask>
<g mask="url(#mask0_5470_4251)">
<g id="Group 96">
<path id="Vector_2" d="M427.284 700.798C426.676 709.205 424.448 717.916 425.765 725.918C429.107 746.177 414.015 744.557 402.771 745.671C390.514 746.886 380.284 759.143 366.71 752.154C367.318 751.343 367.825 750.634 368.432 749.824C384.335 738.884 392.945 719.03 414.015 713.662C418.066 712.75 417.864 700.595 427.284 700.798Z" fill="#FEDAA5"/>
<path id="Vector_3" d="M170.709 503.066C170.709 389.503 170.709 275.939 170.613 162.375C170.613 159.098 171.288 158.423 174.566 158.423C401.982 158.519 629.495 158.519 856.911 158.423C860.189 158.423 860.864 159.098 860.864 162.375C860.767 389.792 860.767 617.305 860.864 844.721C860.864 847.999 860.189 848.674 856.911 848.674C629.495 848.577 401.982 848.577 174.566 848.674C171.288 848.674 170.613 847.999 170.613 844.721C170.709 730.772 170.709 616.919 170.709 503.066ZM176.976 534.397C176.976 633.211 176.879 732.025 176.879 830.742C178.807 834.502 171.77 842.504 181.025 842.504C383.087 842.697 585.149 842.697 787.211 842.6C788.947 842.6 792.321 840.19 792.128 839.612C788.561 823.898 798.394 824.958 808.709 825.44C819.892 826.019 831.171 825.344 842.451 825.633C846.499 825.729 847.464 824.573 847.464 820.524C847.271 774.828 847.367 729.229 847.367 683.534C847.271 512.321 847.271 341.108 847.174 169.895C846.403 169.22 845.728 168.063 844.957 168.063C824.23 168.063 803.6 168.063 782.873 168.353C781.813 168.353 779.788 171.63 779.788 173.269C779.595 231.401 779.788 289.436 780.56 347.471C780.752 361.353 773.136 366.848 762.243 370.125C758.58 371.09 755.302 371.572 751.638 372.536C737.66 372.536 723.199 368.197 717.319 389.021C713.559 402.035 701.412 412.64 693.025 424.304C689.072 427.197 685.505 430.86 681.167 432.884C661.212 442.525 642.316 427.486 621.011 430.474C597.489 433.752 582.035 440.21 581.264 464.986C581.264 466.143 593.235 489.917 571.073 476.288C570.109 475.71 568.76 476.266 567.507 476.266C551.408 465.565 532.127 476.555 515.642 468.457C514.967 467.204 514.292 465.951 513.521 464.698C513.617 458.239 514.196 451.779 513.617 445.417C512.75 436.451 514.003 431.342 525.186 433.27C531.838 434.427 542.635 436.644 545.045 429.317C553.818 402.806 567.025 378.609 578.401 353.448C584.956 338.891 569.821 324.912 567.989 310.066C566.157 295.798 554.396 283.651 558.927 268.42C565.29 247.404 580.425 231.593 592.669 214.048C593.922 212.795 595.175 211.541 596.525 210.192C607.033 194.574 619.276 181.078 638.171 175.486C641.063 173.944 647.426 176.74 646.462 170.666C645.691 165.46 640.196 168.16 637.111 168.16C489.42 168.256 341.73 168.545 193.943 167.678C176.686 167.581 173.987 173.558 176.783 187.537C176.783 299.269 176.783 411.097 176.783 522.829C176.879 526.685 176.976 530.541 176.976 534.397ZM688.012 252.224C687.819 252.224 687.626 252.224 687.337 252.224C687.337 226.87 687.337 201.515 687.337 176.161C687.337 173.366 688.59 169.799 683.288 169.895C677.986 169.991 679.528 173.751 679.528 176.354C679.143 225.809 678.853 275.264 678.468 324.719C678.468 330.214 680.782 333.781 685.795 335.709C686.18 335.902 687.915 334.263 687.915 333.589C688.012 306.403 688.012 279.313 688.012 252.224Z" fill="#FCE8EC"/>
<path id="Vector_4" d="M847.002 170.088C847.098 341.301 847.098 512.514 847.194 683.726C847.194 729.422 847.194 775.021 847.291 820.716C847.291 824.669 846.327 825.922 842.278 825.826C830.999 825.536 819.816 826.115 808.537 825.633C798.125 825.151 788.292 824.09 791.955 839.804C792.148 840.479 788.774 842.793 787.038 842.793C584.976 842.889 382.914 842.889 180.852 842.696C171.597 842.696 178.538 834.695 176.707 830.935C177.767 829.874 178.828 828.814 179.792 827.754C190.3 816.089 200.808 804.424 211.219 792.759C248.527 792.181 285.932 791.988 323.24 790.253C327.578 790.06 328.542 804.038 333.845 792.277C335.676 788.228 339.05 784.95 341.653 781.287C341.075 783.986 339.147 787.457 340.111 789.288C343.003 794.591 345.22 788.614 346.666 787.553C356.018 780.034 364.019 788.517 371.828 790.445C376.937 791.699 380.311 800.568 384.167 806.159C387.542 811.076 390.434 816.378 393.519 821.487C393.711 817.246 394.097 813.004 394.193 808.858C394.29 805.002 393.808 801.146 394.29 797.29C395.832 785.143 393.229 773.768 408.75 764.609C421.186 757.283 439.792 747.642 436.418 725.373C434.972 715.925 436.418 706.092 436.611 696.355C446.251 689.414 455.988 682.377 465.629 675.436C479.221 663.771 494.935 656.251 511.902 651.046C512.384 651.046 512.866 651.046 513.348 651.046C514.12 650.756 514.987 650.371 515.758 650.082C534.846 652.01 532.629 636.296 536.003 625.402C560.586 616.051 588.543 634.079 612.162 615.762C618.718 610.652 627.49 607.953 632.793 602.073C641.662 592.239 650.916 584.816 664.509 582.503C672.029 581.249 684.176 586.166 686.297 576.526C688.514 566.5 684.561 555.22 683.308 544.423C687.936 538.639 692.466 532.951 697.094 527.167C700.95 515.213 708.759 524.371 714.543 524.178C733.92 523.311 737.005 505.572 746.067 494.39C762.745 473.759 772.771 449.176 780.965 424.497C788.774 401.264 781.447 376.681 781.447 352.58C781.543 293.484 781.447 234.485 781.543 175.39C781.543 173.847 782.315 172.208 782.7 170.666C783.857 171.244 785.014 171.726 786.074 172.305C785.303 172.594 784.436 172.883 783.664 173.172C783.954 173.462 784.146 173.751 784.436 174.137C784.918 173.558 785.496 172.98 785.978 172.305C806.512 171.341 826.757 170.666 847.002 170.088Z" fill="#F18928" stroke-width="1.0759" stroke="#F18928"/>
<path id="Vector_5" d="M176.611 522.827C176.611 411.096 176.611 299.267 176.611 187.535C177.093 187.535 177.672 187.439 178.154 187.439C236.767 187.439 295.381 188.017 353.994 187.053C369.997 186.764 381.084 191.199 391.784 202.864C402.003 213.854 414.921 222.337 426.683 231.881C437.094 241.04 432.274 254.536 431.985 264.273C431.213 292.326 431.213 320.862 422.152 348.24C419.934 355.085 420.223 362.894 417.91 369.738C413.09 384.199 421.766 400.298 410.679 414.373C401.81 425.653 395.544 439.053 388.121 451.489C381.566 461.997 372.022 470.866 370.768 484.17C370.094 484.362 369.419 484.459 368.744 484.652C354.476 481.856 337.413 497.377 325.748 477.807C325.748 477.807 324.977 478.096 324.88 478.289C321.41 489.183 310.034 489.568 302.611 491.496C269.834 500.076 235.9 499.691 202.448 497.28C180.853 495.738 174.008 502.486 177.961 522.731C177.575 522.827 177.093 522.827 176.611 522.827Z" fill="#80C9C1" stroke-width="1.0759" stroke="#80C9C1"/>
<path id="Vector_6" d="M388.216 451.394C395.639 438.958 401.905 425.558 410.774 414.279C421.861 400.204 413.184 384.104 418.005 369.644C420.318 362.799 419.933 355.087 422.246 348.146C431.212 320.864 431.212 292.232 432.079 264.178C432.369 254.345 437.092 240.945 426.777 231.786C440.852 238.246 459.747 230.244 470.063 249.621C478.739 265.913 491.464 245.476 502.551 249.332C504.575 242.777 510.166 238.246 510.359 230.051C510.552 223.592 512.384 209.421 520.192 211.445C544.39 217.808 568.587 209.999 592.399 214.337C580.156 231.883 564.924 247.693 558.658 268.709C554.127 283.844 565.888 296.088 567.719 310.356C569.551 325.202 584.687 339.18 578.131 353.737C566.755 378.899 553.548 403.192 544.775 429.607C542.365 436.934 531.568 434.716 524.916 433.559C513.637 431.631 512.48 436.644 513.348 445.706C513.926 452.069 513.348 458.528 513.251 464.987C499.658 464.312 486.066 464.023 472.569 462.77C468.52 462.384 464.664 459.685 460.711 458.046C462.639 454.19 460.808 448.116 467.363 446.863C468.038 446.767 469.195 444.357 468.906 443.875C465.146 438.669 461.193 433.656 457.144 428.739C457.048 428.643 454.734 429.896 453.867 430.86C436.321 451.105 416.269 464.119 388.216 451.394Z" fill="#95C7DB" stroke-width="1.0759" stroke="#95C7DB"/>
<path id="Vector_7" d="M592.496 214.337C568.684 210.095 544.487 217.808 520.29 211.445C512.385 209.421 510.649 223.592 510.457 230.051C510.167 238.245 504.672 242.776 502.648 249.332C491.561 245.476 478.836 265.913 470.16 249.621C459.845 230.244 440.95 238.245 426.875 231.786C415.113 222.242 402.099 213.759 391.976 202.769C381.276 191.104 370.093 186.669 354.186 186.959C295.38 188.019 236.766 187.441 178.153 187.441C181.238 182.042 171.405 170.377 183.841 170.281C329.314 169.799 474.884 169.895 620.357 170.281C626.237 170.281 632.022 173.848 637.902 175.776C619.007 181.271 606.764 194.864 596.256 210.481C595.003 211.734 593.749 212.988 592.496 214.337Z" fill="#87ADD4" stroke-width="1.0759" stroke="#87ADD4"/>
<path id="Vector_8" d="M574.949 576.624C575.913 566.694 580.734 561.778 591.242 561.006C602.232 560.235 595.291 549.824 598.183 544.232C604.834 531.218 609.365 516.853 608.691 501.622C612.932 500.079 617.463 499.115 621.223 496.801C634.334 488.704 643.685 476.46 660.556 471.833C669.811 469.23 679.258 469.905 687.934 466.916C703.841 461.421 703.07 444.165 710.107 432.307C718.784 425.366 722.736 415.822 727.749 406.182C732.377 397.409 725.339 380.635 742.981 379.767C748.187 379.478 750.115 376.875 750.982 372.441C754.646 371.477 758.388 370.586 762.052 369.622C770.728 412.233 752.718 446.961 721.483 470.772C699.889 487.161 700.563 506.345 697.093 526.976C692.465 532.76 687.934 538.448 683.307 544.232C678.391 553.583 669.618 548.859 662.484 549.727C656.121 550.498 649.759 548.667 643.878 553.583C624.115 570.261 601.171 578.648 574.949 576.624Z" fill="#FEDAA5" stroke-width="1.0759" stroke="#FEDAA5"/>
<path id="Vector_9" d="M710.402 432.148C703.365 444.005 704 461.32 688.093 466.912C679.417 469.9 669.929 469.201 660.674 471.804C643.803 476.431 634.427 488.707 621.316 496.805C617.556 499.118 612.854 500.101 608.708 501.644C594.923 493.257 581.144 484.614 567.261 476.227C568.515 476.227 570.154 475.648 571.021 476.227C592.423 489.434 580.951 466.104 580.951 464.948C581.722 440.172 597.339 433.713 620.765 430.435C642.071 427.447 660.869 442.486 680.921 432.845C685.26 430.724 688.826 427.157 692.779 424.265C698.66 426.868 704.618 429.545 710.402 432.148Z" fill="#FFF1CF" stroke-width="1.0759" stroke-linejoin="bevel" stroke="#FFF1CF"/>
<path id="Vector_10" d="M535.969 625.164C532.492 636.173 534.81 651.818 515.689 649.886C515.276 648.751 513.537 649.965 513.274 650.949C512.792 650.949 512.065 650.605 511.582 650.605C508.685 595.463 511.053 540.278 501.976 485.522C502.748 476.927 508.156 471.905 515.496 468.332C532.009 476.54 551.324 465.531 567.451 476.154C581.261 484.556 594.794 493.21 608.7 501.612C609.376 516.87 605.114 530.91 598.451 544.044C595.553 549.645 602.507 560.075 591.497 560.847C580.971 561.523 576.143 566.545 575.177 576.492C561.947 592.716 548.909 608.94 535.969 625.164Z" fill="#A29EA1" stroke-width="1.0759" stroke="#A29EA1"/>
<path id="Vector_11" d="M176.611 522.829C177.093 522.829 177.575 522.733 178.057 522.733C237.249 524.854 296.441 527.65 355.537 520.901C338.377 542.785 321.41 561.005 288.15 558.113C252.192 555.028 215.558 559.27 179.311 560.427C178.443 551.75 177.672 543.074 176.804 534.398C176.708 530.542 176.611 526.685 176.611 522.829Z" fill="#797AAA" stroke-width="1.0759" stroke="#797AAA"/>
<path id="Vector_12" d="M697.092 526.974C700.563 506.344 699.888 487.16 721.483 470.771C752.717 446.959 770.745 412.254 761.972 369.643C772.866 366.366 780 361.642 779.807 347.76C779.036 289.725 779.518 231.593 779.614 173.558C779.614 171.823 781.542 168.642 782.699 168.642C803.426 168.256 824.056 168.256 844.783 168.352C845.554 168.352 846.229 169.509 847 170.184C826.755 170.763 806.511 171.437 786.266 172.016C785.109 171.437 783.952 170.955 782.892 170.377C782.506 171.919 781.735 173.558 781.735 175.101C781.638 234.196 781.735 293.195 781.638 352.291C781.542 376.295 788.965 400.975 781.156 424.208C772.866 448.887 762.936 473.47 746.258 494.101C737.196 505.283 734.111 523.118 714.734 523.889C708.757 524.179 700.949 515.02 697.092 526.974Z" fill="#F8B570"/>
<path id="Vector_13" d="M515.781 468.227C508.55 471.794 502.838 477.231 501.971 485.81C498.115 488.413 494.162 491.113 490.306 493.716C471.025 493.137 451.841 492.462 432.56 491.884C428.704 491.305 424.848 490.631 420.992 490.052C405.085 480.894 387.636 485.425 370.862 484.075C372.115 470.771 381.659 461.902 388.214 451.394C416.268 464.023 436.223 451.009 453.962 430.764C454.829 429.8 457.143 428.547 457.239 428.643C461.288 433.56 465.241 438.573 469.001 443.778C469.29 444.26 468.229 446.574 467.458 446.767C460.903 448.02 462.831 454.094 460.806 457.95C464.759 459.589 468.615 462.288 472.664 462.674C486.161 463.83 500.136 464.012 513.729 464.687C514.404 465.94 515.106 466.974 515.781 468.227Z" fill="#87ADD4"/>
<path id="Vector_14" d="M688.192 252.109C688.192 279.311 688.048 307.26 687.951 334.462C687.951 335.236 686.209 336.785 685.822 336.592C680.885 334.655 678.465 331.17 678.465 325.556C678.852 275.895 679.015 226.187 679.402 176.527C679.402 173.913 677.942 169.936 683.266 169.839C688.494 169.743 687.494 173.339 687.591 176.146C687.591 201.606 687.514 226.552 687.514 252.012C687.708 252.109 687.902 252.109 688.192 252.109Z" fill="#FCE8EC"/>
<path id="Vector_15" d="M710.109 432.402C704.325 429.799 698.348 426.714 692.564 424.111C700.951 412.446 712.905 401.938 716.665 388.924C722.642 368.197 737.919 372.322 751.801 372.225C750.934 376.66 748.382 379.573 743.176 379.766C725.534 380.633 732.861 397.504 728.233 406.277C723.317 416.013 718.785 425.461 710.109 432.402Z" fill="#F8C9D1" stroke-width="1.0759" stroke="#F8C9D1"/>
<path id="Vector_16" d="M637.9 175.775C632.02 173.847 626.235 170.28 620.355 170.28C474.882 169.991 329.312 169.798 183.839 170.28C171.403 170.28 181.14 181.945 178.151 187.44C177.669 187.44 177.091 187.536 176.609 187.536C173.716 173.558 176.416 167.581 193.672 167.677C341.363 168.545 489.149 168.159 636.84 168.352C640.021 168.352 645.42 165.652 646.191 170.858C647.251 177.028 640.889 174.232 637.9 175.775Z" fill="#95C7DB" stroke-width="1.0759" stroke="#95C7DB"/>
<path id="Vector_17" d="M176.708 534.398C177.575 543.074 178.347 551.75 179.214 560.427C178.829 566.307 178.057 572.188 178.057 578.069C177.961 659.433 177.961 740.798 178.057 822.163C178.057 823.994 179.118 825.826 179.696 827.658C178.636 828.718 177.575 829.779 176.611 830.839C176.611 732.025 176.708 633.211 176.708 534.398Z" fill="#ADA0CC"/>
<path id="Vector_18" d="M592.496 214.339C593.749 213.086 595.002 211.833 596.352 210.483C595.002 211.736 593.749 212.989 592.496 214.339Z" fill="#95C7DB"/>
<path id="Vector_19" d="M179.693 827.656C179.115 825.825 178.054 823.993 178.054 822.161C177.958 740.797 177.958 659.432 178.054 578.067C178.054 572.187 178.826 566.306 179.211 560.425C215.555 559.365 252.092 555.027 288.051 558.112C321.31 561.004 338.277 542.784 355.437 520.9C355.919 520.804 356.498 520.707 356.98 520.514C363.728 518.008 366.909 521.864 372.79 525.334C395.637 538.638 420.51 531.794 442.393 521.189C457.047 514.152 467.073 518.297 472.182 529.191C485.486 558.015 479.702 589.732 483.172 620.099C487.125 654.226 466.494 646.032 448.852 646.128C437.091 646.225 425.426 646.321 413.665 646.032C411.158 645.936 408.266 644.875 406.338 643.236C393.324 632.825 377.996 645.453 364.692 638.898C350.424 631.861 340.88 649.599 327.769 645.743C323.72 644.586 320.732 636.777 317.261 636.874C296.438 637.259 275.422 637.356 257.78 651.816C250.164 658.082 237.343 659.432 236.764 671.964C235.8 691.342 235.993 710.815 236.089 730.192C236.186 748.027 236.379 748.027 246.597 751.69C247.176 751.883 247.32 753.346 247.609 754.214C235.462 767.036 223.364 779.84 211.217 792.566C200.709 804.327 190.201 815.992 179.693 827.656Z" fill="#6E5EA6" stroke-width="1.0759" stroke="#6E5EA6"/>
<path id="Vector_20" d="M247.561 754.292C247.272 753.424 247.176 751.978 246.597 751.785C236.378 748.122 236.186 748.218 236.089 730.287C235.993 710.91 235.896 691.436 236.764 672.059C237.342 659.527 250.164 658.177 257.78 651.911C275.422 637.45 296.438 637.258 317.261 636.968C320.635 636.872 323.72 644.681 327.769 645.838C340.88 649.694 350.424 631.955 364.692 638.993C377.899 645.548 393.324 632.919 406.338 643.331C408.266 644.874 411.158 646.03 413.665 646.127C425.426 646.416 437.091 646.32 448.852 646.223C466.494 646.127 487.028 654.321 483.172 620.194C479.701 589.827 485.485 558.11 472.182 529.285C467.169 518.392 457.046 514.246 442.393 521.284C420.509 531.888 395.637 538.733 372.79 525.429C366.909 521.959 363.728 518.103 356.979 520.609C360.932 508.655 364.873 496.519 368.825 484.565C369.5 484.372 370.195 484.264 370.87 484.071C387.644 485.421 405.078 480.867 420.985 490.025C424.841 490.603 428.724 491.288 432.58 491.866C435.183 496.301 485.511 497.789 490.252 493.734C494.109 491.131 498.127 488.342 501.983 485.74C511.045 540.4 508.815 595.997 511.707 651.043C494.74 656.153 478.93 663.672 465.434 675.433C449.238 674.759 432.656 675.723 416.846 672.927C394.288 668.974 376.935 678.711 359.486 689.798C352.256 694.329 344.35 695.485 336.735 693.268C310.609 685.845 286.797 701.752 288.147 728.166C289.111 745.905 286.797 756.413 265.589 750.436C261.25 749.182 255.948 751.207 251.128 751.689C249.971 752.46 248.718 753.328 247.561 754.292Z" fill="#797AAA" stroke-width="1.0759" stroke="#797AAA"/>
<path id="Vector_21" d="M251.227 751.596C256.047 751.114 261.349 749.089 265.687 750.342C286.993 756.319 289.21 745.908 288.246 728.073C286.896 701.658 310.612 685.752 336.833 693.175C344.449 695.392 352.354 694.235 359.585 689.704C377.034 678.714 394.386 668.881 416.945 672.834C432.755 675.629 449.337 674.665 465.532 675.34C455.892 682.281 446.155 689.319 436.515 696.26C433.141 698.284 429.863 700.309 426.489 702.237C417.62 702.044 417.716 713.613 413.764 714.577C393.712 719.686 385.517 738.581 370.382 748.993C368.839 746.197 367.779 745.426 364.116 747.45C358.428 750.535 351.872 753.331 345.51 753.62C316.396 754.873 287.378 755.259 258.264 755.548C255.854 755.645 253.541 752.945 251.227 751.596Z" fill="#A29EA1" stroke-width="1.0759" stroke="#A29EA1"/>
<path id="Vector_22" d="M574.95 576.62C601.171 578.741 624.116 570.258 643.975 553.58C649.855 548.663 656.218 550.495 662.581 549.724C669.715 548.856 678.487 553.58 683.404 544.229C684.657 555.026 688.513 566.305 686.392 576.331C684.271 586.068 672.125 581.055 664.605 582.308C651.012 584.622 641.757 591.949 632.888 601.878C627.49 607.855 618.717 610.554 612.258 615.567C588.639 633.981 560.682 615.857 536.099 625.208C548.921 609.012 561.935 592.816 574.95 576.62Z" fill="#F8B570"/>
<path id="Vector_23" d="M251.226 751.595C253.594 752.995 255.939 755.63 258.253 755.533C287.367 755.148 316.48 754.77 345.498 753.613C351.861 753.324 358.398 750.558 364.086 747.377C367.749 745.352 368.954 746.14 370.497 748.935C369.918 749.707 369.321 750.535 368.742 751.306C359.68 761.236 350.715 771.262 341.653 781.191C338.954 784.854 335.676 788.132 333.844 792.181C328.542 803.942 327.674 789.964 323.24 790.157C285.932 791.988 248.455 792.052 211.147 792.63C223.294 779.808 235.381 767.009 247.528 754.283C248.685 753.319 249.973 752.463 251.226 751.595Z" fill="#B59259" stroke-width="1.0759" stroke="#B59259"/>
<path id="Vector_24" d="M341.651 781.095C350.712 771.165 359.678 761.139 368.74 751.21C381.658 757.861 391.395 746.197 403.06 745.04C413.76 743.979 428.125 745.522 424.943 726.241C423.69 718.529 425.811 710.334 426.389 702.333C429.763 700.308 433.041 698.284 436.415 696.356C436.319 705.996 434.776 715.926 436.223 725.373C439.597 747.643 420.991 757.283 408.555 764.61C393.034 773.768 395.637 785.144 394.094 797.29C393.612 801.05 394.094 805.003 393.998 808.859C393.901 813.101 393.516 817.342 393.323 821.488C390.238 816.378 387.346 811.076 383.972 806.16C380.116 800.472 376.741 791.699 371.632 790.446C363.823 788.518 355.822 780.131 346.471 787.554C345.121 788.614 342.904 794.591 339.915 789.289C339.144 787.361 341.072 783.89 341.651 781.095Z" fill="#F8B570"/>
<path id="Vector_25" d="M368.838 484.557C364.886 496.511 361.029 508.561 357.077 520.515C356.595 520.612 356.016 520.708 355.534 520.901C296.439 527.746 237.247 524.854 178.055 522.733C174.103 502.584 181.044 495.836 202.542 497.282C235.994 499.596 269.928 499.981 302.705 491.498C310.128 489.57 321.504 489.088 324.974 478.291C325.071 478.001 325.842 477.809 325.842 477.809C337.507 497.282 354.667 481.857 368.838 484.557Z" fill="#A29EA1" stroke-width="1.0759" stroke="#A29EA1"/>
<path id="Vector_26" d="M490.307 493.714C486.162 497.955 435.26 496.317 432.561 491.882C451.842 492.46 471.123 493.135 490.307 493.714Z" fill="#7D89C1" stroke-width="1.0759" stroke="#7D89C1"/>
<path id="Vector_27" d="M432.557 491.884C428.7 491.306 424.844 490.631 420.988 490.052C424.844 490.631 428.7 491.306 432.557 491.884Z" fill="#7D89C1"/>
</g>
</g>
</g>
</g>
<defs>
<clipPath id="clip0_5470_4251">
<rect width="1024" height="1024" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 28 KiB

@@ -0,0 +1,13 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="AppIcon / macOS / mono &#194;&#183; light">
<g clip-path="url(#clip0_5472_439335)">
<path id="02-mark-mono" d="M846.215 406.917C846.56 407.138 846.531 407.651 846.164 407.833C843.297 409.261 840.463 410.762 837.528 412.063C814.995 422.068 793.433 433.719 775.111 450.46C768.848 456.182 763.185 462.678 757.881 469.326C752.851 475.626 751.17 483.342 751.247 491.353C751.263 492.749 751.49 494.145 751.726 495.714C751.887 496.788 752.805 497.598 753.891 497.598H779.404C780.594 497.598 781.564 498.565 781.51 499.754C781.058 509.761 777.584 518.371 771.532 525.565C763.501 535.101 760.227 546.436 756.441 557.815C750.919 574.403 750.548 591.503 751.858 608.473C753.222 626.162 746.675 640.265 736.112 653.42C727.819 663.753 719.787 674.303 711.538 684.679C703.081 695.316 699.927 707.327 700.833 720.83C702.001 738.138 706.06 754.824 709.846 771.674C712.935 785.428 713.949 799.641 715.815 813.657C715.901 814.287 715.864 814.929 715.787 815.796C715.653 817.301 714.047 818.261 712.646 817.695C709.606 816.465 706.843 815.459 704.532 813.809C694.002 806.3 687.52 795.597 682.217 784.099C679.625 778.473 677.112 772.808 674.593 767.216C674.36 766.699 673.932 766.297 673.405 766.089C665.906 763.141 663.858 761.337 661.752 756.183C661.42 755.369 660.646 754.81 659.766 754.798C648.525 754.652 637.34 755.748 626.577 759.478C607.524 766.072 588.439 772.6 569.659 779.892C548.73 788.022 527.07 789.668 505.267 787.041C484.393 784.524 463.758 779.99 443.003 776.469C436.041 775.292 428.992 774.595 421.997 773.538C420.262 773.276 418.538 772.6 416.945 771.826C404.374 765.701 391.957 759.249 379.266 753.385C365.506 747.031 354.877 751.565 349.781 765.669C347.686 771.467 346.442 777.559 344.413 783.379C341.314 792.262 338.073 801.123 334.505 809.82C331.963 816.025 327.118 819.188 319.961 820.095C318.656 820.26 317.583 819.138 317.694 817.827C318.185 812.007 318.478 806.392 319.424 800.894C321.366 789.635 324.64 778.562 325.829 767.238C327.008 755.925 326.43 744.416 326.299 733.004C326.277 731.348 324.749 729.43 323.451 728.1C314.143 718.64 304.835 709.179 295.298 699.959C285.204 690.193 275.394 680.101 268.65 667.763C262.518 656.548 252.937 654.946 242.32 657.017C225.231 660.352 208.35 664.69 191.382 668.635C187.141 669.623 182.929 670.725 178.71 671.781C178.187 671.912 177.642 671.667 177.39 671.19C177.157 670.746 177.232 670.203 177.576 669.838C179.6 667.69 181.492 665.667 183.765 663.774C196.609 653.093 209.747 642.75 222.438 631.895C230.011 625.41 237.071 618.304 244.076 611.198C245.484 609.77 246.368 607.034 246.226 604.985C242.712 554.218 254.519 506.579 275.623 460.858C284.55 441.511 293.821 422.322 303.182 402.563C303.555 401.776 304.371 401.31 305.241 401.26C313.219 400.796 314.444 392.717 317.602 386.832C319.765 382.799 321.34 378.454 323.132 373.9C323.685 372.496 325.478 372.112 326.573 373.151C327.95 374.459 329.253 375.696 330.668 377.043C331.511 377.845 332.838 377.833 333.693 377.044C336.456 374.492 339.332 372.158 341.368 369.241C349.269 357.95 357.038 346.549 364.229 334.811C367.546 329.395 371.235 325.994 377.585 324.218C398.823 318.277 419.859 311.645 440.808 305.309C441.322 305.154 441.778 304.825 442.074 304.377C442.175 304.223 442.246 304.087 442.261 303.945C443.811 291.455 444.825 290.409 457.462 288.338C470.916 286.137 479.395 278.943 481.664 265.341C483.923 251.772 484.589 237.931 485.909 224.198C487.011 212.634 487.83 201.038 489.194 189.507C490.038 182.358 492.002 178.954 496.78 176.066C497.814 175.441 499.14 175.897 499.687 176.974C502.111 181.747 504.846 186.414 506.468 191.436C510.865 205.07 514.357 218.988 518.635 232.666C522.312 244.415 526.186 256.132 530.638 267.608C536.596 282.987 548.785 289.504 564.782 289.777C570.969 289.886 577.156 290.104 583.332 289.831C590.894 289.504 597.922 291.161 603.978 295.499C620.357 307.248 636.888 318.812 652.645 331.356C664.54 340.833 675.405 351.606 687.249 362.36C687.798 362.858 688.563 363.024 689.29 362.885C695.263 361.736 700.529 364.865 705.861 368.54C706.732 369.141 707.028 370.29 706.565 371.241C704.823 374.815 703.1 378.343 701.283 382.079C700.73 383.215 701.275 384.578 702.433 385.084C714.46 390.345 722.85 399.811 732.183 408.063C735.446 410.94 738.949 413.567 742.408 416.216C748.922 421.207 756.135 422.243 764.057 420.248C779.378 416.39 794.688 412.477 810.117 409.131C822.084 406.534 833.414 405.098 846.004 406.839C846.079 406.85 846.151 406.876 846.215 406.917ZM522.481 739.756C522.484 739.753 522.489 739.751 522.494 739.751C535.03 739.751 547.565 739.893 560.1 739.664C563.45 739.598 567.139 739.108 570.074 737.626C585.787 729.702 601.599 721.931 616.799 713.103C632.48 704.002 633.8 699.479 629.152 681.747C624.962 665.78 617.225 651.339 609.041 637.214C599.362 620.495 591.811 603.372 591.331 583.58C591.08 573.193 588.09 563.232 583.147 553.913C578.771 545.662 571.994 541.761 562.861 541.641C537.6 541.314 512.862 544.333 488.801 552.474C426.602 573.542 388.716 624.702 387.668 688.58C387.417 703.991 394.095 714.454 407.572 720.558C416.967 724.819 426.755 728.362 436.62 731.391C464.607 739.99 493.499 740.786 522.468 739.762C522.473 739.762 522.47Line truncated
</g>
</g>
<defs>
<clipPath id="clip0_5472_439335">
<rect width="1024" height="1024" rx="230" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

+37
View File
@@ -0,0 +1,37 @@
{
"fill" : {
"solid" : "srgb:0.12157,0.12157,0.13725,1.00000"
},
"groups" : [
{
"layers" : [
{
"image-name-specializations" : [
{
"value" : "01-mark.svg"
},
{
"appearance" : "tinted",
"value" : "02-mark-mono.svg"
}
],
"name" : "01-mark"
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}
Binary file not shown.
-14
View File
@@ -1,14 +0,0 @@
<svg width="24" height="24" viewBox="0 0 74 74" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4_34)">
<path d="M55.5 12.3333H18.5C15.0942 12.3333 12.3333 15.0943 12.3333 18.5V55.5C12.3333 58.9058 15.0942 61.6667 18.5 61.6667H55.5C58.9057 61.6667 61.6666 58.9058 61.6666 55.5V18.5C61.6666 15.0943 58.9057 12.3333 55.5 12.3333Z" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.5833 24.6667H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.5833 37H52.4167" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.5833 49.3333H40.0833" stroke="#CBCAC8" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="61.5" cy="12.5" r="15" fill="#FBB36B" stroke="#1C1D21" stroke-width="5"/>
</g>
<defs>
<clipPath id="clip0_4_34">
<rect width="74" height="74" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 982 B

+3 -8
View File
@@ -1,8 +1,3 @@
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#CBCAC8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<!-- Основа газеты -->
<rect x="4" y="4" width="16" height="16" rx="2"/>
<!-- Линии текста -->
<line x1="7" y1="8" x2="17" y2="8"/>
<line x1="7" y1="12" x2="17" y2="12"/>
<line x1="7" y1="16" x2="13" y2="16"/>
</svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 1C20.7957 1 21.5585 1.3163 22.1211 1.87891C22.6837 2.44152 23 3.20435 23 4V20C23 20.7957 22.6837 21.5585 22.1211 22.1211C21.5585 22.6837 20.7957 23 20 23H4C3.9647 23 3.92987 22.9977 3.89551 22.9941C3.13785 22.9677 2.41681 22.659 1.87891 22.1211C1.3163 21.5585 1 20.7957 1 20V11C1 9.34772 2.34772 8 4 8H5V4C5 3.20435 5.3163 2.44152 5.87891 1.87891C6.44152 1.3163 7.20435 1 8 1H20ZM4 10C3.45228 10 3 10.4523 3 11V20C3 20.2652 3.10543 20.5195 3.29297 20.707C3.48051 20.8946 3.73478 21 4 21C4.26522 21 4.51949 20.8946 4.70703 20.707C4.89457 20.5195 5 20.2652 5 20V10H4ZM8 3C7.73478 3 7.4805 3.10543 7.29297 3.29297C7.10543 3.4805 7 3.73478 7 4V20C7 20.3444 6.93703 20.6814 6.82422 21H20C20.2652 21 20.5195 20.8946 20.707 20.707C20.8946 20.5195 21 20.2652 21 20V4C21 3.73478 20.8946 3.48051 20.707 3.29297C20.5195 3.10543 20.2652 3 20 3H8ZM15 17C15.5523 17 16 17.4477 16 18C16 18.5523 15.5523 19 15 19H10C9.44772 19 9 18.5523 9 18C9 17.4477 9.44771 17 10 17H15ZM18 13C18.5523 13 19 13.4477 19 14C19 14.5523 18.5523 15 18 15H10C9.44771 15 9 14.5523 9 14C9 13.4477 9.44771 13 10 13H18ZM18 5C18.5523 5 19 5.44772 19 6V10C19 10.5523 18.5523 11 18 11H10C9.44771 11 9 10.5523 9 10V6C9 5.44772 9.44771 5 10 5H18ZM11 9H17V7H11V9Z" fill="#E4E4E7"/>
</svg>

Before

Width:  |  Height:  |  Size: 410 B

After

Width:  |  Height:  |  Size: 1.3 KiB

-1
View File
@@ -48,7 +48,6 @@
<file>controls/map-pin.svg</file>
<file>controls/more-vertical.svg</file>
<file>controls/news.svg</file>
<file>controls/news-unread.svg</file>
<file>controls/unread-dot.svg</file>
<file>controls/plus.svg</file>
<file>controls/qr-code.svg</file>
+1 -2
View File
@@ -57,7 +57,6 @@
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
@@ -68,7 +67,7 @@
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<string>Dark</string>
<key>com.wireguard.ios.app_group_id</key>
<string>${BUILD_IOS_GROUP_IDENTIFIER}</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 767 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

@@ -1,290 +0,0 @@
{
"images" : [
{
"filename" : "40.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "60.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "29.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "58.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "87.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "80.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "120.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "57.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "57x57"
},
{
"filename" : "114.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "57x57"
},
{
"filename" : "120.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "180.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "20.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "40.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "29.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "58.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "40.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "80.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "50.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "50x50"
},
{
"filename" : "100.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "50x50"
},
{
"filename" : "72.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "72x72"
},
{
"filename" : "144.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "72x72"
},
{
"filename" : "76.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "152.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "167.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "1024.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
},
{
"idiom" : "watch",
"role" : "notificationCenter",
"scale" : "2x",
"size" : "24x24",
"subtype" : "38mm"
},
{
"idiom" : "watch",
"role" : "notificationCenter",
"scale" : "2x",
"size" : "27.5x27.5",
"subtype" : "42mm"
},
{
"idiom" : "watch",
"role" : "companionSettings",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "watch",
"role" : "companionSettings",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "watch",
"role" : "notificationCenter",
"scale" : "2x",
"size" : "33x33",
"subtype" : "45mm"
},
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "40x40",
"subtype" : "38mm"
},
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "44x44",
"subtype" : "40mm"
},
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "46x46",
"subtype" : "41mm"
},
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "50x50",
"subtype" : "44mm"
},
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "51x51",
"subtype" : "45mm"
},
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "54x54",
"subtype" : "49mm"
},
{
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "86x86",
"subtype" : "38mm"
},
{
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "98x98",
"subtype" : "42mm"
},
{
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "108x108",
"subtype" : "44mm"
},
{
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "117x117",
"subtype" : "45mm"
},
{
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "129x129",
"subtype" : "49mm"
},
{
"idiom" : "watch-marketing",
"scale" : "1x",
"size" : "1024x1024"
},
{
"idiom" : "car",
"scale" : "2x",
"size" : "60"
},
{
"idiom" : "car",
"scale" : "3x",
"size" : "60"
},
{
"idiom" : "watch",
"role" : "longLook",
"scale" : "2x",
"size" : "44x44",
"subtype" : "42mm"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

@@ -1,68 +0,0 @@
{
"images" : [
{
"filename" : "16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "16@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "32@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "256@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "512@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -1,68 +0,0 @@
{
"images": [
{
"idiom": "mac",
"size": "16x16",
"scale": "1x",
"filename": "16.png"
},
{
"idiom": "mac",
"size": "16x16",
"scale": "2x",
"filename": "16@2x.png"
},
{
"idiom": "mac",
"size": "32x32",
"scale": "1x",
"filename": "32.png"
},
{
"idiom": "mac",
"size": "32x32",
"scale": "2x",
"filename": "32@2x.png"
},
{
"idiom": "mac",
"size": "128x128",
"scale": "1x",
"filename": "128.png"
},
{
"idiom": "mac",
"size": "128x128",
"scale": "2x",
"filename": "128@2x.png"
},
{
"idiom": "mac",
"size": "256x256",
"scale": "1x",
"filename": "256.png"
},
{
"idiom": "mac",
"size": "256x256",
"scale": "2x",
"filename": "256@2x.png"
},
{
"idiom": "mac",
"size": "512x512",
"scale": "1x",
"filename": "512.png"
},
{
"idiom": "mac",
"size": "512x512",
"scale": "2x",
"filename": "512@2x.png"
}
],
"info": {
"version": 1,
"author": "xcode"
}
}
+13 -2
View File
@@ -26,6 +26,7 @@ set(CMAKE_AUTOUIC ON)
qt6_add_resources(TEST_QRC
../server_scripts/serverScripts.qrc
../resources/countries.qrc
)
set(_main_cpp "${CMAKE_CURRENT_SOURCE_DIR}/../main.cpp")
@@ -196,6 +197,15 @@ if(NOT ANDROID)
test_common
)
add_executable(test_favorite_locations_restore
testFavoriteLocationsRestore.cpp
)
target_link_libraries(test_favorite_locations_restore PRIVATE
Qt6::Test
test_common
)
add_executable(test_backup_export_import
testBackupExportImport.cpp
)
@@ -416,8 +426,8 @@ endif()
if(NOT ANDROID)
add_subdirectory(selfHostedAdmin)
endif()
add_subdirectory(api)
endif()
enable_testing()
@@ -438,12 +448,13 @@ if(NOT ANDROID)
add_test(NAME UiAppSTModelAndControllerTest COMMAND test_ui_app_st_model_and_controller)
add_test(NAME UiAllowedDnsModelAndControllerTest COMMAND test_ui_allowed_dns_model_and_controller)
add_test(NAME BackupExportImportTest COMMAND test_backup_export_import)
add_test(NAME FavoriteLocationsRestoreTest COMMAND test_favorite_locations_restore)
add_test(NAME ConnectionTest COMMAND test_connection)
add_test(NAME OpenSSLLibraryTest COMMAND test_openssl_library)
add_test(NAME LogsControllerTest COMMAND test_logs_controller)
endif()
# Android tests are executed on-device via androidtestrunner in CI (see .github/workflows/deploy.yml).
# Android tests are executed on-device via androidtestrunner in CI (see .github/workflows/tests.yml).
# CTest cannot run Android binaries on the host machine, so no add_test() entries are registered here.
amnezia_copy_runtime_dlls()
+18 -63
View File
@@ -1,77 +1,32 @@
function(add_new_test target source)
if(ANDROID)
qt_add_executable(${target}
${source}
${ARGN}
)
else()
add_executable(${target}
${source}
${ARGN}
)
endif()
add_executable(test_ui_api_services_model_and_controller
testUiApiServicesModelAndController.cpp
)
target_link_libraries(${target} PRIVATE
Qt6::Test
test_common
)
target_link_libraries(test_ui_api_services_model_and_controller PRIVATE
Qt6::Test
test_common
)
# These tests are selected for execution on the Android emulator.
if(ANDROID AND "${target}" MATCHES
"^(test_ui_api_account_info_model|test_ui_subscription_controller|test_ui_services_model_and_controller)$")
set_target_properties(${target} PROPERTIES
QT_ANDROID_PACKAGE_SOURCE_DIR "${ANDROID_TEST_PACKAGE_DIR}"
)
endif()
endfunction()
add_new_test(test_ui_news_model_and_controller
add_executable(test_ui_news_model_and_controller
testUiNewsModelAndController.cpp
)
add_new_test(test_gateway_service_import
target_link_libraries(test_ui_news_model_and_controller PRIVATE
Qt6::Test
test_common
)
add_executable(test_gateway_service_import
testGatewayServiceImport.cpp
)
add_new_test(test_ui_services_model_and_controller
testUiServicesModelAndController.cpp
target_link_libraries(test_gateway_service_import PRIVATE
Qt6::Test
test_common
)
add_new_test(test_ui_subscription_controller
testSubscriptionUiController.cpp
)
add_new_test(test_ui_api_account_info_model
testUiApiAccountInfoModel.cpp
)
add_new_test(test_api_benefits_model
testApiBenefitsModel.cpp
)
add_new_test(test_api_country_model
testApiCountryModel.cpp
)
add_new_test(test_api_country_list_model
testApiCountryListModel.cpp
${CLIENT_ROOT_DIR}/resources/countries.qrc
)
add_new_test(test_api_devices_model
testApiDevicesModel.cpp
)
if(NOT ANDROID)
add_test(NAME UiApiServicesModelAndControllerTest COMMAND test_ui_api_services_model_and_controller)
add_test(NAME UiNewsModelAndControllerTest COMMAND test_ui_news_model_and_controller)
add_test(NAME GatewayServiceImportTest COMMAND test_gateway_service_import)
add_test(NAME UiServicesModelAndControllerTest COMMAND test_ui_services_model_and_controller)
add_test(NAME UiSubscriptionControllerTest COMMAND test_ui_subscription_controller)
add_test(NAME UiApiAccountInfoModelTest COMMAND test_ui_api_account_info_model)
add_test(NAME ApiBenefitsModelTest COMMAND test_api_benefits_model)
add_test(NAME ApiCountryModelTest COMMAND test_api_country_model)
add_test(NAME ApiCountryListModelTest COMMAND test_api_country_list_model)
add_test(NAME ApiDevicesModelTest COMMAND test_api_devices_model)
endif()
amnezia_copy_runtime_dlls()
-213
View File
@@ -1,213 +0,0 @@
#include <QJsonArray>
#include <QJsonObject>
#include <QSignalSpy>
#include <QTest>
#include "ui/models/api/apiBenefitsModel.h"
class TestApiBenefitsModel : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void initialState();
void updateModel();
void updateModelClearsOnEmptyArray();
void updateModelSkipsInvalidItems();
void updateModelSkipsItemsWithoutTitleAndBody();
void updateModelTrimsLinkedBody();
void updateModelPreservesQrcIcon();
void updateModelUsesFallbackIcon();
void dataReturnsInvalidForInvalidIndex();
void dataReturnsInvalidForUnknownRole();
void roleNames();
void clear();
};
void TestApiBenefitsModel::initTestCase()
{
qRegisterMetaType<QModelIndex>();
}
void TestApiBenefitsModel::initialState()
{
ApiBenefitsModel model;
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.rowCount(QModelIndex()), 0);
}
void TestApiBenefitsModel::updateModel()
{
ApiBenefitsModel model;
const QJsonArray benefits = { QJsonObject { { "icon", "globe-2" },
{ "title", "Global access" },
{ "body", "Access servers worldwide" },
{ "link", false } },
QJsonObject { { "icon", "smartphone" },
{ "title", "Multiple devices" },
{ "body", "Connect your devices" },
{ "link", true } } };
QSignalSpy aboutToResetSpy(&model, &QAbstractItemModel::modelAboutToBeReset);
QSignalSpy resetSpy(&model, &QAbstractItemModel::modelReset);
model.updateModel(benefits);
QCOMPARE(aboutToResetSpy.count(), 1);
QCOMPARE(resetSpy.count(), 1);
QCOMPARE(model.rowCount(), 2);
const QModelIndex firstIndex = model.index(0, 0);
QVERIFY(firstIndex.isValid());
QCOMPARE(model.rowCount(firstIndex), 0);
QCOMPARE(model.data(firstIndex, ApiBenefitsModel::IconRole).toString(), QStringLiteral("qrc:/images/controls/globe-2.svg"));
QCOMPARE(model.data(firstIndex, ApiBenefitsModel::TitleRole).toString(), QStringLiteral("Global access"));
QCOMPARE(model.data(firstIndex, ApiBenefitsModel::BodyRole).toString(), QStringLiteral("Access servers worldwide"));
QCOMPARE(model.data(firstIndex, ApiBenefitsModel::LinkRole).toBool(), false);
const QModelIndex secondIndex = model.index(1, 0);
QVERIFY(secondIndex.isValid());
QCOMPARE(model.data(secondIndex, ApiBenefitsModel::IconRole).toString(), QStringLiteral("qrc:/images/controls/smartphone.svg"));
QCOMPARE(model.data(secondIndex, ApiBenefitsModel::TitleRole).toString(), QStringLiteral("Multiple devices"));
QCOMPARE(model.data(secondIndex, ApiBenefitsModel::BodyRole).toString(), QStringLiteral("Connect your devices"));
QCOMPARE(model.data(secondIndex, ApiBenefitsModel::LinkRole).toBool(), true);
}
void TestApiBenefitsModel::updateModelClearsOnEmptyArray()
{
ApiBenefitsModel model;
model.updateModel({ QJsonObject { { "title", "Benefit" }, { "body", "Description" } } });
QCOMPARE(model.rowCount(), 1);
QSignalSpy resetSpy(&model, &QAbstractItemModel::modelReset);
model.updateModel(QJsonArray { });
QCOMPARE(resetSpy.count(), 1);
QCOMPARE(model.rowCount(), 0);
}
void TestApiBenefitsModel::updateModelSkipsInvalidItems()
{
ApiBenefitsModel model;
const QJsonArray benefits = { QJsonObject { { "title", "Valid benefit" }, { "body", "Valid description" } },
QJsonValue(QStringLiteral("invalid item")), QJsonValue(42), QJsonValue(false),
QJsonValue(QJsonArray { }) };
model.updateModel(benefits);
QCOMPARE(model.rowCount(), 1);
QCOMPARE(model.data(model.index(0, 0), ApiBenefitsModel::TitleRole).toString(), QStringLiteral("Valid benefit"));
}
void TestApiBenefitsModel::updateModelSkipsItemsWithoutTitleAndBody()
{
ApiBenefitsModel model;
const QJsonArray benefits = { QJsonObject { }, QJsonObject { { "title", "" }, { "body", "" } },
QJsonObject { { "title", "Title only" } }, QJsonObject { { "body", "Body only" } } };
model.updateModel(benefits);
QCOMPARE(model.rowCount(), 2);
QCOMPARE(model.data(model.index(0, 0), ApiBenefitsModel::TitleRole).toString(), QStringLiteral("Title only"));
QCOMPARE(model.data(model.index(1, 0), ApiBenefitsModel::BodyRole).toString(), QStringLiteral("Body only"));
}
void TestApiBenefitsModel::updateModelTrimsLinkedBody()
{
ApiBenefitsModel model;
model.updateModel({ QJsonObject { { "title", "Link" }, { "body", " https://example.com \n" }, { "link", true } },
QJsonObject { { "title", "Text" }, { "body", " Description " }, { "link", false } } });
QCOMPARE(model.data(model.index(0, 0), ApiBenefitsModel::BodyRole).toString(), QStringLiteral("https://example.com"));
QCOMPARE(model.data(model.index(1, 0), ApiBenefitsModel::BodyRole).toString(), QStringLiteral(" Description "));
}
void TestApiBenefitsModel::updateModelPreservesQrcIcon()
{
ApiBenefitsModel model;
model.updateModel({ QJsonObject { { "title", "Benefit" }, { "body", "Description" }, { "icon", "qrc:/custom/icon.svg" } } });
QCOMPARE(model.data(model.index(0, 0), ApiBenefitsModel::IconRole).toString(), QStringLiteral("qrc:/custom/icon.svg"));
}
void TestApiBenefitsModel::updateModelUsesFallbackIcon()
{
ApiBenefitsModel model;
model.updateModel({ QJsonObject { { "title", "Benefit" }, { "body", "Description" }, { "icon", "unknown-icon" } } });
QCOMPARE(model.data(model.index(0, 0), ApiBenefitsModel::IconRole).toString(), QStringLiteral("qrc:/images/controls/info.svg"));
}
void TestApiBenefitsModel::dataReturnsInvalidForInvalidIndex()
{
ApiBenefitsModel model;
model.updateModel({ QJsonObject { { "title", "Benefit" }, { "body", "Description" } } });
QVERIFY(!model.data(QModelIndex(), ApiBenefitsModel::TitleRole).isValid());
QVERIFY(!model.data(model.index(-1, 0), ApiBenefitsModel::TitleRole).isValid());
QVERIFY(!model.data(model.index(1, 0), ApiBenefitsModel::TitleRole).isValid());
QVERIFY(!model.data(model.index(0, 1), ApiBenefitsModel::TitleRole).isValid());
}
void TestApiBenefitsModel::dataReturnsInvalidForUnknownRole()
{
ApiBenefitsModel model;
model.updateModel({ QJsonObject { { "title", "Benefit" }, { "body", "Description" } } });
const QModelIndex index = model.index(0, 0);
QVERIFY(!model.data(index, Qt::DisplayRole).isValid());
QVERIFY(!model.data(index, ApiBenefitsModel::LinkRole + 1).isValid());
}
void TestApiBenefitsModel::roleNames()
{
ApiBenefitsModel model;
const QHash<int, QByteArray> roles = model.roleNames();
QCOMPARE(roles.value(ApiBenefitsModel::IconRole), QByteArray("icon"));
QCOMPARE(roles.value(ApiBenefitsModel::TitleRole), QByteArray("title"));
QCOMPARE(roles.value(ApiBenefitsModel::BodyRole), QByteArray("body"));
QCOMPARE(roles.value(ApiBenefitsModel::LinkRole), QByteArray("link"));
}
void TestApiBenefitsModel::clear()
{
ApiBenefitsModel model;
model.updateModel({ QJsonObject { { "title", "Benefit 1" }, { "body", "Description 1" } },
QJsonObject { { "title", "Benefit 2" }, { "body", "Description 2" } } });
QSignalSpy aboutToResetSpy(&model, &QAbstractItemModel::modelAboutToBeReset);
QSignalSpy resetSpy(&model, &QAbstractItemModel::modelReset);
model.clear();
QCOMPARE(aboutToResetSpy.count(), 1);
QCOMPARE(resetSpy.count(), 1);
QCOMPARE(model.rowCount(), 0);
model.clear();
QCOMPARE(model.rowCount(), 0);
QCOMPARE(resetSpy.count(), 2);
}
QTEST_MAIN(TestApiBenefitsModel)
#include "testApiBenefitsModel.moc"
@@ -1,165 +0,0 @@
#include <QSignalSpy>
#include <QTest>
#include "ui/models/api/apiCountryListModel.h"
class TestApiCountryListModel : public QObject
{
Q_OBJECT
private slots:
void initialState();
void dataReturnsInvalidForInvalidIndex();
void searchTextChanges();
void searchTextChangedSignal();
void clearSearch();
void sortModeChanges();
void invalidSortModeFallsBackToByRegion();
void favoritesAreSortedAndDeduplicated();
void collapsedSectionsAreStored();
void sanitizeSearchFieldText();
};
void TestApiCountryListModel::initialState()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.rowCount(QModelIndex()), 0);
QVERIFY(!model.hasResults());
QVERIFY(!model.isSearchActive());
QVERIFY(model.searchText().isEmpty());
QCOMPARE(model.sortMode(), ApiCountryListModel::ByRegion);
QVERIFY(model.isGrouped());
QCOMPARE(model.listId(), QStringLiteral("test"));
QCOMPARE(model.activeUseCaseId(), QStringLiteral("all"));
QCOMPARE(model.favoritesLimit(), 15);
}
void TestApiCountryListModel::dataReturnsInvalidForInvalidIndex()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
QVERIFY(!model.data(QModelIndex(), Qt::DisplayRole).isValid());
QVERIFY(!model.data(model.index(-1, 0), ApiCountryListModel::CountryNameRole).isValid());
QVERIFY(!model.data(model.index(0, 0), ApiCountryListModel::CountryNameRole).isValid());
}
void TestApiCountryListModel::searchTextChanges()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
model.setSearchText(QStringLiteral("Estonia"));
QCOMPARE(model.searchText(), QStringLiteral("Estonia"));
QVERIFY(model.isSearchActive());
QCOMPARE(model.rowCount(), 0);
QVERIFY(!model.hasResults());
model.setSearchText(QStringLiteral(" "));
QVERIFY(!model.isSearchActive());
QCOMPARE(model.searchText(), QStringLiteral(" "));
}
void TestApiCountryListModel::searchTextChangedSignal()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
QSignalSpy spy(&model, &ApiCountryListModel::searchTextChanged);
QVERIFY(spy.isValid());
model.setSearchText(QStringLiteral("Estonia"));
QCOMPARE(spy.count(), 1);
model.setSearchText(QStringLiteral("Estonia"));
QCOMPARE(spy.count(), 1);
model.setSearchText(QStringLiteral("Latvia"));
QCOMPARE(spy.count(), 2);
}
void TestApiCountryListModel::clearSearch()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
model.setSearchText(QStringLiteral("Estonia"));
QVERIFY(model.isSearchActive());
QSignalSpy spy(&model, &ApiCountryListModel::searchTextChanged);
QVERIFY(spy.isValid());
model.clearSearch();
QVERIFY(model.searchText().isEmpty());
QVERIFY(!model.isSearchActive());
QCOMPARE(spy.count(), 1);
model.clearSearch();
QCOMPARE(spy.count(), 1);
}
void TestApiCountryListModel::sortModeChanges()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
QSignalSpy sortSpy(&model, &ApiCountryListModel::sortModeChanged);
QSignalSpy groupingSpy(&model, &ApiCountryListModel::groupingChanged);
QVERIFY(sortSpy.isValid());
QVERIFY(groupingSpy.isValid());
model.setSortMode(ApiCountryListModel::Alphabetical);
QCOMPARE(model.sortMode(), ApiCountryListModel::Alphabetical);
QVERIFY(!model.isGrouped());
QCOMPARE(sortSpy.count(), 1);
QCOMPARE(groupingSpy.count(), 1);
model.setSortMode(ApiCountryListModel::Alphabetical);
QCOMPARE(sortSpy.count(), 1);
QCOMPARE(groupingSpy.count(), 1);
}
void TestApiCountryListModel::invalidSortModeFallsBackToByRegion()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
model.setSortMode(ApiCountryListModel::Alphabetical);
model.setSortMode(999);
QCOMPARE(model.sortMode(), ApiCountryListModel::ByRegion);
QVERIFY(model.isGrouped());
}
void TestApiCountryListModel::favoritesAreSortedAndDeduplicated()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
model.setFavorites({ QStringLiteral("US"), QStringLiteral("DE"), QStringLiteral("US"), QString() });
QCOMPARE(model.favorites(), QStringList({ QStringLiteral("DE"), QStringLiteral("US") }));
}
void TestApiCountryListModel::collapsedSectionsAreStored()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
model.setCollapsedSections({ QStringLiteral("europe"), QStringLiteral("asia"), QStringLiteral("europe"), QString() });
QCOMPARE(model.collapsedSections(), QStringList({ QStringLiteral("asia"), QStringLiteral("europe") }));
}
void TestApiCountryListModel::sanitizeSearchFieldText()
{
ApiCountryListModel model(nullptr, QStringLiteral("test"));
QCOMPARE(model.sanitizeSearchFieldText(QStringLiteral("Estonia")), QStringLiteral("Estonia"));
QCOMPARE(model.sanitizeSearchFieldText(QStringLiteral("Hello, World! 123")), QStringLiteral("Hello World "));
QCOMPARE(model.sanitizeSearchFieldText(QStringLiteral("")), QString());
QCOMPARE(model.sanitizeSearchFieldText(QStringLiteral("123")), QString());
}
QTEST_MAIN(TestApiCountryListModel)
#include "testApiCountryListModel.moc"
-190
View File
@@ -1,190 +0,0 @@
#include <QJsonArray>
#include <QJsonObject>
#include <QSignalSpy>
#include <QTest>
#include "core/utils/constants/apiKeys.h"
#include "ui/models/api/apiCountryModel.h"
class TestApiCountryModel : public QObject
{
Q_OBJECT
private:
ApiCountryModel m_model;
static QJsonObject makeCountry(const QString &name, const QString &code, const QString &localizedCode)
{
return { { apiDefs::key::serverCountryName, name },
{ apiDefs::key::serverCountryCode, code },
{ apiDefs::key::serverCountryCodeL10n, localizedCode } };
}
static QJsonObject makeIssuedConfig(const QString &countryCode, const QString &sourceType,
const QString &lastDownloaded, const QString &workerLastUpdated)
{
return { { apiDefs::key::serverCountryCode, countryCode },
{ apiDefs::key::sourceType, sourceType },
{ apiDefs::key::lastDownloaded, lastDownloaded },
{ apiDefs::key::workerLastUpdated, workerLastUpdated },
{ apiDefs::key::installationUuid, "test-installation" },
{ apiDefs::key::osVersion, "test-os" } };
}
private slots:
void init()
{
m_model.updateIssuedConfigsInfo(QJsonArray {});
m_model.updateModel(QJsonArray {}, QString {}, QString {});
}
void testInitialState()
{
QCOMPARE(m_model.rowCount(), 0);
QCOMPARE(m_model.getCurrentIndex(), -1);
QVERIFY(m_model.getCurrentCountryCode().isEmpty());
QVERIFY(m_model.getUserCountryCode().isEmpty());
QVERIFY(!m_model.hasExpiredWorkerConfigs());
QVERIFY(!m_model.data(m_model.index(0, 0), ApiCountryModel::CountryNameRole).isValid());
}
void testUpdateModel()
{
const QJsonArray countries { makeCountry("Germany", "DE", "de"), makeCountry("Estonia", "EE", "ee"),
makeCountry("United States", "US", "us") };
QSignalSpy currentIndexSpy(&m_model, &ApiCountryModel::currentIndexChanged);
m_model.updateModel(countries, "EE", "us");
QCOMPARE(m_model.rowCount(), 3);
QCOMPARE(m_model.getCurrentIndex(), 1);
QCOMPARE(m_model.getCurrentCountryCode(), QString("EE"));
QCOMPARE(m_model.getUserCountryCode(), QString("US"));
QCOMPARE(currentIndexSpy.count(), 1);
QCOMPARE(currentIndexSpy.constFirst().constFirst().toInt(), 1);
const QModelIndex germanyIndex = m_model.index(0, 0);
const QModelIndex estoniaIndex = m_model.index(1, 0);
const QModelIndex unitedStatesIndex = m_model.index(2, 0);
QVERIFY(germanyIndex.isValid());
QCOMPARE(m_model.data(germanyIndex, ApiCountryModel::CountryNameRole).toString(), QString("Germany"));
QCOMPARE(m_model.data(germanyIndex, ApiCountryModel::CountryCodeRole).toString(), QString("DE"));
QCOMPARE(m_model.data(estoniaIndex, ApiCountryModel::CountryNameRole).toString(), QString("Estonia"));
QCOMPARE(m_model.data(estoniaIndex, ApiCountryModel::CountryCodeRole).toString(), QString("EE"));
QCOMPARE(m_model.data(unitedStatesIndex, ApiCountryModel::CountryNameRole).toString(), QString("United States"));
QCOMPARE(m_model.data(unitedStatesIndex, ApiCountryModel::CountryCodeRole).toString(), QString("US"));
}
void testCurrentCountryNotFound()
{
const QJsonArray countries { makeCountry("Germany", "DE", "de"), makeCountry("Estonia", "EE", "ee") };
m_model.updateModel(countries, "FI", "ee");
QCOMPARE(m_model.rowCount(), 2);
QCOMPARE(m_model.getCurrentIndex(), -1);
QCOMPARE(m_model.getCurrentCountryCode(), QString("FI"));
QCOMPARE(m_model.getUserCountryCode(), QString("EE"));
}
void testCountryImageCodeRole()
{
m_model.updateModel(QJsonArray { makeCountry("Estonia", "EE", "ee") }, "EE", "EE");
const QModelIndex index = m_model.index(0, 0);
QCOMPARE(m_model.data(index, ApiCountryModel::CountryImageCodeRole).toString(), QString("EE"));
}
void testCountryImageCodeFallback()
{
m_model.updateModel(QJsonArray { makeCountry("Estonia", "EE", "") }, "EE", "EE");
QCOMPARE(m_model.data(m_model.index(0, 0), ApiCountryModel::CountryImageCodeRole).toString(), QString("EE"));
}
void testInvalidIndexesAndRoles()
{
m_model.updateModel(QJsonArray { makeCountry("Estonia", "EE", "ee") }, "EE", "EE");
QVERIFY(!m_model.data(QModelIndex(), ApiCountryModel::CountryNameRole).isValid());
QVERIFY(!m_model.data(m_model.index(-1, 0), ApiCountryModel::CountryNameRole).isValid());
QVERIFY(!m_model.data(m_model.index(1, 0), ApiCountryModel::CountryNameRole).isValid());
QVERIFY(!m_model.data(m_model.index(0, 0), Qt::UserRole + 100).isValid());
}
void testUpdateIssuedConfigsInfo()
{
m_model.updateModel(QJsonArray { makeCountry("Estonia", "EE", "ee"), makeCountry("Germany", "DE", "de") }, "EE", "EE");
const QJsonArray issuedConfigs {
// Worker timestamp equals download timestamp: not expired.
makeIssuedConfig("EE", "country_config", "2026-10-10T10:00:00", "2026-10-10T10:00:00"),
// Worker timestamp is newer than the download timestamp: expired.
makeIssuedConfig("DE", "country_config", "2026-10-09T10:00:00", "2026-10-10T10:00:00"),
// Non-country configurations must be ignored.
makeIssuedConfig("EE", "other_config", "2026-10-01T10:00:00", "2026-10-20T10:00:00")
};
m_model.updateIssuedConfigsInfo(issuedConfigs);
const QModelIndex estoniaIndex = m_model.index(0, 0);
const QModelIndex germanyIndex = m_model.index(1, 0);
QVERIFY(m_model.data(estoniaIndex, ApiCountryModel::IsIssuedRole).toBool());
QVERIFY(!m_model.data(estoniaIndex, ApiCountryModel::IsWorkerExpiredRole).toBool());
QVERIFY(m_model.data(germanyIndex, ApiCountryModel::IsIssuedRole).toBool());
QVERIFY(m_model.data(germanyIndex, ApiCountryModel::IsWorkerExpiredRole).toBool());
QVERIFY(m_model.hasExpiredWorkerConfigs());
}
void testNonCountryConfigIsIgnored()
{
m_model.updateModel(QJsonArray { makeCountry("Estonia", "EE", "ee") }, "EE", "EE");
m_model.updateIssuedConfigsInfo(QJsonArray { makeIssuedConfig("EE", "other_config", "2026-10-01T10:00:00", "2026-10-20T10:00:00") });
const QModelIndex index = m_model.index(0, 0);
QVERIFY(!m_model.data(index, ApiCountryModel::IsIssuedRole).toBool());
QVERIFY(!m_model.data(index, ApiCountryModel::IsWorkerExpiredRole).toBool());
QVERIFY(!m_model.hasExpiredWorkerConfigs());
}
void testCountryWithoutIssuedConfig()
{
m_model.updateModel(QJsonArray { makeCountry("Estonia", "EE", "ee") }, "EE", "EE");
m_model.updateIssuedConfigsInfo(QJsonArray { });
const QModelIndex index = m_model.index(0, 0);
QVERIFY(!m_model.data(index, ApiCountryModel::IsIssuedRole).toBool());
QVERIFY(!m_model.data(index, ApiCountryModel::IsWorkerExpiredRole).toBool());
QVERIFY(!m_model.hasExpiredWorkerConfigs());
}
void testUpdateModelResetsCountries()
{
m_model.updateModel(QJsonArray { makeCountry("Germany", "DE", "de"), makeCountry("Estonia", "EE", "ee") }, "EE", "EE");
m_model.updateModel(QJsonArray { makeCountry("Finland", "FI", "fi") }, "FI", "FI");
QCOMPARE(m_model.rowCount(), 1);
QCOMPARE(m_model.getCurrentIndex(), 0);
QCOMPARE(m_model.getCurrentCountryCode(), QString("FI"));
QCOMPARE(m_model.getUserCountryCode(), QString("FI"));
QCOMPARE(m_model.data(m_model.index(0, 0), ApiCountryModel::CountryNameRole).toString(), QString("Finland"));
}
};
QTEST_MAIN(TestApiCountryModel)
#include "testApiCountryModel.moc"
-158
View File
@@ -1,158 +0,0 @@
#include <QJsonArray>
#include <QJsonObject>
#include <QSignalSpy>
#include <QTest>
#include "core/utils/constants/apiKeys.h"
#include "ui/models/api/apiDevicesModel.h"
class TestApiDevicesModel : public QObject
{
Q_OBJECT
private:
static QJsonObject createDevice(const QString &installationUuid, const QString &sourceType,
const QString &osVersion, const QString &countryCode, const QString &lastDownloaded)
{
return { { apiDefs::key::installationUuid, installationUuid },
{ apiDefs::key::sourceType, sourceType },
{ apiDefs::key::osVersion, osVersion },
{ apiDefs::key::serverCountryCode, countryCode },
{ apiDefs::key::lastDownloaded, lastDownloaded },
{ apiDefs::key::workerLastUpdated, QStringLiteral("2026-10-01T12:00:00Z") } };
}
private slots:
void testEmptyModel()
{
ApiDevicesModel model;
QCOMPARE(model.rowCount(), 0);
QCOMPARE(model.rowCount(QModelIndex()), 0);
const QJsonArray devices { createDevice(QStringLiteral("device-1"), QStringLiteral("gateway_account"),
QStringLiteral("Windows 11"), QStringLiteral("EE"),
QStringLiteral("2026-10-10T12:00:00Z")) };
model.updateModel(devices, QStringLiteral("device-1"));
QCOMPARE(model.rowCount(), 1);
const QModelIndex index = model.index(0, 0);
QVERIFY(index.isValid());
}
void testUpdateModel()
{
ApiDevicesModel model;
const QString currentUuid = QStringLiteral("device-1");
QJsonArray devices {
createDevice(QStringLiteral("device-1"), QStringLiteral("gateway_account"), QStringLiteral("Windows 11"), QStringLiteral("EE"), QStringLiteral("2026-10-10T12:00:00Z")),
createDevice(QStringLiteral("device-2"), QStringLiteral("gateway_account"), QStringLiteral("Android 15"), QStringLiteral("DE"), QStringLiteral("2026-10-11T12:00:00Z"))
};
model.updateModel(devices, currentUuid);
QCOMPARE(model.rowCount(), 2);
const QModelIndex firstIndex = model.index(0, 0);
const QModelIndex secondIndex = model.index(1, 0);
QVERIFY(firstIndex.isValid());
QVERIFY(secondIndex.isValid());
QCOMPARE(model.data(firstIndex, ApiDevicesModel::OsVersionRole).toString(), QStringLiteral("Windows 11"));
QCOMPARE(model.data(firstIndex, ApiDevicesModel::SupportTagRole).toString(), QStringLiteral("device-1"));
QCOMPARE(model.data(firstIndex, ApiDevicesModel::CountryCodeRole).toString(), QStringLiteral("EE"));
QCOMPARE(model.data(secondIndex, ApiDevicesModel::OsVersionRole).toString(), QStringLiteral("Android 15"));
QCOMPARE(model.data(secondIndex, ApiDevicesModel::SupportTagRole).toString(), QStringLiteral("device-2"));
QCOMPARE(model.data(secondIndex, ApiDevicesModel::CountryCodeRole).toString(), QStringLiteral("DE"));
}
void testCurrentDeviceRole()
{
ApiDevicesModel model;
QJsonArray devices {
createDevice(QStringLiteral("device-1"), QStringLiteral("gateway_account"), QStringLiteral("Windows 11"), QStringLiteral("EE"), QStringLiteral("2026-10-10T12:00:00Z")),
createDevice(QStringLiteral("device-2"), QStringLiteral("gateway_account"), QStringLiteral("Android 15"), QStringLiteral("DE"), QStringLiteral("2026-10-11T12:00:00Z"))
};
model.updateModel(devices, QStringLiteral("device-2"));
QCOMPARE(model.rowCount(), 2);
QCOMPARE(model.data(model.index(0, 0), ApiDevicesModel::IsCurrentDeviceRole).toBool(), false);
QCOMPARE(model.data(model.index(1, 0), ApiDevicesModel::IsCurrentDeviceRole).toBool(), true);
}
void testLastUpdateRole()
{
ApiDevicesModel model;
const QString lastDownloaded = QStringLiteral("2026-10-10T12:00:00Z");
model.updateModel(QJsonArray { createDevice(QStringLiteral("device-1"), QStringLiteral("gateway_account"), QStringLiteral("Windows 11"), QStringLiteral("EE"), lastDownloaded) }, QStringLiteral("device-1"));
const QModelIndex index = model.index(0, 0);
QVERIFY(index.isValid());
const QString expected = QDateTime::fromString(lastDownloaded, Qt::ISODate).toLocalTime().toString(QStringLiteral("d MMM yyyy"));
QCOMPARE(model.data(index, ApiDevicesModel::LastUpdateRole).toString(), expected);
}
void testFiltersNonGatewayDevices()
{
ApiDevicesModel model;
QJsonArray devices {
createDevice(QStringLiteral("gateway-1"), QStringLiteral("gateway_account"), QStringLiteral("Windows 11"), QStringLiteral("EE"), QStringLiteral("2026-10-10T12:00:00Z")),
createDevice(QStringLiteral("other-1"), QStringLiteral("other_source"), QStringLiteral("Linux"), QStringLiteral("DE"), QStringLiteral("2026-10-11T12:00:00Z")),
createDevice(QStringLiteral("gateway-2"), QStringLiteral("gateway_account"), QStringLiteral("Android 15"), QStringLiteral("FI"), QStringLiteral("2026-10-12T12:00:00Z"))
};
model.updateModel(devices, QStringLiteral("gateway-2"));
QCOMPARE(model.rowCount(), 2);
QCOMPARE(model.data(model.index(0, 0), ApiDevicesModel::SupportTagRole).toString(), QStringLiteral("gateway-1"));
QCOMPARE(model.data(model.index(1, 0), ApiDevicesModel::SupportTagRole).toString(), QStringLiteral("gateway-2"));
QCOMPARE(model.data(model.index(1, 0), ApiDevicesModel::IsCurrentDeviceRole).toBool(), true);
}
void testEmptyUpdateClearsModel()
{
ApiDevicesModel model;
QJsonArray devices { createDevice(QStringLiteral("device-1"), QStringLiteral("gateway_account"), QStringLiteral("Windows 11"), QStringLiteral("EE"), QStringLiteral("2026-10-10T12:00:00Z")) };
model.updateModel(devices, QStringLiteral("device-1"));
QCOMPARE(model.rowCount(), 1);
model.updateModel(QJsonArray { }, QString { });
QCOMPARE(model.rowCount(), 0);
}
void testInvalidIndexes()
{
ApiDevicesModel model;
const QModelIndex invalidIndex;
QVERIFY(!model.data(invalidIndex, ApiDevicesModel::OsVersionRole).isValid());
QJsonArray devices { createDevice(QStringLiteral("device-1"), QStringLiteral("gateway_account"), QStringLiteral("Windows 11"), QStringLiteral("EE"), QStringLiteral("2026-10-10T12:00:00Z")) };
model.updateModel(devices, QStringLiteral("device-1"));
QVERIFY(!model.data(model.index(0, 1), ApiDevicesModel::OsVersionRole).isValid());
QVERIFY(!model.data(model.index(-1, 0), ApiDevicesModel::OsVersionRole).isValid());
QVERIFY(!model.data(model.index(1, 0), ApiDevicesModel::OsVersionRole).isValid());
QVERIFY(!model.data(model.index(0, 0), Qt::DisplayRole).isValid());
}
};
QTEST_MAIN(TestApiDevicesModel)
#include "testApiDevicesModel.moc"
Loaded 100 of 321 files, more files were not shown because too many files have changed in this diff. Show more