diff --git a/.gitignore b/.gitignore index 1b97c30..f866cce 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,10 @@ .DS_Store .idea/ +# Mock server state +tests/mock_server/.mock_state.json +tests/mock_server/.mock_screenshots/ + cmake_install.cmake CMakeCache.txt Makefile @@ -21,3 +25,15 @@ Makefile /.ninja_log /ARCDesktopProject_autogen/ /ARCDesktopProject +/Contents/ +/application.log +/database/app_data.db +/build_x64/ +/tests/mock_server/scenarios/pool/ +/.cache/ +/.claude/ +/dumps/ +/ARC_autogen/ +/build_test/ +/build_x64/ +/tests/mock_server/scenarios/ diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..39f5f1f --- /dev/null +++ b/BUILD.md @@ -0,0 +1,81 @@ +# ARCDesktopProject — сборка и запуск + +## Windows (x64) + +### Требования +- Visual Studio 2022 +- Qt 6.9.0 (msvc2022_64) +- vcpkg с пакетом `tesseract:x64-windows` + +### Сборка + +```bash +mkdir build_x64 +cd build_x64 +cmake .. -G "Visual Studio 17 2022" -A x64 -DCMAKE_PREFIX_PATH="C:/Qt/6.9.0/msvc2022_64" -DCMAKE_BUILD_TYPE=Release +cmake --build . --config Release +``` + +### Деплой Qt DLL + +```bash +"C:\Qt\6.9.0\msvc2022_64\bin\windeployqt.exe" .\Release\ARCDesktopProject.exe +``` + +### Копирование vcpkg DLL + +Скопировать все DLL из vcpkg в папку с exe: + +```bash +copy C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\bin\*.dll .\Release\ +``` + +### База данных + +БД не включена в сборку. Перед первым запуском скопировать файл БД: + +```bash +copy database\app_data.db .\Release\database\app_data.db +``` + +Папка `database/` создаётся автоматически при сборке. Путь к БД настраивается в `config.ini` (ключ `db/dbPath`), по умолчанию `./database/app_data.db`. + +### Запуск + +```bash +cd Release +ARCDesktopProject.exe +``` + +--- + +## macOS + +> TODO: инструкция для macOS будет добавлена позже. + +### Требования +- Qt 6 (`brew install qt`) +- Tesseract + Leptonica (`brew install tesseract leptonica`) + +### Сборка + +```bash +mkdir build && cd build +cmake .. -DCMAKE_BUILD_TYPE=Release +cmake --build . +``` + +### База данных + +Перед первым запуском скопировать файл БД: + +```bash +mkdir -p database +cp ../database/app_data.db ./database/app_data.db +``` + +### Запуск + +```bash +./ARCDesktopProject +``` diff --git a/CMakeLists.txt b/CMakeLists.txt index e1683ca..9d59db3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,82 +1,302 @@ cmake_minimum_required(VERSION 3.16) -project(ARCDesktopProject LANGUAGES CXX) +project(ARC LANGUAGES CXX) + +#mkdir build +#cd build +#cmake .. -A x64 -DCMAKE_PREFIX_PATH="C:/Qt/6.9.0/msvc2022_64" -DCMAKE_BUILD_TYPE=Release +#cmake .. -G "Visual Studio 17 2022" -A x64 -DCMAKE_PREFIX_PATH="C:/Qt/6.9.0/msvc2022_64" -DCMAKE_BUILD_TYPE=Release +#cmake --build . --config Release +#"C:\Qt\6.9.0\msvc2022_64\bin\windeployqt.exe" .\Release\ARC.exe set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) +add_compile_definitions(QT_MESSAGELOGCONTEXT) + if (WIN32) - # Укажи путь к Qt6 - set(CMAKE_PREFIX_PATH "C:/Qt/6.9.0/msvc2022_arm64") - list(APPEND CMAKE_PREFIX_PATH "C:/Users/User/.vcpkg-clion/vcpkg/installed/arm64-windows") - #vcpkg install tesseract:arm64-windows - set(Tesseract_DIR "C:/Users/User/.vcpkg-clion/vcpkg/installed/arm64-windows/share/tesseract") + # Определяем целевую архитектуру. + # CMAKE_GENERATOR_PLATFORM (-A flag) — самый надёжный способ определения при кросс-компиляции. + # На ARM64 хосте CMAKE_SYSTEM_PROCESSOR может быть "ARM64" даже при сборке под x64, + # поэтому сначала проверяем явные x64-признаки. + if (CMAKE_GENERATOR_PLATFORM STREQUAL "x64" + OR CMAKE_GENERATOR_PLATFORM STREQUAL "Win64" + OR VCPKG_TARGET_TRIPLET MATCHES "x64") + message(STATUS "Detected x64 target architecture") + set(CMAKE_PREFIX_PATH "C:/Qt/6.9.0/msvc2022_64") + list(APPEND CMAKE_PREFIX_PATH "C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows") + set(Tesseract_DIR "C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/tesseract") + set(VCPKG_BIN_DIR "C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/bin") + elseif (VCPKG_TARGET_TRIPLET MATCHES "arm64" + OR CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64" + OR CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64" + OR CMAKE_CXX_COMPILER MATCHES "arm64") + message(STATUS "Detected ARM64 target architecture") + set(CMAKE_PREFIX_PATH "C:/Qt/6.9.0/msvc2022_arm64") + list(APPEND CMAKE_PREFIX_PATH "C:/Users/User/.vcpkg-clion/vcpkg/installed/arm64-windows") + set(Tesseract_DIR "C:/Users/User/.vcpkg-clion/vcpkg/installed/arm64-windows/share/tesseract") + set(VCPKG_BIN_DIR "C:/Users/User/.vcpkg-clion/vcpkg/installed/arm64-windows/bin") + else () + message(STATUS "Detected x64 target architecture (default)") + set(CMAKE_PREFIX_PATH "C:/Qt/6.9.0/msvc2022_64") + list(APPEND CMAKE_PREFIX_PATH "C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows") + set(Tesseract_DIR "C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/tesseract") + set(VCPKG_BIN_DIR "C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/bin") + endif () find_package(Tesseract CONFIG REQUIRED) elseif (APPLE) # Укажи путь к Qt6 set(CMAKE_PREFIX_PATH "/opt/homebrew/opt/qt") - include_directories("/opt/homebrew/opt/tesseract/include") - link_directories("/opt/homebrew/opt/tesseract/lib") + include_directories("/opt/homebrew/include") + link_directories("/opt/homebrew/lib") endif () find_package(Qt6 REQUIRED COMPONENTS + Network Widgets Sql + Xml ) -file(GLOB_RECURSE DB_SOURCES +file(GLOB_RECURSE DB_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/database/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/database/*.h" ) -file(GLOB_RECURSE VIEW_SOURCES +file(GLOB_RECURSE VIEW_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/views/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/views/*.h" ) -file(GLOB_RECURSE SERVICES_SOURCES +file(GLOB_RECURSE SERVICES_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/services/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/services/*.h" ) -file(GLOB_RECURSE MODELS_SOURCES +file(GLOB_RECURSE MODELS_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/models/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/models/*.h" ) -add_executable(ARCDesktopProject +file(GLOB_RECURSE ANDROID_SOURCES CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/android/*.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/android/*.h" +) + +if (WIN32) +add_executable(ARC WIN32 main.cpp + app.rc ${VIEW_SOURCES} ${SERVICES_SOURCES} ${DB_SOURCES} ${MODELS_SOURCES} + ${ANDROID_SOURCES} + views/bank/BankSettingsWindow.cpp + views/bank/BankSettingsWindow.h + views/bank/BankSetupWizard.cpp + views/bank/BankSetupWizard.h + views/log/FileLogWindow.cpp + views/log/FileLogWindow.h + views/log/LogDetailWindow.cpp + views/log/LogDetailWindow.h + database/dao/GeneralLogDAO.cpp + database/dao/GeneralLogDAO.h + android/dushanbe/CommonScript.cpp + android/dushanbe/CommonScript.h + android/dushanbe/GetCardInfoScript.cpp + android/dushanbe/GetCardInfoScript.h + android/dushanbe/GetLastTransactionsScript.cpp + android/dushanbe/GetLastTransactionsScript.h + android/dushanbe/GetProfileInfoScript.cpp + android/dushanbe/GetProfileInfoScript.h + android/dushanbe/LoginAndCheckAccountsScript.cpp + android/dushanbe/LoginAndCheckAccountsScript.h + android/dushanbe/ScreenXmlParser.cpp + android/dushanbe/ScreenXmlParser.h ) +elseif (APPLE) + add_executable(ARC + main.cpp + ${VIEW_SOURCES} + ${SERVICES_SOURCES} + ${DB_SOURCES} + ${MODELS_SOURCES} + ${ANDROID_SOURCES} + views/bank/BankSettingsWindow.cpp + views/bank/BankSettingsWindow.h + views/bank/BankSetupWizard.cpp + views/bank/BankSetupWizard.h + views/log/FileLogWindow.cpp + views/log/FileLogWindow.h + views/log/LogDetailWindow.cpp + views/log/LogDetailWindow.h + database/dao/GeneralLogDAO.cpp + database/dao/GeneralLogDAO.h + android/dushanbe/CommonScript.cpp + android/dushanbe/CommonScript.h + android/dushanbe/GetCardInfoScript.cpp + android/dushanbe/GetCardInfoScript.h + android/dushanbe/GetLastTransactionsScript.cpp + android/dushanbe/GetLastTransactionsScript.h + android/dushanbe/GetProfileInfoScript.cpp + android/dushanbe/GetProfileInfoScript.h + android/dushanbe/LoginAndCheckAccountsScript.cpp + android/dushanbe/LoginAndCheckAccountsScript.h + android/dushanbe/ScreenXmlParser.cpp + android/dushanbe/ScreenXmlParser.h + ) +endif () if (WIN32) - target_link_libraries(ARCDesktopProject + target_link_libraries(ARC Qt6::Widgets + Qt6::Xml Qt6::Sql + Qt6::Network Tesseract::libtesseract ) # Копирование изображений в директорию сборки - file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/database" DESTINATION "${CMAKE_BINARY_DIR}") +# file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/database/app_data.db" DESTINATION "${CMAKE_BINARY_DIR}/database") + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/tutorials" DESTINATION "${CMAKE_BINARY_DIR}") file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/images" DESTINATION "${CMAKE_BINARY_DIR}") file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/rus.traineddata" DESTINATION "${CMAKE_BINARY_DIR}") + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/eng.traineddata" DESTINATION "${CMAKE_BINARY_DIR}") + # tessdata/ — канонический путь, ожидаемый Tesseract 5+ через TESSDATA_PREFIX/datapath + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/rus.traineddata" DESTINATION "${CMAKE_BINARY_DIR}/tessdata") + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/eng.traineddata" DESTINATION "${CMAKE_BINARY_DIR}/tessdata") + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/config.ini" DESTINATION "${CMAKE_BINARY_DIR}") + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/no_devices.png" DESTINATION "${CMAKE_BINARY_DIR}") + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/assets/ADBKeyboard.apk" DESTINATION "${CMAKE_BINARY_DIR}/assets") + # adb копируется в post-build чтобы не падать при configure если adb.exe занят + file(GLOB _adb_files "${CMAKE_CURRENT_SOURCE_DIR}/tools/adb/windows/*") + foreach(_adb_file ${_adb_files}) + get_filename_component(_adb_fname "${_adb_file}" NAME) + add_custom_command(TARGET ARC POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "$/tools/adb/windows" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_adb_file}" "$/tools/adb/windows/${_adb_fname}" + ) + endforeach() + + add_custom_command(TARGET ARC POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory + "$/database" + + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_CURRENT_SOURCE_DIR}/tutorials" + "$/tutorials" + + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_CURRENT_SOURCE_DIR}/images" + "$/images" + + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_CURRENT_SOURCE_DIR}/rus.traineddata" + "$/rus.traineddata" + + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_CURRENT_SOURCE_DIR}/eng.traineddata" + "$/eng.traineddata" + + COMMAND ${CMAKE_COMMAND} -E make_directory + "$/tessdata" + + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_CURRENT_SOURCE_DIR}/rus.traineddata" + "$/tessdata/rus.traineddata" + + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_CURRENT_SOURCE_DIR}/eng.traineddata" + "$/tessdata/eng.traineddata" + + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_CURRENT_SOURCE_DIR}/config.ini" + "$/config.ini" + + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_CURRENT_SOURCE_DIR}/no_devices.png" + "$/no_devices.png" + + COMMAND ${CMAKE_COMMAND} -E make_directory + "$/assets" + + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_CURRENT_SOURCE_DIR}/assets/ADBKeyboard.apk" + "$/assets/ADBKeyboard.apk" + ) + + # Copy OpenSSL DLLs required for HTTPS (Qt SSL backend) + file(GLOB _openssl_dlls "${VCPKG_BIN_DIR}/libssl*.dll" + "${VCPKG_BIN_DIR}/libcrypto*.dll") + foreach(_dll ${_openssl_dlls}) + add_custom_command(TARGET ARC POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_dll}" "$" + ) + endforeach() + + # Copy mock-server test scripts into the build so they ship with the archive + add_custom_command(TARGET ARC POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_CURRENT_SOURCE_DIR}/tests/mock_server" + "$/tests/mock_server" + ) + + # Copy Tesseract, Leptonica, and their transitive dependency DLLs + file(GLOB _tesseract_dlls "${VCPKG_BIN_DIR}/tesseract*.dll" + "${VCPKG_BIN_DIR}/leptonica*.dll" + "${VCPKG_BIN_DIR}/libpng*.dll" + "${VCPKG_BIN_DIR}/jpeg*.dll" + "${VCPKG_BIN_DIR}/tiff*.dll" + "${VCPKG_BIN_DIR}/zlib*.dll" + "${VCPKG_BIN_DIR}/gif*.dll" + "${VCPKG_BIN_DIR}/libwebp*.dll" + "${VCPKG_BIN_DIR}/libsharpyuv*.dll" + "${VCPKG_BIN_DIR}/openjp2*.dll" + "${VCPKG_BIN_DIR}/archive*.dll" + "${VCPKG_BIN_DIR}/libcurl*.dll" + "${VCPKG_BIN_DIR}/lz4*.dll" + "${VCPKG_BIN_DIR}/zstd*.dll" + "${VCPKG_BIN_DIR}/liblzma*.dll" + "${VCPKG_BIN_DIR}/bz2*.dll" + "${VCPKG_BIN_DIR}/iconv*.dll" + "${VCPKG_BIN_DIR}/charset*.dll" + "${VCPKG_BIN_DIR}/libxml2*.dll") + foreach(_dll ${_tesseract_dlls}) + add_custom_command(TARGET ARC POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_dll}" "$" + ) + endforeach() elseif (APPLE) - target_link_libraries(ARCDesktopProject + target_link_libraries(ARC Qt6::Widgets Qt6::Sql + Qt6::Xml + Qt6::Network tesseract + leptonica ) + + # Копирование изображений в директорию сборки + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/tutorials" DESTINATION "${CMAKE_BINARY_DIR}") + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/images" DESTINATION "${CMAKE_BINARY_DIR}") + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/rus.traineddata" DESTINATION "${CMAKE_BINARY_DIR}") + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/eng.traineddata" DESTINATION "${CMAKE_BINARY_DIR}") + # tessdata/ — канонический путь, ожидаемый Tesseract 5+ через TESSDATA_PREFIX/datapath + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/rus.traineddata" DESTINATION "${CMAKE_BINARY_DIR}/tessdata") + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/eng.traineddata" DESTINATION "${CMAKE_BINARY_DIR}/tessdata") + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/config.ini" DESTINATION "${CMAKE_BINARY_DIR}") + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/no_devices.png" DESTINATION "${CMAKE_BINARY_DIR}") + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/assets/ADBKeyboard.apk" DESTINATION "${CMAKE_BINARY_DIR}/assets") endif () -target_include_directories(ARCDesktopProject PRIVATE +target_include_directories(ARC PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/views ${CMAKE_CURRENT_SOURCE_DIR}/database ${CMAKE_CURRENT_SOURCE_DIR}/services ${CMAKE_CURRENT_SOURCE_DIR}/models + ${CMAKE_CURRENT_SOURCE_DIR}/android ) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..30d217c --- /dev/null +++ b/README.md @@ -0,0 +1,136 @@ +# ARC Desktop + +Десктопное Qt6/C++ приложение для мониторинга и автоматизации Android-приложений мобильных банков через ADB. Принимает задачи с бэкенда (получение профиля/балансов, выписка по транзакциям, создание платежей), исполняет их в реальном приложении банка на подключённом устройстве и возвращает результат. + +> **Репозиторий:** `git@git.brtservice.io:BRT/arc.git` + +--- + +## Быстрый старт + +```bash +git clone git@git.brtservice.io:BRT/arc.git +cd arc + +# macOS +brew install qt tesseract leptonica +mkdir -p build && cd build +cmake .. -DCMAKE_BUILD_TYPE=Release +cmake --build . +./ARCDesktopProject +``` + +Подробнее — см. [BUILD.md](BUILD.md) (macOS + Windows). + +При первом запуске потребуется токен авторизации; настройки лежат в [`config.ini`](config.ini). + +--- + +## Документация + +Все актуальные материалы в каталоге [`docs/`](docs/). Точка входа — [docs/index.md](docs/index.md). + +### Архитектура и фичи + +| Документ | О чём | +|---|---| +| [docs/index.md](docs/index.md) | Общая схема приложения, потоки, технологии | +| [docs/events.md](docs/events.md) | Жизненный цикл задачи: API → БД → выполнение скриптом → отчёт | +| [docs/bank-setup-wizard.md](docs/bank-setup-wizard.md) | `BankSetupWizard` — мастер заведения банковского профиля (Ozon, Black, Dushanbe) | +| [docs/status-sync.md](docs/status-sync.md) | Синхронизация online/offline статусов устройств и сервиса | +| [docs/black_tasks.md](docs/black_tasks.md) | Скрипты для банка Black (Birbank) | +| [docs/ozon_tasks.md](docs/ozon_tasks.md) | Скрипты для Ozon | + +### Backend API + +| Документ | О чём | +|---|---| +| [docs/api/auth.md](docs/api/auth.md) | Логин, refresh, хранение токенов | +| [docs/api/profile.md](docs/api/profile.md) | `bank_profile` и `material` (карты) | +| [docs/api/device.md](docs/api/device.md) | Регистрация десктопов и Android-устройств, отправка скриншотов | +| [docs/api/task.md](docs/api/task.md) | Получение задач, отправка результатов | +| [docs/api/network.md](docs/api/network.md) | Полная карта HTTP-вызовов приложения | +| [docs/api/openapi.json](docs/api/openapi.json) | OpenAPI спецификация бэкенда | + +### Сборка и тесты + +| Документ | О чём | +|---|---| +| [BUILD.md](BUILD.md) | Сборка под macOS и Windows | +| [tests/mock_server/README.md](tests/mock_server/README.md) | Локальный mock-бэкенд для интеграционных тестов | +| [tests/mock_server/WINDOWS.md](tests/mock_server/WINDOWS.md) | Особенности запуска моков на Windows | +| [tutorials/ui_info.md](tutorials/ui_info.md) | Туториал по UI приложения | + +--- + +## Архитектура + +``` +MainWindow (Qt6 GUI) + ├── DeviceScreener (QThread) — опрос ADB-устройств, скриншоты + ├── EventHandler (QThread) — оркестратор задач, запуск bank-скриптов + └── NetworkService (QThread) — REST API + monitoring relay +``` + +``` +android// — автоматизация конкретного банка (ozon, black, dushanbe, ...) + ├── *Script.{h,cpp} — CommonScript, LoginAndCheckAccountsScript, PayByCardScript, GetLastTransactionsScript, ... + └── ScreenXmlParser — парсинг ADB UI dump + +android/adb/ — обёртка над adb (скриншот, tap, swipe, input, uiautomator dump) +android/ocr/ — Tesseract + утилиты поиска текста на скриншоте +android/dump/ — захват UI-дампов + +database/ — SQLite через DatabaseManager (singleton) + DAO-слой + Таблицы: devices → applications → accounts → transactions/events + +models/ — POCO: DeviceInfo, AccountInfo, EventInfo, TransactionInfo, ... +services/ — фоновые потоки (DeviceScreener, EventHandler, NetworkService, AppLogger, LogArchiveSender) +views/ — Qt-виджеты: MainWindow, DeviceWidget, BankSetupWizard, AccountWindow, ... +``` + +Полная диаграмма потоков и сигналов — [docs/index.md](docs/index.md), детально по задачам — [docs/events.md](docs/events.md). + +--- + +## Технологии + +- **Qt 6** (Widgets, Network, Sql, Xml) +- **C++17**, CMake (опционально Ninja) +- **SQLite** (через QtSql) +- **ADB** (платформенные бинари в `tools/adb/`) +- **Tesseract OCR** + Leptonica (`eng.traineddata`, `rus.traineddata`) + +Поддерживаемые банки (в текущем `config.ini`): **Ozon**, **Black** (Birbank), **Dushanbe City Bank**. Список расширяется через `[apps] list=` в [`config.ini`](config.ini). + +--- + +## Конфигурация + +Главный файл — [`config.ini`](config.ini): + +| Секция | Назначение | +|---|---| +| `[common]` | Версия, валюты, токен | +| `[apps] list=` | Активные банки | +| `[db] dbPath=` | Путь к SQLite (`./database/app_data.db`) | +| `[net]` | `apiBase`, пути ручек, мониторинг-релей | +| `[]` | `android_package_name`, `currency`, `country`, `app_version`, ... | + +Схема БД — [`migrations.sql`](migrations.sql) (5 таблиц). + +--- + +## Полезные пути + +- Логи приложения: `application.log` (в корне), архив отправляется через `LogArchiveSender` на `monitoringBase` +- ADB-бинари для Windows: `tools/adb/windows/` +- Тестовые скриншоты/XML: `assets//` +- Иконки и ресурсы: `images/`, `arc.icns`, `arc.ico`, `app.rc` + +--- + +## Скрипты бэкенда и операционные ссылки + +- Backend API (prod): см. `apiBase` в [`config.ini`](config.ini) +- Monitoring relay: `142.93.102.40:8080` — релеит `/monitoring/log` и `/monitoring/dump` в Telegram (systemd `arc-monitoring.service`) diff --git a/android/adb/AdbUtils.cpp b/android/adb/AdbUtils.cpp new file mode 100644 index 0000000..f8063c3 --- /dev/null +++ b/android/adb/AdbUtils.cpp @@ -0,0 +1,786 @@ +#include "AdbUtils.h" +#include +#include +#include +#include +#include +#include +#include + +#include "dump/TaskDumpRecorder.h" + +AdbUtils::AdbUtils(QObject *parent) : QObject(parent) { +} + +QString AdbUtils::adbPath() { +#ifdef Q_OS_WIN + QString path = QCoreApplication::applicationDirPath() + "/tools/adb/windows/adb.exe"; +#else + QString path = QCoreApplication::applicationDirPath() + "/tools/adb/macos/adb"; +#endif + static bool checked = false; + if (!checked) { + checked = true; + QFileInfo fi(path); + if (!fi.exists()) { + qWarning() << "[AdbUtils] adb not found at:" << path; + } else if (!fi.isExecutable()) { + qWarning() << "[AdbUtils] adb is not executable:" << path; + } else { + qDebug() << "[AdbUtils] adb path:" << path; + } + } + return path; +} + +QString AdbUtils::adbDir() { + return QFileInfo(adbPath()).absolutePath(); +} + +bool AdbUtils::startProcess(const QString &program, const QStringList &args, + QString *stdOut, QString *stdErr) { + QProcess process; + process.setWorkingDirectory(adbDir()); + process.start(program, args); + + if (!process.waitForStarted(5000)) { + qWarning() << "[AdbUtils] Failed to start process:" << program << args + << "error:" << process.errorString(); + if (stdErr) { + *stdErr = process.errorString(); + } + return false; + } + + if (!process.waitForFinished(30000) || + process.exitStatus() != QProcess::NormalExit || + process.exitCode() != 0) { + if (stdErr) { + *stdErr = QString::fromUtf8(process.readAllStandardError()); + } + qWarning() << "[AdbUtils] Process failed:" << program << args + << "exitCode:" << process.exitCode() + << "stderr:" << (stdErr ? *stdErr : QString::fromUtf8(process.readAllStandardError())); + return false; + } + + if (stdOut) { + *stdOut = QString::fromUtf8(process.readAllStandardOutput()); + } + return true; +} + +// adb shell dumpsys package ru.rshb.dbo | grep permission +// TODO доделать обработку пермишинов +// adb shell pm revoke ru.rshb.dbo android.permission.WRITE_CONTACTS; adb shell pm revoke ru.rshb.dbo android.permission.READ_CONTACTS; adb shell pm revoke ru.rshb.dbo android.permission.ACCESS_COARSE_LOCATION; adb shell pm revoke ru.rshb.dbo android.permission.ACCESS_FINE_LOCATION + +// adb shell cmd appops set ru.rshb.dbo POST_NOTIFICATION ignore +/** + * Возвращает список найденных устройств в виде пар (ID устройства и Статус) + * @return [(id, status), ...] + */ +QList > AdbUtils::getListDevices() { + const QString adb = adbPath(); + const QString dir = adbDir(); + + // Убиваем чужой adb-сервер (возможно другой версии) и запускаем свой, + // чтобы избежать конфликта версий (например, scrcpy adb 35 vs bundled adb 36) + static bool serverRestarted = false; + if (!serverRestarted) { + serverRestarted = true; + QProcess kill; + kill.setWorkingDirectory(dir); + kill.start(adb, {"kill-server"}); + kill.waitForFinished(5000); + qDebug() << "[AdbUtils] Killed existing adb server"; + + QProcess start; + start.setWorkingDirectory(dir); + start.start(adb, {"start-server"}); + start.waitForFinished(10000); + qDebug() << "[AdbUtils] Started own adb server"; + } + + QProcess process; + process.setWorkingDirectory(dir); + process.start(adb, {"devices"}); + + if (!process.waitForStarted(5000)) { + qWarning() << "[AdbUtils] Failed to start adb devices:" << process.errorString(); + return {}; + } + process.waitForFinished(10000); + + const QString output = process.readAllStandardOutput(); + QList > devices; + + const QStringList lines = output.split('\n'); + for (int i = 1; i < lines.size(); ++i) { + QString line = lines.at(i).trimmed(); + + if (line.isEmpty()) { + continue; + } + /** + * device — устройство готово к использованию. + * offline — устройство подключено, но не может быть использовано. + * unauthorized — устройство не авторизовано для работы с ADB (нужно подтвердить запрос на подключение на самом устройстве). + * recovery — устройство в режиме восстановления. + */ + if (QStringList parts = line.split('\t'); parts.size() == 2) { + const QString &id = parts.at(0); + const QString &status = parts.at(1); + devices.append(qMakePair(id, status)); + } + } + return devices; +} + +QString AdbUtils::tryToGetRunningAppPid(const QString &deviceId, const QString &packageName) { + QProcess process; + process.setWorkingDirectory(adbDir()); + process.start(adbPath(), { + "-s", deviceId, + "shell", "pidof", packageName + }); + if (!process.waitForStarted(5000)) { + qWarning() << "[AdbUtils] Failed to start adb pidof:" << process.errorString(); + return {}; + } + process.waitForFinished(); + QString pid = process.readAllStandardOutput(); + return pid.replace("\n", ""); +} + +// Логирует длинный вывод построчно с префиксом, иначе Qt-логгер режет строку. +static void logMultiline(const char *tag, const QString &text) { + if (text.isEmpty()) { + qDebug().noquote() << tag << "(empty)"; + return; + } + const QStringList lines = text.split(QRegularExpression("\r?\n")); + for (const QString &line : lines) { + if (!line.isEmpty()) qDebug().noquote() << tag << line; + } +} + +// Прогоняет adb-команду и возвращает stdout (stderr игнорируется — этим функциям важен только stdout). +static QString adbShellStdout(const QStringList &args, int timeoutMs = 10000) { + QProcess p; + p.setWorkingDirectory(QFileInfo(AdbUtils::adbPath()).absolutePath()); + p.start(AdbUtils::adbPath(), args); + if (!p.waitForStarted(5000)) return {}; + p.waitForFinished(timeoutMs); + return QString::fromUtf8(p.readAllStandardOutput()); +} + +// Возвращает {launcherActivity, summary}. launcherActivity вида "pkg/.MainActivity" или пустая строка. +static QPair probePackageState(const QString &deviceId, const QString &packageName) { + const QString resolveOut = adbShellStdout( + {"-s", deviceId, "shell", "cmd", "package", "resolve-activity", "--brief", packageName}); + QString launcher; + // Формат: первая строка — приоритет, вторая — "pkg/activity". + for (const QString &raw : resolveOut.split(QRegularExpression("\r?\n"))) { + const QString line = raw.trimmed(); + if (line.contains('/') && line.startsWith(packageName + "/")) { + launcher = line; + break; + } + } + + const QString dump = adbShellStdout( + {"-s", deviceId, "shell", "dumpsys", "package", packageName}, 15000); + + auto pickFlag = [&dump](const QString &key) -> QString { + const QRegularExpression re(key + "=(\\S+)"); + const auto m = re.match(dump); + return m.hasMatch() ? m.captured(1) : QStringLiteral("?"); + }; + const bool installed = dump.contains("Package [" + packageName + "]"); + const QString enabled = pickFlag("enabled"); + const QString stopped = pickFlag("stopped"); + const QString hidden = pickFlag("hidden"); + const bool hasLauncherCategory = dump.contains("android.intent.category.LAUNCHER"); + + const QString summary = QString( + "installed=%1 enabled=%2 stopped=%3 hidden=%4 hasLauncherCategory=%5 resolveActivity=%6") + .arg(installed ? "true" : "false", enabled, stopped, hidden, + hasLauncherCategory ? "true" : "false", + launcher.isEmpty() ? "(none)" : launcher); + return {launcher, summary}; +} + +bool AdbUtils::tryToStartApplication(const QString &deviceId, const QString &packageName) { + // Pre-flight: состояние пакета. Без этого по логу нельзя отличить + // "нет иконки" от "disabled" от "hidden" от "не установлен". + const auto [launcherActivity, stateSummary] = probePackageState(deviceId, packageName); + qDebug().noquote() << "[AdbUtils::tryToStartApplication] device=" << deviceId + << "pkg=" << packageName << "state:" << stateSummary; + + QProcess process; + process.setWorkingDirectory(adbDir()); + process.start(adbPath(), { + "-s", deviceId, + "shell", "monkey", + "-p", packageName, + "-c", "android.intent.category.LAUNCHER", + "1" + }); + if (!process.waitForStarted(5000)) { + qWarning() << "[AdbUtils] Failed to start adb monkey:" << process.errorString(); + return false; + } + process.waitForFinished(); + const QString stdOut = QString::fromUtf8(process.readAllStandardOutput()); + const QString stdErr = QString::fromUtf8(process.readAllStandardError()); + const int exitCode = process.exitCode(); + const QProcess::ExitStatus exitStatus = process.exitStatus(); + + // Полный вывод monkey пишем построчно — Qt-логгер режет длинные QString, + // из-за чего хвост (`Events injected` / `No activities found`) терялся. + qDebug().noquote() << "[AdbUtils::tryToStartApplication] monkey exitStatus=" + << exitStatus << "exitCode=" << exitCode; + logMultiline("[AdbUtils::tryToStartApplication] monkey stdout:", stdOut); + logMultiline("[AdbUtils::tryToStartApplication] monkey stderr:", stdErr); + + if (stdOut.contains("Events injected: 1")) { + return true; + } + + // Фолбэк: am start с конкретной activity. У него осмысленный stderr и exit code, + // в отличие от monkey, который при пустом resolve просто молча печатает "No activities found". + if (!launcherActivity.isEmpty()) { + qDebug().noquote() << "[AdbUtils::tryToStartApplication] monkey не дал Events injected;" + << "пробую am start -n" << launcherActivity; + QProcess am; + am.setWorkingDirectory(adbDir()); + am.start(adbPath(), + {"-s", deviceId, "shell", "am", "start", "-W", "-n", launcherActivity}); + am.waitForStarted(5000); + am.waitForFinished(15000); + const QString amOut = QString::fromUtf8(am.readAllStandardOutput()); + const QString amErr = QString::fromUtf8(am.readAllStandardError()); + qDebug().noquote() << "[AdbUtils::tryToStartApplication] am exitCode=" << am.exitCode(); + logMultiline("[AdbUtils::tryToStartApplication] am stdout:", amOut); + logMultiline("[AdbUtils::tryToStartApplication] am stderr:", amErr); + // `am start -W` печатает "Status: ok" при успехе. + if (am.exitCode() == 0 && amOut.contains("Status: ok")) { + return true; + } + } else { + qWarning() << "[AdbUtils::tryToStartApplication] нет launcher-активити для" << packageName + << "— приложение либо не установлено, либо disabled/hidden, либо без LAUNCHER intent-filter"; + } + + return false; +} + +bool AdbUtils::tryToKillApplication(const QString &deviceId, const QString &packageName) { + QProcess process; + process.setWorkingDirectory(adbDir()); + process.start(adbPath(), { + "-s", deviceId, + "shell", "am", + "force-stop", packageName + }); + if (!process.waitForStarted(5000)) { + qWarning() << "[AdbUtils] Failed to start adb force-stop:" << process.errorString(); + return false; + } + process.waitForFinished(); + return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; +} + +// idx - временный лог +// adb -s 6edc4a47 shell 'uiautomator dump /sdcard/uidump.xml' && adb -s 6edc4a47 pull /sdcard/uidump.xml step_1.xml && adb -s 6edc4a47 shell 'rm /sdcard/uidump.xml' +QString AdbUtils::getScreenDumpAsXml(const QString &deviceId, int idx, bool screenshot) { + const QString adb = adbPath(); + + for (int retry = 0; retry < 3; ++retry) { + // 1. Dump UI hierarchy (retry up to 3 times — uiautomator may be killed by Android) + bool dumpOk = false; + for (int attempt = 0; attempt < 3; ++attempt) { + if (startProcess(adb, {"-s", deviceId, "shell", "uiautomator", "dump", "/sdcard/uidump.xml"})) { + dumpOk = true; + break; + } + qWarning() << "[AdbUtils] uiautomator dump failed, attempt" << (attempt + 1) << "of 3"; + QThread::msleep(1000); + } + if (!dumpOk) { + qWarning() << "Failed to dump UI hierarchy after 3 attempts."; + return {}; + } + // 2. Optionally pull and save the XML file + if (screenshot) { + if (!startProcess(adb, {"-s", deviceId, "pull", "/sdcard/uidump.xml", + QString("step_%1.xml").arg(idx)})) { + qWarning() << "Failed to pull UIDump XML."; + return {}; + } + } + // 3. Read the XML content + QString xmlOut; + if (!startProcess(adb, {"-s", deviceId, "shell", "cat", "/sdcard/uidump.xml"}, &xmlOut)) { + qWarning() << "Failed to read UIDump XML."; + return {}; + } + + // 4. Remove temp XML file + startProcess(adb, {"-s", deviceId, "shell", "rm", "/sdcard/uidump.xml"}); + + // 5. Минимальная санитарная проверка: дамп не пустой, достаточного размера + // и содержит хотя бы одну ноду с непустым text или content-desc. + // Этого хватает, чтобы отсеять WebView-shell-only состояния Ozon + // (там в шелле labeled нод вообще нет), и при этом не отбрасывать + // экраны RN/Compose (Dushanbe, Black), где интерактив строится на + // LinearLayout/View с content-desc, без Button/text/EditText. + static const QRegularExpression reNonEmpty(R"((text|content-desc)="[^"]+")"); + const bool hasContent = reNonEmpty.match(xmlOut).hasMatch(); + if (xmlOut.trimmed().isEmpty() || !xmlOut.contains("saveXml(idx, cleanXml); + } + return cleanXml; + } + + qWarning() << "[AdbUtils] getScreenDumpAsXml: empty dump after 3 retries"; + return {}; +} + +QString AdbUtils::getRawScreenDumpAsXml(const QString &deviceId) { + const QString adb = adbPath(); + + // 1. Дампим иерархию (uiautomator может умирать — до 3 попыток) + bool dumpOk = false; + for (int attempt = 0; attempt < 3; ++attempt) { + if (startProcess(adb, {"-s", deviceId, "shell", "uiautomator", "dump", "/sdcard/uidump.xml"})) { + dumpOk = true; + break; + } + QThread::msleep(500); + } + if (!dumpOk) { + qWarning() << "[AdbUtils] getRawScreenDumpAsXml: uiautomator dump failed"; + return {}; + } + + // 2. Читаем + QString xmlOut; + if (!startProcess(adb, {"-s", deviceId, "shell", "cat", "/sdcard/uidump.xml"}, &xmlOut)) { + qWarning() << "[AdbUtils] getRawScreenDumpAsXml: cat failed"; + return {}; + } + startProcess(adb, {"-s", deviceId, "shell", "rm", "/sdcard/uidump.xml"}); + + // 3. Обрезка к началу XML; в отличие от getScreenDumpAsXml — без фильтра по контенту, + // оператору важно видеть РЕАЛЬНОЕ состояние экрана (PIN, сплэш, WebView и т.д.). + QByteArray xmlBytes = xmlOut.toUtf8(); + if (const int start = xmlBytes.indexOf(" 0) args << QString::number(durationMs); + + QProcess process; + process.setWorkingDirectory(adbDir()); + process.start(adbPath(), args); + if (!process.waitForStarted(5000)) { + qWarning() << "[AdbUtils] Failed to start adb swipe:" << process.errorString(); + return false; + } + process.waitForFinished(); + return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; +} + +bool AdbUtils::inputText(const QString &deviceId, const QString &text) { + QProcess process; + process.setWorkingDirectory(adbDir()); + process.start(adbPath(), { + "-s", deviceId, + "shell", "input", + "text", text + }); + if (!process.waitForStarted(5000)) { + qWarning() << "[AdbUtils] Failed to start adb input text:" << process.errorString(); + return false; + } + process.waitForFinished(); + return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; +} + +bool AdbUtils::hideKeyboard(const QString &deviceId) { + // Метод 1: KEYCODE_ESCAPE (111) — работает на большинстве Android-устройств, + // но игнорируется на некоторых прошивках (например, TECNO/HiOS). + { + QProcess p; + p.setWorkingDirectory(adbDir()); + p.start(adbPath(), {"-s", deviceId, "shell", "input", "keyevent", "111"}); + if (p.waitForStarted(5000)) p.waitForFinished(3000); + } + // Метод 2: KEYCODE_BACK (4) — Android посылает BACK сначала в IME; если + // IME (клавиатура) видим — он его потребляет и закрывает, не уходя + // на предыдущий экран. На TECNO/HiOS это часто единственный рабочий + // вариант. Если клавиатура уже скрыта (после метода 1), BACK уйдёт + // в Activity — нежелательно. Поэтому сначала проверим через dumpsys + // input_method, видна ли всё ещё клавиатура. + bool imeVisible = false; + { + QProcess p; + p.setWorkingDirectory(adbDir()); + p.start(adbPath(), {"-s", deviceId, "shell", "dumpsys", "input_method"}); + if (p.waitForStarted(5000) && p.waitForFinished(5000)) { + const QString out = QString::fromUtf8(p.readAllStandardOutput()); + imeVisible = out.contains(QStringLiteral("mInputShown=true")) + || out.contains(QStringLiteral("mServedInputConnection=")) // fallback heuristic + && !out.contains(QStringLiteral("mInputShown=false")); + } + } + if (imeVisible) { + qDebug() << "[AdbUtils] hideKeyboard: ESC didn't dismiss IME, trying BACK"; + QProcess p; + p.setWorkingDirectory(adbDir()); + p.start(adbPath(), {"-s", deviceId, "shell", "input", "keyevent", "4"}); + if (p.waitForStarted(5000)) p.waitForFinished(3000); + } + return true; +} + +bool AdbUtils::goBack(const QString &deviceId) { + QProcess process; + process.setWorkingDirectory(adbDir()); + process.start(adbPath(), { + "-s", deviceId, + "shell", "input", + "keyevent", "4" + }); + if (!process.waitForStarted(5000)) { + qWarning() << "[AdbUtils] Failed to start adb keyevent:" << process.errorString(); + return false; + } + process.waitForFinished(); + return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; +} + +QSet AdbUtils::getListApps(const QString &deviceId) { + QProcess process; + process.setWorkingDirectory(adbDir()); + process.start(adbPath(), { + "-s", deviceId, + "shell", "pm list", + "packages", "-3" + }); + if (!process.waitForStarted(5000)) { + qWarning() << "[AdbUtils] Failed to start adb pm list:" << process.errorString(); + return {}; + } + process.waitForFinished(); + + const QString output = process.readAllStandardOutput(); + QSet apps; + + const QStringList lines = output.split("\n"); + for (int i = 0; i < lines.size(); ++i) { + QString line = lines.at(i).trimmed().replace("package:", ""); + + if (line.isEmpty()) { + continue; + } + apps.insert(line); + } + return apps; +} + +QString AdbUtils::getDeviceTimezone(const QString &deviceId) { + QString out; + if (!startProcess(adbPath(), {"-s", deviceId, "shell", "getprop", "persist.sys.timezone"}, &out)) { + return {}; + } + return out.trimmed(); +} + +QString AdbUtils::getAppVersion(const QString &deviceId, const QString &packageName) { + if (deviceId.isEmpty() || packageName.isEmpty()) { + return {}; + } + QString out; + if (!startProcess(adbPath(), + {"-s", deviceId, "shell", "dumpsys", "package", packageName}, + &out)) { + return {}; + } + static const QRegularExpression re(R"(versionName=([^\s]+))"); + const QRegularExpressionMatch m = re.match(out); + return m.hasMatch() ? m.captured(1).trimmed() : QString(); +} + +void AdbUtils::unlockScreen(const QString &deviceId, const int screenWidth, const int screenHeight) { + const QString adb = adbPath(); + + // Проверяем, включён ли экран (mWakefulness=Awake) + QString powerState; + startProcess(adb, {"-s", deviceId, "shell", "dumpsys", "power"}, &powerState); + const bool isAwake = powerState.contains("mWakefulness=Awake"); + + if (!isAwake) { + // Включаем экран + startProcess(adb, {"-s", deviceId, "shell", "input", "keyevent", "KEYCODE_WAKEUP"}); + QThread::msleep(500); + qDebug() << "[AdbUtils] unlockScreen: woke up device" << deviceId; + } + + // Проверяем, залочен ли экран (mDreamingSleepToken / mShowingLockscreen) + QString wmState; + startProcess(adb, {"-s", deviceId, "shell", "dumpsys", "window"}, &wmState); + const bool isLocked = wmState.contains("mDreamingSleepToken") || wmState.contains("mShowingLockscreen=true") + || wmState.contains("StatusBar"); + + if (isLocked) { + // Свайп вверх для разблокировки + const int x = screenWidth / 2; + makeSwipe(deviceId, x, screenHeight * 3 / 4, x, screenHeight / 4); + QThread::msleep(500); + qDebug() << "[AdbUtils] unlockScreen: swiped to unlock" << deviceId; + } +} diff --git a/android/adb/AdbUtils.h b/android/adb/AdbUtils.h new file mode 100644 index 0000000..154ff6f --- /dev/null +++ b/android/adb/AdbUtils.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include + +class AdbUtils final : public QObject { + Q_OBJECT + +public: + explicit AdbUtils(QObject *parent = nullptr); + + static QString adbPath(); + + static QList > getListDevices(); + + static QString tryToGetRunningAppPid(const QString &deviceId, const QString &packageName); + + static bool tryToStartApplication(const QString &deviceId, const QString &packageName); + + static bool tryToKillApplication(const QString &deviceId, const QString &packageName); + + static QString getScreenDumpAsXml(const QString &deviceId, int idx, bool screenshot = false); + + // Сырой дамп без фильтра "useful UI" — для ручного превью оператора: + // отдаёт что есть на экране, даже если это PIN-пад / сплэш / WebView-shell. + static QString getRawScreenDumpAsXml(const QString &deviceId); + + static bool takeScreenshot(const QString &deviceId, const QString &name); + + // Захватывает скриншот напрямую в память (PNG-байты), без временного файла + static QByteArray captureScreenshotBytes(const QString &deviceId); + + static bool takeXmlDump(const QString &deviceId, const QString &name); + + static bool makeTap(const QString &deviceId, int x, int y); + + static bool enableAdbIMEKeyboard(const QString &deviceId); + + static bool inputAdbIMEText(const QString &deviceId, const QString &text); + + static bool disableAdbIMEKeyboard(const QString &deviceId); + + static bool makeSwipe(const QString &deviceId, int x1, int y1, int x2, int y2); + static bool makeSwipe(const QString &deviceId, int x1, int y1, int x2, int y2, int durationMs); + + static bool inputText(const QString &deviceId, const QString &text); + + static bool hideKeyboard(const QString &deviceId); + + static bool goBack(const QString &deviceId); + + static void unlockScreen(const QString &deviceId, int screenWidth, int screenHeight); + + static QSet getListApps(const QString &deviceId); + + // Возвращает таймзону устройства (напр. "Asia/Bangkok"), либо пустую строку при ошибке + static QString getDeviceTimezone(const QString &deviceId); + + // Возвращает versionName установленного пакета (напр. "19.17.0"), либо пустую строку при ошибке/отсутствии + static QString getAppVersion(const QString &deviceId, const QString &packageName); + +private: + static QString adbDir(); + static bool startProcess(const QString &program, const QStringList &args, + QString *stdOut = nullptr, QString *stdErr = nullptr); +}; diff --git a/android/black/CommonScript.cpp b/android/black/CommonScript.cpp new file mode 100644 index 0000000..8cdf649 --- /dev/null +++ b/android/black/CommonScript.cpp @@ -0,0 +1,293 @@ +#include "CommonScript.h" + +#include +#include +#include +#include +#include +#include + +#include "adb/AdbUtils.h" +#include "dao/DeviceDAO.h" +#include "dao/BankProfileDAO.h" +#include "net/NetworkService.h" + +namespace Black { + +CommonScript::CommonScript(QObject *parent) + : QObject(parent) { +} + +CommonScript::~CommonScript() = default; + +void CommonScript::start() { + doStart(); + emit finished(); +} + +Node CommonScript::swipeToButton( + const QString &deviceId, + const QString &text, int &counter, const int width, const int height +) { + for (int i = 0; i < 10; ++i) { + if (Node button = xmlScreenParser.findButtonNode(text, true); button.x() > 0 && button.y() > 0) { + return button; + } + const int x = width / 2; + const int offset = height / 4; + AdbUtils::makeSwipe(deviceId, x, height - offset, x, offset); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++counter)); + } + return {}; +} + +bool CommonScript::inputPinCode(const QString &deviceId, const QString &pinCode) { + // BlackBanx использует EditText с password=true, а не кнопочную сетку + Node editText = xmlScreenParser.findPinCodeEditText(); + if (editText.isEmpty()) { + qWarning() << "[Black::inputPinCode] PIN EditText not found"; + return false; + } + + // Тапаем по полю для фокуса + AdbUtils::makeTap(deviceId, editText.x(), editText.y()); + QThread::msleep(500); + + // Вводим PIN через текстовый ввод + AdbUtils::inputText(deviceId, pinCode); + QThread::msleep(500); + + return true; +} + +bool CommonScript::runAppAndGoToHomeScreen( + const QString &deviceId, + const QString &packageName, + const QString &pinCode, + const int width, + const int height +) { + // Разблокируем экран перед запуском + AdbUtils::unlockScreen(deviceId, width, height); + + // Убить приложение если запущено + if (const QString pid = AdbUtils::tryToGetRunningAppPid(deviceId, packageName); !pid.isEmpty()) { + qDebug() << "[Black] Process already running, pid:" << pid; + AdbUtils::getScreenDumpAsXml(deviceId, 1000); + AdbUtils::tryToKillApplication(deviceId, packageName); + } + if (!AdbUtils::tryToStartApplication(deviceId, packageName)) { + AdbUtils::takeScreenshot(deviceId, "run_app_error"); + qWarning() << "[Black] Process has not started"; + return false; + } + + // Даем время на запуск + QThread::msleep(4500); + int counter = 0; + bool findAndInputPincode = false; + + while (true) { + counter++; + + if (counter > 10) { + AdbUtils::takeScreenshot(deviceId, "too_many_attempts"); + return false; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + + if (xmlScreenParser.isHomeScreen()) { + return true; + } + + // Проверяем экран ввода PIN-кода + if (!findAndInputPincode && xmlScreenParser.isPinCodeScreen()) { + inputPinCode(deviceId, pinCode); + findAndInputPincode = true; + QThread::msleep(3000); + continue; + } + + QThread::msleep(1000); + } +} + +void CommonScript::postAccountsData(const QList> &accounts) { + if (accounts.isEmpty()) { + return; + } + + auto *thread = new QThread; + auto *worker = new NetworkService; + + QJsonArray list; + for (const QPair &pair : accounts) { + QJsonObject obj; + MaterialInfo account = pair.first; + obj["appName"] = account.appCode; + obj["lastNumbers"] = account.lastNumbers; + obj["amount"] = account.amount; + obj["description"] = account.description; + obj["currency"] = account.currency; + obj["status"] = materialStatusToString(account.status); + list.append(obj); + } + + worker->setDeviceId(accounts.first().first.deviceId); + worker->setPayload(QJsonDocument(list).toJson()); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &NetworkService::postAccountsData); + connect(worker, &NetworkService::finished, thread, &QThread::quit); + connect(worker, &NetworkService::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +void CommonScript::postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction) { + auto *thread = new QThread; + auto *worker = new NetworkService; + + QJsonObject obj; + obj["id"] = transaction.id; + obj["amount"] = transaction.amount; + obj["name"] = transaction.name; + obj["fee"] = transaction.fee; + obj["status"] = transactionStatusToString(transaction.status); + obj["type"] = transactionTypeToString(transaction.type); + obj["phone"] = transaction.phone; + obj["description"] = transaction.description; + obj["updatedDescription"] = transaction.updatedDescription; + obj["bankName"] = transaction.bankName; + obj["bankTime"] = transaction.bankTime; + obj["bankTransactionId"] = transaction.bankTrExternalId; + + worker->setDeviceId(account.deviceId); + worker->addExtra("lastNumbers", account.lastNumbers); + worker->addExtra("transactionId", transaction.id); + worker->setPayload(QJsonDocument(obj).toJson()); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &NetworkService::insertTransactionData); + connect(worker, &NetworkService::finished, thread, &QThread::quit); + connect(worker, &NetworkService::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +void CommonScript::updateTransactionData( + const MaterialInfo &account, + const int transactionId, + const TransactionStatus status, + const QString &oldDesc, + const QString &newDesc +) { + auto *thread = new QThread; + auto *worker = new NetworkService; + + QJsonObject obj; + obj["id"] = transactionId; + obj["status"] = transactionStatusToString(status); + obj["description"] = newDesc; + obj["updatedDescription"] = oldDesc; + + worker->setDeviceId(account.deviceId); + worker->addExtra("lastNumbers", account.lastNumbers); + worker->setPayload(QJsonDocument(obj).toJson()); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &NetworkService::updateTransactionData); + connect(worker, &NetworkService::finished, thread, &QThread::quit); + connect(worker, &NetworkService::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +QString CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance) { + const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode); + + if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) { + qWarning() << "[Black] createBankProfileIfNeeded: skipping — fullName or phone is empty"; + return {}; + } + + const QMap currencyCodes = { + {"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972} + }; + const int currencyCode = currencyCodes.value(app.currency.toUpper(), 840); + const QStringList parts = app.fullName.split(' ', Qt::SkipEmptyParts); + + // Нормализуем телефон: оставляем только цифры, добавляем код страны и "+" + const QSettings settings("config.ini", QSettings::IniFormat); + const QString phoneCode = settings.value(appCode + "/phone_code").toString(); + QString phone = app.phone; + phone.remove(QRegularExpression("[^0-9]")); + if (!phoneCode.isEmpty() && !phone.startsWith(phoneCode)) { + if (phone.startsWith('8') && phoneCode != "8") { + phone.replace(0, 1, phoneCode); + } else { + phone.prepend(phoneCode); + } + } + phone.prepend('+'); + + const DeviceInfo device = DeviceDAO::getDeviceById(deviceId); + + QJsonObject profileBody; + profileBody["first_name"] = parts.value(0); + profileBody["middle_name"] = parts.size() >= 3 ? parts.value(1) : QString(""); + profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1); + profileBody["bank_name"] = appCode; + profileBody["phone_number"] = phone; + const double balance = (parsedBalance >= 0.0) ? parsedBalance : app.amount; + profileBody["balance"] = QString::number(qRound64(balance * 100)); + profileBody["state"] = (app.status == "active") ? "enabled" : "disabled"; + profileBody["device_id"] = device.apiId; + profileBody["currency"] = currencyCode; + + NetworkService ns; + ns.setDeviceId(deviceId); + ns.setPayload(QJsonDocument(profileBody).toJson()); + + if (app.bankProfileId.isEmpty()) { + ns.addExtra("app_id", app.id); + ns.postBankProfile(); + const BankProfileInfo afterPost = BankProfileDAO::getApplication(deviceId, appCode); + if (afterPost.bankProfileId.isEmpty()) { + const QString err = "postBankProfile failed for " + deviceId + " " + appCode; + qWarning() << "[Black] createBankProfileIfNeeded:" << err; + return err; + } + qDebug() << "[Black] createBankProfileIfNeeded: postBankProfile done"; + } else { + ns.addExtra("bank_profile_id", app.bankProfileId); + ns.patchBankProfile(); + qDebug() << "[Black] createBankProfileIfNeeded: patchBankProfile done"; + } + return {}; +} + +void CommonScript::stop() { + m_running = false; +} + +bool CommonScript::closeGooglePlayBannerIfVisible( + const QString &deviceId, const int width, const int height +) { + // 1. Системный диалог запроса разрешения Android (permissioncontroller) — тапаем "ЗАПРЕТИТЬ" + if (Node denyBtn = xmlScreenParser.tryToFindSystemPermissionDenyButton(); !denyBtn.isEmpty()) { + qDebug() << "[Black] System permission dialog detected mid-task, tapping 'ЗАПРЕТИТЬ'..."; + AdbUtils::makeTap(deviceId, denyBtn.x(), denyBtn.y()); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + return true; + } + + // 2. Google Play in-app update bottom sheet + Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(width, height); + if (gpNode.isEmpty()) return false; + qDebug() << "[Black] Google Play update banner detected mid-task, closing..."; + AdbUtils::makeTap(deviceId, gpNode.x(), gpNode.y()); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + return true; +} + +} // namespace Black diff --git a/android/black/CommonScript.h b/android/black/CommonScript.h new file mode 100644 index 0000000..d061d29 --- /dev/null +++ b/android/black/CommonScript.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include "ScreenXmlParser.h" +#include "db/TransactionInfo.h" + +namespace Black { + +class CommonScript : public QObject { + Q_OBJECT + +public: + explicit CommonScript(QObject *parent = nullptr); + + ~CommonScript() override; + +protected: + virtual void doStart() = 0; + + // Ввод PIN-кода через EditText (BlackBanx использует текстовое поле, а не кнопки) + bool inputPinCode(const QString &deviceId, const QString &pinCode); + + bool runAppAndGoToHomeScreen( + const QString &deviceId, + const QString &packageName, + const QString &pinCode, + int width, + int height + ); + + Node swipeToButton( + const QString &deviceId, + const QString &text, + int &counter, int width, int height + ); + + void postAccountsData(const QList> &accounts); + + void postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction); + + void updateTransactionData( + const MaterialInfo &account, + int transactionId, + TransactionStatus status, + const QString &oldDesc, + const QString &newDesc + ); + + // Создаёт bank_profile на сервере если ещё не создан. + // Возвращает пустую строку при успехе, текст ошибки при провале. + QString createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance = -1.0); + + // Если виден баннер Google Play "Доступно обновление" или системный + // диалог разрешений — закрывает его и перепарсивает XML. Безопасно + // вызывать на любом экране; возвращает true если что-то было закрыто. + bool closeGooglePlayBannerIfVisible( + const QString &deviceId, int width, int height + ); + + bool m_running = true; + ScreenXmlParser xmlScreenParser; + int m_counter = 0; + +signals: + void finished(); + + void finishedWithResult(const QString &result); + +public slots: + virtual void start() final; + + virtual void stop() final; + +private: + Q_DISABLE_COPY(CommonScript); +}; + +} // namespace Black diff --git a/android/black/GetCardInfoScript.cpp b/android/black/GetCardInfoScript.cpp new file mode 100644 index 0000000..181a9d4 --- /dev/null +++ b/android/black/GetCardInfoScript.cpp @@ -0,0 +1,164 @@ +#include "GetCardInfoScript.h" + +#include + +#include +#include + +#include "adb/AdbUtils.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "dump/TaskDumpRecorder.h" +#include "net/NetworkService.h" + +namespace Black { + +GetCardInfoScript::GetCardInfoScript( + QString deviceId, + QString appCode, + QObject *parent +) : CommonScript(parent), + m_deviceId(std::move(deviceId)), + m_appCode(std::move(appCode)) { +} + +GetCardInfoScript::~GetCardInfoScript() = default; + +void GetCardInfoScript::doStart() { + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + const TaskDumpMeta dumpMeta{}; + TaskDumpRecorder::Scope dumpScope("black", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error); + + // Читаем текущий экран + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + + // Если не на домашнем экране — перезапускаем + if (!xmlScreenParser.isHomeScreen()) { + qDebug() << "[Black::GetCardInfo] Not on home screen, restarting app..."; + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, + device.screenWidth, device.screenHeight)) { + m_error = "Cannot reach home screen: " + device.name + " (" + m_deviceId + ") " + m_appCode; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + } + + // 1. Парсим баланс с домашнего экрана + const double balance = xmlScreenParser.parseBalanceFromHomeScreen(); + qDebug() << "[Black::GetCardInfo] balance from home:" << balance; + BankProfileDAO::updateAmount(app.id, balance); + + // 2. Нажимаем "Tu CVU" на домашнем экране + Node cvuButton = xmlScreenParser.findButtonNode("Tu CVU", false); + if (cvuButton.isEmpty()) { + m_error = "Cannot find Tu CVU button: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + AdbUtils::makeTap(m_deviceId, cvuButton.x(), cvuButton.y()); + QThread::msleep(2000); + + // 3. Ждём экран CVU + bool cvuScreenFound = false; + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + if (xmlScreenParser.isCVUScreen()) { + cvuScreenFound = true; + break; + } + QThread::msleep(1000); + } + + if (!cvuScreenFound) { + m_error = "CVU screen not detected: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + // 4. Парсим номер CVU + const QString cvuNumber = xmlScreenParser.parseCVUNumber(); + qDebug() << "[Black::GetCardInfo] CVU number:" << cvuNumber; + + if (cvuNumber.isEmpty()) { + m_error = "Cannot parse CVU number: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + // 5. Сохраняем аккаунт в БД (lastNumbers = последние 4 цифры CVU) + const QString lastNumbers = cvuNumber.right(4); + + MaterialInfo account; + account.deviceId = device.id; + account.appCode = m_appCode; + account.lastNumbers = lastNumbers; + account.cardNumber = cvuNumber; + account.amount = balance; + account.currency = "ARS"; + account.description = "CVU"; + account.status = MaterialStatus::Off; + account.updateTime = QDateTime::currentDateTimeUtc(); + + if (!MaterialDAO::upsertAccount(account)) { + qWarning() << "[Black::GetCardInfo] Failed to upsert account"; + } + + // 6. Отправляем материал на сервер (если ещё нет material_id) + const MaterialInfo savedAccount = MaterialDAO::findAppAccount(device.id, m_appCode, lastNumbers); + const BankProfileInfo updatedApp = BankProfileDAO::getApplication(device.id, m_appCode); + + if (savedAccount.id != -1 && !updatedApp.bankProfileId.isEmpty()) { + if (savedAccount.materialId.isEmpty()) { + NetworkService ns; + ns.setDeviceId(m_deviceId); + + const QMap currencyCodes = { + {"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972} + }; + QJsonObject materialBody; + materialBody["bank_profile_id"] = updatedApp.bankProfileId; + materialBody["card_number"] = savedAccount.cardNumber; + materialBody["name"] = updatedApp.fullName; + materialBody["state"] = materialStatusToString(savedAccount.status) == "active" ? "enabled" : "disabled"; + materialBody["currency"] = currencyCodes.value(savedAccount.currency.toUpper(), 32); + ns.setPayload(QJsonDocument(materialBody).toJson()); + ns.addExtra("account_id", savedAccount.id); + ns.postMaterial(); + qDebug() << "[Black::GetCardInfo] postMaterial done for CVU" << lastNumbers; + } else { + qDebug() << "[Black::GetCardInfo] material already exists for CVU" << lastNumbers << ", skipping"; + } + } else { + qWarning() << "[Black::GetCardInfo] bankProfileId empty or account not found, skipping material"; + } + + qDebug() << "[Black::GetCardInfo] Done. CVU:" << cvuNumber << "balance:" << balance; + + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); +} + +} // namespace Black diff --git a/android/black/GetCardInfoScript.h b/android/black/GetCardInfoScript.h new file mode 100644 index 0000000..af4c7d5 --- /dev/null +++ b/android/black/GetCardInfoScript.h @@ -0,0 +1,27 @@ +#pragma once +#include "CommonScript.h" + +namespace Black { + +class GetCardInfoScript final : public CommonScript { + Q_OBJECT + +public: + explicit GetCardInfoScript( + QString deviceId, + QString appCode, + QObject *parent = nullptr + ); + + ~GetCardInfoScript() override; + +protected: + void doStart() override; + +private: + QString m_deviceId; + const QString m_appCode; + QString m_error; +}; + +} // namespace Black diff --git a/android/black/GetLastTransactionsScript.cpp b/android/black/GetLastTransactionsScript.cpp new file mode 100644 index 0000000..952e7ce --- /dev/null +++ b/android/black/GetLastTransactionsScript.cpp @@ -0,0 +1,166 @@ +#include "GetLastTransactionsScript.h" + +#include +#include + +#include "adb/AdbUtils.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "dao/TransactionDAO.h" +#include "AppLogger.h" + +namespace Black { + +GetLastTransactionsScript::GetLastTransactionsScript( + QString deviceId, + QString appCode, + QObject *parent +) : CommonScript(parent), + m_deviceId(std::move(deviceId)), m_appCode(std::move(appCode)) { +} + +GetLastTransactionsScript::~GetLastTransactionsScript() = default; + +void GetLastTransactionsScript::doStart() { + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Если не на домашнем экране — перезапускаем приложение + if (!xmlScreenParser.isHomeScreen()) { + qDebug() << "[Black::GetLastTransactions] Not on home screen, restarting app..."; + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, + device.screenWidth, device.screenHeight)) { + m_error = "Cannot reach home screen: " + device.name + " (" + m_deviceId + ")"; + qWarning() << m_error; + AppLogger::log("black/GetLastTransactions", m_deviceId, m_error, {}, "FETCH_TRANSACTIONS"); + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + + // Тапаем "Ver más" рядом с "Tu actividad" для перехода на экран транзакций + Node verMasBtn = xmlScreenParser.findVerMasAfterTuActividad(); + if (verMasBtn.isEmpty()) { + // Попробуем проскроллить вниз чтобы увидеть "Tu actividad" + "Ver más" + for (int i = 0; i < 5; ++i) { + const int x = device.screenWidth / 2; + const int offset = device.screenHeight / 4; + AdbUtils::makeSwipe(m_deviceId, x, device.screenHeight - offset, x, offset); + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + verMasBtn = xmlScreenParser.findVerMasAfterTuActividad(); + if (!verMasBtn.isEmpty()) break; + } + } + + if (verMasBtn.isEmpty()) { + m_error = "Button 'Ver más' not found: " + m_deviceId; + qWarning() << m_error; + emit finishedWithResult(m_error); + return; + } + + qDebug() << "[Black::GetLastTransactions] Tapping 'Ver más' at" << verMasBtn.x() << verMasBtn.y(); + AdbUtils::makeTap(m_deviceId, verMasBtn.x(), verMasBtn.y()); + QThread::msleep(2000); + + // Ждём загрузки экрана транзакций + // Сперва появляется экран загрузки (Tu Actividad без транзакций), + // затем подгружаются транзакции + bool loaded = false; + for (int attempt = 0; attempt < 15; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + if (xmlScreenParser.isTransactionListScreen()) { + loaded = true; + break; + } + if (xmlScreenParser.isTransactionLoadingScreen()) { + qDebug() << "[Black::GetLastTransactions] Loading screen detected, waiting for transactions, attempt" << attempt + 1 << "/ 15"; + } else { + qDebug() << "[Black::GetLastTransactions] Waiting for transaction screen, attempt" << attempt + 1 << "/ 15"; + } + QThread::msleep(1500); + } + + if (!loaded) { + // Проверяем: если экран загрузки есть, но транзакций нет — это нормально (пустой аккаунт) + if (xmlScreenParser.isTransactionLoadingScreen()) { + qDebug() << "[Black::GetLastTransactions] No transactions found (empty account)"; + AdbUtils::goBack(m_deviceId); + QThread::msleep(1000); + emit finishedWithResult(m_error); + return; + } + m_error = "Transaction list screen not loaded: " + m_deviceId; + qWarning() << m_error; + emit finishedWithResult(m_error); + return; + } + + // Находим accountId для сохранения транзакций + int accountId = -1; + const QList accounts = MaterialDAO::getAccounts(device.id, m_appCode); + if (!accounts.isEmpty()) { + accountId = accounts.first().id; + qDebug() << "[Black::GetLastTransactions] Using accountId:" << accountId; + } else { + qWarning() << "[Black::GetLastTransactions] No account found for" << m_deviceId << m_appCode; + } + + // Берём все транзакции что видны на экране + const QTimeZone argTz("America/Argentina/Buenos_Aires"); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + QList pageItems = xmlScreenParser.parseTransactionItems(); + + qDebug() << "[Black::GetLastTransactions] Found" << pageItems.size() << "transactions on screen"; + + for (const TransactionInfo &tx : pageItems) { + TransactionInfo saveTx = tx; + saveTx.bankName = "black"; + saveTx.status = TransactionStatus::Complete; + saveTx.timestamp = QDateTime::currentDateTimeUtc(); + + // Парсим дату из bankTime: "21 de febrero de 2026 - 09:39" + QDateTime parsed = xmlScreenParser.parseSpanishDate(saveTx.bankTime); + if (parsed.isValid()) { + parsed.setTimeZone(argTz); + saveTx.completeTime = parsed; + } + + qDebug() << "[Black::GetLastTransactions]" + << "date:" << saveTx.bankTime + << "desc:" << saveTx.description + << "name:" << saveTx.name + << "amount:" << (saveTx.amount >= 0 ? "+" : "") + QString::number(saveTx.amount, 'f', 2); + + // Сохраняем в БД + if (accountId != -1) { + saveTx.accountId = accountId; + if (TransactionDAO::insertTransactionIfNotExists(saveTx)) { + qDebug() << "[Black::GetLastTransactions] Saved transaction to DB"; + } + } + } + + qDebug() << "[Black::GetLastTransactions] Total transactions:" << pageItems.size(); + + // Возвращаемся на домашний экран + AdbUtils::goBack(m_deviceId); + QThread::msleep(1000); + + emit finishedWithResult(m_error); +} + +} // namespace Black diff --git a/android/black/GetLastTransactionsScript.h b/android/black/GetLastTransactionsScript.h new file mode 100644 index 0000000..c0a7698 --- /dev/null +++ b/android/black/GetLastTransactionsScript.h @@ -0,0 +1,27 @@ +#pragma once +#include "CommonScript.h" + +namespace Black { + +class GetLastTransactionsScript final : public CommonScript { + Q_OBJECT + +public: + explicit GetLastTransactionsScript( + QString deviceId, + QString appCode, + QObject *parent = nullptr + ); + + ~GetLastTransactionsScript() override; + +protected: + void doStart() override; + +private: + QString m_deviceId; + const QString m_appCode; + QString m_error; +}; + +} // namespace Black diff --git a/android/black/GetProfileInfoScript.cpp b/android/black/GetProfileInfoScript.cpp new file mode 100644 index 0000000..23bf45e --- /dev/null +++ b/android/black/GetProfileInfoScript.cpp @@ -0,0 +1,188 @@ +#include "GetProfileInfoScript.h" + +#include +#include +#include + +#include "adb/AdbUtils.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "dump/TaskDumpRecorder.h" +#include "net/NetworkService.h" + +namespace Black { + +GetProfileInfoScript::GetProfileInfoScript( + QString deviceId, + QString appCode, + QObject *parent +) : CommonScript(parent), + m_deviceId(std::move(deviceId)), + m_appCode(std::move(appCode)) { +} + +GetProfileInfoScript::~GetProfileInfoScript() = default; + +void GetProfileInfoScript::doStart() { + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + const TaskDumpMeta dumpMeta{}; + TaskDumpRecorder::Scope dumpScope("black", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error); + + // Читаем текущий экран + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Если не на домашнем экране — перезапускаем + if (!xmlScreenParser.isHomeScreen()) { + qDebug() << "[Black::GetProfileInfo] Not on home screen, restarting app..."; + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, + device.screenWidth, device.screenHeight)) { + m_error = "Cannot reach home screen: " + device.name + " (" + m_deviceId + ") " + m_appCode; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + + // 1. Тапаем по кнопке "¡Hola!" (аватар с именем) на домашнем экране + Node holaNode = xmlScreenParser.findHolaButton(); + if (holaNode.isEmpty()) { + m_error = "Cannot find Hola button on home screen: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + // Парсим имя и баланс с домашнего экрана + const QString fullName = xmlScreenParser.parseUserNameFromHomeScreen(); + const double balance = xmlScreenParser.parseBalanceFromHomeScreen(); + qDebug() << "[Black::GetProfileInfo] fullName from home:" << fullName << "balance:" << balance; + + // Сохраняем баланс + BankProfileDAO::updateAmount(app.id, balance); + + AdbUtils::makeTap(m_deviceId, holaNode.x(), holaNode.y()); + QThread::msleep(2000); + + // 2. Ждём боковое меню + bool sideMenuFound = false; + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + if (xmlScreenParser.isSideMenu()) { + sideMenuFound = true; + break; + } + QThread::msleep(1000); + } + + if (!sideMenuFound) { + m_error = "Side menu not detected: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + // 3. Тапаем "Tu Información" + Node tuInfoNode = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Tu Información")); + if (tuInfoNode.isEmpty()) { + m_error = "Cannot find Tu Información button: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + AdbUtils::makeTap(m_deviceId, tuInfoNode.x(), tuInfoNode.y()); + QThread::msleep(2500); + + // 4. Ждём страницу профиля "Tu información" + bool profileFound = false; + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + if (xmlScreenParser.isProfilePage()) { + profileFound = true; + break; + } + QThread::msleep(1000); + } + + if (!profileFound) { + m_error = "Profile page not detected: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + // 5. Парсим данные профиля (первый экран: nombre, apellido, email) + const QString nombre = xmlScreenParser.parseProfileNombre(); + const QString apellido = xmlScreenParser.parseProfileApellido(); + const QString email = xmlScreenParser.parseProfileEmail(); + + // 6. Скроллим вниз чтобы увидеть телефон + const int x = device.screenWidth / 2; + const int fromY = device.screenHeight * 3 / 4; + const int toY = device.screenHeight / 4; + AdbUtils::makeSwipe(m_deviceId, x, fromY, x, toY); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + const QString phone = xmlScreenParser.parseProfilePhone(); + + // Собираем полное имя: "имя фамилия" + QString parsedFullName; + if (!nombre.isEmpty() && !apellido.isEmpty()) { + parsedFullName = nombre + " " + apellido; + } else if (!nombre.isEmpty()) { + parsedFullName = nombre; + } else { + parsedFullName = fullName; // fallback с домашнего экрана + } + + qDebug() << "[Black::GetProfileInfo] nombre:" << nombre + << "apellido:" << apellido + << "email:" << email + << "phone:" << phone + << "fullName:" << parsedFullName; + + // 7. Сохраняем в БД + if (!BankProfileDAO::updateProfileInfo(app.id, parsedFullName, phone, email)) { + qWarning() << "[Black::GetProfileInfo] Failed to save profile info to DB"; + } + + // 7. Создаём bank profile если нет + if (const QString err = createBankProfileIfNeeded(m_deviceId, m_appCode); !err.isEmpty()) { + m_error = err; + emit finishedWithResult(m_error); + return; + } + + // 8. Отправляем данные аккаунтов на сервер если нет material_id + const QList allAccounts = MaterialDAO::getAccounts(device.id, m_appCode); + QList> accountsToPost; + for (const auto &acc : allAccounts) { + if (acc.materialId.isEmpty()) { + accountsToPost.append({acc, Node()}); + } + } + if (!accountsToPost.isEmpty()) { + postAccountsData(accountsToPost); + } + + emit finishedWithResult(m_error); +} + +} // namespace Black diff --git a/android/black/GetProfileInfoScript.h b/android/black/GetProfileInfoScript.h new file mode 100644 index 0000000..26a76b4 --- /dev/null +++ b/android/black/GetProfileInfoScript.h @@ -0,0 +1,27 @@ +#pragma once +#include "CommonScript.h" + +namespace Black { + +class GetProfileInfoScript final : public CommonScript { + Q_OBJECT + +public: + ~GetProfileInfoScript() override; + + explicit GetProfileInfoScript( + QString deviceId, + QString appCode, + QObject *parent = nullptr + ); + +protected: + void doStart() override; + +private: + QString m_deviceId; + const QString m_appCode; + QString m_error; +}; + +} // namespace Black diff --git a/android/black/LoginAndCheckAccountsScript.cpp b/android/black/LoginAndCheckAccountsScript.cpp new file mode 100644 index 0000000..2596222 --- /dev/null +++ b/android/black/LoginAndCheckAccountsScript.cpp @@ -0,0 +1,68 @@ +#include "LoginAndCheckAccountsScript.h" + +#include + +#include "adb/AdbUtils.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "dump/TaskDumpRecorder.h" +#include "AppLogger.h" +#include "GetProfileInfoScript.h" + +namespace Black { + +LoginAndCheckAccountsScript::LoginAndCheckAccountsScript( + QString deviceId, + QString appCode, + QObject *parent, + bool pinCheckOnly +) : CommonScript(parent), + m_deviceId(std::move(deviceId)), + m_appCode(std::move(appCode)), + m_pinCheckOnly(pinCheckOnly) { +} + +LoginAndCheckAccountsScript::~LoginAndCheckAccountsScript() = default; + +void LoginAndCheckAccountsScript::doStart() { + // m_deviceId = ADB serial, device.id = android_id (для БД) + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + const TaskDumpMeta dumpMeta{}; + TaskDumpRecorder::Scope dumpScope("black", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { + m_error = "Cant open the home screen: " + device.name + " (" + m_deviceId + ") " + m_appCode; + qWarning() << m_error; + AppLogger::log("black/LoginAndCheck", m_deviceId, m_error, {}, "FETCH_PROFILE"); + BankProfileDAO::updateComment(app.id, + "Не прошла проверка " + QDateTime::currentDateTime().toString("dd.MM.yyyy HH:mm:ss")); + + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + if (m_pinCheckOnly) { + emit finishedWithResult(m_error); + return; + } + + // Проверяем профиль и аккаунты (приложение уже на home screen) + GetProfileInfoScript profileScript(m_deviceId, m_appCode); + profileScript.start(); + + emit finishedWithResult(m_error); +} + +} // namespace Black diff --git a/android/black/LoginAndCheckAccountsScript.h b/android/black/LoginAndCheckAccountsScript.h new file mode 100644 index 0000000..43c327d --- /dev/null +++ b/android/black/LoginAndCheckAccountsScript.h @@ -0,0 +1,29 @@ +#pragma once +#include "CommonScript.h" + +namespace Black { + +class LoginAndCheckAccountsScript final : public CommonScript { + Q_OBJECT + +public: + ~LoginAndCheckAccountsScript() override; + + explicit LoginAndCheckAccountsScript( + QString deviceId, + QString appCode, + QObject *parent = nullptr, + bool pinCheckOnly = false + ); + +protected: + void doStart() override; + +private: + QString m_deviceId; + const QString m_appCode; + const bool m_pinCheckOnly; + QString m_error; +}; + +} // namespace Black diff --git a/android/black/PayByCardScript.cpp b/android/black/PayByCardScript.cpp new file mode 100644 index 0000000..041f1ea --- /dev/null +++ b/android/black/PayByCardScript.cpp @@ -0,0 +1,384 @@ +#include "PayByCardScript.h" + +#include +#include + +#include "adb/AdbUtils.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "dao/TransactionDAO.h" +#include "AppLogger.h" + +namespace Black { + +PayByCardScript::PayByCardScript( + MaterialInfo account, + QString cardNumber, + double amount, + QObject *parent +) : CommonScript(parent), + m_account(std::move(account)), + m_cardNumber(std::move(cardNumber)), + m_amount(amount) { +} + +PayByCardScript::~PayByCardScript() = default; + +void PayByCardScript::doStart() { + const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId); + const BankProfileInfo app = BankProfileDAO::getApplication(m_account.deviceId, m_account.appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_account.deviceId; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + makePayment(device.adbSerial, app.pinCode, device.screenWidth, device.screenHeight); + emit finishedWithResult(m_error); +} + +void PayByCardScript::makePayment( + const QString &deviceId, + const QString &pinCode, + const int width, + const int height +) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + + // 0. Убеждаемся что на домашнем экране + if (!xmlScreenParser.isHomeScreen()) { + qDebug() << "[Black::PayByCard] Not on home screen, restarting app..."; + const BankProfileInfo app = BankProfileDAO::getApplication(m_account.deviceId, m_account.appCode); + if (!runAppAndGoToHomeScreen(deviceId, app.package, pinCode, width, height)) { + m_error = "Cannot reach home screen: " + DeviceDAO::getDeviceById(m_account.deviceId).name + " (" + deviceId + ")"; + qWarning() << m_error; + AppLogger::log("black/PayByCard", deviceId, m_error, {}, "CREATE_TRANSACTION"); + AdbUtils::tryToKillApplication(deviceId, app.package); + return; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + } + + // 1. Тапаем "Enviar a\nCBU o alias" на домашнем экране + Node enviarBtn = xmlScreenParser.findNodeByContentDesc("Enviar a\nCBU o alias", true); + if (enviarBtn.isEmpty()) { + // Попробуем проскроллить + for (int i = 0; i < 5; ++i) { + const int x = width / 2; + const int offset = height / 4; + AdbUtils::makeSwipe(deviceId, x, height - offset, x, offset); + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + enviarBtn = xmlScreenParser.findNodeByContentDesc("Enviar a\nCBU o alias", true); + if (!enviarBtn.isEmpty()) break; + } + } + + if (enviarBtn.isEmpty()) { + m_error = "Button 'Enviar a CBU o alias' not found: " + deviceId; + qWarning() << m_error; + AppLogger::log("black/PayByCard", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + qDebug() << "[Black::PayByCard] Tapping 'Enviar a CBU o alias'"; + AdbUtils::makeTap(deviceId, enviarBtn.x(), enviarBtn.y()); + QThread::msleep(2000); + + // 2. Ждём экран ввода номера карты (CBU/CVU/Alias) + bool onInputScreen = false; + for (int attempt = 0; attempt < 10; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + if (xmlScreenParser.isPayInputCardNumberScreen()) { + onInputScreen = true; + break; + } + qDebug() << "[Black::PayByCard] Waiting for input card screen, attempt" << attempt + 1; + QThread::msleep(1500); + } + + if (!onInputScreen) { + m_error = "Input card number screen not loaded: " + deviceId; + qWarning() << m_error; + AppLogger::log("black/PayByCard", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + // Вводим номер карты в EditText + Node editText = xmlScreenParser.findEditTextByHint("Ingresar CBU, CVU o Alias"); + if (editText.isEmpty()) { + editText = xmlScreenParser.findFirstEditText(); + } + if (editText.isEmpty()) { + m_error = "EditText for card number not found: " + deviceId; + qWarning() << m_error; + return; + } + + AdbUtils::makeTap(deviceId, editText.x(), editText.y()); + QThread::msleep(500); + AdbUtils::inputText(deviceId, m_cardNumber); + QThread::msleep(1000); + + // Нажимаем "Continuar" + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + Node continuarBtn = xmlScreenParser.findNodeByContentDesc("Continuar"); + if (continuarBtn.isEmpty()) { + continuarBtn = xmlScreenParser.findButtonNode("Continuar", false); + } + if (continuarBtn.isEmpty()) { + m_error = "Button 'Continuar' not found on input card screen: " + deviceId; + qWarning() << m_error; + return; + } + + qDebug() << "[Black::PayByCard] Tapping 'Continuar' after card number input"; + AdbUtils::makeTap(deviceId, continuarBtn.x(), continuarBtn.y()); + QThread::msleep(2000); + + // 3. Ждём экран подтверждения получателя (pay_transfer_confirm) + bool onTransferConfirm = false; + for (int attempt = 0; attempt < 10; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + if (xmlScreenParser.isPayTransferConfirmScreen()) { + onTransferConfirm = true; + break; + } + qDebug() << "[Black::PayByCard] Waiting for transfer confirm screen, attempt" << attempt + 1; + QThread::msleep(1500); + } + + if (!onTransferConfirm) { + m_error = "Transfer confirm screen not loaded: " + deviceId; + qWarning() << m_error; + AppLogger::log("black/PayByCard", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + // Нажимаем "Transferir" + Node transferirBtn = xmlScreenParser.findNodeByContentDesc("Transferir"); + if (transferirBtn.isEmpty()) { + transferirBtn = xmlScreenParser.findButtonNode("Transferir", false); + } + if (transferirBtn.isEmpty()) { + m_error = "Button 'Transferir' not found on transfer confirm screen: " + deviceId; + qWarning() << m_error; + return; + } + + qDebug() << "[Black::PayByCard] Tapping 'Transferir' on confirm screen"; + AdbUtils::makeTap(deviceId, transferirBtn.x(), transferirBtn.y()); + QThread::msleep(2000); + + // 4. Ждём экран ввода суммы (pay_input_sum) + bool onInputSum = false; + for (int attempt = 0; attempt < 10; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + if (xmlScreenParser.isPayInputSumScreen()) { + onInputSum = true; + break; + } + qDebug() << "[Black::PayByCard] Waiting for input sum screen, attempt" << attempt + 1; + QThread::msleep(1500); + } + + if (!onInputSum) { + m_error = "Input sum screen not loaded: " + deviceId; + qWarning() << m_error; + AppLogger::log("black/PayByCard", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + // Вводим сумму в EditText + Node sumEditText = xmlScreenParser.findFirstEditText(); + if (sumEditText.isEmpty()) { + m_error = "EditText for sum not found: " + deviceId; + qWarning() << m_error; + return; + } + + AdbUtils::makeTap(deviceId, sumEditText.x(), sumEditText.y()); + QThread::msleep(500); + + // Удаляем содержимое поля (там "0,00") — удаляем пока не станет "0,00" или пусто + for (int d = 0; d < 20; ++d) { + QProcess proc; + proc.start(AdbUtils::adbPath(), {"-s", deviceId, "shell", "input", "keyevent", "67"}); + proc.waitForFinished(2000); + } + QThread::msleep(500); + + // Проверяем что осталось в поле после очистки + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + Node afterClear = xmlScreenParser.findFirstEditText(); + qDebug() << "[Black::PayByCard] After clear, field text:" << afterClear.text; + + // Поле работает как ввод в центах: каждая цифра сдвигает значение влево + // Для 500,00 нужно ввести "50000" + const QString amountStr = QString::number(static_cast(m_amount * 100)); + AdbUtils::inputText(deviceId, amountStr); + QThread::msleep(1000); + + // Проверяем что сумма ввелась верно + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + Node currentEdit = xmlScreenParser.findFirstEditText(); + qDebug() << "[Black::PayByCard] Sum field text:" << currentEdit.text; + + // Если сумма неверная — пробуем ещё раз: удаляем всё и вводим заново + const QString expectedAmount = QString::number(static_cast(m_amount)); + QString cleanText = currentEdit.text.trimmed(); + cleanText.remove('.'); + if (cleanText.endsWith(",00")) cleanText.chop(3); + if (cleanText != expectedAmount) { + qDebug() << "[Black::PayByCard] Sum mismatch (" << cleanText << "!=" << expectedAmount << "), retrying..."; + AdbUtils::makeTap(deviceId, currentEdit.x(), currentEdit.y()); + QThread::msleep(300); + for (int d = 0; d < 20; ++d) { + QProcess proc; + proc.start(AdbUtils::adbPath(), {"-s", deviceId, "shell", "input", "keyevent", "67"}); + proc.waitForFinished(2000); + } + QThread::msleep(500); + AdbUtils::inputText(deviceId, amountStr); + QThread::msleep(1000); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + currentEdit = xmlScreenParser.findFirstEditText(); + qDebug() << "[Black::PayByCard] Sum field text (retry):" << currentEdit.text; + } + + // Нажимаем "Continuar" + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + Node continuarSumBtn = xmlScreenParser.findNodeByContentDesc("Continuar"); + if (continuarSumBtn.isEmpty()) { + continuarSumBtn = xmlScreenParser.findButtonNode("Continuar", false); + } + if (continuarSumBtn.isEmpty()) { + m_error = "Button 'Continuar' not found on input sum screen: " + deviceId; + qWarning() << m_error; + return; + } + + qDebug() << "[Black::PayByCard] Tapping 'Continuar' after sum input"; + AdbUtils::makeTap(deviceId, continuarSumBtn.x(), continuarSumBtn.y()); + QThread::msleep(2000); + + // 5. Ждём экран финального подтверждения (pay_confirm) + bool onPayConfirm = false; + for (int attempt = 0; attempt < 10; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + if (xmlScreenParser.isPayConfirmScreen()) { + onPayConfirm = true; + break; + } + qDebug() << "[Black::PayByCard] Waiting for pay confirm screen, attempt" << attempt + 1; + QThread::msleep(1500); + } + + if (!onPayConfirm) { + m_error = "Pay confirm screen not loaded: " + deviceId; + qWarning() << m_error; + AppLogger::log("black/PayByCard", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + // Парсим имя получателя из "Vas a transferir \na ИМЯ" + const QString recipientName = xmlScreenParser.parseRecipientNameFromConfirm(); + qDebug() << "[Black::PayByCard] Recipient name:" << recipientName; + + // Нажимаем "Transferir" для финального подтверждения + Node finalTransferirBtn = xmlScreenParser.findNodeByContentDesc("Transferir"); + if (finalTransferirBtn.isEmpty()) { + finalTransferirBtn = xmlScreenParser.findButtonNode("Transferir", false); + } + if (finalTransferirBtn.isEmpty()) { + m_error = "Button 'Transferir' not found on pay confirm screen: " + deviceId; + qWarning() << m_error; + return; + } + + qDebug() << "[Black::PayByCard] Tapping final 'Transferir'"; + AdbUtils::makeTap(deviceId, finalTransferirBtn.x(), finalTransferirBtn.y()); + QThread::msleep(3000); + + // 5.5. Проверяем экран ввода PIN-кода + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + if (xmlScreenParser.isPinCodeScreen()) { + qDebug() << "[Black::PayByCard] PIN code screen detected, entering PIN..."; + inputPinCode(deviceId, pinCode); + QThread::msleep(3000); + } + + // 6. Ждём экран успеха (pay_success) — "Dinero enviado!" + bool onSuccess = false; + for (int attempt = 0; attempt < 15; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + if (xmlScreenParser.isPaySuccessScreen()) { + onSuccess = true; + break; + } + // Если PIN-код появился позже — вводим + if (xmlScreenParser.isPinCodeScreen()) { + qDebug() << "[Black::PayByCard] PIN code screen detected (retry), entering PIN..."; + inputPinCode(deviceId, pinCode); + QThread::msleep(3000); + continue; + } + qDebug() << "[Black::PayByCard] Waiting for success screen, attempt" << attempt + 1; + QThread::msleep(2000); + } + + if (!onSuccess) { + m_error = "Pay success screen not loaded: " + deviceId; + qWarning() << m_error; + AppLogger::log("black/PayByCard", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + qDebug() << "[Black::PayByCard] Payment successful!"; + + // Парсим данные с экрана успеха + const QString bankTime = xmlScreenParser.parsePaySuccessDateTime(); + const QString coelsaId = xmlScreenParser.parsePaySuccessCoelsaId(); + qDebug() << "[Black::PayByCard] DateTime:" << bankTime << "Id Coelsa:" << coelsaId; + + // Делаем скриншот экрана успеха как receipt + const QByteArray receiptImage = AdbUtils::captureScreenshotBytes(deviceId); + + // Сохраняем транзакцию + TransactionInfo tx; + tx.accountId = m_account.id; + tx.amount = -m_amount; // исходящий перевод + tx.fee = 0; + tx.status = TransactionStatus::Complete; + tx.type = TransactionType::Card; + tx.name = recipientName; + tx.description = "Transferencia a " + recipientName; + tx.phone = m_cardNumber; + tx.bankName = "black"; + tx.bankTime = bankTime; + tx.bankTrExternalId = coelsaId; + tx.timestamp = QDateTime::currentDateTimeUtc(); + tx.completeTime = QDateTime::currentDateTimeUtc(); + tx.receiptImage = receiptImage; + + const int txId = TransactionDAO::insertTransaction(tx); + if (txId != -1) { + qDebug() << "[Black::PayByCard] Transaction saved to DB, id:" << txId; + if (!receiptImage.isEmpty()) { + TransactionDAO::updateReceiptImage(txId, receiptImage); + } + } else { + qWarning() << "[Black::PayByCard] Failed to save transaction to DB"; + } +} + +} // namespace Black diff --git a/android/black/PayByCardScript.h b/android/black/PayByCardScript.h new file mode 100644 index 0000000..66e4577 --- /dev/null +++ b/android/black/PayByCardScript.h @@ -0,0 +1,36 @@ +#pragma once +#include "CommonScript.h" + +namespace Black { + +class PayByCardScript final : public CommonScript { + Q_OBJECT + +public: + PayByCardScript( + MaterialInfo account, + QString cardNumber, + double amount, + QObject *parent = nullptr + ); + + ~PayByCardScript() override; + +protected: + void doStart() override; + +private: + const MaterialInfo m_account; + const QString m_cardNumber; + const double m_amount; + QString m_error; + + void makePayment( + const QString &deviceId, + const QString &pinCode, + int width, + int height + ); +}; + +} // namespace Black diff --git a/android/black/ScreenXmlParser.cpp b/android/black/ScreenXmlParser.cpp new file mode 100644 index 0000000..b3b2df4 --- /dev/null +++ b/android/black/ScreenXmlParser.cpp @@ -0,0 +1,578 @@ +#include "ScreenXmlParser.h" + +#include +#include +#include +#include + +#include "android/xml/Node.h" + +namespace Black { + +static const QRegularExpression boundsRegex(R"(\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\])"); + +ScreenXmlParser::ScreenXmlParser(QObject *parent) : QObject(parent) { +} + +ScreenXmlParser::~ScreenXmlParser() = default; + +void ScreenXmlParser::parseAndSaveXml(const QString &xml) { + QDomDocument doc; + doc.setContent(xml); + m_xml = doc.documentElement(); + m_nodes.clear(); + + QXmlStreamReader reader(xml); + while (!reader.atEnd()) { + reader.readNext(); + if (reader.isStartElement() && reader.name() == u"node") { + Node node; + if (reader.attributes().hasAttribute("bounds")) { + QString bounds = reader.attributes().value("bounds").toString(); + if (QRegularExpressionMatch match = boundsRegex.match(bounds); match.hasMatch()) { + node.setBounds( + match.captured(1).toInt(), + match.captured(2).toInt(), + match.captured(3).toInt(), + match.captured(4).toInt() + ); + } + } + if (reader.attributes().hasAttribute("text")) { + node.text = reader.attributes().value("text").toString(); + } + if (reader.attributes().hasAttribute("content-desc")) { + node.contentDesc = reader.attributes().value("content-desc").toString(); + } + if (reader.attributes().hasAttribute("class")) { + node.className = reader.attributes().value("class").toString(); + } + if (reader.attributes().hasAttribute("clickable")) { + node.clickable = reader.attributes().value("clickable").toString() == "true"; + } + if (reader.attributes().hasAttribute("focusable")) { + node.focusable = reader.attributes().value("focusable").toString() == "true"; + } + if (reader.attributes().hasAttribute("resource-id")) { + node.resourceId = reader.attributes().value("resource-id").toString(); + } + if (reader.attributes().hasAttribute("hint")) { + node.hint = reader.attributes().value("hint").toString(); + } + if (reader.attributes().hasAttribute("password")) { + node.password = reader.attributes().value("password").toString() == "true"; + } + m_nodes.append(node); + } + } +} + +const QList &ScreenXmlParser::findAllNodes() const { + return m_nodes; +} + +Node ScreenXmlParser::findTextNode(const QString &text) { + for (const Node &node : m_nodes) { + if (node.text.trimmed() == text) { + return node; + } + } + return {}; +} + +Node ScreenXmlParser::findNodeByContentDesc(const QString &contentDesc) { + for (const Node &node : m_nodes) { + if (node.contentDesc == contentDesc) { + return node; + } + } + return {}; +} + +Node ScreenXmlParser::findNodeByContentDesc(const QString &contentDesc, bool contains) { + for (const Node &node : m_nodes) { + if (contains) { + if (node.contentDesc.contains(contentDesc)) { + return node; + } + } else { + if (node.contentDesc == contentDesc) { + return node; + } + } + } + return {}; +} + +Node ScreenXmlParser::findButtonNode(const QString &text, bool contains) { + for (const Node &node : m_nodes) { + if (!node.clickable) continue; + if (contains) { + if (node.text.contains(text) || node.contentDesc.contains(text)) { + return node; + } + } else { + if (node.text.trimmed() == text || node.contentDesc == text) { + return node; + } + } + } + return {}; +} + +Node ScreenXmlParser::findFirstEditText() { + for (const Node &node : m_nodes) { + if (node.className == "android.widget.EditText") { + return node; + } + } + return {}; +} + +bool ScreenXmlParser::isPinCodeScreen() { + for (const Node &node : m_nodes) { + if (node.contentDesc.contains(QString::fromUtf8("¿Olvidaste tu PIN?"))) { + return true; + } + if (node.contentDesc.contains(QString::fromUtf8("Ingresá tu PIN"))) { + return true; + } + } + return false; +} + +Node ScreenXmlParser::findPinCodeEditText() { + for (const Node &node : m_nodes) { + if (node.className == "android.widget.EditText" && node.password) { + return node; + } + } + return {}; +} + +bool ScreenXmlParser::isHomeScreen() { + bool hasSaldo = false; + bool hasInicio = false; + for (const Node &node : m_nodes) { + if (node.contentDesc == "Saldo disponible") { + hasSaldo = true; + } + if (node.contentDesc == "Inicio") { + hasInicio = true; + } + if (hasSaldo && hasInicio) { + return true; + } + } + return false; +} + +bool ScreenXmlParser::isSideMenu() { + bool hasTuInfo = false; + bool hasCerrar = false; + for (const Node &node : m_nodes) { + if (node.contentDesc == QString::fromUtf8("Tu Información")) { + hasTuInfo = true; + } + if (node.contentDesc == QString::fromUtf8("Cerrar sesión")) { + hasCerrar = true; + } + if (hasTuInfo && hasCerrar) { + return true; + } + } + return false; +} + +Node ScreenXmlParser::findHolaButton() { + for (const Node &node : m_nodes) { + if (node.clickable && node.contentDesc.startsWith(QString::fromUtf8("¡Hola!"))) { + return node; + } + } + return {}; +} + +QString ScreenXmlParser::parseUserNameFromHomeScreen() { + // content-desc: "¡Hola!\nveronica cintia heredia \nvc" + for (const Node &node : m_nodes) { + if (node.contentDesc.startsWith(QString::fromUtf8("¡Hola!"))) { + QStringList lines = node.contentDesc.split('\n'); + if (lines.size() >= 2) { + return lines[1].trimmed(); + } + } + } + return {}; +} + +double ScreenXmlParser::parseBalanceFromHomeScreen() { + // Баланс идёт после "Saldo disponible", формат: "$1.857,00" или "$0,00" + bool afterSaldo = false; + for (const Node &node : m_nodes) { + if (node.contentDesc == "Saldo disponible") { + afterSaldo = true; + continue; + } + if (afterSaldo && node.contentDesc.startsWith("$")) { + // "$1.857,00" → "1857.00" + QString raw = node.contentDesc.mid(1); // убираем "$" + raw.remove('.'); // убираем разделитель тысяч + raw.replace(',', '.'); // запятая → точка + bool ok = false; + double val = raw.toDouble(&ok); + if (ok) return val; + } + } + return 0.0; +} + +bool ScreenXmlParser::isProfilePage() { + for (const Node &node : m_nodes) { + if (node.contentDesc == QString::fromUtf8("Tu información")) { + return true; + } + } + return false; +} + +QString ScreenXmlParser::parseProfileNombre() { + for (const Node &node : m_nodes) { + if (node.hint == "Nombre" && !node.text.isEmpty()) { + return node.text.trimmed(); + } + } + return {}; +} + +QString ScreenXmlParser::parseProfileApellido() { + for (const Node &node : m_nodes) { + if (node.hint == "Apellido" && !node.text.isEmpty()) { + return node.text.trimmed(); + } + } + return {}; +} + +QString ScreenXmlParser::parseProfileEmail() { + for (const Node &node : m_nodes) { + if (node.hint == "E-mail" && !node.text.isEmpty()) { + return node.text.trimmed(); + } + } + return {}; +} + +QString ScreenXmlParser::parseProfilePhone() { + for (const Node &node : m_nodes) { + if (node.hint == QString::fromUtf8("Teléfono celular") && !node.text.isEmpty()) { + return node.text.trimmed(); + } + } + return {}; +} + +bool ScreenXmlParser::isCVUScreen() { + for (const Node &node : m_nodes) { + if (node.contentDesc == "CVU") { + return true; + } + } + return false; +} + +QString ScreenXmlParser::parseCVUNumber() { + // CVU номер идёт после content-desc="CVU", это 22-значный номер + bool afterCVU = false; + for (const Node &node : m_nodes) { + if (node.contentDesc == "CVU") { + afterCVU = true; + continue; + } + if (afterCVU && !node.contentDesc.isEmpty()) { + // Проверяем что это числовая строка (CVU — 22 цифры) + QString cvu = node.contentDesc.trimmed(); + bool allDigits = true; + for (const QChar &c : cvu) { + if (!c.isDigit()) { + allDigits = false; + break; + } + } + if (allDigits && !cvu.isEmpty()) { + return cvu; + } + } + } + return {}; +} + +Node ScreenXmlParser::findVerMasAfterTuActividad() { + // "Ver más" идёт сразу после "Tu actividad" на домашнем экране + bool afterTuActividad = false; + for (const Node &node : m_nodes) { + if (node.contentDesc == "Tu actividad") { + afterTuActividad = true; + continue; + } + if (afterTuActividad && node.contentDesc == QString::fromUtf8("Ver más") && node.clickable) { + return node; + } + // Если встретили другую секцию — прекращаем поиск + if (afterTuActividad && node.contentDesc == "Tarjetas") { + break; + } + } + return {}; +} + +bool ScreenXmlParser::isTransactionLoadingScreen() { + // Экран загрузки: заголовок "Tu Actividad" есть, но транзакций нет + // Может содержать "Aún no tienes actividad" + bool hasTuActividad = false; + bool hasTransferencia = false; + for (const Node &node : m_nodes) { + if (node.contentDesc == "Tu Actividad") { + hasTuActividad = true; + } + if (node.contentDesc.startsWith("Transferencia")) { + hasTransferencia = true; + } + } + return hasTuActividad && !hasTransferencia; +} + +bool ScreenXmlParser::isTransactionListScreen() { + // Экран транзакций определяется по заголовку "Tu Actividad" (с большой A) + // + наличие транзакций (ImageView с "Transferencia") + // На домашнем экране — "Tu actividad" (с маленькой a) + bool hasTuActividad = false; + bool hasTransferencia = false; + for (const Node &node : m_nodes) { + if (node.contentDesc == "Tu Actividad") { + hasTuActividad = true; + } + if (node.contentDesc.startsWith("Transferencia")) { + hasTransferencia = true; + } + } + return hasTuActividad && hasTransferencia; +} + +QList ScreenXmlParser::parseTransactionItems() { + // Транзакции — ImageView с content-desc формата: + // "Transferencia\n21 de febrero de 2026 - 09:39\n$-1.857,\n00" + // "Transferencia externa de NOELIA ISABEL LEZCANO\n21 de febrero de 2026 - 09:35\n$123,\n45" + QList result; + + for (const Node &node : m_nodes) { + if (node.className != "android.widget.ImageView") continue; + if (!node.contentDesc.startsWith("Transferencia")) continue; + + QStringList lines = node.contentDesc.split('\n'); + if (lines.size() < 3) continue; + + TransactionInfo tx; + + // Строка 1: описание, может содержать имя отправителя + // "Transferencia" или "Transferencia externa de NOELIA ISABEL LEZCANO" + const QString descLine = lines[0].trimmed(); + tx.description = descLine; + if (descLine.contains("externa de ")) { + tx.name = descLine.mid(descLine.indexOf("externa de ") + 11).trimmed(); + } + + // Строка 2: дата "21 de febrero de 2026 - 09:39" + tx.bankTime = lines[1].trimmed(); + + // Строки 3+: сумма "$-1.857,\n00" → объединяем + QString amountStr; + for (int i = 2; i < lines.size(); ++i) { + amountStr += lines[i].trimmed(); + } + // "$-1.857,00" → убираем "$", "." (тысячный разделитель), заменяем "," на "." + amountStr.remove('$'); + amountStr.remove(' '); + bool isNegative = amountStr.contains('-'); + amountStr.remove('-'); + amountStr.remove('.'); // тысячный разделитель + amountStr.replace(',', '.'); + bool ok = false; + tx.amount = amountStr.toDouble(&ok); + if (ok && isNegative) tx.amount = -tx.amount; + + tx.type = TransactionType::Card; + + result.append(tx); + } + + return result; +} + +QDateTime ScreenXmlParser::parseSpanishDate(const QString &dateStr) { + // Формат: "21 de febrero de 2026 - 09:39" + static const QMap spanishMonths = { + {"enero", 1}, {"febrero", 2}, {"marzo", 3}, {"abril", 4}, + {"mayo", 5}, {"junio", 6}, {"julio", 7}, {"agosto", 8}, + {"septiembre", 9}, {"octubre", 10}, {"noviembre", 11}, {"diciembre", 12} + }; + + QRegularExpression re(R"((\d{1,2})\s+de\s+(\w+)\s+de\s+(\d{4})\s*-\s*(\d{2}):(\d{2}))"); + QRegularExpressionMatch match = re.match(dateStr); + if (!match.hasMatch()) return {}; + + const int day = match.captured(1).toInt(); + const QString monthName = match.captured(2).toLower(); + const int year = match.captured(3).toInt(); + const int hour = match.captured(4).toInt(); + const int minute = match.captured(5).toInt(); + + const int month = spanishMonths.value(monthName, 0); + if (month == 0) return {}; + + return QDateTime(QDate(year, month, day), QTime(hour, minute)); +} + +bool ScreenXmlParser::isPayInputCardNumberScreen() { + for (const Node &node : m_nodes) { + if (node.hint == "Ingresar CBU, CVU o Alias") { + return true; + } + } + return false; +} + +bool ScreenXmlParser::isPayTransferConfirmScreen() { + // Экран с именем получателя, Alias/CBU и кнопкой "Transferir" + // Но без "Vas a transferir" (это уже финальный экран) + bool hasTransferir = false; + bool hasVasATransferir = false; + bool hasAlias = false; + for (const Node &node : m_nodes) { + if (node.contentDesc == "Transferir" && node.clickable) { + hasTransferir = true; + } + if (node.contentDesc.startsWith("Vas a transferir")) { + hasVasATransferir = true; + } + if (node.contentDesc.startsWith("Alias:")) { + hasAlias = true; + } + } + return hasTransferir && hasAlias && !hasVasATransferir; +} + +bool ScreenXmlParser::isPayInputSumScreen() { + for (const Node &node : m_nodes) { + if (node.contentDesc.contains(QString::fromUtf8("Cuánto vas a transferír"))) { + return true; + } + } + return false; +} + +bool ScreenXmlParser::isPayConfirmScreen() { + for (const Node &node : m_nodes) { + if (node.contentDesc.startsWith("Vas a transferir")) { + return true; + } + } + return false; +} + +bool ScreenXmlParser::isPaySuccessScreen() { + for (const Node &node : m_nodes) { + if (node.contentDesc == "Dinero enviado!") { + return true; + } + } + return false; +} + +QString ScreenXmlParser::parseRecipientNameFromConfirm() { + // "Vas a transferir \na JOEL ARCHENTI" + for (const Node &node : m_nodes) { + if (node.contentDesc.startsWith("Vas a transferir")) { + QStringList lines = node.contentDesc.split('\n'); + if (lines.size() >= 2) { + QString nameLine = lines[1].trimmed(); + // Убираем "a " в начале + if (nameLine.startsWith("a ")) { + nameLine = nameLine.mid(2); + } + return nameLine.trimmed(); + } + } + } + return {}; +} + +QString ScreenXmlParser::parsePaySuccessDateTime() { + // Дата/время: "12/03/26 04:01" — содержит "/" и ":" + for (const Node &node : m_nodes) { + const QString desc = node.contentDesc.trimmed(); + if (desc.contains('/') && desc.contains(':') && desc.length() <= 20) { + return desc; + } + } + return {}; +} + +QString ScreenXmlParser::parsePaySuccessCoelsaId() { + // "Id Coelsa" → следующий node содержит ID + bool afterIdCoelsa = false; + for (const Node &node : m_nodes) { + if (node.contentDesc == "Id Coelsa") { + afterIdCoelsa = true; + continue; + } + if (afterIdCoelsa && !node.contentDesc.isEmpty()) { + return node.contentDesc.trimmed(); + } + } + return {}; +} + +Node ScreenXmlParser::findEditTextByHint(const QString &hint) { + for (const Node &node : m_nodes) { + if (node.className == "android.widget.EditText" && node.hint == hint) { + return node; + } + } + return {}; +} + +Node ScreenXmlParser::tryToFindGooglePlayBanner(const int screenWidth, const int screenHeight) { + for (const Node &node : m_nodes) { + if (node.contentDesc.contains("Закрыть диалоговое окно") && node.clickable) { + return node; + } + } + for (const Node &node : m_nodes) { + if (node.contentDesc.contains("Доступно обновление")) { + Node tapNode; + const int cx = screenWidth / 2; + const int cy = screenHeight / 20; + tapNode.setBounds(cx, cy, cx + 1, cy + 1); + return tapNode; + } + } + return {}; +} + +Node ScreenXmlParser::tryToFindSystemPermissionDenyButton() { + for (const Node &node : m_nodes) { + if (node.resourceId == "com.android.permissioncontroller:id/permission_deny_button" + && node.clickable) { + return node; + } + } + for (const Node &node : m_nodes) { + if (node.clickable && node.text.trimmed() == QString::fromUtf8("ЗАПРЕТИТЬ")) { + return node; + } + } + return {}; +} + +} // namespace Black diff --git a/android/black/ScreenXmlParser.h b/android/black/ScreenXmlParser.h new file mode 100644 index 0000000..eb0afae --- /dev/null +++ b/android/black/ScreenXmlParser.h @@ -0,0 +1,125 @@ +#pragma once + +#include +#include +#include "android/xml/Node.h" +#include "db/MaterialInfo.h" +#include "db/TransactionInfo.h" + +namespace Black { + +class ScreenXmlParser final : public QObject { + Q_OBJECT + +public: + explicit ScreenXmlParser(QObject *parent = nullptr); + + ~ScreenXmlParser() override; + + void parseAndSaveXml(const QString &xml); + + const QList &findAllNodes() const; + + Node findTextNode(const QString &text); + + Node findNodeByContentDesc(const QString &contentDesc); + + Node findNodeByContentDesc(const QString &contentDesc, bool contains); + + Node findButtonNode(const QString &text, bool contains); + + Node findFirstEditText(); + + // Определяет экран ввода PIN-кода по content-desc "¿Olvidaste tu PIN?" + bool isPinCodeScreen(); + + // Находит EditText с password=true для ввода PIN + Node findPinCodeEditText(); + + bool isHomeScreen(); + + // Определяет боковое меню по content-desc "Tu Información" + "Cerrar sesión" + bool isSideMenu(); + + // Находит кнопку "¡Hola!" (аватар с именем) на домашнем экране + Node findHolaButton(); + + // Парсит имя пользователя из content-desc "¡Hola!\nимя\nинициалы" + QString parseUserNameFromHomeScreen(); + + // Парсит баланс с домашнего экрана (content-desc "$0,00" после "Saldo disponible") + double parseBalanceFromHomeScreen(); + + // Определяет страницу профиля "Tu información" + bool isProfilePage(); + + // Парсит данные профиля по hint полей + QString parseProfileNombre(); // hint="Nombre" + QString parseProfileApellido(); // hint="Apellido" + QString parseProfileEmail(); // hint="E-mail" + QString parseProfilePhone(); // hint="Teléfono celular" + + // Определяет экран CVU (окно поверх домашнего экрана) + bool isCVUScreen(); + + // Парсит номер CVU (22-значный номер после content-desc="CVU") + QString parseCVUNumber(); + + // Находит кнопку "Ver más" рядом с "Tu actividad" (для перехода на экран транзакций) + Node findVerMasAfterTuActividad(); + + // Определяет экран загрузки транзакций (заголовок "Tu Actividad", но транзакций ещё нет) + bool isTransactionLoadingScreen(); + + // Определяет экран списка транзакций (заголовок "Tu Actividad" + есть транзакции) + bool isTransactionListScreen(); + + // Парсит все транзакции из content-desc ImageView элементов + // Формат: "Transferencia\nDD de MES de YYYY - HH:MM\n$-1.857,\n00" + QList parseTransactionItems(); + + // Парсит дату на испанском: "21 de febrero de 2026 - 09:39" → QDateTime + static QDateTime parseSpanishDate(const QString &dateStr); + + // --- Экраны оплаты --- + + // Экран ввода CBU/CVU/Alias (hint="Ingresar CBU, CVU o Alias") + bool isPayInputCardNumberScreen(); + + // Экран подтверждения получателя (кнопка "Transferir" + имя получателя) + bool isPayTransferConfirmScreen(); + + // Экран ввода суммы ("¿Cuánto vas a transferír" + EditText + "Continuar") + bool isPayInputSumScreen(); + + // Экран финального подтверждения ("Vas a transferir" + "Transferir") + bool isPayConfirmScreen(); + + // Экран успешного перевода ("Dinero enviado!") + bool isPaySuccessScreen(); + + // Парсит имя получателя из "Vas a transferir \na ИМЯ" + QString parseRecipientNameFromConfirm(); + + // Парсит дату/время с экрана успеха: "12/03/26 04:01" + QString parsePaySuccessDateTime(); + + // Парсит Id Coelsa с экрана успеха + QString parsePaySuccessCoelsaId(); + + // Находит EditText по hint + Node findEditTextByHint(const QString &hint); + + // Находит кнопку закрытия Google Play in-app update bottom sheet + // ("com.android.vending" оверлей "Доступно обновление"), если он показан + Node tryToFindGooglePlayBanner(int screenWidth, int screenHeight); + + // Находит кнопку "ЗАПРЕТИТЬ" в системном диалоге разрешений Android + Node tryToFindSystemPermissionDenyButton(); + +private: + QList m_nodes; + QDomElement m_xml; +}; + +} // namespace Black diff --git a/android/dump/TaskDumpRecorder.cpp b/android/dump/TaskDumpRecorder.cpp new file mode 100644 index 0000000..38664e1 --- /dev/null +++ b/android/dump/TaskDumpRecorder.cpp @@ -0,0 +1,327 @@ +#include "TaskDumpRecorder.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "adb/AdbUtils.h" +#include "dao/SettingsDAO.h" +#include "net/NetworkService.h" +#include "LogArchiveSender.h" + +namespace { + +QThreadStorage &tlsRecorder() { + static QThreadStorage tls; + return tls; +} + +QPointer &networkServiceRef() { + static QPointer ref; + return ref; +} + +QString dumpsRoot() { + // Пишем в пользовательский temp — UAC/Program Files не мешают. + QString base = QStandardPaths::writableLocation(QStandardPaths::TempLocation); + if (base.isEmpty()) base = QDir::tempPath(); + return base + "/arc_dumps"; +} + +QString sanitize(const QString &raw, int maxLen = 40) { + static const QRegularExpression re(QStringLiteral("[^\\p{L}\\p{N}_-]+")); + QString s = raw; + s.replace(re, "_"); + if (s.size() > maxLen) s = s.left(maxLen); + while (s.endsWith('_')) s.chop(1); + while (s.startsWith('_')) s = s.mid(1); + return s; +} + +QString extractScreenTag(const QString &xml) { + static const QRegularExpression re(QStringLiteral("text=\"([^\"]{4,40})\"")); + static const QRegularExpression noiseRe(QStringLiteral("^[0-9\\s,\\.\\-+₽$€%]+$")); + auto it = re.globalMatch(xml); + while (it.hasNext()) { + QString text = it.next().captured(1).trimmed(); + if (text.isEmpty() || noiseRe.match(text).hasMatch()) continue; + QString tag = sanitize(text); + if (!tag.isEmpty()) return tag; + } + return QStringLiteral("screen"); +} + +bool zipFolder(const QString &folder, const QString &archivePath) { +#ifdef Q_OS_WIN + const QString cmd = QString( + "Compress-Archive -Path '%1\\*' -DestinationPath '%2' -Force" + ).arg(QDir::toNativeSeparators(folder), QDir::toNativeSeparators(archivePath)); + QProcess p; + p.start("powershell", {"-NoProfile", "-Command", cmd}); + if (!p.waitForStarted(5000) || !p.waitForFinished(180000)) { + qWarning() << "[TaskDumpRecorder] zip failed:" << p.errorString(); + return false; + } + if (p.exitCode() != 0) { + qWarning() << "[TaskDumpRecorder] zip exit=" << p.exitCode() + << "stderr:" << QString::fromUtf8(p.readAllStandardError()).trimmed(); + return false; + } + return true; +#else + const QFileInfo fi(folder); + QProcess p; + p.setWorkingDirectory(fi.absolutePath()); + p.start("zip", {"-rq", archivePath, fi.fileName()}); + if (!p.waitForStarted(5000) || !p.waitForFinished(180000)) { + qWarning() << "[TaskDumpRecorder] zip failed:" << p.errorString(); + return false; + } + return p.exitCode() == 0; +#endif +} + +QByteArray readFileBytes(const QString &path) { + QFile f(path); + if (!f.open(QIODevice::ReadOnly)) return {}; + QByteArray bytes = f.readAll(); + f.close(); + return bytes; +} + +} // namespace + +void TaskDumpRecorder::setNetworkService(NetworkService *service) { + networkServiceRef() = service; +} + +NetworkService *TaskDumpRecorder::networkService() { + return networkServiceRef().data(); +} + +TaskDumpRecorder *TaskDumpRecorder::current() { + return tlsRecorder().hasLocalData() ? tlsRecorder().localData() : nullptr; +} + +void TaskDumpRecorder::begin(const QString &bank, const QString &taskKind, + const QString &adbDeviceId, const TaskDumpMeta &meta) { + if (auto *prev = current()) { + prev->finishInternal(QStringLiteral("interrupted (new task started before previous finished)")); + // Не вызываем `delete prev` — QThreadStorage сам удалит предыдущий + // указатель при замене через setLocalData. + } + auto *rec = new TaskDumpRecorder(bank, taskKind, adbDeviceId, meta); + tlsRecorder().setLocalData(rec); +} + +void TaskDumpRecorder::finish(const QString &errorMessage) { + auto *rec = current(); + if (!rec) return; + rec->finishInternal(errorMessage); + // setLocalData(nullptr) сам вызовет delete на текущем указателе. + tlsRecorder().setLocalData(nullptr); +} + +TaskDumpRecorder::TaskDumpRecorder(const QString &bank, const QString &taskKind, + const QString &adbDeviceId, const TaskDumpMeta &meta) + : m_bank(sanitize(bank)), + m_taskKind(sanitize(taskKind)), + m_adbDeviceIdRaw(adbDeviceId), + m_adbDeviceIdSafe(sanitize(adbDeviceId)), + m_meta(meta) { + // Короткий суффикс для имени папки — чтобы визуально отличать задачи, + // но не раздувать путь. Берём materialId (первые 8 символов — UUID segment) + // + сумму, если есть. + QString folderTail; + if (!m_meta.materialId.isEmpty()) { + folderTail = sanitize(m_meta.materialId.left(8)); + } + if (m_meta.amount != 0) { + const QString amt = QString::number(static_cast(m_meta.amount)); + folderTail = folderTail.isEmpty() ? amt : folderTail + "_" + amt; + } + + const QString ts = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss"); + const QString sub = folderTail.isEmpty() + ? QString("%1_%2").arg(ts, m_adbDeviceIdSafe) + : QString("%1_%2_%3").arg(ts, m_adbDeviceIdSafe, folderTail); + m_folder = QString("%1/%2_%3/%4") + .arg(dumpsRoot(), m_bank, m_taskKind, sub); + if (!QDir().mkpath(m_folder)) { + qWarning() << "[TaskDumpRecorder] failed to mkpath" << m_folder; + m_folder.clear(); + return; + } + qDebug() << "[TaskDumpRecorder] begin:" << m_folder; +} + +QString TaskDumpRecorder::buildCaption(const QString &errorMessage) const { + const QString apiToken = SettingsDAO::get("api_key"); + const QString timestamp = QDateTime::currentDateTime().toString(Qt::ISODate); + + QStringList head{QStringLiteral("[TASK_ERROR]")}; + if (!apiToken.isEmpty()) + head << QStringLiteral("[%1]").arg(apiToken.toHtmlEscaped()); + head << QStringLiteral("[%1]").arg(m_bank.toHtmlEscaped()); + head << QStringLiteral("[%1]").arg(m_taskKind.toHtmlEscaped()); + + QString body = QStringLiteral("Device: %1").arg(m_adbDeviceIdRaw.toHtmlEscaped()); + if (!m_meta.materialId.isEmpty()) + body += QStringLiteral("\nMaterial: %1").arg(m_meta.materialId.toHtmlEscaped()); + if (m_meta.amount != 0) + body += QStringLiteral("\nAmount: %1").arg(static_cast(m_meta.amount)); + if (!m_meta.phone.isEmpty()) + body += QStringLiteral("\nPhone: %1").arg(m_meta.phone.toHtmlEscaped()); + if (!m_meta.card.isEmpty()) + body += QStringLiteral("\nCard: %1").arg(m_meta.card.toHtmlEscaped()); + body += QStringLiteral("\nError: %1").arg(errorMessage.left(800).toHtmlEscaped()); + + return QStringLiteral("%1\n%2\n%3") + .arg(head.join(QStringLiteral(" ")), timestamp.toHtmlEscaped(), body); +} + +void TaskDumpRecorder::saveXml(int counter, const QString &xml) { + if (m_folder.isEmpty() || xml.isEmpty()) return; + const QString tag = extractScreenTag(xml); + const QString fname = QString("%1/%2_%3.xml") + .arg(m_folder, + QString("%1").arg(counter, 4, 10, QChar('0')), + tag); + QFile f(fname); + if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + f.write(xml.toUtf8()); + f.close(); + } else { + qWarning() << "[TaskDumpRecorder] failed to write" << fname << f.errorString(); + } +} + +void TaskDumpRecorder::finishInternal(const QString &errorMessage) { + if (m_finished) return; + m_finished = true; + if (m_folder.isEmpty()) return; + + if (errorMessage.isEmpty()) { + QDir(m_folder).removeRecursively(); + qDebug() << "[TaskDumpRecorder] success — removed" << m_folder; + return; + } + + qWarning() << "[TaskDumpRecorder] task failed:" << errorMessage + << "folder:" << m_folder; + + // Финальный скриншот текущего экрана + if (!m_adbDeviceIdRaw.isEmpty()) { + const QByteArray png = AdbUtils::captureScreenshotBytes(m_adbDeviceIdRaw); + if (!png.isEmpty()) { + QFile f(m_folder + "/_final_screen.png"); + if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + f.write(png); + f.close(); + } + } + } + + // Метаданные + { + QFile ef(m_folder + "/_error.txt"); + if (ef.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + QTextStream ts(&ef); + ts.setEncoding(QStringConverter::Utf8); + ts << "bank: " << m_bank << "\n" + << "task: " << m_taskKind << "\n" + << "device: " << m_adbDeviceIdRaw << "\n"; + if (!m_meta.materialId.isEmpty()) ts << "material_id: " << m_meta.materialId << "\n"; + if (m_meta.amount != 0) ts << "amount: " << static_cast(m_meta.amount) << "\n"; + if (!m_meta.phone.isEmpty()) ts << "phone: " << m_meta.phone << "\n"; + if (!m_meta.card.isEmpty()) ts << "card: " << m_meta.card << "\n"; + ts << "time: " << QDateTime::currentDateTime().toString(Qt::ISODate) << "\n" + << "error: " << errorMessage << "\n"; + ef.close(); + } + } + + const QString folder = m_folder; + const QString archivePath = m_folder + ".zip"; + const QString text = buildCaption(errorMessage); + + // Финальный скриншот шлём отдельным полем `image`, поэтому сразу читаем + // его в память, пока папка ещё цела. + QByteArray finalScreenshot; + { + const QString screenPath = folder + "/_final_screen.png"; + if (QFile::exists(screenPath)) { + finalScreenshot = readFileBytes(screenPath); + } + } + + QThread *bg = QThread::create([folder, archivePath, text, finalScreenshot]() { + QByteArray archiveBytes; + if (zipFolder(folder, archivePath)) { + archiveBytes = readFileBytes(archivePath); + if (archiveBytes.isEmpty()) { + qWarning() << "[TaskDumpRecorder] zip is empty, sending text-only dump for" << folder; + } + } else { + qWarning() << "[TaskDumpRecorder] zip failed, sending text-only dump for" << folder; + } + QDir(folder).removeRecursively(); + QFile::remove(archivePath); + + NetworkService *svc = networkServiceRef().data(); + if (svc) { + qDebug() << "[TaskDumpRecorder] dispatching dump to NetworkService" + << "archive.size=" << archiveBytes.size() + << "image.size=" << finalScreenshot.size() + << "text.size=" << text.size(); + const bool dispatched = QMetaObject::invokeMethod(svc, "postMonitoringDump", + Qt::QueuedConnection, + Q_ARG(QString, text), + Q_ARG(QByteArray, archiveBytes), + Q_ARG(QByteArray, finalScreenshot)); + if (!dispatched) { + qCritical() << "[TaskDumpRecorder] invokeMethod(postMonitoringDump) FAILED — " + "slot not found or wrong signature for" << folder; + } + } else { + qWarning() << "[TaskDumpRecorder] no NetworkService — dump dropped for" << folder + << "(setNetworkService was not called at app startup?)"; + } + + // Следом — application.log + БД (+ WAL/SHM) отдельным архивом. + // Запускаем уже ПОСЛЕ того, как task-дамп поставлен в очередь + // отправки, чтобы порядок прихода на relay был "дамп → логи". + LogArchiveSender::dispatch(QStringLiteral("TASK_ERROR")); + }); + QObject::connect(bg, &QThread::finished, bg, &QObject::deleteLater); + bg->start(); +} + +TaskDumpRecorder::Scope::Scope(const QString &bank, const QString &taskKind, + const QString &adbDeviceId, const TaskDumpMeta &meta, + const QString *errorPtr) + : m_errorPtr(errorPtr), m_owned(false) { + // Вложенные скоупы не перетирают рекордер родителя — иначе внутренний + // скрипт прервал бы запись внешней задачи и сгенерировал ложный дамп + // с пометкой "interrupted". + if (TaskDumpRecorder::current() == nullptr) { + TaskDumpRecorder::begin(bank, taskKind, adbDeviceId, meta); + m_owned = true; + } +} + +TaskDumpRecorder::Scope::~Scope() { + if (m_owned) { + TaskDumpRecorder::finish(m_errorPtr ? *m_errorPtr : QString()); + } +} diff --git a/android/dump/TaskDumpRecorder.h b/android/dump/TaskDumpRecorder.h new file mode 100644 index 0000000..9aab596 --- /dev/null +++ b/android/dump/TaskDumpRecorder.h @@ -0,0 +1,69 @@ +#pragma once + +#include + +class NetworkService; + +// Мета-поля задачи. Пустые/нулевые значения не попадают в text дампа +// и в имя папки — выводятся только те, что релевантны конкретному типу задачи. +struct TaskDumpMeta { + QString materialId; + double amount = 0; // 0 = нет + QString phone; // пусто = нет + QString card; // пусто = нет +}; + +class TaskDumpRecorder { +public: + // NetworkService, через который дамп отправляется на бэкенд (/monitoring/dump). + // Вызывать один раз при старте приложения. Без него рекордер собирает дамп, + // но не отправляет его — только пишет warning в лог. + static void setNetworkService(NetworkService *service); + + // Доступ к зарегистрированному NetworkService для других потребителей, + // которые тоже хотят дернуть /monitoring/dump (отправка ручных логов). + static NetworkService* networkService(); + + // Возвращает рекордер текущего потока (nullptr если задача не начата). + static TaskDumpRecorder* current(); + + // RAII-скоуп: при создании регистрирует рекордер на текущий поток, + // при разрушении — финализирует (успех/ошибка определяются по errorPtr). + // Если в текущем потоке уже есть активный рекордер (вложенный вызов из + // другого скрипта), новый скоуп становится no-op — родитель остаётся + // владельцем дампа, а внутренние скрины пишутся в его папку. + class Scope { + public: + Scope(const QString &bank, const QString &taskKind, + const QString &adbDeviceId, const TaskDumpMeta &meta, + const QString *errorPtr); + ~Scope(); + Scope(const Scope &) = delete; + Scope &operator=(const Scope &) = delete; + private: + const QString *m_errorPtr; + bool m_owned; + }; + + // Сохраняет XML-дамп в папку задачи. Имя файла: NNNN_.xml. + void saveXml(int counter, const QString &xml); + +private: + TaskDumpRecorder(const QString &bank, const QString &taskKind, + const QString &adbDeviceId, const TaskDumpMeta &meta); + + static void begin(const QString &bank, const QString &taskKind, + const QString &adbDeviceId, const TaskDumpMeta &meta); + static void finish(const QString &errorMessage); + + void finishInternal(const QString &errorMessage); + QString buildCaption(const QString &errorMessage) const; + + QString m_folder; + QString m_bank; + QString m_taskKind; + QString m_adbDeviceIdRaw; + QString m_adbDeviceIdSafe; + TaskDumpMeta m_meta; + bool m_finished = false; +}; diff --git a/android/dushanbe/CommonScript.cpp b/android/dushanbe/CommonScript.cpp new file mode 100644 index 0000000..27bc7b8 --- /dev/null +++ b/android/dushanbe/CommonScript.cpp @@ -0,0 +1,359 @@ +#include "CommonScript.h" + +#include +#include +#include +#include +#include +#include + +#include "adb/AdbUtils.h" +#include "dao/DeviceDAO.h" +#include "dao/BankProfileDAO.h" +#include "net/NetworkService.h" + +namespace Dushanbe { + +CommonScript::CommonScript(QObject *parent) + : QObject(parent) { +} + +CommonScript::~CommonScript() = default; + +void CommonScript::start() { + doStart(); + emit finished(); +} + +Node CommonScript::swipeToButton( + const QString &deviceId, + const QString &text, int &counter, const int width, const int height +) { + for (int i = 0; i < 10; ++i) { + if (Node button = xmlScreenParser.findButtonNode(text, true); button.x() > 0 && button.y() > 0) { + return button; + } + const int x = width / 2; + const int offset = height / 4; + AdbUtils::makeSwipe(deviceId, x, height - offset, x, offset); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++counter)); + } + return {}; +} + +bool CommonScript::inputPinCode(const QString &deviceId, const QString &pinCode) { + if (pinCode.isEmpty()) { + qWarning() << "[Dushanbe::inputPinCode] pinCode is EMPTY — nothing to enter!"; + return false; + } + + // Ждём, пока на экране появятся все уникальные цифры из PIN. + // DC Next на некоторых устройствах успевает показать заголовок + // "Введите код доступа" раньше, чем дорисует кнопочную сетку, — + // если начать тапать в этот момент, часть цифр не найдётся и PIN + // не введётся целиком. + bool seenInPin[10] = {false}; + for (const QChar &d : pinCode) { + if (!d.isDigit()) continue; + const int idx = d.digitValue(); + if (idx >= 0 && idx <= 9) seenInPin[idx] = true; + } + auto allDigitsPresent = [this, &seenInPin]() { + for (int d = 0; d < 10; ++d) { + if (!seenInPin[d]) continue; + if (xmlScreenParser.findNodeByContentDesc(QString::number(d)).isEmpty()) return false; + } + return true; + }; + for (int i = 0; i < 6 && !allDigitsPresent(); ++i) { + QThread::msleep(500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + } + if (!allDigitsPresent()) { + qWarning() << "[Dushanbe::inputPinCode] keypad incomplete after wait; aborting"; + return false; + } + + qDebug() << "[Dushanbe::inputPinCode] entering" << pinCode.length() << "digits"; + // DC Next использует кнопочную сетку для ввода PIN (content-desc = "0"-"9") + for (const QChar &digit : pinCode) { + Node button = xmlScreenParser.findNodeByContentDesc(QString(digit)); + if (button.isEmpty()) { + qWarning() << "[Dushanbe::inputPinCode] Button not found for digit:" << digit; + return false; + } + AdbUtils::makeTap(deviceId, button.x(), button.y()); + QThread::msleep(300); + } + return true; +} + +bool CommonScript::runAppAndGoToHomeScreen( + const QString &deviceId, + const QString &packageName, + const QString &pinCode, + const int width, + const int height +) { + AdbUtils::unlockScreen(deviceId, width, height); + + // Быстрый path: если приложение уже запущено — не перезапускаем сразу, + // а смотрим текущий экран. Если это Home — готово. Если PIN — вводим + // и ждём Home. Только если не смогли — убиваем и рестартим. + if (const QString pid = AdbUtils::tryToGetRunningAppPid(deviceId, packageName); !pid.isEmpty()) { + qDebug() << "[Dushanbe] Process already running, pid:" << pid; + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + if (xmlScreenParser.isHomeScreen()) { + qDebug() << "[Dushanbe] Fast path: already on Home screen"; + return true; + } + if (xmlScreenParser.isPinCodeScreen()) { + qDebug() << "[Dushanbe] Fast path: PIN screen, entering PIN"; + if (inputPinCode(deviceId, pinCode)) { + QThread::msleep(3000); + for (int i = 0; i < 10; ++i) { + xmlScreenParser.parseAndSaveXml( + AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + if (xmlScreenParser.isHomeScreen()) { + qDebug() << "[Dushanbe] Fast path: Home reached after PIN"; + return true; + } + QThread::msleep(1000); + } + } + } + qDebug() << "[Dushanbe] Fast path failed, restarting app"; + AdbUtils::tryToKillApplication(deviceId, packageName); + } + if (!AdbUtils::tryToStartApplication(deviceId, packageName)) { + AdbUtils::takeScreenshot(deviceId, "run_app_error"); + qWarning() << "[Dushanbe] Process has not started"; + return false; + } + + QThread::msleep(4500); + int counter = 0; + bool findAndInputPincode = false; + + while (true) { + counter++; + + if (counter > 20) { + qWarning() << "[Dushanbe] runAppAndGoToHomeScreen: too many attempts, giving up"; + AdbUtils::takeScreenshot(deviceId, "too_many_attempts"); + return false; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + + if (xmlScreenParser.isHomeScreen()) { + qDebug() << "[Dushanbe] Home reached on attempt" << counter; + return true; + } + + if (!findAndInputPincode && xmlScreenParser.isPinCodeScreen()) { + qDebug() << "[Dushanbe] PIN screen detected on attempt" << counter << "entering PIN"; + if (inputPinCode(deviceId, pinCode)) { + findAndInputPincode = true; + } else { + // Клавиатура могла не дорисоваться или PIN ввёлся частично — + // оставляем флаг false, следующая итерация повторит ввод, + // когда кнопки появятся. + qWarning() << "[Dushanbe] inputPinCode failed on attempt" << counter << "— will retry"; + } + QThread::msleep(3000); + continue; + } + + // Диагностика: что реально на экране? + QStringList sample; + for (const Node &n : xmlScreenParser.findAllNodes()) { + if (n.contentDesc.isEmpty() && n.text.isEmpty()) continue; + QString s = !n.contentDesc.isEmpty() ? n.contentDesc : n.text; + s.replace('\n', ' '); + if (s.size() > 60) s = s.left(60) + "..."; + sample.append(s); + if (sample.size() >= 8) break; + } + qDebug() << "[Dushanbe] attempt" << counter << "not home, not PIN. Sample:" << sample; + QThread::msleep(1000); + } +} + +void CommonScript::postAccountsData(const QList> &accounts) { + if (accounts.isEmpty()) { + return; + } + + auto *thread = new QThread; + auto *worker = new NetworkService; + + QJsonArray list; + for (const QPair &pair : accounts) { + QJsonObject obj; + MaterialInfo account = pair.first; + obj["appName"] = account.appCode; + obj["lastNumbers"] = account.lastNumbers; + obj["amount"] = account.amount; + obj["description"] = account.description; + obj["currency"] = account.currency; + obj["status"] = materialStatusToString(account.status); + list.append(obj); + } + + worker->setDeviceId(accounts.first().first.deviceId); + worker->setPayload(QJsonDocument(list).toJson()); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &NetworkService::postAccountsData); + connect(worker, &NetworkService::finished, thread, &QThread::quit); + connect(worker, &NetworkService::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +void CommonScript::postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction) { + auto *thread = new QThread; + auto *worker = new NetworkService; + + QJsonObject obj; + obj["id"] = transaction.id; + obj["amount"] = transaction.amount; + obj["name"] = transaction.name; + obj["fee"] = transaction.fee; + obj["status"] = transactionStatusToString(transaction.status); + obj["type"] = transactionTypeToString(transaction.type); + obj["phone"] = transaction.phone; + obj["description"] = transaction.description; + obj["updatedDescription"] = transaction.updatedDescription; + obj["bankName"] = transaction.bankName; + obj["bankTime"] = transaction.bankTime; + obj["bankTransactionId"] = transaction.bankTrExternalId; + + worker->setDeviceId(account.deviceId); + worker->addExtra("lastNumbers", account.lastNumbers); + worker->addExtra("transactionId", transaction.id); + worker->setPayload(QJsonDocument(obj).toJson()); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &NetworkService::insertTransactionData); + connect(worker, &NetworkService::finished, thread, &QThread::quit); + connect(worker, &NetworkService::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +void CommonScript::updateTransactionData( + const MaterialInfo &account, + const int transactionId, + const TransactionStatus status, + const QString &oldDesc, + const QString &newDesc +) { + auto *thread = new QThread; + auto *worker = new NetworkService; + + QJsonObject obj; + obj["id"] = transactionId; + obj["status"] = transactionStatusToString(status); + obj["description"] = newDesc; + obj["updatedDescription"] = oldDesc; + + worker->setDeviceId(account.deviceId); + worker->addExtra("lastNumbers", account.lastNumbers); + worker->setPayload(QJsonDocument(obj).toJson()); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &NetworkService::updateTransactionData); + connect(worker, &NetworkService::finished, thread, &QThread::quit); + connect(worker, &NetworkService::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +QString CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance) { + const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode); + + if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) { + qWarning() << "[Dushanbe] createBankProfileIfNeeded: skipping — fullName or phone is empty"; + return {}; + } + + const QMap currencyCodes = { + {"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972} + }; + const int currencyCode = currencyCodes.value(app.currency.toUpper(), 840); + const QStringList parts = app.fullName.split(' ', Qt::SkipEmptyParts); + + const QSettings settings("config.ini", QSettings::IniFormat); + const QString phoneCode = settings.value(appCode + "/phone_code").toString(); + QString phone = app.phone; + phone.remove(QRegularExpression("[^0-9]")); + if (!phoneCode.isEmpty() && !phone.startsWith(phoneCode)) { + if (phone.startsWith('8') && phoneCode != "8") { + phone.replace(0, 1, phoneCode); + } else { + phone.prepend(phoneCode); + } + } + phone.prepend('+'); + + const DeviceInfo device = DeviceDAO::getDeviceById(deviceId); + + QJsonObject profileBody; + profileBody["first_name"] = parts.value(0); + profileBody["middle_name"] = parts.size() >= 3 ? parts.value(1) : QString(""); + profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1); + profileBody["bank_name"] = appCode; + profileBody["phone_number"] = phone; + const double balance = (parsedBalance >= 0.0) ? parsedBalance : app.amount; + profileBody["balance"] = QString::number(qRound64(balance * 100)); + profileBody["state"] = (app.status == "active") ? "enabled" : "disabled"; + profileBody["device_id"] = device.apiId; + profileBody["currency"] = currencyCode; + + NetworkService ns; + ns.setDeviceId(deviceId); + ns.setPayload(QJsonDocument(profileBody).toJson()); + + if (app.bankProfileId.isEmpty()) { + ns.addExtra("app_id", app.id); + ns.postBankProfile(); + const BankProfileInfo afterPost = BankProfileDAO::getApplication(deviceId, appCode); + if (afterPost.bankProfileId.isEmpty()) { + const QString err = "postBankProfile failed for " + deviceId + " " + appCode; + qWarning() << "[Dushanbe] createBankProfileIfNeeded:" << err; + return err; + } + qDebug() << "[Dushanbe] createBankProfileIfNeeded: postBankProfile done"; + } else { + ns.addExtra("bank_profile_id", app.bankProfileId); + ns.patchBankProfile(); + qDebug() << "[Dushanbe] createBankProfileIfNeeded: patchBankProfile done"; + } + return {}; +} + +void CommonScript::stop() { + m_running = false; +} + +bool CommonScript::closeGooglePlayBannerIfVisible( + const QString &deviceId, const int width, const int height +) { + // 1. Системный диалог запроса разрешения Android (permissioncontroller) — тапаем "ЗАПРЕТИТЬ" + if (Node denyBtn = xmlScreenParser.tryToFindSystemPermissionDenyButton(); !denyBtn.isEmpty()) { + qDebug() << "[Dushanbe] System permission dialog detected mid-task, tapping 'ЗАПРЕТИТЬ'..."; + AdbUtils::makeTap(deviceId, denyBtn.x(), denyBtn.y()); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + return true; + } + + // 2. Google Play in-app update bottom sheet + Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(width, height); + if (gpNode.isEmpty()) return false; + qDebug() << "[Dushanbe] Google Play update banner detected mid-task, closing..."; + AdbUtils::makeTap(deviceId, gpNode.x(), gpNode.y()); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + return true; +} + +} // namespace Dushanbe diff --git a/android/dushanbe/CommonScript.h b/android/dushanbe/CommonScript.h new file mode 100644 index 0000000..f3f925e --- /dev/null +++ b/android/dushanbe/CommonScript.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include "ScreenXmlParser.h" +#include "db/TransactionInfo.h" + +namespace Dushanbe { + +class CommonScript : public QObject { + Q_OBJECT + +public: + explicit CommonScript(QObject *parent = nullptr); + + ~CommonScript() override; + +protected: + virtual void doStart() = 0; + + bool inputPinCode(const QString &deviceId, const QString &pinCode); + + bool runAppAndGoToHomeScreen( + const QString &deviceId, + const QString &packageName, + const QString &pinCode, + int width, + int height + ); + + Node swipeToButton( + const QString &deviceId, + const QString &text, + int &counter, int width, int height + ); + + void postAccountsData(const QList> &accounts); + + void postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction); + + void updateTransactionData( + const MaterialInfo &account, + int transactionId, + TransactionStatus status, + const QString &oldDesc, + const QString &newDesc + ); + + QString createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance = -1.0); + + // Если виден баннер Google Play "Доступно обновление" — закрывает его и перепарсивает XML. + // Безопасно вызывать на любом экране; возвращает true если баннер был закрыт. + bool closeGooglePlayBannerIfVisible( + const QString &deviceId, int width, int height + ); + + bool m_running = true; + ScreenXmlParser xmlScreenParser; + int m_counter = 0; + + QByteArray m_resultScreenshot; + +signals: + void finished(); + + void finishedWithResult(const QString &result); + + void screenshotReady(const QByteArray &screenshot); + +public slots: + virtual void start() final; + + virtual void stop() final; + +private: + Q_DISABLE_COPY(CommonScript); +}; + +} // namespace Dushanbe diff --git a/android/dushanbe/GetCardInfoScript.cpp b/android/dushanbe/GetCardInfoScript.cpp new file mode 100644 index 0000000..44d39c9 --- /dev/null +++ b/android/dushanbe/GetCardInfoScript.cpp @@ -0,0 +1,307 @@ +#include "GetCardInfoScript.h" + +#include +#include +#include + +#include "adb/AdbUtils.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "dump/TaskDumpRecorder.h" +#include "net/NetworkService.h" + +namespace Dushanbe { + +GetCardInfoScript::GetCardInfoScript( + QString deviceId, + QString appCode, + QObject *parent +) : CommonScript(parent), + m_deviceId(std::move(deviceId)), + m_appCode(std::move(appCode)) { +} + +GetCardInfoScript::~GetCardInfoScript() = default; + +void GetCardInfoScript::doStart() { + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + const TaskDumpMeta dumpMeta{}; + TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + + if (!xmlScreenParser.isHomeScreen()) { + qDebug() << "[Dushanbe::GetCardInfo] Not on home screen, restarting app..."; + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, + device.screenWidth, device.screenHeight)) { + m_error = "Cannot reach home screen: " + device.name + " (" + m_deviceId + ") " + m_appCode; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + } + + // Парсим баланс с домашнего экрана + const double balance = xmlScreenParser.parseBalanceFromHomeScreen(); + qDebug() << "[Dushanbe::GetCardInfo] balance:" << balance; + if (!BankProfileDAO::updateAmount(app.id, balance)) { + qWarning() << "[Dushanbe::GetCardInfo] Failed to update balance for app" << app.id; + } + + // Тапаем "Карты" на домашнем экране + Node cardsButton = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Карты")); + if (cardsButton.x() == 0 && cardsButton.y() == 0) { + m_error = "Cannot find Cards button: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + AdbUtils::makeTap(m_deviceId, cardsButton.x(), cardsButton.y()); + QThread::msleep(3000); + + // Ждём экран "Мои карты" + bool cardsScreenFound = false; + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + if (xmlScreenParser.isCardsScreen()) { + cardsScreenFound = true; + break; + } + QThread::msleep(1000); + } + + if (!cardsScreenFound) { + m_error = "Cards screen not detected: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + // Парсим карты из списка (lastNumbers, баланс) + const QList cards = xmlScreenParser.parseCards(); + qDebug() << "[Dushanbe::GetCardInfo] Found" << cards.size() << "cards in list"; + + if (cards.isEmpty()) { + m_error = "No cards found: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + // Находим все clickable ImageView с "****" — это карточки в списке + // Пропускаем кредитные карты + QList cardNodes; + for (const Node &node : xmlScreenParser.findAllNodes()) { + if (node.className == "android.widget.ImageView" && node.clickable && + node.contentDesc.contains("****") && + !node.contentDesc.contains(QString::fromUtf8("Кредитная карта"))) { + cardNodes.append(node); + } + } + + // Кликаем по каждой карте для получения полного номера + for (int i = 0; i < cardNodes.size(); ++i) { + if (i >= cards.size()) break; + const Node &cardNode = cardNodes[i]; + + AdbUtils::makeTap(m_deviceId, cardNode.x(), cardNode.y()); + QThread::msleep(2000); + + // Ждём экран детали карты + bool detailFound = false; + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + if (xmlScreenParser.isCardDetailScreen()) { + detailFound = true; + break; + } + QThread::msleep(1000); + } + + if (!detailFound) { + qWarning() << "[Dushanbe::GetCardInfo] Card detail screen not found for card" << i; + continue; + } + + // Ждём загрузки баланса (может быть 0 пока данные подтягиваются) + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + + QString fullNumber = xmlScreenParser.parseFullCardNumber(); + // Баланс берём с домашнего экрана (parseBalanceFromHomeScreen выше): + // в Душанбе все карты привязаны к одному счёту, и на экране детали + // карты баланс часто отображается как placeholder "0,00 TJS" пока + // данные подгружаются с сервера. + const double cardBalance = balance; + const QString holder = xmlScreenParser.parseCardDetailHolder(); + const QString fullNumberClean = QString(fullNumber).remove(' '); + + qDebug() << "[Dushanbe::GetCardInfo] Card" << i << ":" << fullNumber + << "balance:" << cardBalance << "holder:" << holder; + + // Без полного номера карты материал не создаём и на сервер не отправляем — + // защита от записей с пустым card_number (см. Ozon::GetCardInfo). + if (fullNumberClean.isEmpty()) { + qWarning() << "[Dushanbe::GetCardInfo] Full card number not parsed for card" << i + << "(home-screen lastNumbers=" << cards[i].lastNumbers << ") — skipping"; + AdbUtils::goBack(m_deviceId); + QThread::msleep(1500); + continue; + } + + const QString lastNumbers = fullNumberClean.right(4); + + // Достаём номер счёта: Настройки карты → Реквизиты карты → таб "Реквизиты счета". + // ВРЕМЕННО ОТКЛЮЧЕНО + QString accountNumber; + // const Node settingsBtn = xmlScreenParser.findCardSettingsButton(); + // if (settingsBtn.x() > 0) { + // AdbUtils::makeTap(m_deviceId, settingsBtn.x(), settingsBtn.y()); + // QThread::msleep(1500); + // + // bool sheetFound = false; + // for (int attempt = 0; attempt < 5; ++attempt) { + // xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + // if (xmlScreenParser.isCardSettingsBottomSheet()) { + // sheetFound = true; + // break; + // } + // QThread::msleep(1000); + // } + // + // if (sheetFound) { + // const Node requisitesItem = xmlScreenParser.findCardRequisitesMenuItem(); + // if (requisitesItem.x() > 0) { + // AdbUtils::makeTap(m_deviceId, requisitesItem.x(), requisitesItem.y()); + // QThread::msleep(1500); + // + // bool reqScreenFound = false; + // for (int attempt = 0; attempt < 5; ++attempt) { + // xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + // if (xmlScreenParser.isCardRequisitesScreen()) { + // reqScreenFound = true; + // break; + // } + // QThread::msleep(1000); + // } + // + // if (reqScreenFound) { + // const Node accountTab = xmlScreenParser.findAccountRequisitesTab(); + // if (accountTab.x() > 0) { + // AdbUtils::makeTap(m_deviceId, accountTab.x(), accountTab.y()); + // QThread::msleep(1500); + // + // // Несколько попыток — данные могут подгружаться + // for (int attempt = 0; attempt < 5; ++attempt) { + // xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + // accountNumber = xmlScreenParser.parseAccountNumber(); + // if (!accountNumber.isEmpty()) break; + // QThread::msleep(1000); + // } + // qDebug() << "[Dushanbe::GetCardInfo] accountNumber for card" << i + // << ":" << accountNumber; + // } else { + // qWarning() << "[Dushanbe::GetCardInfo] Account tab not found for card" << i; + // } + // } else { + // qWarning() << "[Dushanbe::GetCardInfo] Requisites screen not detected for card" << i; + // } + // } else { + // qWarning() << "[Dushanbe::GetCardInfo] 'Реквизиты карты' menu item not found for card" << i; + // } + // } else { + // qWarning() << "[Dushanbe::GetCardInfo] Card settings bottom sheet not detected for card" << i; + // } + // + // // Возврат на экран детали карты — bottom sheet и/или экран Реквизитов + // for (int attempt = 0; attempt < 4; ++attempt) { + // xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + // if (xmlScreenParser.isCardDetailScreen()) break; + // AdbUtils::goBack(m_deviceId); + // QThread::msleep(1200); + // } + // } else { + // qWarning() << "[Dushanbe::GetCardInfo] 'Настройки карты' button not found for card" << i; + // } + + MaterialInfo account; + account.deviceId = device.id; + account.appCode = m_appCode; + account.lastNumbers = lastNumbers; + account.cardNumber = QString(fullNumber).remove(' '); + account.accountNumber = accountNumber; + account.amount = cardBalance; + account.currency = "TJS"; + account.description = cards[i].description.isEmpty() ? holder : cards[i].description; + account.status = MaterialStatus::Off; + account.updateTime = QDateTime::currentDateTimeUtc(); + + qDebug() << "[Dushanbe::GetCardInfo] Saving card:" << "lastNumbers=" << lastNumbers + << "cardNumber=" << account.cardNumber << "amount=" << cardBalance; + if (!MaterialDAO::upsertAccount(account)) { + qWarning() << "[Dushanbe::GetCardInfo] Failed to upsert account" << lastNumbers; + } + + // Отправляем на сервер если нет material_id + const BankProfileInfo updatedApp = BankProfileDAO::getApplication(device.id, m_appCode); + if (!updatedApp.bankProfileId.isEmpty()) { + const MaterialInfo savedAccount = MaterialDAO::findAppAccount(device.id, m_appCode, lastNumbers); + if (savedAccount.id != -1 && savedAccount.materialId.isEmpty()) { + NetworkService ns; + ns.setDeviceId(m_deviceId); + const QMap currencyCodes = { + {"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972} + }; + QJsonObject materialBody; + materialBody["bank_profile_id"] = updatedApp.bankProfileId; + materialBody["card_number"] = QString(fullNumber).remove(' '); + materialBody["account_number"] = accountNumber; + materialBody["name"] = updatedApp.fullName; + materialBody["state"] = materialStatusToString(savedAccount.status) == "active" ? "enabled" : "disabled"; + materialBody["currency"] = currencyCodes.value(savedAccount.currency.toUpper(), 972); + ns.setPayload(QJsonDocument(materialBody).toJson()); + ns.addExtra("account_id", savedAccount.id); + ns.postMaterial(); + qDebug() << "[Dushanbe::GetCardInfo] postMaterial done for" << lastNumbers; + } + } + + // Назад к списку карт + AdbUtils::goBack(m_deviceId); + QThread::msleep(1500); + + // Ждём возврата на экран "Мои карты" + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + if (xmlScreenParser.isCardsScreen()) break; + QThread::msleep(1000); + } + } + + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); +} + +} // namespace Dushanbe diff --git a/android/dushanbe/GetCardInfoScript.h b/android/dushanbe/GetCardInfoScript.h new file mode 100644 index 0000000..e2001e3 --- /dev/null +++ b/android/dushanbe/GetCardInfoScript.h @@ -0,0 +1,27 @@ +#pragma once +#include "CommonScript.h" + +namespace Dushanbe { + +class GetCardInfoScript final : public CommonScript { + Q_OBJECT + +public: + explicit GetCardInfoScript( + QString deviceId, + QString appCode, + QObject *parent = nullptr + ); + + ~GetCardInfoScript() override; + +protected: + void doStart() override; + +private: + QString m_deviceId; + const QString m_appCode; + QString m_error; +}; + +} // namespace Dushanbe diff --git a/android/dushanbe/GetLastTransactionsScript.cpp b/android/dushanbe/GetLastTransactionsScript.cpp new file mode 100644 index 0000000..ba46dfe --- /dev/null +++ b/android/dushanbe/GetLastTransactionsScript.cpp @@ -0,0 +1,918 @@ +#include "GetLastTransactionsScript.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include "adb/AdbUtils.h" +#include "dump/TaskDumpRecorder.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "AppLogger.h" + +#include + +// Душанбе показывает суммы как "100,50", "100.50 TJS", "1 000,50", +// "1 000.50 TJS" и т.п. Голый QString::toDouble на таком вернёт 0, +// потому что требует, чтобы вся строка была валидным числом. +static double parseDushanbeAmount(const QString &raw) { + QString s = raw; + s.remove(QRegularExpression(R"([^\d,.\-])")); + s.replace(',', '.'); + const int firstDot = s.indexOf('.'); + if (firstDot >= 0) { + QString head = s.left(firstDot); + QString tail = s.mid(firstDot + 1); + tail.remove('.'); + s = head + '.' + tail; + } + return s.toDouble(); +} + +static QString recognizeTextFromImage(const QByteArray &pngData) { + auto *api = new tesseract::TessBaseAPI(); + if (api->Init(QCoreApplication::applicationDirPath().toUtf8().constData(), "rus+eng")) { + qWarning() << "[Dushanbe::OCR] Failed to init Tesseract"; + delete api; + return {}; + } + Pix *pix = pixReadMem(reinterpret_cast(pngData.constData()), + static_cast(pngData.size())); + if (!pix) { + api->End(); delete api; + return {}; + } + api->SetImage(pix); + char *text = api->GetUTF8Text(); + QString result = QString::fromUtf8(text).trimmed(); + delete[] text; + pixDestroy(&pix); + api->End(); + delete api; + return result; +} + +// Точечный OCR: находит "Номер операции" через общий OCR, затем кропает строку правее +// и распознаёт только цифры. Работает с любым разрешением экрана. +static QString recognizeOperationId(const QByteArray &pngData) { + Pix *pix = pixReadMem(reinterpret_cast(pngData.constData()), + static_cast(pngData.size())); + if (!pix) return {}; + + const int w = pixGetWidth(pix); + const int h = pixGetHeight(pix); + + // 1. Полный OCR с координатами слов — ищем "операции" + auto *api = new tesseract::TessBaseAPI(); + if (api->Init(QCoreApplication::applicationDirPath().toUtf8().constData(), "rus+eng")) { + pixDestroy(&pix); delete api; + return {}; + } + api->SetImage(pix); + api->Recognize(nullptr); + + int opLineY = -1; + int opLineH = 0; + bool prevWasNomer = false; + tesseract::ResultIterator *ri = api->GetIterator(); + if (ri) { + do { + const char *word = ri->GetUTF8Text(tesseract::RIL_WORD); + if (word) { + const QString w_str = QString::fromUtf8(word); + delete[] word; + if (w_str.contains(QString::fromUtf8("омер")) || + w_str.contains(QString::fromUtf8("omep"))) { + prevWasNomer = true; + continue; + } + if (prevWasNomer && + (w_str.contains(QString::fromUtf8("операции")) || + w_str.contains(QString::fromUtf8("onepaции")))) { + int x1, y1, x2, y2; + ri->BoundingBox(tesseract::RIL_WORD, &x1, &y1, &x2, &y2); + opLineY = y1; + opLineH = y2 - y1; + break; + } + prevWasNomer = false; + } + } while (ri->Next(tesseract::RIL_WORD)); + } + api->End(); + delete api; + + if (opLineY < 0) { + pixDestroy(&pix); + return {}; + } + + // 2. Кропаем правую половину этой строки и OCR только цифры + const int cropY = qMax(0, opLineY - opLineH / 2); + const int cropH = opLineH * 2; + BOX *box = boxCreate(w / 2, cropY, w / 2, qMin(cropH, h - cropY)); + Pix *cropped = pixClipRectangle(pix, box, nullptr); + boxDestroy(&box); + pixDestroy(&pix); + + if (!cropped) return {}; + + auto *api2 = new tesseract::TessBaseAPI(); + if (api2->Init(QCoreApplication::applicationDirPath().toUtf8().constData(), "eng")) { + pixDestroy(&cropped); delete api2; + return {}; + } + api2->SetVariable("tessedit_char_whitelist", "0123456789"); + api2->SetPageSegMode(tesseract::PSM_SINGLE_LINE); + api2->SetImage(cropped); + char *text = api2->GetUTF8Text(); + QString result = QString::fromUtf8(text).trimmed(); + delete[] text; + pixDestroy(&cropped); + api2->End(); + delete api2; + + result.remove(QRegularExpression("[^0-9]")); + if (result.length() >= 8) { + qDebug() << "[Dushanbe::OCR] Precise operation ID:" << result; + return result; + } + return {}; +} + +struct OcrTransactionDetail { + QString date; + QString time; + QString operationId; + QString provider; + QString senderAccount; + QString receiverAccount; + double amount = 0.0; + double fee = 0.0; + QString status; +}; + +static OcrTransactionDetail parseDetailFromOcr(const QString &ocrText) { + OcrTransactionDetail d; + const QStringList lines = ocrText.split('\n'); + + for (const QString &rawLine : lines) { + const QString line = rawLine.trimmed(); + + // "Дата операции: 05.04.2026" или "Дата операции:" на отдельной строке + if (line.contains(QString::fromUtf8("Дата операции"))) { + QRegularExpression re(R"((\d{2}\.\d{2}\.\d{4}))"); + if (auto m = re.match(line); m.hasMatch()) d.date = m.captured(1); + } + // "Время операции: 18:19:54" + else if (line.contains(QString::fromUtf8("Время операции"))) { + QRegularExpression re(R"((\d{2}:\d{2}:\d{2}))"); + if (auto m = re.match(line); m.hasMatch()) d.time = m.captured(1); + } + // "Номер операции: 1598279763" + else if (line.contains(QString::fromUtf8("омер операции"))) { + QRegularExpression re(R"((\d{5,}))"); + if (auto m = re.match(line); m.hasMatch()) d.operationId = m.captured(1); + } + // "Поставщик: DC (по номеру карты)" + else if (line.contains(QString::fromUtf8("Поставщик"))) { + QRegularExpression re(R"(Поставщик[:\s]*(.+))"); + if (auto m = re.match(line); m.hasMatch()) d.provider = m.captured(1).trimmed(); + } + // "Счет отправителя: 9762***3258" + else if (line.contains(QString::fromUtf8("Счет отправителя")) || + line.contains(QString::fromUtf8("чет отправителя"))) { + QRegularExpression re(R"((\d[\d*]+))"); + if (auto m = re.match(line); m.hasMatch()) d.senderAccount = m.captured(1); + } + // "Счет получателя: 9762000186263942" или "992111100664" + else if (line.contains(QString::fromUtf8("Счет получателя")) || + line.contains(QString::fromUtf8("чет получателя"))) { + QRegularExpression re(R"((\d{6,}))"); + if (auto m = re.match(line); m.hasMatch()) d.receiverAccount = m.captured(1); + } + // "Сумма операции: 1.00" + else if (line.contains(QString::fromUtf8("Сумма операции")) || + line.contains(QString::fromUtf8("умма операции"))) { + QRegularExpression re(R"((\d+[\.,]\d{2}))"); + if (auto m = re.match(line); m.hasMatch()) { + QString val = m.captured(1); + val.replace(',', '.'); + d.amount = val.toDouble(); + } + } + // "Комиссия: 0.00" + else if (line.contains(QString::fromUtf8("Комиссия")) || + line.contains(QString::fromUtf8("омиссия"))) { + QRegularExpression re(R"((\d+[\.,]\d{2}))"); + if (auto m = re.match(line); m.hasMatch()) { + QString val = m.captured(1); + val.replace(',', '.'); + d.fee = val.toDouble(); + } + } + // "Статус: Успешный" + else if (line.contains(QString::fromUtf8("Статус"))) { + if (line.contains(QString::fromUtf8("Успешн"))) { + d.status = QString::fromUtf8("Успешный"); + } + } + } + + // Fallback: если дата/время не найдены в "ключ: значение", ищем паттерном + if (d.date.isEmpty()) { + QRegularExpression re(R"((\d{2}\.\d{2}\.\d{4}))"); + if (auto m = re.match(ocrText); m.hasMatch()) d.date = m.captured(1); + } + if (d.time.isEmpty()) { + QRegularExpression re(R"((\d{2}:\d{2}:\d{2}))"); + auto it = re.globalMatch(ocrText); + QStringList times; + while (it.hasNext()) times.append(it.next().captured(1)); + if (times.size() >= 2) d.time = times[1]; + else if (times.size() == 1) d.time = times[0]; + } + + if (ocrText.contains(QString::fromUtf8("Успешн"))) d.status = QString::fromUtf8("Успешный"); + + return d; +} + +namespace Dushanbe { + +GetLastTransactionsScript::GetLastTransactionsScript( + QString deviceId, + QString appCode, + double searchAmount, + QDateTime searchTime, + QString materialId, + QObject *parent +) : CommonScript(parent), + m_deviceId(std::move(deviceId)), + m_appCode(std::move(appCode)), + m_searchAmount(searchAmount), + m_searchTime(std::move(searchTime)), + m_materialId(std::move(materialId)) { +} + +GetLastTransactionsScript::~GetLastTransactionsScript() = default; + +void GetLastTransactionsScript::doStart() { + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); + qDebug() << "[Dushanbe::GetLastTransactions] device.id=" << device.id << "adbSerial=" << device.adbSerial + << "appCode=" << m_appCode << "app.id=" << app.id + << "pinCode.length=" << app.pinCode.length() << "package=" << app.package; + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + const TaskDumpMeta dumpMeta{m_materialId, m_searchAmount, {}, {}}; + TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + if (!xmlScreenParser.isHomeScreen()) { + qDebug() << "[Dushanbe::GetLastTransactions] Not on home screen, restarting app..."; + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, + device.screenWidth, device.screenHeight)) { + m_error = "Cannot reach home screen: " + device.name + " (" + m_deviceId + ")"; + qWarning() << m_error; + AppLogger::log("dushanbe/GetLastTransactions", m_deviceId, m_error, {}, "FETCH_TRANSACTIONS"); + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + + // Тапаем "История" в нижнем меню + Node historyBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("История")); + if (historyBtn.x() == 0 && historyBtn.y() == 0) { + m_error = "Cannot find History button: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + AdbUtils::makeTap(m_deviceId, historyBtn.x(), historyBtn.y()); + QThread::msleep(3000); + + // Ждём экран истории + bool historyLoaded = false; + for (int attempt = 0; attempt < 10; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + + // Проверяем диалог "Ошибка" — нажимаем "ОК" и обновляем свайпом вниз + if (!xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Ошибка")).isEmpty()) { + Node okBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("ОК")); + if (!okBtn.isEmpty()) { + qDebug() << "[Dushanbe::GetLastTransactions] Error dialog detected, tapping OK"; + AdbUtils::makeTap(m_deviceId, okBtn.x(), okBtn.y()); + QThread::msleep(1000); + } + continue; + } + + if (xmlScreenParser.isHistoryScreen()) { + historyLoaded = true; + break; + } + QThread::msleep(1500); + } + + if (!historyLoaded) { + m_error = "History screen not loaded: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + // Переключаемся на вкладку "Выписка" (по умолчанию открыта "Операции") + Node vypiskaBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Выписка")); + if (vypiskaBtn.isEmpty()) { + m_error = "Cannot find 'Выписка' tab: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + qDebug() << "[Dushanbe::GetLastTransactions] Switching to 'Выписка' tab"; + AdbUtils::makeTap(m_deviceId, vypiskaBtn.x(), vypiskaBtn.y()); + QThread::msleep(3000); + + // Pull-to-refresh: свайп сверху вниз для обновления списка + { + const int x = device.screenWidth / 2; + const int fromY = device.screenHeight / 3; + const int toY = device.screenHeight * 2 / 3; + AdbUtils::makeSwipe(m_deviceId, x, fromY, x, toY, 500); + QThread::msleep(3000); + qDebug() << "[Dushanbe::GetLastTransactions] Pull-to-refresh on Выписка"; + } + + // Сохраняем дамп Выписки для анализа формата + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Закрываем возможный диалог "Ошибка" на Выписке + if (!xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Ошибка")).isEmpty()) { + Node okBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("ОК")); + if (!okBtn.isEmpty()) { + qDebug() << "[Dushanbe::GetLastTransactions] Error dialog on Выписка, tapping OK"; + AdbUtils::makeTap(m_deviceId, okBtn.x(), okBtn.y()); + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + } + + // Скриншот списка транзакций — отправим при ошибке, удалим при успехе + const QByteArray listScreenshot = AdbUtils::captureScreenshotBytes(m_deviceId); + + // Находим accountId + int accountId = -1; + const QList accounts = MaterialDAO::getAccounts(device.id, m_appCode); + if (!accounts.isEmpty()) { + accountId = accounts.first().id; + } else { + qWarning() << "[Dushanbe::GetLastTransactions] No account found"; + } + + qDebug() << "[Dushanbe::GetLastTransactions] Searching: amount=" << m_searchAmount + << "time=" << m_searchTime.toString(Qt::ISODate); + + // Время в банковском приложении показывается в таймзоне Душанбе (UTC+5), не устройства + const QTimeZone tjTz("Asia/Dushanbe"); + const int maxScrolls = 10; + + auto interrupted = []() { + return QThread::currentThread()->isInterruptionRequested(); + }; + + struct VypiskaItem { + double amount = 0.0; + QString time; // "21:31:21" + QString date; // "05.04.2026" + bool dateReliable = false; // true если дата из content-desc (P2P), false если из groupDates + Node node; + }; + + for (int scroll = 0; scroll <= maxScrolls; ++scroll) { + if (interrupted()) { + m_error = "Interrupted"; + emit finishedWithResult(m_error); + return; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + + // Верх нижнего меню: минимальный y1() среди табов и FAB "Сканировать". + // Тап по строке с y >= navTopY - navMargin промахивается и попадает в FAB/таб. + int navTopY = device.screenHeight; + for (const QString &lbl : { + QString::fromUtf8("Главная"), QString::fromUtf8("История"), + QString::fromUtf8("Переводы"), QString::fromUtf8("Сканировать")}) { + Node nav = xmlScreenParser.findNodeByContentDesc(lbl); + if (!nav.isEmpty() && nav.y1() > 0 && nav.y1() < navTopY) navTopY = nav.y1(); + } + const int navMargin = 20; + qDebug() << "[Dushanbe::GetLastTransactions] navTopY=" << navTopY; + + // 1. Собираем список дат-групп + извлекаем "встроенную" верхнюю транзакцию, + // которая впечатана в content-desc родительского ImageView "Обновлено: ..." + // вместо отдельного android.view.View (самая свежая запись в Выписке). + const QDate todayLocal = QDateTime::currentDateTimeUtc().toTimeZone(tjTz).date(); + QStringList groupDates; + static const QRegularExpression reDate(R"(^\d{2}\.\d{2}\.\d{4}$)"); + static const QRegularExpression reEmbTime(R"(^\s*(\d{2}:\d{2}:\d{2})\s*$)"); + Node updatedImgNode; + VypiskaItem embedded; + bool hasEmbedded = false; + for (const Node &n : xmlScreenParser.findAllNodes()) { + if (n.className != "android.widget.ImageView") continue; + if (!n.contentDesc.startsWith(QString::fromUtf8("Обновлено"))) continue; + updatedImgNode = n; + const QStringList descLines = n.contentDesc.split('\n'); + for (const QString &line : descLines) { + const QString t = line.trimmed(); + if (reDate.match(t).hasMatch()) { + groupDates.append(t); + } else if (t.compare(QString::fromUtf8("Сегодня"), Qt::CaseInsensitive) == 0) { + groupDates.append(todayLocal.toString("dd.MM.yyyy")); + } else if (t.compare(QString::fromUtf8("Вчера"), Qt::CaseInsensitive) == 0) { + groupDates.append(todayLocal.addDays(-1).toString("dd.MM.yyyy")); + } + } + // Паттерн встроенной: ...\ncard\ncard\namount\nописание\n HH:MM:SS\n... + for (int i = 4; i < descLines.size(); ++i) { + const auto tm = reEmbTime.match(descLines[i]); + if (!tm.hasMatch()) continue; + bool ok = false; + const double amt = descLines[i - 2].trimmed().toDouble(&ok); + if (!ok) continue; + embedded.amount = amt; + embedded.time = tm.captured(1); + if (!groupDates.isEmpty()) embedded.date = groupDates.first(); + hasEmbedded = true; + break; + } + break; + } + + // 2. Собираем View-транзакции (формат Выписки) + // Известные форматы content-desc (всё через '\n'): + // a) DCWallet (старый, 5 строк): DCWallet*WDC...\ncard\namount\nописание\n HH:MM:SS + // b) DCWallet (новый, 6 строк): DCWallet*WDC...\nphone-card-\ncard_owner\namount\nописание\n HH:MM:SS + // c) P2P: Сегодня|dd.MM.yyyy\ncard\ncard\namount\nописание\n HH:MM:SS + // Позиция amount уехала между версиями приложения, поэтому ищем по + // паттерну "[-]?digits.dd" среди всех строк — устойчиво к перестановке полей. + static const QRegularExpression reTime(R"(\d{2}:\d{2}:\d{2})"); + static const QRegularExpression reDateOrToday( + QString("^(?:\\d{2}\\.\\d{2}\\.\\d{4}|%1|%2)$") + .arg(QString::fromUtf8("Сегодня"), QString::fromUtf8("Вчера"))); + static const QRegularExpression reAmountLine(R"(^-?\d+\.\d{2}$)"); + QList items; + for (const Node &n : xmlScreenParser.findAllNodes()) { + if (n.className != "android.view.View") continue; + const QStringList lines = n.contentDesc.split('\n'); + if (lines.size() < 5) continue; + const QString last = lines.last().trimmed(); + if (!reTime.match(last).hasMatch()) continue; + + // Определяем формат: если первая строка — дата/Сегодня/Вчера, это P2P формат + const QString firstLine = lines.first().trimmed(); + const bool isP2P = reDateOrToday.match(firstLine).hasMatch(); + if (!isP2P && !n.contentDesc.contains("DCWallet") && !n.contentDesc.contains("***")) continue; + + VypiskaItem it; + bool ok = false; + // Ищем строку вида "-?\d+\.\d{2}" — устойчиво и к 5-, и к 6-строчному + // DCWallet, и к P2P. Карты с пробелами ("9762 0002 ...") и текстовые + // поля не подпадают под паттерн. + for (const QString &line : lines) { + if (!reAmountLine.match(line.trimmed()).hasMatch()) continue; + it.amount = line.trimmed().toDouble(&ok); + if (ok) break; + } + if (!ok) continue; + it.time = last; + it.node = n; + + // В P2P формате дата встроена в первую строку — надёжная + if (isP2P) { + if (firstLine.compare(QString::fromUtf8("Сегодня"), Qt::CaseInsensitive) == 0) + it.date = todayLocal.toString("dd.MM.yyyy"); + else if (firstLine.compare(QString::fromUtf8("Вчера"), Qt::CaseInsensitive) == 0) + it.date = todayLocal.addDays(-1).toString("dd.MM.yyyy"); + else + it.date = firstLine; + it.dateReliable = true; + } + items.append(it); + } + + // 2a. Если нашли встроенную транзакцию — создаём синтетический Node с + // координатами тапа над первой "реальной" View-записью и добавляем + // как первый элемент списка. + if (hasEmbedded && !updatedImgNode.isEmpty()) { + int firstViewY1 = -1; + int firstViewRowH = 0; + for (const VypiskaItem &v : items) { + if (firstViewY1 < 0 || v.node.y1() < firstViewY1) { + firstViewY1 = v.node.y1(); + firstViewRowH = v.node.height(); + } + } + const int topY = updatedImgNode.y1(); + // Если первая реальная View-запись уже находится у верха ImageView + // (значит скролл поднял контент и встроенная запись ушла за экран), + // пропускаем embedded — её визуально нет. + const int minEmbeddedSpace = 120; + const bool embeddedVisible = firstViewY1 < 0 || (firstViewY1 - topY) >= minEmbeddedSpace; + if (embeddedVisible) { + int tapY; + if (firstViewY1 > 0 && firstViewRowH > 0) { + // Между встроенной записью и первой "реальной" View лежит + // заголовок даты (~0.4 * rowH). Ставим тап по центру + // строки: firstViewY1 - headerH - rowH/2 ≈ firstViewY1 - rowH * 0.9. + // Дополнительно ограничиваем диапазоном [topY + rowH/2 .. firstViewY1 - 40]. + tapY = firstViewY1 - (firstViewRowH * 9 / 10); + const int minY = topY + firstViewRowH / 2; + const int maxY = firstViewY1 - 40; + if (tapY < minY) tapY = minY; + if (tapY > maxY) tapY = maxY; + } else { + tapY = topY + 200; + } + Node synth; + synth.setBounds(updatedImgNode.x1(), tapY - 10, updatedImgNode.x2(), tapY + 10); + synth.className = "android.view.View"; + synth.contentDesc = updatedImgNode.contentDesc; + synth.clickable = true; + embedded.node = synth; + items.prepend(embedded); + qDebug() << "[Dushanbe::GetLastTransactions] Embedded tx added:" + << "amount=" << embedded.amount << "time=" << embedded.time + << "date=" << embedded.date << "tapY=" << tapY; + } else { + qDebug() << "[Dushanbe::GetLastTransactions] Embedded tx skipped " + "(scrolled off): firstViewY1=" << firstViewY1 << "topY=" << topY; + } + } + + // 3. Сортируем по Y и группируем по разрывам (> 30px → новая группа-день) + std::sort(items.begin(), items.end(), [](const VypiskaItem &a, const VypiskaItem &b) { + return a.node.y1() < b.node.y1(); + }); + int groupIdx = 0; + int prevY2 = -1; + for (VypiskaItem &it : items) { + if (prevY2 >= 0 && it.node.y1() - prevY2 > 30) ++groupIdx; + // Не перезаписываем дату если она уже установлена (P2P формат) + if (it.date.isEmpty() && groupIdx < groupDates.size()) + it.date = groupDates[groupIdx]; + prevY2 = it.node.y2(); + } + + qDebug() << "[Dushanbe::GetLastTransactions] scroll=" << scroll + << "items:" << items.size() << "groups:" << groupDates + << "hasEmbedded:" << hasEmbedded; + // Логируем все items для отладки + for (int i = 0; i < items.size(); ++i) { + qDebug() << "[Dushanbe::GetLastTransactions] item[" << i << "] amount=" << items[i].amount + << "date=" << items[i].date << "time=" << items[i].time + << "y=" << items[i].node.y(); + } + + const double searchAbs = qAbs(m_searchAmount); + for (const auto &tx : items) { + const double absListAmt = qAbs(tx.amount); + const bool amountDirect = qAbs(absListAmt - searchAbs) < 0.01; + const bool amountX100 = qAbs(absListAmt * 100.0 - searchAbs) < 0.01; + if (!amountDirect && !amountX100) continue; + const bool matchedX100 = amountX100 && !amountDirect; + + qDebug() << "[Dushanbe::GetLastTransactions] Amount match:" << tx.amount + << (matchedX100 ? "(scaled x100)" : "(direct)") + << "date:" << tx.date << "time:" << tx.time; + + // Pre-filter по ДАТЕ: только если дата надёжная (из content-desc P2P формата). + // DCWallet транзакции получают дату из groupDates ImageView, которая + // неправильно присваивается при скролле — не фильтруем по ней. + if (tx.dateReliable && m_searchTime.isValid() && !tx.date.isEmpty()) { + const QDate listDate = QDate::fromString(tx.date, "dd.MM.yyyy"); + const QDate searchLocalDate = m_searchTime.toTimeZone(tjTz).date(); + if (listDate.isValid() && searchLocalDate.isValid()) { + const qint64 dayDiff = qAbs(listDate.daysTo(searchLocalDate)); + if (dayDiff > 3) { + qDebug() << "[Dushanbe::GetLastTransactions] Date pre-filter skip:" + << tx.date << "vs" << searchLocalDate.toString("dd.MM.yyyy") + << "diff=" << dayDiff << "days"; + continue; + } + } + } + + // Проверка времени по HH:MM:SS (±10 минут, с учётом wraparound полуночи) + if (m_searchTime.isValid() && !tx.time.isEmpty()) { + const QTime listTime = QTime::fromString(tx.time, "HH:mm:ss"); + const QTime searchLocalTime = m_searchTime.toTimeZone(tjTz).time(); + if (listTime.isValid() && searchLocalTime.isValid()) { + const int diffSecs = qAbs(listTime.secsTo(searchLocalTime)); + if (diffSecs > 600 && diffSecs < 85800) { // 85800 = 23h50m + qDebug() << "[Dushanbe::GetLastTransactions] Time pre-filter skip:" + << tx.time << "vs" << searchLocalTime.toString("HH:mm:ss") + << "diff=" << diffSecs << "sec"; + continue; + } + } + } + + Node txNode = tx.node; + + if (txNode.x() == 0 && txNode.y() == 0) { + qWarning() << "[Dushanbe::GetLastTransactions] Cannot find node for transaction"; + continue; + } + + // Строка под нижним меню — тап попадёт в FAB "Сканировать" или таб. + // Пропускаем кандидата, ищем совпадение выше; если таких нет — скролл. + if (txNode.y() >= navTopY - navMargin) { + qDebug() << "[Dushanbe::GetLastTransactions] Candidate y=" << txNode.y() + << "behind nav (navTopY=" << navTopY << "), skipping, will scroll"; + continue; + } + + // Тап по левому краю ряда — уходим от центра экрана, где FAB "Сканировать". + const int tapX = txNode.x1() + 80; + AdbUtils::makeTap(m_deviceId, tapX, txNode.y()); + QThread::msleep(3000); + if (interrupted()) { m_error = "Interrupted"; emit finishedWithResult(m_error); return; } + + // Ждём экран деталей Выписки — ImageView с content-desc начинающимся с "Успешный платеж" + OcrTransactionDetail detail; + bool detailLoaded = false; + for (int attempt = 0; attempt < 10; ++attempt) { + if (interrupted()) { m_error = "Interrupted"; emit finishedWithResult(m_error); return; } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + // 1) Bottom-sheet формат (3.2.33, см. assets/dushanbe/3.2.33/tx_detail.xml): + // одна нода со всем content-desc через \n. Лейблы без двоеточий, сумма — + // отдельной строкой со знаком сразу после даты+времени. + // "Успешный платёж\n21.05.2026 16:58:32\n-10\nКарта\n...\nПолучатель\n...\nЗакрыть" + for (const Node &n : xmlScreenParser.findAllNodes()) { + const QString cd = n.contentDesc; + if (!cd.contains(QString::fromUtf8("Карта"))) continue; + if (!cd.contains(QString::fromUtf8("Получатель"))) continue; + if (!cd.contains(QString::fromUtf8("Операция"))) continue; + const QStringList lines = cd.split('\n', Qt::SkipEmptyParts); + if (lines.size() < 6) continue; + + // lines[0] — заголовок: "Успешный платёж" / "Поступление" / ... + if (lines[0].contains(QString::fromUtf8("Успешн")) + || lines[0].contains(QString::fromUtf8("Поступление")) + || lines[0].contains(QString::fromUtf8("выполнен"))) { + detail.status = QString::fromUtf8("Успешный"); + } + // lines[1] — "dd.MM.yyyy HH:mm:ss" + { + static const QRegularExpression reDt( + R"((\d{2}\.\d{2}\.\d{4})\s+(\d{2}:\d{2}:\d{2}))"); + const auto m = reDt.match(lines[1]); + if (m.hasMatch()) { + detail.date = m.captured(1); + detail.time = m.captured(2); + } + } + // lines[2] — сумма со знаком: "-10" / "30" / "30.50" / "1 000,50". + detail.amount = parseDushanbeAmount(lines[2]); + + // Дальше — пары label/value; значение может быть multi-line (Получатель). + // На сервер шлём первую строку получателя — таков сложившийся контракт. + static const QSet labels{ + QString::fromUtf8("Карта"), + QString::fromUtf8("Получатель"), + QString::fromUtf8("Операция"), + QString::fromUtf8("Дата"), + QString::fromUtf8("Время"), + QString::fromUtf8("Закрыть"), + QString::fromUtf8("Назад"), + }; + for (int i = 3; i < lines.size(); ++i) { + const QString lbl = lines[i].trimmed(); + if (!labels.contains(lbl)) continue; + QStringList vals; + for (int j = i + 1; j < lines.size(); ++j) { + const QString cand = lines[j].trimmed(); + if (labels.contains(cand)) break; + vals << cand; + } + if (vals.isEmpty()) continue; + if (lbl == QString::fromUtf8("Получатель")) { + detail.receiverAccount = vals.first(); + } else if (lbl == QString::fromUtf8("Дата") && detail.date.isEmpty()) { + detail.date = vals.first(); + } else if (lbl == QString::fromUtf8("Время") && detail.time.isEmpty()) { + detail.time = vals.first(); + } + } + detailLoaded = true; + break; + } + // 2) Старый blob-формат (до 3.2.33): "Успешный платеж" / "Поступление" — + // одна нода с ключами и значениями через \n. + if (!detailLoaded) for (const Node &n : xmlScreenParser.findAllNodes()) { + if (!n.contentDesc.contains(QString::fromUtf8("Дата"))) continue; + if (!n.contentDesc.contains(QString::fromUtf8("Время"))) continue; + const QStringList lines = n.contentDesc.split('\n'); + for (int i = 0; i < lines.size(); ++i) { + QString key = lines[i].trimmed(); + if (key.endsWith(':')) key.chop(1); // нормализуем "Дата:" → "Дата" + if (i + 1 >= lines.size()) continue; + const QString val = lines[i + 1].trimmed(); + if (key == QString::fromUtf8("Получатель")) { + detail.receiverAccount = val; + } else if (key == QString::fromUtf8("Сумма")) { + detail.amount = parseDushanbeAmount(val); + } else if (key == QString::fromUtf8("Дата")) { + detail.date = val; + } else if (key == QString::fromUtf8("Время")) { + detail.time = val; + } + } + detailLoaded = true; + break; + } + // 3) Split-формат: лейблы (с ':') и значения в отдельных View-нодах + // на одной Y-полосе. Промежуточная версия между blob и bottom-sheet. + if (!detailLoaded) { + const QMap pairs = xmlScreenParser.parseLabelValuePairs(); + const QString d = pairs.value(QString::fromUtf8("Дата")); + const QString t = pairs.value(QString::fromUtf8("Время")); + if (!d.isEmpty() && !t.isEmpty()) { + detail.date = d; + detail.time = t; + detail.receiverAccount = pairs.value(QString::fromUtf8("Получатель")); + detail.amount = parseDushanbeAmount(pairs.value(QString::fromUtf8("Сумма"))); + if (detail.amount == 0.0) { + detail.amount = parseDushanbeAmount(pairs.value(QString::fromUtf8("К зачислению"))); + } + detail.fee = parseDushanbeAmount(pairs.value(QString::fromUtf8("Комиссия"))); + detail.operationId = pairs.value(QString::fromUtf8("Номер операции")); + detail.status = pairs.value(QString::fromUtf8("Статус")); + detailLoaded = true; + } + } + if (detailLoaded) break; + QThread::msleep(1500); + } + + if (!detailLoaded) { + qWarning() << "[Dushanbe::GetLastTransactions] Detail screen not loaded " + "(tap missed or detail format unknown); staying on Выписка"; + // Не вызываем goBack — после промаха тапа мы всё ещё на Выписке. + // goBack тут закрывает Выписку и ломает дальнейший скролл. + // Если же тап сработал и открылся НЕ-распознанный экран — следующая + // итерация parse обнаружит это, и мы обработаем штатно. + QThread::msleep(500); + continue; + } + qDebug() << "[Dushanbe::GetLastTransactions] Detail parsed:" + << "date=" << detail.date << "time=" << detail.time + << "amount=" << detail.amount << "receiver=" << detail.receiverAccount; + + // Проверяем время ±10 минут + if (m_searchTime.isValid() && !detail.date.isEmpty() && !detail.time.isEmpty()) { + QDateTime txTime = QDateTime::fromString( + detail.date + " " + detail.time, "dd.MM.yyyy HH:mm:ss"); + if (!txTime.isValid()) { + txTime = QDateTime::fromString(detail.date + " " + detail.time, "dd.MM.yyyy HH:mm"); + } + if (txTime.isValid()) { + txTime.setTimeZone(tjTz); + const qint64 diffSecs = qAbs(txTime.toUTC().secsTo(m_searchTime.toUTC())); + if (diffSecs > 600) { + qDebug() << "[Dushanbe::GetLastTransactions] Time mismatch: diff=" << diffSecs << "sec (>10min), skipping"; + AdbUtils::goBack(m_deviceId); + QThread::msleep(1000); + continue; + } + } + } + + // Нашли! Сохраняем + qDebug() << "[Dushanbe::GetLastTransactions] FOUND:" + << "amount=" << detail.amount + << "date=" << detail.date << detail.time + << "opId=" << detail.operationId + << "receiver=" << detail.receiverAccount + << "status=" << detail.status; + + // Формируем task_result напрямую из распарсенных данных и отдаём + // EventHandler через сигнал — без записи/чтения БД. + { + const double finalAmount = matchedX100 ? detail.amount * 100.0 : detail.amount; + const double finalFee = matchedX100 ? detail.fee * 100.0 : detail.fee; + + QDateTime txTime = QDateTime::fromString( + detail.date + " " + detail.time, "dd.MM.yyyy HH:mm:ss"); + if (!txTime.isValid()) { + txTime = QDateTime::fromString(detail.date + " " + detail.time, "dd.MM.yyyy HH:mm"); + } + if (txTime.isValid()) txTime.setTimeZone(tjTz); + + QJsonObject txJson; + txJson["bank_transaction_id"] = detail.operationId.isEmpty() + ? QString("dushanbe_") + detail.date + "_" + detail.time + : detail.operationId; + // Списание хранится с отрицательным знаком, пополнение — положительным. + // detail.amount всегда положителен (OCR), знак берём из списка Выписки. + const double signedAmount = (tx.amount < 0 ? -1.0 : 1.0) * qAbs(finalAmount); + txJson["amount"] = static_cast(signedAmount * 100); + txJson["currency"] = 972; // TJS + txJson["transaction_type"] = "bank"; + txJson["bank_participator_id"] = detail.receiverAccount; + txJson["status"] = detail.status.contains(QString::fromUtf8("Успешн")) + ? QStringLiteral("completed") : QStringLiteral("progress"); + QJsonObject extraObj; + extraObj["fee"] = static_cast(finalFee * 100); + txJson["extra"] = extraObj; + txJson["created_at"] = txTime.isValid() + ? txTime.toUTC().toString(Qt::ISODate) : QJsonValue(QJsonValue::Null); + + qDebug() << "[Dushanbe::GetLastTransactions] Emitting parsed tx:" + << QJsonDocument(txJson).toJson(QJsonDocument::Compact); + emit transactionParsed(txJson); + } + + // Скриншот экрана деталей транзакции — отправляем в мониторинг + m_resultScreenshot = AdbUtils::captureScreenshotBytes(m_deviceId); + if (!m_resultScreenshot.isEmpty()) + emit screenshotReady(m_resultScreenshot); + + // Назад к истории + Node nazadBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Назад")); + if (nazadBtn.x() > 0 && nazadBtn.y() > 0) { + AdbUtils::makeTap(m_deviceId, nazadBtn.x(), nazadBtn.y()); + } else { + AdbUtils::goBack(m_deviceId); + } + QThread::msleep(1000); + + emit finishedWithResult(m_error); // m_error пустой — успех + return; + } + + // Скроллим вниз — свайп внутри области транзакций + if (scroll < maxScrolls) { + if (interrupted()) { m_error = "Interrupted"; emit finishedWithResult(m_error); return; } + const int x = device.screenWidth / 2; + // Определяем область контента из реальных Y позиций items + int contentBottom = device.screenHeight * 2 / 3; + int contentTop = device.screenHeight / 3; + if (!items.isEmpty()) { + contentBottom = items.last().node.y(); + contentTop = items.first().node.y(); + } + const int fromY = qMin(contentBottom, device.screenHeight * 2 / 3); + const int toY = qMax(contentTop, device.screenHeight / 4); + qDebug() << "[Dushanbe::GetLastTransactions] Swipe from" << fromY << "to" << toY; + AdbUtils::makeSwipe(m_deviceId, x, fromY, x, toY, 500); + QThread::msleep(2000); + } + } + + m_error = "Transaction not found: amount=" + QString::number(m_searchAmount, 'f', 2) + + " time=" + m_searchTime.toString(Qt::ISODate); + qWarning() << "[Dushanbe::GetLastTransactions]" << m_error; + + // Отправляем скриншот списка транзакций, сделанный до начала поиска + m_resultScreenshot = listScreenshot.isEmpty() + ? AdbUtils::captureScreenshotBytes(m_deviceId) : listScreenshot; + if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot); + + // Возвращаемся на главную + Node glavnayaBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Главная")); + if (glavnayaBtn.x() > 0 && glavnayaBtn.y() > 0) { + AdbUtils::makeTap(m_deviceId, glavnayaBtn.x(), glavnayaBtn.y()); + } else { + AdbUtils::goBack(m_deviceId); + } + QThread::msleep(1000); + + emit finishedWithResult(m_error); +} + +} // namespace Dushanbe diff --git a/android/dushanbe/GetLastTransactionsScript.h b/android/dushanbe/GetLastTransactionsScript.h new file mode 100644 index 0000000..a8ffbfa --- /dev/null +++ b/android/dushanbe/GetLastTransactionsScript.h @@ -0,0 +1,43 @@ +#pragma once +#include "CommonScript.h" +#include +#include +#include "db/DeviceInfo.h" +#include "db/BankProfileInfo.h" + +namespace Dushanbe { + +class GetLastTransactionsScript final : public CommonScript { + Q_OBJECT + +public: + explicit GetLastTransactionsScript( + QString deviceId, + QString appCode, + double searchAmount = 0, + QDateTime searchTime = {}, + QString materialId = {}, + QObject *parent = nullptr + ); + + ~GetLastTransactionsScript() override; + +signals: + // Испускается, когда транзакция найдена и распарсена с экрана деталей. + // EventHandler использует эти данные напрямую для task_result вместо + // повторного чтения из БД. + void transactionParsed(const QJsonObject &tx); + +protected: + void doStart() override; + +private: + QString m_deviceId; + const QString m_appCode; + const double m_searchAmount; // сумма в сомони (0 = все последние) + const QDateTime m_searchTime; // примерное время (UTC) + const QString m_materialId; // UUID задачи (для сохранения дампов) + QString m_error; +}; + +} // namespace Dushanbe diff --git a/android/dushanbe/GetProfileInfoScript.cpp b/android/dushanbe/GetProfileInfoScript.cpp new file mode 100644 index 0000000..0fab758 --- /dev/null +++ b/android/dushanbe/GetProfileInfoScript.cpp @@ -0,0 +1,156 @@ +#include "GetProfileInfoScript.h" + +#include +#include +#include +#include + +#include "adb/AdbUtils.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "dump/TaskDumpRecorder.h" +#include "net/NetworkService.h" + +namespace Dushanbe { + +GetProfileInfoScript::GetProfileInfoScript( + QString deviceId, + QString appCode, + QObject *parent +) : CommonScript(parent), + m_deviceId(std::move(deviceId)), + m_appCode(std::move(appCode)) { +} + +GetProfileInfoScript::~GetProfileInfoScript() = default; + +void GetProfileInfoScript::doStart() { + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + const TaskDumpMeta dumpMeta{}; + TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + if (!xmlScreenParser.isHomeScreen()) { + qDebug() << "[Dushanbe::GetProfileInfo] Not on home screen, restarting app..."; + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, + device.screenWidth, device.screenHeight)) { + m_error = "Cannot reach home screen: " + device.name + " (" + m_deviceId + ") " + m_appCode; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + + // Всегда тапаем кнопку обновления — баланс на экране может быть закэширован + // и не отражать актуальное состояние счёта. Лоадера у refresh нет, + // поэтому ждём фиксированно. + Node refreshBtn = xmlScreenParser.findRefreshButtonOnHomeScreen(); + if (refreshBtn.x() > 0) { + qDebug() << "[Dushanbe::GetProfileInfo] Tapping refresh to get up-to-date balance"; + AdbUtils::makeTap(m_deviceId, refreshBtn.x(), refreshBtn.y()); + QThread::msleep(3000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } else { + qWarning() << "[Dushanbe::GetProfileInfo] Refresh button not found, parsing as-is"; + } + + const double parsedBalance = xmlScreenParser.parseBalanceFromHomeScreen(); + qDebug() << "[Dushanbe::GetProfileInfo] Parsed balance after refresh:" << parsedBalance; + if (parsedBalance <= 0.0) { + m_error = "Balance is zero after refresh: " + m_deviceId + " " + m_appCode; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + BankProfileDAO::updateAmount(app.id, parsedBalance); + // У Душанбе все карты сидят на одном счёте, а экран деталей карты + // отдаёт placeholder "0,00 TJS" — единственный источник правды + // для materials.amount это баланс home-screen. + MaterialDAO::setAmountByApp(device.id, m_appCode, parsedBalance); + + // Тапаем по кнопке меню (гамбургер, левый верхний угол) + // Это первый clickable ImageView в верхней панели + Node menuButton; + for (const Node &node : xmlScreenParser.findAllNodes()) { + if (node.className == "android.widget.ImageView" && node.clickable && node.y() < 300) { + menuButton = node; + break; + } + } + + if (menuButton.x() == 0 && menuButton.y() == 0) { + m_error = "Cannot find menu button: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + AdbUtils::makeTap(m_deviceId, menuButton.x(), menuButton.y()); + QThread::msleep(2000); + + // Ждём боковое меню + bool sideMenuFound = false; + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + if (xmlScreenParser.isSideMenu()) { + sideMenuFound = true; + break; + } + QThread::msleep(1000); + } + + if (!sideMenuFound) { + m_error = "Side menu not detected: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + // Парсим ФИО и телефон из бокового меню + const QString fullName = xmlScreenParser.parseProfileFullName(); + QString phone = xmlScreenParser.parseProfilePhone(); + phone.remove(QRegularExpression("[^0-9+]")); + qDebug() << "[Dushanbe::GetProfileInfo] fullName:" << fullName << "phone:" << phone; + + if (!fullName.isEmpty()) { + BankProfileDAO::updateProfileInfo(app.id, fullName, phone, ""); + } + + if (const QString err = createBankProfileIfNeeded(device.id, m_appCode, parsedBalance); !err.isEmpty()) { + m_error = err; + emit finishedWithResult(m_error); + return; + } + + const QList allAccounts = MaterialDAO::getAccounts(device.id, m_appCode); + QList> accountsToPost; + for (const auto &acc : allAccounts) { + if (acc.materialId.isEmpty()) { + accountsToPost.append({acc, Node()}); + } + } + if (!accountsToPost.isEmpty()) { + postAccountsData(accountsToPost); + } + + emit finishedWithResult(m_error); +} + +} // namespace Dushanbe diff --git a/android/dushanbe/GetProfileInfoScript.h b/android/dushanbe/GetProfileInfoScript.h new file mode 100644 index 0000000..5774ca0 --- /dev/null +++ b/android/dushanbe/GetProfileInfoScript.h @@ -0,0 +1,27 @@ +#pragma once +#include "CommonScript.h" + +namespace Dushanbe { + +class GetProfileInfoScript final : public CommonScript { + Q_OBJECT + +public: + ~GetProfileInfoScript() override; + + explicit GetProfileInfoScript( + QString deviceId, + QString appCode, + QObject *parent = nullptr + ); + +protected: + void doStart() override; + +private: + QString m_deviceId; + const QString m_appCode; + QString m_error; +}; + +} // namespace Dushanbe diff --git a/android/dushanbe/LoginAndCheckAccountsScript.cpp b/android/dushanbe/LoginAndCheckAccountsScript.cpp new file mode 100644 index 0000000..e774313 --- /dev/null +++ b/android/dushanbe/LoginAndCheckAccountsScript.cpp @@ -0,0 +1,80 @@ +#include "LoginAndCheckAccountsScript.h" + +#include + +#include "adb/AdbUtils.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "dump/TaskDumpRecorder.h" +#include "AppLogger.h" +#include "GetProfileInfoScript.h" + +namespace Dushanbe { + +LoginAndCheckAccountsScript::LoginAndCheckAccountsScript( + QString deviceId, + QString appCode, + QObject *parent, + bool pinCheckOnly +) : CommonScript(parent), + m_deviceId(std::move(deviceId)), + m_appCode(std::move(appCode)), + m_pinCheckOnly(pinCheckOnly) { +} + +LoginAndCheckAccountsScript::~LoginAndCheckAccountsScript() = default; + +void LoginAndCheckAccountsScript::doStart() { + // Канонический ключ материала (device.id в БД) — для лукапов BankProfile; + // m_deviceId же перезаписываем ADB-серийником для команд. + const QString canonicalDeviceId = m_deviceId; + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); + qDebug() << "[Dushanbe::Login] device.id=" << device.id << "adbSerial=" << device.adbSerial + << "appCode=" << m_appCode << "app.id=" << app.id + << "pinCode.length=" << app.pinCode.length() << "package=" << app.package; + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + const TaskDumpMeta dumpMeta{}; + TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { + m_error = "Cant open the home screen: " + device.name + " (" + m_deviceId + ") " + m_appCode; + qWarning() << m_error; + m_resultScreenshot = AdbUtils::captureScreenshotBytes(m_deviceId); + if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot); + AppLogger::log("dushanbe/LoginAndCheck", m_deviceId, m_error, {}, "FETCH_PROFILE"); + BankProfileDAO::updateComment(app.id, + "Не прошла проверка " + QDateTime::currentDateTime().toString("dd.MM.yyyy HH:mm:ss")); + + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + // Скриншот домашнего экрана — отправляем в мониторинг + m_resultScreenshot = AdbUtils::captureScreenshotBytes(m_deviceId); + if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot); + + if (m_pinCheckOnly) { + emit finishedWithResult(m_error); + return; + } + + // В GetProfileInfoScript отдаём КАНОНИЧЕСКИЙ ключ материала, а не ADB-серийник, + // чтобы BankProfileDAO::getApplication нашла правильную запись с bank_profile_id. + GetProfileInfoScript profileScript(canonicalDeviceId, m_appCode); + profileScript.start(); + + emit finishedWithResult(m_error); +} + +} // namespace Dushanbe diff --git a/android/dushanbe/LoginAndCheckAccountsScript.h b/android/dushanbe/LoginAndCheckAccountsScript.h new file mode 100644 index 0000000..af99e1d --- /dev/null +++ b/android/dushanbe/LoginAndCheckAccountsScript.h @@ -0,0 +1,29 @@ +#pragma once +#include "CommonScript.h" + +namespace Dushanbe { + +class LoginAndCheckAccountsScript final : public CommonScript { + Q_OBJECT + +public: + ~LoginAndCheckAccountsScript() override; + + explicit LoginAndCheckAccountsScript( + QString deviceId, + QString appCode, + QObject *parent = nullptr, + bool pinCheckOnly = false + ); + +protected: + void doStart() override; + +private: + QString m_deviceId; + const QString m_appCode; + const bool m_pinCheckOnly; + QString m_error; +}; + +} // namespace Dushanbe diff --git a/android/dushanbe/PayByCardScript.cpp b/android/dushanbe/PayByCardScript.cpp new file mode 100644 index 0000000..6bc73cb --- /dev/null +++ b/android/dushanbe/PayByCardScript.cpp @@ -0,0 +1,618 @@ +#include "PayByCardScript.h" + +#include +#include + +#include +#include + +#include "adb/AdbUtils.h" +#include "dump/TaskDumpRecorder.h" +#include "ocr/OcrUtils.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "dao/TransactionDAO.h" +#include "AppLogger.h" + +#include +// Точечный OCR: находит строку "операции" и кропает правую часть для digit-only OCR +static QString recognizeOperationId(const QByteArray &pngData) { + Pix *pix = pixReadMem(reinterpret_cast(pngData.constData()), + static_cast(pngData.size())); + if (!pix) return {}; + + const int w = pixGetWidth(pix); + const int h = pixGetHeight(pix); + + auto *api = new tesseract::TessBaseAPI(); + if (api->Init(QCoreApplication::applicationDirPath().toUtf8().constData(), "rus+eng")) { + pixDestroy(&pix); delete api; + return {}; + } + api->SetImage(pix); + api->Recognize(nullptr); + + int opLineY = -1, opLineH = 0; + bool prevWasNomer = false; + tesseract::ResultIterator *ri = api->GetIterator(); + if (ri) { + do { + const char *word = ri->GetUTF8Text(tesseract::RIL_WORD); + if (word) { + const QString w_str = QString::fromUtf8(word); + delete[] word; + // Ищем "Номер" (или OCR-артефакты: "Homep", "Номep") + if (w_str.contains(QString::fromUtf8("омер")) || + w_str.contains(QString::fromUtf8("omep"))) { + prevWasNomer = true; + continue; + } + // После "Номер" ищем "операции" + if (prevWasNomer && + (w_str.contains(QString::fromUtf8("операции")) || + w_str.contains(QString::fromUtf8("onepaции")))) { + int x1, y1, x2, y2; + ri->BoundingBox(tesseract::RIL_WORD, &x1, &y1, &x2, &y2); + opLineY = y1; + opLineH = y2 - y1; + break; + } + prevWasNomer = false; + } + } while (ri->Next(tesseract::RIL_WORD)); + } + api->End(); + delete api; + + if (opLineY < 0) { pixDestroy(&pix); return {}; } + + const int cropY = qMax(0, opLineY - opLineH / 2); + const int cropH = opLineH * 2; + BOX *box = boxCreate(w / 2, cropY, w / 2, qMin(cropH, h - cropY)); + Pix *cropped = pixClipRectangle(pix, box, nullptr); + boxDestroy(&box); + pixDestroy(&pix); + if (!cropped) return {}; + + auto *api2 = new tesseract::TessBaseAPI(); + if (api2->Init(QCoreApplication::applicationDirPath().toUtf8().constData(), "eng")) { + pixDestroy(&cropped); delete api2; + return {}; + } + api2->SetVariable("tessedit_char_whitelist", "0123456789"); + api2->SetPageSegMode(tesseract::PSM_SINGLE_LINE); + api2->SetImage(cropped); + char *text = api2->GetUTF8Text(); + QString result = QString::fromUtf8(text).trimmed(); + delete[] text; + pixDestroy(&cropped); + api2->End(); + delete api2; + + result.remove(QRegularExpression("[^0-9]")); + if (result.length() >= 8) { + qDebug() << "[Dushanbe::OCR] Precise operation ID:" << result; + return result; + } + return {}; +} + +namespace Dushanbe { + +PayByCardScript::PayByCardScript( + MaterialInfo account, + QString cardNumber, + double amount, + QObject *parent +) : CommonScript(parent), + m_account(std::move(account)), + m_cardNumber(std::move(cardNumber)), + m_amount(amount) { +} + +PayByCardScript::~PayByCardScript() = default; + +void PayByCardScript::doStart() { + const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId); + const BankProfileInfo app = BankProfileDAO::getApplication(m_account.deviceId, m_account.appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_account.deviceId; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber}; + TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_CARD", device.adbSerial, dumpMeta, &m_error); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter)); + + if (!xmlScreenParser.isHomeScreen()) { + qDebug() << "[Dushanbe::PayByCard] Not on home screen, restarting app..."; + if (!runAppAndGoToHomeScreen(device.adbSerial, app.package, app.pinCode, + device.screenWidth, device.screenHeight)) { + m_error = "Cannot reach home screen: " + device.name + " (" + device.id + ")"; + qWarning() << m_error; + AppLogger::log("dushanbe/PayByCard", device.id, m_error, {}, "CREATE_TRANSACTION"); + AdbUtils::tryToKillApplication(device.adbSerial, app.package); + emit finishedWithResult(m_error); + return; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter)); + } + + makePayment(device.adbSerial, app.pinCode, app.phone, device.screenWidth, device.screenHeight); + emit finishedWithResult(m_error); +} + +void PayByCardScript::makePayment( + const QString &deviceId, + const QString &pinCode, + const QString &bankProfilePhone, + const int width, + const int height +) { + // 1. Тапаем "Переводы" в нижней навигации + Node transfersTab = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Переводы")); + if (transfersTab.isEmpty()) { + m_error = "Tab 'Переводы' not found on home screen"; + qWarning() << "[Dushanbe::PayByCard]" << m_error; + AppLogger::log("dushanbe/PayByCard", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + qDebug() << "[Dushanbe::PayByCard] Tapping 'Переводы' tab"; + AdbUtils::makeTap(deviceId, transfersTab.x(), transfersTab.y()); + QThread::msleep(2000); + + // 2. Ждём экран переводов — ищем "DC (по номеру карты)" + bool transfersLoaded = false; + Node cardTransferBtn; + for (int attempt = 0; attempt < 10; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + cardTransferBtn = xmlScreenParser.findNodeByContentDesc( + QString::fromUtf8("DC (по номеру карты)"), true); + if (!cardTransferBtn.isEmpty()) { + transfersLoaded = true; + break; + } + qDebug() << "[Dushanbe::PayByCard] Waiting for transfers screen, attempt" << attempt + 1; + QThread::msleep(1500); + } + + if (!transfersLoaded) { + m_error = "Transfers screen not loaded or 'DC (по номеру карты)' not found"; + qWarning() << "[Dushanbe::PayByCard]" << m_error; + AppLogger::log("dushanbe/PayByCard", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + // 3. Тапаем "DC (по номеру карты)" + qDebug() << "[Dushanbe::PayByCard] Tapping 'DC (по номеру карты)'"; + AdbUtils::makeTap(deviceId, cardTransferBtn.x(), cardTransferBtn.y()); + QThread::msleep(2000); + + // 4. Ждём экран ввода — ищем EditText с hint "Номер карты получателя" + bool inputScreenLoaded = false; + for (int attempt = 0; attempt < 10; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + Node editText = xmlScreenParser.findEditTextByHint( + QString::fromUtf8("Номер карты получателя")); + if (editText.isEmpty()) { + editText = xmlScreenParser.findFirstEditText(); + } + if (!editText.isEmpty()) { + inputScreenLoaded = true; + break; + } + qDebug() << "[Dushanbe::PayByCard] Waiting for card input screen, attempt" << attempt + 1; + QThread::msleep(1500); + } + + if (!inputScreenLoaded) { + m_error = "Card input screen not loaded"; + qWarning() << "[Dushanbe::PayByCard]" << m_error; + AppLogger::log("dushanbe/PayByCard", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + // 5. Вводим номер карты в поле "Номер карты получателя" + Node cardEditText = xmlScreenParser.findEditTextByHint( + QString::fromUtf8("Номер карты получателя")); + if (cardEditText.isEmpty()) { + cardEditText = xmlScreenParser.findFirstEditText(); + } + AdbUtils::makeTap(deviceId, cardEditText.x(), cardEditText.y()); + QThread::msleep(500); + AdbUtils::inputText(deviceId, m_cardNumber); + QThread::msleep(1000); + + // Скрываем клавиатуру чтобы поле суммы стало видимым + AdbUtils::goBack(deviceId); + QThread::msleep(1000); + + // 6. Вводим сумму в поле "Сумма" (hint содержит "Сумма") + Node sumEditText; + for (int attempt = 0; attempt < 10; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + sumEditText = xmlScreenParser.findEditTextByHint(QString::fromUtf8("Сумма")); + if (sumEditText.isEmpty()) sumEditText = xmlScreenParser.findSumEditText(); + if (!sumEditText.isEmpty()) break; + qDebug() << "[Dushanbe::PayByCard] Waiting for sum input, attempt" << attempt + 1 << "/ 10"; + QThread::msleep(1500); + } + if (sumEditText.isEmpty()) { + m_error = "Sum input field not found"; + qWarning() << "[Dushanbe::PayByCard]" << m_error; + AppLogger::log("dushanbe/PayByCard", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + AdbUtils::makeTap(deviceId, sumEditText.x(), sumEditText.y()); + QThread::msleep(500); + + // Сумма приходит с копейками (например 100.50), форматируем как целое или с точкой + const QString amountStr = (m_amount == static_cast(m_amount)) + ? QString::number(static_cast(m_amount)) + : QString::number(m_amount, 'f', 2); + AdbUtils::inputText(deviceId, amountStr); + QThread::msleep(1000); + + // 6a. Скрываем клавиатуру и выбираем карту-источник (под полем "Комментарий") + AdbUtils::goBack(deviceId); + QThread::msleep(500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + { + const QString cardMask = "****" + m_account.lastNumbers; + Node cardNode; + for (const Node &n : xmlScreenParser.findAllNodes()) { + if (!n.clickable) continue; + if (n.contentDesc.contains(cardMask)) { cardNode = n; break; } + } + if (cardNode.isEmpty()) { + m_error = "Source card not found: " + cardMask; + qWarning() << "[Dushanbe::PayByCard]" << m_error; + AppLogger::log("dushanbe/PayByCard", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + qDebug() << "[Dushanbe::PayByCard] Selecting source card:" << cardMask; + AdbUtils::makeTap(deviceId, cardNode.x(), cardNode.y()); + QThread::msleep(500); + } + + // 7. Нажимаем "Далее" + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + Node daleeBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Далее")); + if (daleeBtn.isEmpty()) { + daleeBtn = xmlScreenParser.findButtonNode(QString::fromUtf8("Далее"), false); + } + if (daleeBtn.isEmpty()) { + m_error = "Button 'Далее' not found"; + qWarning() << "[Dushanbe::PayByCard]" << m_error; + AppLogger::log("dushanbe/PayByCard", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + qDebug() << "[Dushanbe::PayByCard] Tapping 'Далее'"; + AdbUtils::makeTap(deviceId, daleeBtn.x(), daleeBtn.y()); + QThread::msleep(2000); + + // 8. После "Далее" возможны три исхода: + // a) кнопка меняется на "Оплатить" → обычный поток (8 → 9 → 10); + // b) платёж проходит мгновенно, минуя bottom sheet → сразу "Платеж выполнен"; + // c) экран ошибки. + // earlySuccess=true означает b — шаги 9 и 10 пропускаем, идём сразу к парсингу чека. + bool oplatitFound = false; + bool earlySuccess = false; + for (int attempt = 0; attempt < 10; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + + if (!xmlScreenParser.findNodeByContentDesc( + QString::fromUtf8("Платеж выполнен"), true).isEmpty()) { + qDebug() << "[Dushanbe::PayByCard] Payment completed immediately after 'Далее'"; + oplatitFound = true; + earlySuccess = true; + break; + } + if (!xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Ошибка"), true).isEmpty() || + !xmlScreenParser.findTextNode(QString::fromUtf8("Ошибка")).isEmpty()) { + m_error = "Transfer failed: error screen detected after 'Далее'"; + qWarning() << "[Dushanbe::PayByCard]" << m_error; + AppLogger::log("dushanbe/PayByCard", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + Node oplatitBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Оплатить")); + if (oplatitBtn.isEmpty()) { + oplatitBtn = xmlScreenParser.findButtonNode(QString::fromUtf8("Оплатить"), false); + } + if (!oplatitBtn.isEmpty()) { + qDebug() << "[Dushanbe::PayByCard] Tapping 'Оплатить'"; + AdbUtils::makeTap(deviceId, oplatitBtn.x(), oplatitBtn.y()); + oplatitFound = true; + QThread::msleep(3000); + break; + } + qDebug() << "[Dushanbe::PayByCard] Waiting for 'Оплатить', attempt" << attempt + 1; + QThread::msleep(1500); + } + + if (!oplatitFound) { + m_error = "Button 'Оплатить' not found"; + qWarning() << "[Dushanbe::PayByCard]" << m_error; + AppLogger::log("dushanbe/PayByCard", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + // 9. Ждём bottom sheet "Подтверждение платежа" и тапаем "Подтверждаю" + // content-desc содержит "Подтверждение платежа...Подтверждаю" + // Кнопка "Подтверждаю" визуально внизу bottom sheet + bool confirmed = earlySuccess; + for (int attempt = 0; !earlySuccess && attempt < 10; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + + // Пин-код может появиться + if (xmlScreenParser.isPinCodeScreen()) { + qDebug() << "[Dushanbe::PayByCard] PIN code screen, entering PIN..."; + inputPinCode(deviceId, pinCode); + QThread::msleep(3000); + continue; + } + + // Ищем элемент с "Подтверждение платежа" и "Подтверждаю" в content-desc + Node confirmNode = xmlScreenParser.findNodeByContentDesc( + QString::fromUtf8("Подтверждение платежа"), true); + if (!confirmNode.isEmpty() && + confirmNode.contentDesc.contains(QString::fromUtf8("Подтверждаю"))) { + // Compose flatten-ит весь bottom sheet в одну accessibility-кнопку, + // отдельного узла "Подтверждаю" в дампе нет — координаты берём OCR-ом + // по скриншоту. Если OCR не нашёл — fallback на геометрию (центр + // нижней четверти sheet, выше системного nav-бара). + // Compose отдаёт sheet как одну ноду на весь экран ([0,0][720,1640]), + // поэтому confirmNode.y1/y2 не сужают поиск — OCR сканирует весь + // скриншот и ловит ложные совпадения. Ограничиваем нижней половиной. + const int ocrYMin = (confirmNode.y1() + confirmNode.y2()) / 2; + const QByteArray screen = AdbUtils::captureScreenshotBytes(deviceId); + QPoint tap = OcrUtils::findTextCenter(screen, + QString::fromUtf8("Подтверждаю"), + ocrYMin, confirmNode.y2()); + if (tap.x() < 0) { + const int tapY = confirmNode.y2() - qMax(150, height / 14); + tap = QPoint(confirmNode.x(), tapY); + qDebug() << "[Dushanbe::PayByCard] OCR miss, fallback tap at" << tap; + } + qDebug() << "[Dushanbe::PayByCard] Tapping 'Подтверждаю' at" << tap; + AdbUtils::makeTap(deviceId, tap.x(), tap.y()); + confirmed = true; + QThread::msleep(4000); + break; + } + + qDebug() << "[Dushanbe::PayByCard] Waiting for confirmation sheet, attempt" << attempt + 1; + QThread::msleep(2000); + } + + if (!confirmed) { + m_error = "Confirmation sheet not found"; + qWarning() << "[Dushanbe::PayByCard]" << m_error; + AppLogger::log("dushanbe/PayByCard", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + // 10. Ждём результат — "Платеж выполнен" или ошибку. + // Промежуточный статус "Выполняется" может висеть до ~1.5 мин, пока банк + // подтверждает перевод, поэтому общий таймаут — 60 итераций × 2 сек = 2 мин. + // Лимиты повторных тапов: если первый тап "Подтверждаю" не сработал, + // bottom sheet дисмиссится, виден "Оплатить" → перетапываем; следующий + // sheet → пробуем другую Y. Без лимита получали бесконечный + // "Оплатить ↔ Подтверждаю" со случайными попаданиями в системные экраны + // (security/launcher). + bool success = earlySuccess; + int confirmRetries = 0; + int oplatitRetries = 0; + const int maxConfirmRetries = 2; + const int maxOplatitRetries = 2; + for (int attempt = 0; !earlySuccess && attempt < 60; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + + // Пин-код + if (xmlScreenParser.isPinCodeScreen()) { + qDebug() << "[Dushanbe::PayByCard] PIN code screen (retry), entering PIN..."; + inputPinCode(deviceId, pinCode); + QThread::msleep(3000); + continue; + } + + // Успех: content-desc начинается с "Платеж выполнен" + if (!xmlScreenParser.findNodeByContentDesc( + QString::fromUtf8("Платеж выполнен"), true).isEmpty()) { + success = true; + break; + } + + // Ошибка + if (!xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Ошибка"), true).isEmpty() || + !xmlScreenParser.findTextNode(QString::fromUtf8("Ошибка")).isEmpty()) { + m_error = "Transfer failed: error screen detected"; + qWarning() << "[Dushanbe::PayByCard]" << m_error; + AppLogger::log("dushanbe/PayByCard", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + // Промежуточный статус — банк ещё обрабатывает перевод + if (!xmlScreenParser.findNodeByContentDesc( + QString::fromUtf8("Выполняется"), true).isEmpty()) { + qDebug() << "[Dushanbe::PayByCard] Status 'Выполняется', waiting for completion, attempt" + << attempt + 1; + QThread::msleep(2000); + continue; + } + + // Если bottom sheet "Подтверждение платежа" всё ещё виден — первый тап + // не сработал (мимо OCR-координат), повторяем его той же логикой. + Node retryConfirm = xmlScreenParser.findNodeByContentDesc( + QString::fromUtf8("Подтверждение платежа"), true); + if (!retryConfirm.isEmpty() && + retryConfirm.contentDesc.contains(QString::fromUtf8("Подтверждаю"))) { + if (confirmRetries >= maxConfirmRetries) { + m_error = "Confirm tap missed " + QString::number(confirmRetries) + + " times (sheet still visible)"; + qWarning() << "[Dushanbe::PayByCard]" << m_error; + AppLogger::log("dushanbe/PayByCard", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + ++confirmRetries; + qDebug() << "[Dushanbe::PayByCard] Confirm sheet still visible, re-tap" + << confirmRetries << "/" << maxConfirmRetries; + const int ocrYMin = (retryConfirm.y1() + retryConfirm.y2()) / 2; + const QByteArray screen = AdbUtils::captureScreenshotBytes(deviceId); + QPoint tap = OcrUtils::findTextCenter(screen, + QString::fromUtf8("Подтверждаю"), + ocrYMin, retryConfirm.y2()); + if (tap.x() < 0) { + // Варьируем Y: первый retry — выше fallback на 100px, + // второй — ещё на 100px выше. Compose-кнопка действия + // обычно на y2 - 150..350; перебираем диапазон. + const int extra = 100 * confirmRetries; + const int tapY = retryConfirm.y2() - qMax(150 + extra, height / 14 + extra); + tap = QPoint(retryConfirm.x(), tapY); + qDebug() << "[Dushanbe::PayByCard] OCR miss, fallback tap at" << tap; + } + AdbUtils::makeTap(deviceId, tap.x(), tap.y()); + QThread::msleep(4000); + continue; + } + + // Sheet "Подтверждение платежа" пропал, но снова виден input-экран + // (кнопка "Оплатить" вернулась) — значит тап по "Подтверждаю" промахнулся + // и Compose дисмиснул bottom sheet (clip на scrim/Закрыть/X). Перезапускаем + // цикл: тапаем "Оплатить" → следующая итерация увидит sheet и retryConfirm + // выше повторно тапнет "Подтверждаю". + Node retryOplatit = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Оплатить")); + if (retryOplatit.isEmpty()) { + retryOplatit = xmlScreenParser.findButtonNode(QString::fromUtf8("Оплатить"), false); + } + if (!retryOplatit.isEmpty() && retryOplatit.clickable) { + if (oplatitRetries >= maxOplatitRetries) { + m_error = "Sheet dismissed and re-tap exhausted (" + + QString::number(oplatitRetries) + " retries)"; + qWarning() << "[Dushanbe::PayByCard]" << m_error; + AppLogger::log("dushanbe/PayByCard", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + ++oplatitRetries; + qDebug() << "[Dushanbe::PayByCard] Sheet dismissed, re-tap 'Оплатить'" + << oplatitRetries << "/" << maxOplatitRetries; + AdbUtils::makeTap(deviceId, retryOplatit.x(), retryOplatit.y()); + QThread::msleep(3000); + continue; + } + + qDebug() << "[Dushanbe::PayByCard] Waiting for result, attempt" << attempt + 1; + QThread::msleep(2000); + } + + if (!success) { + m_error = "Payment result not received within timeout"; + qWarning() << "[Dushanbe::PayByCard]" << m_error; + AppLogger::log("dushanbe/PayByCard", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + qDebug() << "[Dushanbe::PayByCard] Payment successful!"; + m_error.clear(); + + // Скриншот чека — сохраняем как m_resultScreenshot и отправляем в мониторинг + m_resultScreenshot = AdbUtils::captureScreenshotBytes(deviceId); + const QByteArray &receiptImage = m_resultScreenshot; + if (!m_resultScreenshot.isEmpty()) + emit screenshotReady(m_resultScreenshot); + + // Парсим детали из экрана "Платеж выполнен". + // До 3.2.33 все поля шли одним content-desc с ключами "Дата:" / "Время:" / "Комиссия:". + // С 3.2.33 лейблы и значения — отдельные View-ноды в двух колонках. + QString bankTime; + double fee = 0; + + Node resultNode = xmlScreenParser.findNodeByContentDesc( + QString::fromUtf8("Платеж выполнен"), true); + if (!resultNode.isEmpty()) { + const QStringList lines = resultNode.contentDesc.split('\n'); + for (int i = 0; i < lines.size(); ++i) { + const QString line = lines[i].trimmed(); + if (line.startsWith(QString::fromUtf8("Комиссия:")) && i + 1 < lines.size()) { + fee = lines[i + 1].trimmed().toDouble(); + } else if (line.startsWith(QString::fromUtf8("Дата:")) && i + 1 < lines.size()) { + bankTime = lines[i + 1].trimmed(); + } else if (line.startsWith(QString::fromUtf8("Время:")) && i + 1 < lines.size()) { + bankTime += " " + lines[i + 1].trimmed(); + } + } + } + if (bankTime.isEmpty()) { + const QMap pairs = xmlScreenParser.parseLabelValuePairs(); + const QString d = pairs.value(QString::fromUtf8("Дата")); + const QString t = pairs.value(QString::fromUtf8("Время")); + if (!d.isEmpty() || !t.isEmpty()) { + bankTime = (d + " " + t).trimmed(); + } + if (fee == 0) { + fee = pairs.value(QString::fromUtf8("Комиссия")).toDouble(); + } + } + qDebug() << "[Dushanbe::PayByCard] Result: date=" << bankTime << "fee=" << fee; + + // bank_transaction_id: phone аккаунта + unix timestamp (без перехода в историю). + // Уникальность за счёт . + const QString operationId = QString("dushanbe_") + bankProfilePhone + "_" + + QString::number(QDateTime::currentSecsSinceEpoch()); + qDebug() << "[Dushanbe::PayByCard] Generated operationId:" << operationId; + + // Сохраняем транзакцию + TransactionInfo tx; + tx.accountId = m_account.id; + tx.amount = -m_amount; + tx.fee = fee; + tx.status = TransactionStatus::Complete; + tx.type = TransactionType::Card; + tx.phone = m_cardNumber; + tx.bankName = "dushanbe_city_bank"; + tx.bankTime = bankTime; + tx.bankTrExternalId = operationId; + tx.timestamp = QDateTime::currentDateTimeUtc(); + tx.completeTime = QDateTime::currentDateTimeUtc(); + tx.receiptImage = receiptImage; + + const int txId = TransactionDAO::insertTransaction(tx); + if (txId != -1) { + qDebug() << "[Dushanbe::PayByCard] Transaction saved, id:" << txId + << "operationId:" << operationId; + if (!receiptImage.isEmpty()) { + TransactionDAO::updateReceiptImage(txId, receiptImage); + } + } else { + qWarning() << "[Dushanbe::PayByCard] Failed to save transaction"; + } +} + +} // namespace Dushanbe \ No newline at end of file diff --git a/android/dushanbe/PayByCardScript.h b/android/dushanbe/PayByCardScript.h new file mode 100644 index 0000000..8df31ae --- /dev/null +++ b/android/dushanbe/PayByCardScript.h @@ -0,0 +1,37 @@ +#pragma once +#include "CommonScript.h" + +namespace Dushanbe { + +class PayByCardScript final : public CommonScript { + Q_OBJECT + +public: + PayByCardScript( + MaterialInfo account, + QString cardNumber, + double amount, + QObject *parent = nullptr + ); + + ~PayByCardScript() override; + +protected: + void doStart() override; + +private: + const MaterialInfo m_account; + const QString m_cardNumber; + const double m_amount; + QString m_error; + + void makePayment( + const QString &deviceId, + const QString &pinCode, + const QString &bankProfilePhone, + int width, + int height + ); +}; + +} // namespace Dushanbe \ No newline at end of file diff --git a/android/dushanbe/PayByPhoneScript.cpp b/android/dushanbe/PayByPhoneScript.cpp new file mode 100644 index 0000000..8f3c1b5 --- /dev/null +++ b/android/dushanbe/PayByPhoneScript.cpp @@ -0,0 +1,610 @@ +#include "PayByPhoneScript.h" + +#include +#include + +#include +#include + +#include "adb/AdbUtils.h" +#include "dump/TaskDumpRecorder.h" +#include "ocr/OcrUtils.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "dao/TransactionDAO.h" +#include "AppLogger.h" + +#include +static QString recognizeOperationId(const QByteArray &pngData) { + Pix *pix = pixReadMem(reinterpret_cast(pngData.constData()), + static_cast(pngData.size())); + if (!pix) return {}; + + const int w = pixGetWidth(pix); + const int h = pixGetHeight(pix); + + auto *api = new tesseract::TessBaseAPI(); + if (api->Init(QCoreApplication::applicationDirPath().toUtf8().constData(), "rus+eng")) { + pixDestroy(&pix); delete api; + return {}; + } + api->SetImage(pix); + api->Recognize(nullptr); + + int opLineY = -1, opLineH = 0; + bool prevWasNomer = false; + tesseract::ResultIterator *ri = api->GetIterator(); + if (ri) { + do { + const char *word = ri->GetUTF8Text(tesseract::RIL_WORD); + if (word) { + const QString w_str = QString::fromUtf8(word); + delete[] word; + if (w_str.contains(QString::fromUtf8("омер")) || + w_str.contains(QString::fromUtf8("omep"))) { + prevWasNomer = true; + continue; + } + if (prevWasNomer && + (w_str.contains(QString::fromUtf8("операции")) || + w_str.contains(QString::fromUtf8("onepaции")))) { + int x1, y1, x2, y2; + ri->BoundingBox(tesseract::RIL_WORD, &x1, &y1, &x2, &y2); + opLineY = y1; + opLineH = y2 - y1; + break; + } + prevWasNomer = false; + } + } while (ri->Next(tesseract::RIL_WORD)); + } + api->End(); + delete api; + + if (opLineY < 0) { pixDestroy(&pix); return {}; } + + const int cropY = qMax(0, opLineY - opLineH / 2); + const int cropH = opLineH * 2; + BOX *box = boxCreate(w / 2, cropY, w / 2, qMin(cropH, h - cropY)); + Pix *cropped = pixClipRectangle(pix, box, nullptr); + boxDestroy(&box); + pixDestroy(&pix); + if (!cropped) return {}; + + auto *api2 = new tesseract::TessBaseAPI(); + if (api2->Init(QCoreApplication::applicationDirPath().toUtf8().constData(), "eng")) { + pixDestroy(&cropped); delete api2; + return {}; + } + api2->SetVariable("tessedit_char_whitelist", "0123456789"); + api2->SetPageSegMode(tesseract::PSM_SINGLE_LINE); + api2->SetImage(cropped); + char *text = api2->GetUTF8Text(); + QString result = QString::fromUtf8(text).trimmed(); + delete[] text; + pixDestroy(&cropped); + api2->End(); + delete api2; + + result.remove(QRegularExpression("[^0-9]")); + if (result.length() >= 8) { + qDebug() << "[Dushanbe::OCR] Precise operation ID:" << result; + return result; + } + return {}; +} + +namespace Dushanbe { + +PayByPhoneScript::PayByPhoneScript( + MaterialInfo account, + QString phone, + double amount, + QObject *parent +) : CommonScript(parent), + m_account(std::move(account)), + m_phone(std::move(phone)), + m_amount(amount) { +} + +PayByPhoneScript::~PayByPhoneScript() = default; + +void PayByPhoneScript::doStart() { + const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId); + const BankProfileInfo app = BankProfileDAO::getApplication(m_account.deviceId, m_account.appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_account.deviceId; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}}; + TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_PHONE", device.adbSerial, dumpMeta, &m_error); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter)); + + if (!xmlScreenParser.isHomeScreen()) { + qDebug() << "[Dushanbe::PayByPhone] Not on home screen, restarting app..."; + if (!runAppAndGoToHomeScreen(device.adbSerial, app.package, app.pinCode, + device.screenWidth, device.screenHeight)) { + m_error = "Cannot reach home screen: " + device.name + " (" + device.id + ")"; + qWarning() << m_error; + AppLogger::log("dushanbe/PayByPhone", device.id, m_error, {}, "CREATE_TRANSACTION"); + AdbUtils::tryToKillApplication(device.adbSerial, app.package); + emit finishedWithResult(m_error); + return; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter)); + } + + makePayment(device.adbSerial, app.pinCode, app.phone, device.screenWidth, device.screenHeight); + emit finishedWithResult(m_error); +} + +void PayByPhoneScript::makePayment( + const QString &deviceId, + const QString &pinCode, + const QString &bankProfilePhone, + const int width, + const int height +) { + // 1. Тапаем "Переводы" в нижней навигации + Node transfersTab = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Переводы")); + if (transfersTab.isEmpty()) { + m_error = "Tab 'Переводы' not found on home screen"; + qWarning() << "[Dushanbe::PayByPhone]" << m_error; + AppLogger::log("dushanbe/PayByPhone", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + qDebug() << "[Dushanbe::PayByPhone] Tapping 'Переводы' tab"; + AdbUtils::makeTap(deviceId, transfersTab.x(), transfersTab.y()); + QThread::msleep(2000); + + // 2. Ждём экран переводов — ищем "DC (по номеру телефона)" + bool transfersLoaded = false; + Node phoneTransferBtn; + for (int attempt = 0; attempt < 10; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + phoneTransferBtn = xmlScreenParser.findNodeByContentDesc( + QString::fromUtf8("DC (по номеру телефона)"), true); + if (!phoneTransferBtn.isEmpty()) { + transfersLoaded = true; + break; + } + qDebug() << "[Dushanbe::PayByPhone] Waiting for transfers screen, attempt" << attempt + 1; + QThread::msleep(1500); + } + + if (!transfersLoaded) { + m_error = "Transfers screen not loaded or 'DC (по номеру телефона)' not found"; + qWarning() << "[Dushanbe::PayByPhone]" << m_error; + AppLogger::log("dushanbe/PayByPhone", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + // 3. Тапаем "DC (по номеру телефона)" + qDebug() << "[Dushanbe::PayByPhone] Tapping 'DC (по номеру телефона)'"; + AdbUtils::makeTap(deviceId, phoneTransferBtn.x(), phoneTransferBtn.y()); + QThread::msleep(2000); + + // 4. Ждём экран ввода — ищем EditText с hint "Номер телефона получателя" + bool inputScreenLoaded = false; + for (int attempt = 0; attempt < 10; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + Node editText = xmlScreenParser.findEditTextByHint( + QString::fromUtf8("Номер телефона получателя")); + if (editText.isEmpty()) { + editText = xmlScreenParser.findFirstEditText(); + } + if (!editText.isEmpty()) { + inputScreenLoaded = true; + break; + } + qDebug() << "[Dushanbe::PayByPhone] Waiting for phone input screen, attempt" << attempt + 1; + QThread::msleep(1500); + } + + if (!inputScreenLoaded) { + m_error = "Phone input screen not loaded"; + qWarning() << "[Dushanbe::PayByPhone]" << m_error; + AppLogger::log("dushanbe/PayByPhone", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + // 5. Вводим номер телефона + Node phoneEditText = xmlScreenParser.findEditTextByHint( + QString::fromUtf8("Номер телефона получателя")); + if (phoneEditText.isEmpty()) { + phoneEditText = xmlScreenParser.findFirstEditText(); + } + AdbUtils::makeTap(deviceId, phoneEditText.x(), phoneEditText.y()); + QThread::msleep(500); + + // Убираем "+" из номера если есть — вводим только цифры + QString phoneDigits = m_phone; + phoneDigits.remove('+'); + AdbUtils::inputText(deviceId, phoneDigits); + QThread::msleep(1000); + + // Скрываем клавиатуру чтобы поле суммы стало видимым + AdbUtils::goBack(deviceId); + QThread::msleep(1000); + + // 6. Вводим сумму в поле "Сумма" + Node sumEditText; + for (int attempt = 0; attempt < 10; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + sumEditText = xmlScreenParser.findEditTextByHint(QString::fromUtf8("Сумма")); + if (sumEditText.isEmpty()) sumEditText = xmlScreenParser.findSumEditText(); + if (!sumEditText.isEmpty()) break; + qDebug() << "[Dushanbe::PayByPhone] Waiting for sum input, attempt" << attempt + 1 << "/ 10"; + QThread::msleep(1500); + } + if (sumEditText.isEmpty()) { + m_error = "Sum input field not found"; + qWarning() << "[Dushanbe::PayByPhone]" << m_error; + AppLogger::log("dushanbe/PayByPhone", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + AdbUtils::makeTap(deviceId, sumEditText.x(), sumEditText.y()); + QThread::msleep(500); + + const QString amountStr = (m_amount == static_cast(m_amount)) + ? QString::number(static_cast(m_amount)) + : QString::number(m_amount, 'f', 2); + AdbUtils::inputText(deviceId, amountStr); + QThread::msleep(1000); + + // 6a. Скрываем клавиатуру и выбираем карту-источник (под полем "Комментарий") + AdbUtils::goBack(deviceId); + QThread::msleep(500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + { + const QString cardMask = "****" + m_account.lastNumbers; + Node cardNode; + for (const Node &n : xmlScreenParser.findAllNodes()) { + if (!n.clickable) continue; + if (n.contentDesc.contains(cardMask)) { cardNode = n; break; } + } + if (cardNode.isEmpty()) { + m_error = "Source card not found: " + cardMask; + qWarning() << "[Dushanbe::PayByPhone]" << m_error; + AppLogger::log("dushanbe/PayByPhone", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + qDebug() << "[Dushanbe::PayByPhone] Selecting source card:" << cardMask; + AdbUtils::makeTap(deviceId, cardNode.x(), cardNode.y()); + QThread::msleep(500); + } + + // 7. Нажимаем "Далее" + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + Node daleeBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Далее")); + if (daleeBtn.isEmpty()) { + daleeBtn = xmlScreenParser.findButtonNode(QString::fromUtf8("Далее"), false); + } + if (daleeBtn.isEmpty()) { + m_error = "Button 'Далее' not found"; + qWarning() << "[Dushanbe::PayByPhone]" << m_error; + AppLogger::log("dushanbe/PayByPhone", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + qDebug() << "[Dushanbe::PayByPhone] Tapping 'Далее'"; + AdbUtils::makeTap(deviceId, daleeBtn.x(), daleeBtn.y()); + QThread::msleep(2000); + + // 8. После "Далее" возможны три исхода: + // a) кнопка меняется на "Оплатить" → обычный поток (8 → 9 → 10); + // b) платёж проходит мгновенно, минуя bottom sheet → сразу "Платеж выполнен"; + // c) экран ошибки. + // earlySuccess=true означает b — шаги 9 и 10 пропускаем, идём сразу к парсингу чека. + bool oplatitFound = false; + bool earlySuccess = false; + for (int attempt = 0; attempt < 10; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + + if (!xmlScreenParser.findNodeByContentDesc( + QString::fromUtf8("Платеж выполнен"), true).isEmpty()) { + qDebug() << "[Dushanbe::PayByPhone] Payment completed immediately after 'Далее'"; + oplatitFound = true; + earlySuccess = true; + break; + } + if (!xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Ошибка"), true).isEmpty() || + !xmlScreenParser.findTextNode(QString::fromUtf8("Ошибка")).isEmpty()) { + m_error = "Transfer failed: error screen detected after 'Далее'"; + qWarning() << "[Dushanbe::PayByPhone]" << m_error; + AppLogger::log("dushanbe/PayByPhone", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + Node oplatitBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Оплатить")); + if (oplatitBtn.isEmpty()) { + oplatitBtn = xmlScreenParser.findButtonNode(QString::fromUtf8("Оплатить"), false); + } + if (!oplatitBtn.isEmpty()) { + qDebug() << "[Dushanbe::PayByPhone] Tapping 'Оплатить'"; + AdbUtils::makeTap(deviceId, oplatitBtn.x(), oplatitBtn.y()); + oplatitFound = true; + QThread::msleep(3000); + break; + } + qDebug() << "[Dushanbe::PayByPhone] Waiting for 'Оплатить', attempt" << attempt + 1; + QThread::msleep(1500); + } + + if (!oplatitFound) { + m_error = "Button 'Оплатить' not found"; + qWarning() << "[Dushanbe::PayByPhone]" << m_error; + AppLogger::log("dushanbe/PayByPhone", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + // 9. Ждём bottom sheet "Подтверждение платежа" и тапаем "Подтверждаю" + bool confirmed = earlySuccess; + for (int attempt = 0; !earlySuccess && attempt < 10; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + + if (xmlScreenParser.isPinCodeScreen()) { + qDebug() << "[Dushanbe::PayByPhone] PIN code screen, entering PIN..."; + inputPinCode(deviceId, pinCode); + QThread::msleep(3000); + continue; + } + + Node confirmNode = xmlScreenParser.findNodeByContentDesc( + QString::fromUtf8("Подтверждение платежа"), true); + if (!confirmNode.isEmpty() && + confirmNode.contentDesc.contains(QString::fromUtf8("Подтверждаю"))) { + // Compose flatten-ит весь bottom sheet в одну accessibility-кнопку, + // отдельного узла "Подтверждаю" в дампе нет — координаты берём OCR-ом + // по скриншоту. Если OCR не нашёл — fallback на геометрию (центр + // нижней четверти sheet, выше системного nav-бара). + // Compose отдаёт sheet как одну ноду на весь экран ([0,0][720,1640]), + // поэтому confirmNode.y1/y2 не сужают поиск — OCR сканирует весь + // скриншот и ловит ложные совпадения (буква "в" в шапке). + // Ограничиваем нижней половиной — действие-кнопка всегда внизу sheet'a. + const int ocrYMin = (confirmNode.y1() + confirmNode.y2()) / 2; + const QByteArray screen = AdbUtils::captureScreenshotBytes(deviceId); + QPoint tap = OcrUtils::findTextCenter(screen, + QString::fromUtf8("Подтверждаю"), + ocrYMin, confirmNode.y2()); + if (tap.x() < 0) { + const int tapY = confirmNode.y2() - qMax(150, height / 14); + tap = QPoint(confirmNode.x(), tapY); + qDebug() << "[Dushanbe::PayByPhone] OCR miss, fallback tap at" << tap; + } + qDebug() << "[Dushanbe::PayByPhone] Tapping 'Подтверждаю' at" << tap; + AdbUtils::makeTap(deviceId, tap.x(), tap.y()); + confirmed = true; + QThread::msleep(4000); + break; + } + + qDebug() << "[Dushanbe::PayByPhone] Waiting for confirmation sheet, attempt" << attempt + 1; + QThread::msleep(2000); + } + + if (!confirmed) { + m_error = "Confirmation sheet not found"; + qWarning() << "[Dushanbe::PayByPhone]" << m_error; + AppLogger::log("dushanbe/PayByPhone", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + // 10. Ждём результат — "Платеж выполнен" или ошибку. + // Промежуточный статус "Выполняется" может висеть до ~1.5 мин, пока банк + // подтверждает перевод, поэтому общий таймаут — 60 итераций × 2 сек = 2 мин. + // Лимиты повторных тапов: если первый тап "Подтверждаю" не сработал, + // bottom sheet дисмиссится, виден "Оплатить" → перетапываем; следующий + // sheet → пробуем другую Y. Без лимита получали бесконечный + // "Оплатить ↔ Подтверждаю" со случайными попаданиями в системные экраны + // (security/launcher). + bool success = earlySuccess; + int confirmRetries = 0; + int oplatitRetries = 0; + const int maxConfirmRetries = 2; + const int maxOplatitRetries = 2; + for (int attempt = 0; !earlySuccess && attempt < 60; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + + if (xmlScreenParser.isPinCodeScreen()) { + qDebug() << "[Dushanbe::PayByPhone] PIN code screen (retry), entering PIN..."; + inputPinCode(deviceId, pinCode); + QThread::msleep(3000); + continue; + } + + if (!xmlScreenParser.findNodeByContentDesc( + QString::fromUtf8("Платеж выполнен"), true).isEmpty()) { + success = true; + break; + } + + if (!xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Ошибка"), true).isEmpty() || + !xmlScreenParser.findTextNode(QString::fromUtf8("Ошибка")).isEmpty()) { + m_error = "Transfer failed: error screen detected"; + qWarning() << "[Dushanbe::PayByPhone]" << m_error; + AppLogger::log("dushanbe/PayByPhone", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + if (!xmlScreenParser.findNodeByContentDesc( + QString::fromUtf8("Выполняется"), true).isEmpty()) { + qDebug() << "[Dushanbe::PayByPhone] Status 'Выполняется', waiting for completion, attempt" + << attempt + 1; + QThread::msleep(2000); + continue; + } + + // Если bottom sheet "Подтверждение платежа" всё ещё виден — первый тап + // не сработал (мимо OCR-координат), повторяем его той же логикой. + Node retryConfirm = xmlScreenParser.findNodeByContentDesc( + QString::fromUtf8("Подтверждение платежа"), true); + if (!retryConfirm.isEmpty() && + retryConfirm.contentDesc.contains(QString::fromUtf8("Подтверждаю"))) { + if (confirmRetries >= maxConfirmRetries) { + m_error = "Confirm tap missed " + QString::number(confirmRetries) + + " times (sheet still visible)"; + qWarning() << "[Dushanbe::PayByPhone]" << m_error; + AppLogger::log("dushanbe/PayByPhone", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + ++confirmRetries; + qDebug() << "[Dushanbe::PayByPhone] Confirm sheet still visible, re-tap" + << confirmRetries << "/" << maxConfirmRetries; + const int ocrYMin = (retryConfirm.y1() + retryConfirm.y2()) / 2; + const QByteArray screen = AdbUtils::captureScreenshotBytes(deviceId); + QPoint tap = OcrUtils::findTextCenter(screen, + QString::fromUtf8("Подтверждаю"), + ocrYMin, retryConfirm.y2()); + if (tap.x() < 0) { + // Варьируем Y: первый retry — выше fallback на 100px, + // второй — ещё на 100px выше. Compose-кнопка действия + // обычно на y2 - 150..350; перебираем диапазон. + const int extra = 100 * confirmRetries; + const int tapY = retryConfirm.y2() - qMax(150 + extra, height / 14 + extra); + tap = QPoint(retryConfirm.x(), tapY); + qDebug() << "[Dushanbe::PayByPhone] OCR miss, fallback tap at" << tap; + } + AdbUtils::makeTap(deviceId, tap.x(), tap.y()); + QThread::msleep(4000); + continue; + } + + // Sheet "Подтверждение платежа" пропал, но снова виден input-экран + // (кнопка "Оплатить" вернулась) — значит тап по "Подтверждаю" промахнулся + // и Compose дисмиснул bottom sheet (clip на scrim/Закрыть/X). Перезапускаем + // цикл: тапаем "Оплатить" → следующая итерация увидит sheet и retryConfirm + // выше повторно тапнет "Подтверждаю". + Node retryOplatit = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Оплатить")); + if (retryOplatit.isEmpty()) { + retryOplatit = xmlScreenParser.findButtonNode(QString::fromUtf8("Оплатить"), false); + } + if (!retryOplatit.isEmpty() && retryOplatit.clickable) { + if (oplatitRetries >= maxOplatitRetries) { + m_error = "Sheet dismissed and re-tap exhausted (" + + QString::number(oplatitRetries) + " retries)"; + qWarning() << "[Dushanbe::PayByPhone]" << m_error; + AppLogger::log("dushanbe/PayByPhone", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + ++oplatitRetries; + qDebug() << "[Dushanbe::PayByPhone] Sheet dismissed, re-tap 'Оплатить'" + << oplatitRetries << "/" << maxOplatitRetries; + AdbUtils::makeTap(deviceId, retryOplatit.x(), retryOplatit.y()); + QThread::msleep(3000); + continue; + } + + qDebug() << "[Dushanbe::PayByPhone] Waiting for result, attempt" << attempt + 1; + QThread::msleep(2000); + } + + if (!success) { + m_error = "Payment result not received within timeout"; + qWarning() << "[Dushanbe::PayByPhone]" << m_error; + AppLogger::log("dushanbe/PayByPhone", deviceId, m_error, + AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION"); + return; + } + + qDebug() << "[Dushanbe::PayByPhone] Payment successful!"; + m_error.clear(); + + // Скриншот чека — сохраняем как m_resultScreenshot и отправляем в мониторинг + m_resultScreenshot = AdbUtils::captureScreenshotBytes(deviceId); + const QByteArray &receiptImage = m_resultScreenshot; + if (!m_resultScreenshot.isEmpty()) + emit screenshotReady(m_resultScreenshot); + + // Парсим детали из экрана "Платеж выполнен". С 3.2.33 поля разнесены по + // отдельным View-узлам — пробуем сначала старый blob-формат, потом fallback. + QString bankTime; + double fee = 0; + + Node resultNode = xmlScreenParser.findNodeByContentDesc( + QString::fromUtf8("Платеж выполнен"), true); + if (!resultNode.isEmpty()) { + const QStringList lines = resultNode.contentDesc.split('\n'); + for (int i = 0; i < lines.size(); ++i) { + const QString line = lines[i].trimmed(); + if (line.startsWith(QString::fromUtf8("Комиссия:")) && i + 1 < lines.size()) { + fee = lines[i + 1].trimmed().toDouble(); + } else if (line.startsWith(QString::fromUtf8("Дата:")) && i + 1 < lines.size()) { + bankTime = lines[i + 1].trimmed(); + } else if (line.startsWith(QString::fromUtf8("Время:")) && i + 1 < lines.size()) { + bankTime += " " + lines[i + 1].trimmed(); + } + } + } + if (bankTime.isEmpty()) { + const QMap pairs = xmlScreenParser.parseLabelValuePairs(); + const QString d = pairs.value(QString::fromUtf8("Дата")); + const QString t = pairs.value(QString::fromUtf8("Время")); + if (!d.isEmpty() || !t.isEmpty()) { + bankTime = (d + " " + t).trimmed(); + } + if (fee == 0) { + fee = pairs.value(QString::fromUtf8("Комиссия")).toDouble(); + } + } + qDebug() << "[Dushanbe::PayByPhone] Result: date=" << bankTime << "fee=" << fee; + + // bank_transaction_id: phone аккаунта + unix timestamp (без перехода в историю). + // Уникальность за счёт . + const QString operationId = QString("dushanbe_") + bankProfilePhone + "_" + + QString::number(QDateTime::currentSecsSinceEpoch()); + qDebug() << "[Dushanbe::PayByPhone] Generated operationId:" << operationId; + + // Сохраняем транзакцию + TransactionInfo tx; + tx.accountId = m_account.id; + tx.amount = -m_amount; + tx.fee = fee; + tx.status = TransactionStatus::Complete; + tx.type = TransactionType::Phone; + tx.phone = m_phone; + tx.bankName = "dushanbe_city_bank"; + tx.bankTime = bankTime; + tx.bankTrExternalId = operationId; + tx.timestamp = QDateTime::currentDateTimeUtc(); + tx.completeTime = QDateTime::currentDateTimeUtc(); + tx.receiptImage = receiptImage; + + const int txId = TransactionDAO::insertTransaction(tx); + if (txId != -1) { + qDebug() << "[Dushanbe::PayByPhone] Transaction saved, id:" << txId + << "operationId:" << operationId; + if (!receiptImage.isEmpty()) { + TransactionDAO::updateReceiptImage(txId, receiptImage); + } + } else { + qWarning() << "[Dushanbe::PayByPhone] Failed to save transaction"; + } +} + +} // namespace Dushanbe diff --git a/android/dushanbe/PayByPhoneScript.h b/android/dushanbe/PayByPhoneScript.h new file mode 100644 index 0000000..98f9371 --- /dev/null +++ b/android/dushanbe/PayByPhoneScript.h @@ -0,0 +1,37 @@ +#pragma once +#include "CommonScript.h" + +namespace Dushanbe { + +class PayByPhoneScript final : public CommonScript { + Q_OBJECT + +public: + PayByPhoneScript( + MaterialInfo account, + QString phone, + double amount, + QObject *parent = nullptr + ); + + ~PayByPhoneScript() override; + +protected: + void doStart() override; + +private: + const MaterialInfo m_account; + const QString m_phone; + const double m_amount; + QString m_error; + + void makePayment( + const QString &deviceId, + const QString &pinCode, + const QString &bankProfilePhone, + int width, + int height + ); +}; + +} // namespace Dushanbe diff --git a/android/dushanbe/ScreenXmlParser.cpp b/android/dushanbe/ScreenXmlParser.cpp new file mode 100644 index 0000000..a49f71e --- /dev/null +++ b/android/dushanbe/ScreenXmlParser.cpp @@ -0,0 +1,702 @@ +#include "ScreenXmlParser.h" + +#include +#include +#include + +#include "android/xml/Node.h" + +namespace Dushanbe { + +static const QRegularExpression boundsRegex(R"(\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\])"); + +ScreenXmlParser::ScreenXmlParser(QObject *parent) : QObject(parent) { +} + +ScreenXmlParser::~ScreenXmlParser() = default; + +void ScreenXmlParser::parseAndSaveXml(const QString &xml) { + QDomDocument doc; + doc.setContent(xml); + m_xml = doc.documentElement(); + m_nodes.clear(); + + QXmlStreamReader reader(xml); + while (!reader.atEnd()) { + reader.readNext(); + if (reader.isStartElement() && reader.name() == u"node") { + Node node; + if (reader.attributes().hasAttribute("bounds")) { + QString bounds = reader.attributes().value("bounds").toString(); + if (QRegularExpressionMatch match = boundsRegex.match(bounds); match.hasMatch()) { + node.setBounds( + match.captured(1).toInt(), + match.captured(2).toInt(), + match.captured(3).toInt(), + match.captured(4).toInt() + ); + } + } + if (reader.attributes().hasAttribute("text")) { + node.text = reader.attributes().value("text").toString(); + } + if (reader.attributes().hasAttribute("content-desc")) { + node.contentDesc = reader.attributes().value("content-desc").toString(); + } + if (reader.attributes().hasAttribute("class")) { + node.className = reader.attributes().value("class").toString(); + } + if (reader.attributes().hasAttribute("clickable")) { + node.clickable = reader.attributes().value("clickable").toString() == "true"; + } + if (reader.attributes().hasAttribute("focusable")) { + node.focusable = reader.attributes().value("focusable").toString() == "true"; + } + if (reader.attributes().hasAttribute("resource-id")) { + node.resourceId = reader.attributes().value("resource-id").toString(); + } + if (reader.attributes().hasAttribute("hint")) { + node.hint = reader.attributes().value("hint").toString(); + } + if (reader.attributes().hasAttribute("password")) { + node.password = reader.attributes().value("password").toString() == "true"; + } + m_nodes.append(node); + } + } +} + +const QList &ScreenXmlParser::findAllNodes() const { + return m_nodes; +} + +Node ScreenXmlParser::findTextNode(const QString &text) { + for (const Node &node : m_nodes) { + if (node.text.trimmed() == text) { + return node; + } + } + return {}; +} + +Node ScreenXmlParser::findNodeByContentDesc(const QString &contentDesc) { + for (const Node &node : m_nodes) { + if (node.contentDesc == contentDesc) { + return node; + } + } + return {}; +} + +Node ScreenXmlParser::findNodeByContentDesc(const QString &contentDesc, bool contains) { + for (const Node &node : m_nodes) { + if (contains) { + if (node.contentDesc.contains(contentDesc)) { + return node; + } + } else { + if (node.contentDesc == contentDesc) { + return node; + } + } + } + return {}; +} + +Node ScreenXmlParser::findButtonNode(const QString &text, bool contains) { + for (const Node &node : m_nodes) { + if (!node.clickable) continue; + if (contains) { + if (node.text.contains(text) || node.contentDesc.contains(text)) { + return node; + } + } else { + if (node.text.trimmed() == text || node.contentDesc == text) { + return node; + } + } + } + return {}; +} + +Node ScreenXmlParser::findFirstEditText() { + for (const Node &node : m_nodes) { + if (node.className == "android.widget.EditText") { + return node; + } + } + return {}; +} + +bool ScreenXmlParser::isPinCodeScreen() { + // 1. Прямое совпадение по тексту заголовка + for (const Node &node : m_nodes) { + if (node.contentDesc.contains(QString::fromUtf8("Введите код доступа")) || + node.contentDesc.contains(QString::fromUtf8("Введите ПИН")) || + node.contentDesc.contains(QString::fromUtf8("код доступа")) || + node.text.contains(QString::fromUtf8("код доступа"))) { + return true; + } + } + // 2. Fallback: если на экране есть полная цифровая клавиатура 0-9 + // (content-desc каждой кнопки = одна цифра) — это PIN-экран. + int digitButtons = 0; + bool seen[10] = {false}; + for (const Node &node : m_nodes) { + if (node.contentDesc.size() != 1) continue; + const QChar c = node.contentDesc.at(0); + if (!c.isDigit()) continue; + const int d = c.digitValue(); + if (d < 0 || d > 9 || seen[d]) continue; + seen[d] = true; + ++digitButtons; + } + return digitButtons >= 10; +} + +Node ScreenXmlParser::findPinCodeEditText() { + for (const Node &node : m_nodes) { + if (node.className == "android.widget.EditText" && node.password) { + return node; + } + } + return {}; +} + +bool ScreenXmlParser::isHomeScreen() { + bool hasGlavnaya = false; + bool hasBottomNav = false; + for (const Node &node : m_nodes) { + if (node.contentDesc.contains(QString::fromUtf8("Главная"))) { + hasGlavnaya = true; + } + // Нижняя навигация — любая из соседних вкладок указывает, что мы на + // основной странице приложения, а не на splash/PIN/диалоге. + if (node.contentDesc == QString::fromUtf8("Переводы") || + node.contentDesc == QString::fromUtf8("Сервисы") || + node.contentDesc == QString::fromUtf8("MiniApps") || + node.contentDesc == QString::fromUtf8("История")) { + hasBottomNav = true; + } + if (hasGlavnaya && hasBottomNav) { + return true; + } + } + return false; +} + +double ScreenXmlParser::parseBalanceFromHomeScreen() { + // На домашнем экране: parent node content-desc="TJS\n03.04.26 - 21:46:15" + // внутри него child node content-desc="0.00" (или "10.00") + // Стратегия: находим индекс parent, затем child сразу после него с числовым content-desc + for (int i = 0; i < m_nodes.size(); ++i) { + if (!m_nodes[i].contentDesc.startsWith("TJS")) continue; + // Проверяем что это блок с датой (не просто "TJS" текст) + if (!m_nodes[i].contentDesc.contains('\n')) continue; + + // Child node с балансом идёт сразу после parent или рядом + for (int j = i + 1; j < qMin(i + 5, m_nodes.size()); ++j) { + QString raw = m_nodes[j].contentDesc.trimmed(); + if (raw.isEmpty()) continue; + raw.replace(',', '.'); + raw.remove(' '); + bool ok = false; + double val = raw.toDouble(&ok); + if (ok) return val; + } + } + return 0.0; +} + +QString ScreenXmlParser::parseUserNameFromHomeScreen() { + // TODO: реализовать парсинг имени + return {}; +} + +Node ScreenXmlParser::findRefreshButtonOnHomeScreen() { + // В блоке content-desc="TJS\n<дата>" три clickable дочерних элемента: + // View с балансом, Button (глазик) и ImageView (рефреш). Тапать нужно + // именно ImageView — он правее глазика и единственный ImageView внутри + // bounds parent'а. + Node parent; + for (const Node &node : m_nodes) { + if (node.contentDesc.startsWith("TJS") && node.contentDesc.contains('\n')) { + parent = node; + break; + } + } + if (parent.x1() < 0) return {}; + + Node best; + for (const Node &node : m_nodes) { + if (!node.clickable) continue; + if (node.className != "android.widget.ImageView") continue; + if (node.x1() < parent.x1() || node.x2() > parent.x2()) continue; + if (node.y1() < parent.y1() || node.y2() > parent.y2()) continue; + if (best.x1() < 0 || node.x1() > best.x1()) { + best = node; + } + } + return best; +} + +bool ScreenXmlParser::isSideMenu() { + bool hasVyhod = false; + bool hasIdentified = false; + for (const Node &node : m_nodes) { + // До 3.2.33 кнопка была "Выход", с 3.2.33 — "Выйти". + if (node.contentDesc == QString::fromUtf8("Выход") || + node.contentDesc == QString::fromUtf8("Выйти")) { + hasVyhod = true; + } + // До 3.2.33: "Идентифицирован\nУспешно" — отдельная нода (startsWith). + // С 3.2.33: "Идентифицирован" вшито в блок профиля + // ("IN\nIsroil Nazarov\n+992 ...\nИдентифицирован\nУспешно") — нужен contains. + if (node.contentDesc.contains(QString::fromUtf8("Идентифицирован"))) { + hasIdentified = true; + } + if (hasVyhod && hasIdentified) { + return true; + } + } + return false; +} + +QString ScreenXmlParser::parseProfileFullName() { + // До 3.2.33 одна нода содержит: + // "Имя Фамилия\n+992 (18) 555-5519\nВерсия: 3.2.5\nСборка: 390:3443" + // С 3.2.33 "Версия"/"Сборка" вынесены в отдельные ноды, а профиль: + // "IN\nIsroil Nazarov\n+992 (17) 682-7668\nИдентифицирован\nУспешно" + // Находим ноду по наличию "Идентифицирован" ИЛИ "Версия:+Сборка:", затем + // выбираем строку, идущую непосредственно перед строкой с телефоном "+...". + static const QRegularExpression phoneRe(R"(^\+\d)"); + for (const Node &node : m_nodes) { + const bool isNewProfileBlock = node.contentDesc.contains(QString::fromUtf8("Идентифицирован")); + const bool isOldProfileBlock = node.contentDesc.contains(QString::fromUtf8("Версия:")) && + node.contentDesc.contains(QString::fromUtf8("Сборка:")); + if (!isNewProfileBlock && !isOldProfileBlock) continue; + const QStringList lines = node.contentDesc.split('\n'); + for (int i = 0; i < lines.size(); ++i) { + if (phoneRe.match(lines[i].trimmed()).hasMatch() && i > 0) { + return lines[i - 1].trimmed(); + } + } + } + return {}; +} + +QString ScreenXmlParser::parseProfilePhone() { + static const QRegularExpression phoneRe(R"(^\+\d)"); + for (const Node &node : m_nodes) { + const bool isNewProfileBlock = node.contentDesc.contains(QString::fromUtf8("Идентифицирован")); + const bool isOldProfileBlock = node.contentDesc.contains(QString::fromUtf8("Версия:")) && + node.contentDesc.contains(QString::fromUtf8("Сборка:")); + if (!isNewProfileBlock && !isOldProfileBlock) continue; + for (const QString &line : node.contentDesc.split('\n')) { + const QString trimmed = line.trimmed(); + if (phoneRe.match(trimmed).hasMatch()) { + return trimmed; + } + } + } + return {}; +} + +QMap ScreenXmlParser::parseLabelValuePairs() { + // Применяется к экранам "Платеж выполнен" и подобным, где с 3.2.33 + // лейблы и значения сидят в отдельных View-нодах: лейбл слева + // (content-desc оканчивается ':'), значение — справа на той же Y-полосе. + QMap result; + for (int i = 0; i < m_nodes.size(); ++i) { + const QString rawLabel = m_nodes[i].contentDesc.trimmed(); + if (rawLabel.isEmpty() || !rawLabel.endsWith(':')) continue; + const QString label = rawLabel.left(rawLabel.size() - 1).trimmed(); + if (label.isEmpty() || result.contains(label)) continue; + + const Node &lbl = m_nodes[i]; + int bestIdx = -1; + for (int j = 0; j < m_nodes.size(); ++j) { + if (j == i) continue; + const QString candidate = m_nodes[j].contentDesc.trimmed(); + if (candidate.isEmpty()) continue; + if (candidate.endsWith(':')) continue; + if (m_nodes[j].x1() < lbl.x2()) continue; + if (qAbs(m_nodes[j].y1() - lbl.y1()) > 8) continue; + if (bestIdx < 0 || m_nodes[j].x1() < m_nodes[bestIdx].x1()) { + bestIdx = j; + } + } + if (bestIdx >= 0) { + result[label] = m_nodes[bestIdx].contentDesc.trimmed(); + } + } + return result; +} + +bool ScreenXmlParser::isCardsScreen() { + for (const Node &node : m_nodes) { + if (node.contentDesc == QString::fromUtf8("Мои карты")) { + return true; + } + } + return false; +} + +QList ScreenXmlParser::parseCards() { + // Карты — ImageView с content-desc формата: + // "Обновлено: 03.04.26 - 22:43:57\n0.00 TJS\n**** 3258\n до \n11/35\nIMOMKULZODA MEKHRUBON" + // или "-\n**** \n до \n12/29\n \nКредитная карта" + QList result; + + for (const Node &node : m_nodes) { + if (node.className != "android.widget.ImageView") continue; + if (!node.clickable) continue; + if (!node.contentDesc.contains("****")) continue; + if (node.contentDesc.contains(QString::fromUtf8("Кредитная карта"))) continue; + + QStringList lines; + for (const QString &line : node.contentDesc.split('\n')) { + QString trimmed = line.trimmed(); + if (!trimmed.isEmpty()) { + lines.append(trimmed); + } + } + + CardInfo card; + + for (const QString &line : lines) { + // Баланс: "0.00 TJS" + if (line.contains("TJS")) { + QStringList parts = line.split(' '); + if (parts.size() >= 2) { + card.amount = parts[0].toDouble(); + card.currency = "TJS"; + } + continue; + } + // Последние цифры: "**** 3258" + if (line.startsWith("****")) { + QString nums = line.mid(4).trimmed(); + if (!nums.isEmpty() && nums[0].isDigit()) { + card.lastNumbers = nums; + } + continue; + } + // Срок: "11/35" или "12/29" + static const QRegularExpression expiryRe(R"(^\d{2}/\d{2}$)"); + if (expiryRe.match(line).hasMatch()) { + card.expiry = line; + continue; + } + // Пропускаем "до", "Обновлено:...", "-" + if (line == QString::fromUtf8("до") || line.startsWith(QString::fromUtf8("Обновлено")) || line == "-") { + continue; + } + // Имя держателя (CAPS) или описание + if (line == line.toUpper() && line.length() > 3) { + card.holderName = line; + } else { + card.description = line; + } + } + + result.append(card); + } + + return result; +} + +bool ScreenXmlParser::isCardDetailScreen() { + for (const Node &node : m_nodes) { + if (node.contentDesc == QString::fromUtf8("Карта")) { + return true; + } + } + return false; +} + +QString ScreenXmlParser::parseFullCardNumber() { + // Полный номер карты: "9762 0002 0711 3258" — clickable View + static const QRegularExpression cardNumberRe(R"(^\d{4}\s\d{4}\s\d{4}\s\d{4}$)"); + for (const Node &node : m_nodes) { + if (cardNumberRe.match(node.contentDesc).hasMatch()) { + return node.contentDesc; + } + } + return {}; +} + +double ScreenXmlParser::parseCardDetailBalance() { + // "0,00 TJS" или "- TJS" — содержит TJS, но не "сомони" + for (const Node &node : m_nodes) { + if (node.contentDesc.contains("TJS") && !node.contentDesc.contains(QString::fromUtf8("сомони"))) { + QString raw = node.contentDesc.trimmed(); + raw.remove("TJS"); + raw = raw.trimmed(); + if (raw == "-" || raw.isEmpty()) return 0.0; + raw.replace(',', '.'); + raw.remove(' '); + bool ok = false; + double val = raw.toDouble(&ok); + if (ok) return val; + } + } + return 0.0; +} + +QString ScreenXmlParser::parseCardDetailHolder() { + // Имя держателя — текст в CAPS, не содержит цифр, длина > 3 + static const QRegularExpression cardNumberRe(R"(\d{4}\s\d{4})"); + for (const Node &node : m_nodes) { + const QString &desc = node.contentDesc; + if (desc.isEmpty() || desc.length() < 4) continue; + if (desc == desc.toUpper() && !desc.contains(QRegularExpression(R"(\d)")) && + desc != QString::fromUtf8("TJS")) { + return desc.trimmed(); + } + } + return {}; +} + +Node ScreenXmlParser::findCardSettingsButton() { + for (const Node &node : m_nodes) { + if (node.clickable && node.contentDesc == QString::fromUtf8("Настройки карты")) { + return node; + } + } + return {}; +} + +bool ScreenXmlParser::isCardSettingsBottomSheet() { + bool hasRequisites = false; + bool hasBlock = false; + for (const Node &node : m_nodes) { + if (node.contentDesc == QString::fromUtf8("Реквизиты карты")) hasRequisites = true; + if (node.contentDesc == QString::fromUtf8("Заблокировать карту")) hasBlock = true; + if (hasRequisites && hasBlock) return true; + } + return false; +} + +Node ScreenXmlParser::findCardRequisitesMenuItem() { + // На bottom sheet это ImageView; на экране "Реквизиты и информация" + // одноимённый таб — Button. Берём именно ImageView, чтобы не перепутать. + for (const Node &node : m_nodes) { + if (node.clickable + && node.className == "android.widget.ImageView" + && node.contentDesc == QString::fromUtf8("Реквизиты карты")) { + return node; + } + } + return {}; +} + +bool ScreenXmlParser::isCardRequisitesScreen() { + bool hasCardTab = false; + bool hasAccountTab = false; + for (const Node &node : m_nodes) { + if (node.className != "android.widget.Button") continue; + if (node.contentDesc == QString::fromUtf8("Реквизиты карты")) hasCardTab = true; + if (node.contentDesc == QString::fromUtf8("Реквизиты счета")) hasAccountTab = true; + if (hasCardTab && hasAccountTab) return true; + } + return false; +} + +Node ScreenXmlParser::findAccountRequisitesTab() { + for (const Node &node : m_nodes) { + if (node.clickable + && node.className == "android.widget.Button" + && node.contentDesc == QString::fromUtf8("Реквизиты счета")) { + return node; + } + } + return {}; +} + +QString ScreenXmlParser::parseAccountNumber() { + // Берём ноду "Номер счета" (label) и ищем ближайший узел снизу + // с чисто цифровым content-desc — это и есть номер счёта. + Node label; + for (const Node &node : m_nodes) { + if (node.contentDesc == QString::fromUtf8("Номер счета")) { + label = node; + break; + } + } + if (label.y2() < 0) return {}; + + static const QRegularExpression digitsOnly(R"(^\d{10,}$)"); + Node best; + for (const Node &node : m_nodes) { + if (node.y1() < label.y2()) continue; + if (!digitsOnly.match(node.contentDesc).hasMatch()) continue; + if (best.y1() < 0 || node.y1() < best.y1()) { + best = node; + } + } + return best.contentDesc; +} + +bool ScreenXmlParser::isHistoryScreen() { + bool hasOperacii = false; + bool hasVypiska = false; + for (const Node &node : m_nodes) { + if (node.contentDesc == QString::fromUtf8("Операции")) { + hasOperacii = true; + } + if (node.contentDesc == QString::fromUtf8("Выписка")) { + hasVypiska = true; + } + if (hasOperacii && hasVypiska) { + return true; + } + } + return false; +} + +QList ScreenXmlParser::parseTransactionItems() { + // Транзакции — ImageView с content-desc формата: + // "9762***3258\n992882770011\n493.00 \nDC (по номеру телефона)\n 18:57:10" + QList result; + + for (const Node &node : m_nodes) { + if (node.className != "android.widget.ImageView") continue; + if (!node.contentDesc.contains("***")) continue; + + QStringList lines; + for (const QString &line : node.contentDesc.split('\n')) { + QString trimmed = line.trimmed(); + if (!trimmed.isEmpty()) { + lines.append(trimmed); + } + } + + if (lines.size() < 4) continue; + + TransactionItem tx; + tx.cardMask = lines[0]; // "9762***3258" + tx.phone = lines[1]; // "992882770011" + + // Сумма: "493.00" (может быть с пробелом в конце) + bool ok = false; + tx.amount = lines[2].toDouble(&ok); + if (!ok) tx.amount = 0.0; + + tx.description = lines[3]; // "DC (по номеру телефона)" или "Tcell" + + // Время — последний элемент + if (lines.size() >= 5) { + tx.time = lines[4]; // "18:57:10" + } + + result.append(tx); + } + + return result; +} + +bool ScreenXmlParser::isTransactionDetailScreen() { + for (const Node &node : m_nodes) { + if (node.contentDesc.contains(QString::fromUtf8("Дата операции:")) && + node.contentDesc.contains(QString::fromUtf8("Номер операции:"))) { + return true; + } + } + return false; +} + +ScreenXmlParser::TransactionDetail ScreenXmlParser::parseTransactionDetail() { + // Все данные в одном content-desc, формат "ключ: \nзначение" + TransactionDetail detail; + + for (const Node &node : m_nodes) { + if (!node.contentDesc.contains(QString::fromUtf8("Дата операции:"))) continue; + + // Разбиваем по \n, парсим пары "ключ:" + следующая строка = значение + QStringList lines = node.contentDesc.split('\n'); + + for (int i = 0; i < lines.size(); ++i) { + const QString line = lines[i].trimmed(); + + if (line.startsWith(QString::fromUtf8("Дата операции:")) && i + 1 < lines.size()) { + detail.date = lines[i + 1].trimmed(); + } else if (line.startsWith(QString::fromUtf8("Время операции:")) && i + 1 < lines.size()) { + detail.time = lines[i + 1].trimmed(); + } else if (line.startsWith(QString::fromUtf8("Номер операции:")) && i + 1 < lines.size()) { + detail.operationId = lines[i + 1].trimmed(); + } else if (line.startsWith(QString::fromUtf8("Поставщик:")) && i + 1 < lines.size()) { + detail.provider = lines[i + 1].trimmed(); + } else if (line.startsWith(QString::fromUtf8("Счет отправителя:")) && i + 1 < lines.size()) { + detail.senderAccount = lines[i + 1].trimmed(); + } else if (line.startsWith(QString::fromUtf8("Счет получателя:")) && i + 1 < lines.size()) { + detail.receiverAccount = lines[i + 1].trimmed(); + } else if (line.startsWith(QString::fromUtf8("Сумма операции:")) && i + 1 < lines.size()) { + detail.amount = lines[i + 1].trimmed().toDouble(); + } else if (line.startsWith(QString::fromUtf8("Комиссия:")) && i + 1 < lines.size()) { + detail.fee = lines[i + 1].trimmed().toDouble(); + } else if (line.startsWith(QString::fromUtf8("Статус:")) && i + 1 < lines.size()) { + detail.status = lines[i + 1].trimmed(); + } + } + break; + } + + return detail; +} + +Node ScreenXmlParser::findEditTextByHint(const QString &hint) { + for (const Node &node : m_nodes) { + if (node.className == "android.widget.EditText" && node.hint.contains(hint)) { + return node; + } + } + return {}; +} + +Node ScreenXmlParser::findSumEditText() { + // На экране перевода по карте порядок EditText'ов сверху вниз: + // [0] номер карты, [1] сумма, [2] комментарий. + QList editTexts; + for (const Node &node : m_nodes) { + if (node.className == "android.widget.EditText") editTexts.append(node); + } + std::sort(editTexts.begin(), editTexts.end(), + [](const Node &a, const Node &b) { return a.y1() < b.y1(); }); + return editTexts.size() >= 2 ? editTexts[1] : Node{}; +} + +Node ScreenXmlParser::tryToFindSystemPermissionDenyButton() { + for (const Node &node : m_nodes) { + if (node.resourceId == "com.android.permissioncontroller:id/permission_deny_button" + && node.clickable) { + return node; + } + } + for (const Node &node : m_nodes) { + if (node.clickable && node.text.trimmed() == QString::fromUtf8("ЗАПРЕТИТЬ")) { + return node; + } + } + return {}; +} + +Node ScreenXmlParser::tryToFindGooglePlayBanner(const int screenWidth, const int screenHeight) { + for (const Node &node : m_nodes) { + if (node.contentDesc.contains("Закрыть диалоговое окно") && node.clickable) { + return node; + } + } + for (const Node &node : m_nodes) { + if (node.contentDesc.contains("Доступно обновление")) { + Node tapNode; + const int cx = screenWidth / 2; + const int cy = screenHeight / 20; + tapNode.setBounds(cx, cy, cx + 1, cy + 1); + return tapNode; + } + } + return {}; +} + +} // namespace Dushanbe diff --git a/android/dushanbe/ScreenXmlParser.h b/android/dushanbe/ScreenXmlParser.h new file mode 100644 index 0000000..56bf00f --- /dev/null +++ b/android/dushanbe/ScreenXmlParser.h @@ -0,0 +1,165 @@ +#pragma once + +#include +#include +#include +#include "android/xml/Node.h" +#include "db/MaterialInfo.h" + +namespace Dushanbe { + +class ScreenXmlParser final : public QObject { + Q_OBJECT + +public: + explicit ScreenXmlParser(QObject *parent = nullptr); + + ~ScreenXmlParser() override; + + void parseAndSaveXml(const QString &xml); + + const QList &findAllNodes() const; + + Node findTextNode(const QString &text); + + Node findNodeByContentDesc(const QString &contentDesc); + + Node findNodeByContentDesc(const QString &contentDesc, bool contains); + + Node findButtonNode(const QString &text, bool contains); + + Node findFirstEditText(); + + // TODO: реализовать после снятия XML дампов экранов + bool isPinCodeScreen(); + + Node findPinCodeEditText(); + + bool isHomeScreen(); + + // Парсит баланс с домашнего экрана + double parseBalanceFromHomeScreen(); + + // Находит кнопку обновления данных в блоке "TJS\n<дата>" — правый clickable + // ImageView (рядом слева сидит глазик-Button, его не трогаем). + Node findRefreshButtonOnHomeScreen(); + + // Парсит имя пользователя с домашнего экрана + QString parseUserNameFromHomeScreen(); + + // Определяет боковое меню (профиль) + bool isSideMenu(); + + // Парсит данные профиля из бокового меню + // content-desc: "Имя Фамилия\n+992...\nВерсия:...\nСборка:..." + QString parseProfileFullName(); + QString parseProfilePhone(); + + // Определяет экран "Мои карты" + bool isCardsScreen(); + + // Структура для карты из экрана "Мои карты" + struct CardInfo { + double amount = 0.0; + QString currency; + QString lastNumbers; // "3258" + QString expiry; // "11/35" + QString holderName; // "IMOMKULZODA MEKHRUBON" + QString description; // "Кредитная карта" и т.д. + }; + + // Парсит все карты с экрана "Мои карты" (lastNumbers только) + QList parseCards(); + + // Определяет экран детали карты (заголовок "Карта") + bool isCardDetailScreen(); + + // Парсит полный номер карты с экрана детали ("9762 0002 0711 3258") + QString parseFullCardNumber(); + + // Парсит баланс с экрана детали ("0,00 TJS") + double parseCardDetailBalance(); + + // Парсит имя держателя с экрана детали + QString parseCardDetailHolder(); + + // Кнопка "Настройки карты" на экране детали карты + Node findCardSettingsButton(); + + // Bottom sheet с пунктами "Реквизиты карты" / "Заблокировать карту" и т.д. + bool isCardSettingsBottomSheet(); + + // Кнопка "Реквизиты карты" внутри bottom sheet "Настройки карты" + Node findCardRequisitesMenuItem(); + + // Экран "Реквизиты и информация" с табами "Реквизиты карты" / "Реквизиты счета" + bool isCardRequisitesScreen(); + + // Таб "Реквизиты счета" (правый) на экране "Реквизиты и информация" + Node findAccountRequisitesTab(); + + // Значение под лейблом "Номер счета" — длинная цифровая строка + QString parseAccountNumber(); + + // Определяет экран истории операций (табы "Операции" + "Выписка") + bool isHistoryScreen(); + + // Структура для транзакции из экрана истории + struct TransactionItem { + QString cardMask; // "9762***3258" + QString phone; // "992882770011" + double amount = 0.0; // 493.00 + QString description; // "DC (по номеру телефона)" + QString time; // "18:57:10" + }; + + // Парсит транзакции с экрана истории + QList parseTransactionItems(); + + // Определяет экран детали транзакции (содержит "Дата операции:" и "Номер операции:") + bool isTransactionDetailScreen(); + + // Структура для деталей транзакции + struct TransactionDetail { + QString date; // "03.04.2026" + QString time; // "18:57:10" + QString operationId; // "1593369670" + QString provider; // "DC (по номеру телефона)" + QString senderAccount; // "9762***3258" + QString receiverAccount;// "992882770011" + double amount = 0.0; // 493.00 + double fee = 0.0; // 0.00 + QString status; // "Успешный" + }; + + // Парсит детали транзакции + TransactionDetail parseTransactionDetail(); + + // Находит EditText по hint + // Парсит пары "Лейбл: " → "Значение" для экранов, где поля разложены по + // отдельным View-узлам в две колонки (формат с 3.2.33). Метод сканирует + // все ноды: лейблом считается контент, оканчивающийся на ':', значением — + // ближайший справа узел на той же Y-полосе. Возвращает map без двоеточий. + QMap parseLabelValuePairs(); + + Node findEditTextByHint(const QString &hint); + + // Поле ввода суммы на экране перевода. У EditText'а нет hint/content-desc + // (NAF), поэтому ищем по якорю — View "Мин. сумма: ..." под полем — + // и берём ближайший EditText выше якоря. + Node findSumEditText(); + + // Находит кнопку закрытия Google Play in-app update bottom sheet + // ("com.android.vending" оверлей "Доступно обновление"), если он показан + Node tryToFindGooglePlayBanner(int screenWidth, int screenHeight); + + // Находит кнопку "ЗАПРЕТИТЬ" в системном диалоге запроса разрешения Android + // (com.google.android.permissioncontroller). Возвращает пустой Node если диалог не виден. + Node tryToFindSystemPermissionDenyButton(); + +private: + QList m_nodes; + QDomElement m_xml; +}; + +} // namespace Dushanbe diff --git a/android/ocr/OcrUtils.cpp b/android/ocr/OcrUtils.cpp new file mode 100644 index 0000000..2819b3a --- /dev/null +++ b/android/ocr/OcrUtils.cpp @@ -0,0 +1,205 @@ +#include "OcrUtils.h" + +#include +#include +#include +#include +#include + +#include +#include + +namespace { + +// Кэш байт rus.traineddata. Грузим файл через Qt (Unicode-aware пути) и +// отдаём Tesseract в виде in-memory blob через Init(data, size, lang, ...). +// Это обходит проблему, что на Windows Tesseract открывает traineddata через +// `fopen`, который использует ANSI-кодировку (cp1251 на русской системе). +// Если путь к exe содержит кириллицу (например, OneDrive\Рабочий стол) — +// fopen не находит файл и Init возвращает -1. Memory-init не зависит от пути. +const QByteArray &tessdataBytes() { + static QByteArray bytes; + static bool tried = false; + if (tried) return bytes; + tried = true; + + const QString appDir = QCoreApplication::applicationDirPath(); + const QStringList candidates = { + appDir + "/tessdata/rus.traineddata", // канонический путь + appDir + "/rus.traineddata", // legacy: рядом с exe + }; + for (const QString &path : candidates) { + QFile f(path); + if (!f.open(QIODevice::ReadOnly)) continue; + bytes = f.readAll(); + f.close(); + if (!bytes.isEmpty()) { + qDebug() << "[OcrUtils] loaded rus.traineddata from" << path + << "(" << bytes.size() << "bytes)"; + return bytes; + } + } + qWarning() << "[OcrUtils] rus.traineddata NOT FOUND. Checked:" << candidates + << "— OCR будет работать в fallback-режиме без поиска текста."; + return bytes; +} + + + +// Расстояние Левенштейна на QString — нужен для нечёткого сравнения OCR-вывода +// с искомым словом. Для "Подтверждаю" допускаем 1-2 ошибки. +int editDistance(const QString &a, const QString &b) { + const int n = a.size(); + const int m = b.size(); + if (n == 0) return m; + if (m == 0) return n; + QVector prev(m + 1), cur(m + 1); + for (int j = 0; j <= m; ++j) prev[j] = j; + for (int i = 1; i <= n; ++i) { + cur[0] = i; + for (int j = 1; j <= m; ++j) { + const int cost = a[i - 1] == b[j - 1] ? 0 : 1; + cur[j] = qMin(qMin(cur[j - 1] + 1, prev[j] + 1), prev[j - 1] + cost); + } + prev.swap(cur); + } + return prev[m]; +} + +bool fuzzyMatch(const QString &word, const QString &needle) { + if (word.isEmpty() || needle.isEmpty()) return false; + const QString w = word.toLower(); + const QString n = needle.toLower(); + // Substring-ветка работает только если word достаточно длинный. + // Иначе любая одиночная буква, входящая в needle (для «подтверждаю»: + // п/о/д/т/в/е/р/ж/а/ю), даст ложный матч на случайный текст экрана — + // ровно так бот тапал в (620,302) по букве «в» где-то в шапке sheet'a. + const int minLen = qMax(qMin(n.size(), 3), n.size() * 3 / 5); + if (w.size() >= minLen && (w.contains(n) || n.contains(w))) return true; + // Допускаем до 20% ошибок (но не меньше 1) — Tesseract на «Подтверждаю» иногда + // даёт «Подтверждак»/«Подтверждак)» из-за оранжевого фона и сглаживания. + const int tolerance = qMax(1, n.size() / 5); + return editDistance(w, n) <= tolerance; +} + +} // namespace + +namespace OcrUtils { + +QPoint findTextCenter(const QByteArray &png, + const QString &needle, + int yMin, + int yMax) { + if (png.isEmpty() || needle.isEmpty()) return {-1, -1}; + + QElapsedTimer timer; + timer.start(); + + Pix *pix = pixReadMem(reinterpret_cast(png.constData()), + static_cast(png.size())); + if (!pix) { + qWarning() << "[OcrUtils] pixReadMem failed"; + return {-1, -1}; + } + + const int fullW = pixGetWidth(pix); + const int fullH = pixGetHeight(pix); + + int cropY = 0; + Pix *target = pix; + Pix *cropped = nullptr; + if (yMin > 0 || (yMax > 0 && yMax < fullH)) { + const int y0 = qMax(0, yMin); + const int y1 = (yMax > 0 && yMax <= fullH) ? yMax : fullH; + if (y1 > y0) { + BOX *box = boxCreate(0, y0, fullW, y1 - y0); + cropped = pixClipRectangle(pix, box, nullptr); + boxDestroy(&box); + if (cropped) { + target = cropped; + cropY = y0; + } + } + } + + // Гоняем OCR двумя проходами: оригинал + инверсия. Текст бывает разный — + // тёмный на светлом (заголовки/поля) и светлый на цветном (sticky-action + // кнопка, оранжевая/синяя). Инверсия белого-на-оранжевом превращает её в + // тёмное-на-голубом, что Tesseract разбирает на порядок надёжнее. + auto runPass = [&](Pix *src, bool invert) -> QPoint { + Pix *gray = pixConvertTo8(src, 0); + if (!gray) return {-1, -1}; + Pix *prepared = gray; + Pix *inverted = nullptr; + if (invert) { + inverted = pixInvert(nullptr, gray); + if (inverted) prepared = inverted; + } + + const QByteArray &td = tessdataBytes(); + if (td.isEmpty()) { + if (inverted) pixDestroy(&inverted); + pixDestroy(&gray); + return {-1, -1}; + } + auto *api = new tesseract::TessBaseAPI(); + // Init(data, size, lang, ...) — грузим traineddata из памяти, чтобы + // обойти fopen на путях с кириллицей (Windows ANSI-API). + if (api->Init(td.constData(), td.size(), "rus", + tesseract::OEM_DEFAULT, + nullptr, 0, nullptr, nullptr, false, nullptr)) { + qWarning() << "[OcrUtils] Tesseract Init(memory) failed —" + << "traineddata повреждён или несовместимая версия"; + delete api; + if (inverted) pixDestroy(&inverted); + pixDestroy(&gray); + return {-1, -1}; + } + api->SetImage(prepared); + api->Recognize(nullptr); + + QPoint hit(-1, -1); + tesseract::ResultIterator *ri = api->GetIterator(); + if (ri) { + do { + const char *raw = ri->GetUTF8Text(tesseract::RIL_WORD); + if (!raw) continue; + const QString word = QString::fromUtf8(raw).trimmed(); + delete[] raw; + if (!fuzzyMatch(word, needle)) continue; + + int x1, y1, x2, y2; + ri->BoundingBox(tesseract::RIL_WORD, &x1, &y1, &x2, &y2); + const int cx = (x1 + x2) / 2; + const int cy = (y1 + y2) / 2 + cropY; + qDebug() << "[OcrUtils] found" << needle << "→" << word + << (invert ? "[inverted]" : "[normal]") + << "at (" << cx << "," << cy << ")"; + hit = QPoint(cx, cy); + break; + } while (ri->Next(tesseract::RIL_WORD)); + } + api->End(); + delete api; + if (inverted) pixDestroy(&inverted); + pixDestroy(&gray); + return hit; + }; + + QPoint result = runPass(target, /*invert=*/false); + if (result.x() < 0) { + result = runPass(target, /*invert=*/true); + } + + if (cropped) pixDestroy(&cropped); + pixDestroy(&pix); + + if (result.x() < 0) { + qDebug() << "[OcrUtils]" << needle << "not found in" << timer.elapsed() << "ms"; + } else { + qDebug() << "[OcrUtils]" << needle << "located in" << timer.elapsed() << "ms"; + } + return result; +} + +} // namespace OcrUtils \ No newline at end of file diff --git a/android/ocr/OcrUtils.h b/android/ocr/OcrUtils.h new file mode 100644 index 0000000..97a57d9 --- /dev/null +++ b/android/ocr/OcrUtils.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + +namespace OcrUtils { + +// Находит надпись `needle` на скриншоте (PNG-байты) и возвращает координаты +// центра bounding box-а в системе координат экрана. Если текст не найден — +// возвращает QPoint(-1, -1). Совпадение нечувствительно к регистру и допускает +// одну-две OCR-ошибки в букве (для "Подтверждаю" на оранжевом фоне это важно). +// +// `yMin`/`yMax` — необязательное окно по Y в координатах экрана. Если задано, +// OCR гоняется только по этой полосе, что в 2-3 раза быстрее на 1080×2340. +// 0/0 — обрабатывать всё изображение. +QPoint findTextCenter(const QByteArray &png, + const QString &needle, + int yMin = 0, + int yMax = 0); + +} // namespace OcrUtils \ No newline at end of file diff --git a/android/ozon/CommonScript.cpp b/android/ozon/CommonScript.cpp new file mode 100644 index 0000000..c77ea3d --- /dev/null +++ b/android/ozon/CommonScript.cpp @@ -0,0 +1,503 @@ +#include "CommonScript.h" + +#include +#include +#include +#include +#include +#include + +#include "adb/AdbUtils.h" +#include "dao/DeviceDAO.h" +#include "dao/BankProfileDAO.h" +#include "net/NetworkService.h" + +namespace Ozon { + +CommonScript::CommonScript(QObject *parent) + : QObject(parent) { +} + +CommonScript::~CommonScript() = default; + +void CommonScript::start() { + doStart(); + emit finished(); +} + +Node CommonScript::swipeToButton( + const QString &deviceId, + const QString &text, int &counter, const int width, const int height +) { + for (int i = 0; i < 10; ++i) { + if (Node button = xmlScreenParser.findButtonNode(text, true); button.x() > 0 && button.y() > 0) { + return button; + } + const int x = width / 2; + const int offset = height / 4; + AdbUtils::makeSwipe(deviceId, x, height - offset, x, offset); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++counter)); + } + return {}; +} + +bool CommonScript::inputPinCode(const QString &deviceId, const QList &pincode) { + for (const Node &node: pincode) { + const bool completePincode = AdbUtils::makeTap(deviceId, node.x(), node.y()); + if (!completePincode) { + AdbUtils::makeTap(deviceId, node.x(), node.y()); + } + QThread::msleep(QRandomGenerator::global()->bounded(100, 901)); + } + return true; +} + +bool CommonScript::runAppAndGoToHomeScreen( + const QString &deviceId, + const QString &packageName, + const QString &pinCode, + const int width, + const int height +) { + // Разблокируем экран перед запуском + AdbUtils::unlockScreen(deviceId, width, height); + + // Быстрый path: если приложение уже запущено — не перезапускаем сразу, + // а смотрим текущий экран. Home → готово. PIN → вводим и ждём Home. + // Только если не смогли — убиваем и рестартим. + if (const QString pid = AdbUtils::tryToGetRunningAppPid(deviceId, packageName); !pid.isEmpty()) { + qDebug() << "Process already running, pid: " << pid; + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + if (xmlScreenParser.isHomeScreen()) { + qDebug() << "[Ozon] Fast path: already on Home screen"; + return true; + } + { + const QList fastPincode = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН"); + if (fastPincode.size() == pinCode.length() && fastPincode.size() >= 4) { + qDebug() << "[Ozon] Fast path: PIN screen, entering PIN"; + if (Node wake = xmlScreenParser.findTextNode("Введите ПИН"); !wake.isEmpty()) { + AdbUtils::makeTap(deviceId, wake.x(), wake.y()); + } + inputPinCode(deviceId, fastPincode); + QThread::msleep(3000); + for (int i = 0; i < 10; ++i) { + xmlScreenParser.parseAndSaveXml( + AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + if (xmlScreenParser.isHomeScreen()) { + qDebug() << "[Ozon] Fast path: Home reached after PIN"; + return true; + } + // Ошибка загрузки — тапаем "Обновить" + { + Node refreshBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Обновить")); + if (refreshBtn.x() > 0 && refreshBtn.y() > 0) { + qDebug() << "[Ozon] Fast path: page error after PIN, tapping 'Обновить'"; + AdbUtils::makeTap(deviceId, refreshBtn.x(), refreshBtn.y()); + QThread::msleep(5000); + } + } + QThread::msleep(1000); + } + } + } + qDebug() << "[Ozon] Fast path failed, restarting app"; + AdbUtils::tryToKillApplication(deviceId, packageName); + } + if (!AdbUtils::tryToStartApplication(deviceId, packageName)) { + AdbUtils::takeScreenshot(deviceId, "run_app_error"); + qWarning() << "Process has not started"; + return false; + } + + // Даем минимальное время на запуск + QThread::msleep(4500); + int counter = 0; + bool findAndInputPincode = false; + + while (true) { + counter++; + + // Если более 10 раз мы пытаемся перейти на домашнюю + if (counter > 10) { + AdbUtils::takeScreenshot(deviceId, "too_many_attempts"); + return false; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + + if (xmlScreenParser.isHomeScreen()) { + return true; + } + + // Проверяем на банеры после пин-кодом + Node closeBannerOrDialogNode = xmlScreenParser.tryToFindBannerOrDialog(width, height); + if (closeBannerOrDialogNode.x() != -1 && closeBannerOrDialogNode.y() != -1) { + AdbUtils::makeTap(deviceId, closeBannerOrDialogNode.x(), closeBannerOrDialogNode.y()); + QThread::msleep(1000); + continue; + } + + // Проверяем на ошибку загрузки WebView ("Обновить") + { + Node refreshBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Обновить")); + if (refreshBtn.x() > 0 && refreshBtn.y() > 0) { + qDebug() << "[runAppAndGoToHomeScreen] Page load error, tapping 'Обновить'"; + AdbUtils::makeTap(deviceId, refreshBtn.x(), refreshBtn.y()); + QThread::msleep(5000); + continue; + } + } + + // Проверяем на банеры перед пин-кодом + Node closeBannerNode = xmlScreenParser.tryToFindSecurityBanner(); + if (!closeBannerNode.isEmpty()) { + AdbUtils::makeTap(deviceId, closeBannerNode.x(), closeBannerNode.y()); + QThread::msleep(1000); + continue; + } + + // Проверяем на баннер Google Play "Доступно обновление" + if (Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(width, height); !gpNode.isEmpty()) { + qDebug() << "[runAppAndGoToHomeScreen] Google Play update banner detected, closing..."; + AdbUtils::makeTap(deviceId, gpNode.x(), gpNode.y()); + QThread::msleep(1500); + continue; + } + + // Проверяем на рекламный bottom sheet (ипотека, открыть счёт и т.п.) + if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) { + qDebug() << "[runAppAndGoToHomeScreen] Ad bottom sheet detected, closing..."; + AdbUtils::makeTap(deviceId, adNode.x(), adNode.y()); + QThread::msleep(1000); + // Если touch_outside не помог — жмём назад + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + if (Node stillAd = xmlScreenParser.tryToFindAdBanner(); !stillAd.isEmpty()) { + qDebug() << "[runAppAndGoToHomeScreen] Ad still visible, pressing back..."; + AdbUtils::goBack(deviceId); + QThread::msleep(1000); + } + continue; + } + + // Проверяем, что это экран ввода пин-кода + if (!findAndInputPincode) { + const QList pincode = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН"); + if (pincode.size() == pinCode.length() && pincode.size() >= 4) { + // чтобы пробудить экран + if (Node textPinCodeNode = xmlScreenParser.findTextNode("Введите ПИН"); !textPinCodeNode.isEmpty()) { + AdbUtils::makeTap(deviceId, textPinCodeNode.x(), textPinCodeNode.y()); + } + inputPinCode(deviceId, pincode); + // После ввода пин-код нужно сделать задержку, пока подгрузка идет, чтобы снова не спарсить + findAndInputPincode = true; + QThread::msleep(3000); + continue; + } + } + + QThread::msleep(1000); + } +} + +bool CommonScript::tryToCloseAllBannersOnHomePage(const QString &deviceId, const int width, const int height) { + bool closedAny = false; + for (int i = 0; i < 5; ++i) { + bool closedThis = false; + + // Баннер Google Play "Доступно обновление" — появляется поверх Ozon при возврате на домашнюю + if (Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(width, height); !gpNode.isEmpty()) { + qDebug() << "[tryToCloseAllBannersOnHomePage] Google Play update banner detected, closing..."; + AdbUtils::makeTap(deviceId, gpNode.x(), gpNode.y()); + QThread::msleep(1500); + closedThis = true; + } else if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) { + // Рекламный bottom sheet (ипотека, бесплатная карта, "Копите звёзды" и т.п.) + qDebug() << "[tryToCloseAllBannersOnHomePage] Ad bottom sheet detected, closing..."; + AdbUtils::makeTap(deviceId, adNode.x(), adNode.y()); + QThread::msleep(1000); + // Если touch_outside не помог — жмём назад + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + if (Node stillAd = xmlScreenParser.tryToFindAdBanner(); !stillAd.isEmpty()) { + qDebug() << "[tryToCloseAllBannersOnHomePage] Ad still visible, pressing back..."; + AdbUtils::goBack(deviceId); + QThread::msleep(1000); + } + closedAny = true; + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + continue; + } else if (Node closeBannerOrDialogNode = xmlScreenParser.tryToFindBannerOrDialog(width, height); + closeBannerOrDialogNode.x() != -1 && closeBannerOrDialogNode.y() != -1) { + AdbUtils::makeTap(deviceId, closeBannerOrDialogNode.x(), closeBannerOrDialogNode.y()); + QThread::msleep(500); + closedThis = true; + } + + if (!closedThis) break; + closedAny = true; + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + } + return closedAny; +} + +bool CommonScript::closeGooglePlayBannerIfVisible( + const QString &deviceId, const int width, const int height +) { + // 1. Системный диалог запроса разрешения Android (permissioncontroller) — тапаем "ЗАПРЕТИТЬ" + if (Node denyBtn = xmlScreenParser.tryToFindSystemPermissionDenyButton(); !denyBtn.isEmpty()) { + qDebug() << "[Ozon] System permission dialog detected mid-task, tapping 'ЗАПРЕТИТЬ'..."; + AdbUtils::makeTap(deviceId, denyBtn.x(), denyBtn.y()); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + return true; + } + + // 2. Google Play in-app update bottom sheet + Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(width, height); + if (gpNode.isEmpty()) return false; + qDebug() << "[Ozon] Google Play update banner detected mid-task, closing..."; + AdbUtils::makeTap(deviceId, gpNode.x(), gpNode.y()); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + return true; +} + +bool CommonScript::goToAllProducts( + const QString &deviceId, + const int width, + const int height +) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, m_counter)); + if (tryToCloseAllBannersOnHomePage(deviceId, width, height)) { + QThread::msleep(500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + } + + // Ozon: домашний экран и есть "Финансы" — счета уже доступны + if (xmlScreenParser.isHomeScreen()) { + return true; + } + + // Если по какой-то причине не на домашнем — пробуем тапнуть по вкладке "Финансы" + for (int i = 0; i < 3; ++i) { + if (Node financeTab = xmlScreenParser.findNodeByContentDesc("Финансы"); !financeTab.isEmpty()) { + AdbUtils::makeTap(deviceId, financeTab.x(), financeTab.y()); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + if (xmlScreenParser.isHomeScreen()) { + return true; + } + } + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + } + + qDebug() << "goToAllProducts: not home screen"; + return false; +} + +bool CommonScript::findAndTapOnAccount(const QString &deviceId, const QString &accountNumbers) { + QList > accounts = xmlScreenParser.parseAccountsInfo(); + for (auto &[account, node]: accounts) { + // FIXME будем считать что числа последние везде уникальные + if (accountNumbers == account.lastNumbers) { + qDebug() << "findAndTapOnAccount: " << accountNumbers; + AdbUtils::makeTap(deviceId, node.x(), node.y()); + QThread::msleep(1000); + return true; + } + } + return false; +} + + +void CommonScript::postAccountsData(const QList > &accounts) { + if (accounts.isEmpty()) { + return; + } + + auto *thread = new QThread; + auto *worker = new NetworkService; + + QJsonArray list; + for (const QPair &pair: accounts) { + QJsonObject obj; + MaterialInfo account = pair.first; + obj["appName"] = account.appCode; + obj["lastNumbers"] = account.lastNumbers; + obj["amount"] = account.amount; + obj["description"] = account.description; + obj["currency"] = account.currency; + obj["status"] = materialStatusToString(account.status); + + list.append(obj); + } + // FIXME доставить десктоп ид из настроеек + worker->setDeviceId(accounts.first().first.deviceId); + worker->setPayload(QJsonDocument(list).toJson()); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &NetworkService::postAccountsData); + connect(worker, &NetworkService::finished, thread, &QThread::quit); + connect(worker, &NetworkService::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +void CommonScript::postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction) { + auto *thread = new QThread; + auto *worker = new NetworkService; + + QJsonObject obj; + obj["id"] = transaction.id; + obj["amount"] = transaction.amount; + obj["name"] = transaction.name; + obj["fee"] = transaction.fee; + obj["status"] = transactionStatusToString(transaction.status); + obj["type"] = transactionTypeToString(transaction.type); + obj["phone"] = transaction.phone; + obj["description"] = transaction.description; + obj["updatedDescription"] = transaction.updatedDescription; + obj["bankName"] = transaction.bankName; + obj["bankTime"] = transaction.bankTime; + obj["bankTransactionId"] = transaction.bankTrExternalId; + + // FIXME доставить десктоп ид из настроеек + worker->setDeviceId(account.deviceId); + worker->addExtra("lastNumbers", account.lastNumbers); + worker->addExtra("transactionId", transaction.id); + worker->setPayload(QJsonDocument(obj).toJson()); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &NetworkService::insertTransactionData); + connect(worker, &NetworkService::finished, thread, &QThread::quit); + connect(worker, &NetworkService::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +void CommonScript::updateTransactionData( + const MaterialInfo &account, + const int transactionId, + const TransactionStatus status, + const QString &oldDesc, + const QString &newDesc +) { + auto *thread = new QThread; + auto *worker = new NetworkService; + + QJsonObject obj; + obj["id"] = transactionId; + obj["status"] = transactionStatusToString(status); + obj["description"] = newDesc; + obj["updatedDescription"] = oldDesc; + + // FIXME доставить десктоп ид из настроеек + worker->setDeviceId(account.deviceId); + worker->addExtra("lastNumbers", account.lastNumbers); + worker->setPayload(QJsonDocument(obj).toJson()); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &NetworkService::updateTransactionData); + connect(worker, &NetworkService::finished, thread, &QThread::quit); + connect(worker, &NetworkService::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +QString CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance) { + const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode); + + if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) { + qWarning() << "[Ozon] createBankProfileIfNeeded: skipping — fullName or phone is empty"; + return {}; + } + + const QMap currencyCodes = { + {"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972} + }; + const int currencyCode = currencyCodes.value(app.currency.toUpper(), 643); + const QStringList parts = app.fullName.split(' ', Qt::SkipEmptyParts); + + // Нормализуем телефон: оставляем только цифры, добавляем код страны и "+" + const QSettings settings("config.ini", QSettings::IniFormat); + const QString phoneCode = settings.value(appCode + "/phone_code").toString(); + QString phone = app.phone; + phone.remove(QRegularExpression("[^0-9]")); + if (!phoneCode.isEmpty() && !phone.startsWith(phoneCode)) { + if (phone.startsWith('8') && phoneCode != "8") { + phone.replace(0, 1, phoneCode); + } else { + phone.prepend(phoneCode); + } + } + phone.prepend('+'); + + const DeviceInfo device = DeviceDAO::getDeviceById(deviceId); + + QJsonObject profileBody; + profileBody["first_name"] = parts.value(0); + profileBody["middle_name"] = parts.size() >= 3 ? parts.value(1) : QString(""); + profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1); + profileBody["bank_name"] = appCode; + profileBody["phone_number"] = phone; + const double balance = (parsedBalance >= 0.0) ? parsedBalance : app.amount; + profileBody["balance"] = QString::number(qRound64(balance * 100)); + profileBody["state"] = (app.status == "active") ? "enabled" : "disabled"; + profileBody["device_id"] = device.apiId; + profileBody["currency"] = currencyCode; + + NetworkService ns; + ns.setDeviceId(deviceId); + ns.setPayload(QJsonDocument(profileBody).toJson()); + + if (app.bankProfileId.isEmpty()) { + ns.addExtra("app_id", app.id); + ns.postBankProfile(); + const BankProfileInfo afterPost = BankProfileDAO::getApplication(deviceId, appCode); + if (afterPost.bankProfileId.isEmpty()) { + const QString err = "postBankProfile failed for " + deviceId + " " + appCode; + qWarning() << "[Ozon] createBankProfileIfNeeded:" << err; + return err; + } + qDebug() << "[Ozon] createBankProfileIfNeeded: postBankProfile done"; + } else { + ns.addExtra("bank_profile_id", app.bankProfileId); + ns.patchBankProfile(); + qDebug() << "[Ozon] createBankProfileIfNeeded: patchBankProfile done"; + } + return {}; +} + +bool CommonScript::isAppRunning(const QString &deviceId, const QString &packageName) { + return !AdbUtils::tryToGetRunningAppPid(deviceId, packageName).isEmpty(); +} + +bool CommonScript::FreezeDetector::isUsefulDump(const QString &xml) { + if (xml.isEmpty()) return false; + // Полезный дамп должен содержать хотя бы один Button с непустым text + // ИЛИ EditText. Иначе — это shell-only state (только нав-табы, у которых + // text пуст и есть только content-desc). + if (!xml.contains(QStringLiteral("class=\"android.widget.Button\""))) return false; + static const QRegularExpression usefulRe( + R"(class="android\.widget\.Button"[^/]*text="[^"]+"|text="[^"]+"[^/]*class="android\.widget\.Button"|class="android\.widget\.EditText")" + ); + return usefulRe.match(xml).hasMatch(); +} + +bool CommonScript::FreezeDetector::noticeAndCheckFrozen(const QString &xml) { + if (!isUsefulDump(xml)) { + // Пустой или WebView-shell-only — не учитываем. + return false; + } + if (!m_prev.isEmpty() && xml == m_prev) { + ++m_streak; + if (m_streak >= m_maxStreak) return true; + } else { + m_streak = 0; + } + m_prev = xml; + return false; +} + +void CommonScript::stop() { + m_running = false; +} + +} // namespace Ozon \ No newline at end of file diff --git a/android/ozon/CommonScript.h b/android/ozon/CommonScript.h new file mode 100644 index 0000000..8afcb6c --- /dev/null +++ b/android/ozon/CommonScript.h @@ -0,0 +1,122 @@ +#pragma once + +#include +#include "ScreenXmlParser.h" + +namespace Ozon { + +class CommonScript : public QObject { + Q_OBJECT + +public: + explicit CommonScript(QObject *parent = nullptr); + + ~CommonScript() override; + +protected: + virtual void doStart() = 0; + + // static method + static bool inputPinCode(const QString &deviceId, const QList &pincode); + + bool goToAllProducts( + const QString &deviceId, + int width, + int height + ); + + bool findAndTapOnAccount(const QString &deviceId, const QString &accountNumbers); + + void postAccountsData(const QList > &accounts); + + void postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction); + + void updateTransactionData( + const MaterialInfo &account, + int transactionId, + TransactionStatus status, + const QString &oldDesc, + const QString &newDesc + ); + + Node swipeToButton( + const QString &deviceId, + const QString &text, + int &counter, int width, int height + ); + + bool runAppAndGoToHomeScreen( + const QString &deviceId, + const QString &packageName, + const QString &pinCode, + int width, + int height + ); + + bool tryToCloseAllBannersOnHomePage( + const QString &deviceId, int width, int height + ); + + // Если виден баннер Google Play "Доступно обновление" — закрывает его и перепарсивает XML. + // Безопасно вызывать на любом экране; возвращает true если баннер был закрыт. + bool closeGooglePlayBannerIfVisible( + const QString &deviceId, int width, int height + ); + + // Создаёт bank_profile на сервере если ещё не создан. + // Возвращает пустую строку при успехе, текст ошибки при провале. + QString createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance = -1.0); + + // Проверяет что приложение ещё запущено на устройстве + static bool isAppRunning(const QString &deviceId, const QString &packageName); + + // Детектор зафризившегося экрана: считает подряд идущие байт-идентичные + // дампы с реальным контентом. Бесполезные дампы (пустые из-за adb-сбоя + // или WebView-shell-only без отрисованного Svelte-контента) пропускаются — + // не обнуляют счётчик и не накручивают его. + // + // Использование в wait-loop: + // FreezeDetector freezeCheck; + // for (int attempt = 0; attempt < N; ++attempt) { + // QString xml = AdbUtils::getScreenDumpAsXml(...); + // xmlScreenParser.parseAndSaveXml(xml); + // if (freezeCheck.noticeAndCheckFrozen(xml)) { + // m_error = "Экран зафризился"; return; + // } + // ... existing logic + // } + class FreezeDetector { + public: + explicit FreezeDetector(int maxStreak = 3) : m_maxStreak(maxStreak) {} + // True если получили (m_maxStreak + 1) подряд байт-идентичных полезных дампов. + bool noticeAndCheckFrozen(const QString &xml); + private: + static bool isUsefulDump(const QString &xml); + QString m_prev; + int m_streak = 0; + int m_maxStreak; + }; + + bool m_running = true; + ScreenXmlParser xmlScreenParser; + int m_counter = 0; + + QByteArray m_resultScreenshot; + +signals: + void finished(); + + void finishedWithResult(const QString &result); + + void screenshotReady(const QByteArray &screenshot); + +public slots: + virtual void start() final; + + virtual void stop() final; + +private: + Q_DISABLE_COPY(CommonScript); +}; + +} // namespace Ozon \ No newline at end of file diff --git a/android/ozon/GetCardInfoScript.cpp b/android/ozon/GetCardInfoScript.cpp new file mode 100644 index 0000000..d8cb0bd --- /dev/null +++ b/android/ozon/GetCardInfoScript.cpp @@ -0,0 +1,578 @@ +#include "GetCardInfoScript.h" + +#include +#include +#include +#include +#include + +#include "adb/AdbUtils.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "dao/SettingsDAO.h" +#include "dump/TaskDumpRecorder.h" +#include "net/NetworkService.h" +#include "AppLogger.h" + +namespace Ozon { + +GetCardInfoScript::GetCardInfoScript( + QString deviceId, + QString appCode, + QString lastNumbers, + QObject *parent +) : CommonScript(parent), + m_deviceId(std::move(deviceId)), + m_appCode(std::move(appCode)), + m_lastNumbers(std::move(lastNumbers)) { +} + +GetCardInfoScript::~GetCardInfoScript() = default; + +void GetCardInfoScript::doStart() { + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + const TaskDumpMeta dumpMeta{}; + TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error); + + // Читаем текущий экран + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Закрываем рекламный bottom sheet, если открыт + if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) { + qDebug() << "[GetCardInfo] Ad banner detected, closing..."; + AdbUtils::makeTap(m_deviceId, adNode.x(), adNode.y()); + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + + // Свежий дамп перед проверкой домашнего экрана + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Если не на домашнем экране — перезапускаем приложение + if (!xmlScreenParser.isHomeScreen()) { + qDebug() << "[GetCardInfo] Not on home screen, restarting app..."; + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, + device.screenWidth, device.screenHeight)) { + m_error = "Cannot reach home screen: " + device.name + " (" + m_deviceId + ")"; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + + // Закрываем баннеры на главной + tryToCloseAllBannersOnHomePage(m_deviceId, device.screenWidth, device.screenHeight); + QThread::msleep(500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Переходим в профиль и парсим имя и телефон (только если ещё нет в БД) + const bool hasProfileInfo = !app.fullName.isEmpty() && !app.phone.isEmpty(); + Node userNameNode; + if (!hasProfileInfo) { + userNameNode = xmlScreenParser.findUserNameOnHomeScreen(device.screenHeight); + } + if (!hasProfileInfo && !userNameNode.isEmpty()) { + qDebug() << "[GetCardInfo] Navigating to profile to parse name/phone..."; + AdbUtils::makeTap(m_deviceId, userNameNode.x(), userNameNode.y()); + QThread::msleep(2500); + + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + if (xmlScreenParser.isProfileScreen()) break; + QThread::msleep(1000); + } + + if (xmlScreenParser.isProfileScreen()) { + const QString fullName = xmlScreenParser.parseProfileFullName(); + const QString phone = xmlScreenParser.parseProfilePhone(); + qDebug() << "[GetCardInfo] Profile parsed - fullName:" << fullName << "phone:" << phone; + BankProfileDAO::updateProfileInfo(app.id, fullName, phone); + } else { + qWarning() << "[GetCardInfo] Profile screen not reached, skipping profile parse"; + } + + AdbUtils::goBack(m_deviceId); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + } else { + qDebug() << "[GetCardInfo] Profile info already in DB or username not found, skipping profile parse"; + } + + // Проверяем что на домашнем экране перед поиском карт + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + if (!xmlScreenParser.isHomeScreen()) { + qDebug() << "[GetCardInfo] Not on home screen before card search, restarting..."; + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, + device.screenWidth, device.screenHeight)) { + m_error = "Cannot reach home screen before card search: " + device.name + " (" + m_deviceId + ")"; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + tryToCloseAllBannersOnHomePage(m_deviceId, device.screenWidth, device.screenHeight); + QThread::msleep(500); + } + + // Ждём пока все карты подгрузятся — до 5 попыток + QList cards; + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + if (closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight)) { + // баннер закрыт, дамп уже обновлён внутри helper'а + } + cards = xmlScreenParser.parseCardsOnHomeScreen(); + if (!cards.isEmpty()) break; + qDebug() << "[GetCardInfo] No cards found on home screen, attempt" << attempt + 1 << "/ 5"; + QThread::msleep(1500); + } + + if (cards.isEmpty()) { + m_error = "No cards found on home screen: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + qDebug() << "[GetCardInfo] Found" << cards.size() << "card(s) on home screen"; + + // Заполняем метаданные на найденных картах. Новые карты (которых ещё нет в БД) + // не создаём здесь — запись появится только после успешного OCR полного номера + // в success-ветке ниже. Это защищает от записей из ленты переводов и не оставляет + // в БД материал с пустым card_number. + // + // Для карт, которые уже в БД, обновляем только amount/update_time — + // прочие поля (cardNumber, materialId, status, ...) сохраняем как было. + for (MaterialInfo &card : cards) { + card.deviceId = device.id; + card.appCode = m_appCode; + card.updateTime = QDateTime::currentDateTimeUtc(); + card.status = MaterialStatus::Off; + + const MaterialInfo existing = MaterialDAO::findAppAccount(device.id, m_appCode, card.lastNumbers); + if (existing.id == -1) continue; + + MaterialInfo refresh = existing; + refresh.amount = card.amount; + refresh.updateTime = card.updateTime; + if (!MaterialDAO::upsertAccount(refresh)) { + qWarning() << "[GetCardInfo] Failed to refresh amount for card" << card.lastNumbers; + } else { + qDebug() << "[GetCardInfo] Refreshed amount for card" + << card.lastNumbers << "to" << card.amount; + } + } + + // Сохраняем баланс домашнего экрана в application + const double totalAmount = cards.isEmpty() ? 0.0 : cards[0].amount; + if (totalAmount > 0.0) { + BankProfileDAO::updateAmount(app.id, totalAmount); + } + + // Синхронная отправка на сервер (NetworkService блокируется через QEventLoop внутри) + NetworkService ns; + ns.setDeviceId(m_deviceId); + + // 1. Создаём девайс если нет api_id + if (device.apiId.isEmpty()) { + QJsonObject deviceBody; + deviceBody["desktop_id"] = SettingsDAO::get("desktop_id"); + deviceBody["status"] = "ONLINE"; + deviceBody["name"] = device.name; + deviceBody["screen_width"] = device.screenWidth; + deviceBody["screen_height"]= device.screenHeight; + deviceBody["battery"] = device.battery; + ns.setPayload(QJsonDocument(deviceBody).toJson()); + ns.createDevice(); + qDebug() << "[GetCardInfo] createDevice done"; + } + + // 2. Отправляем банковский профиль (используем свежие данные из БД — имя/телефон уже спарсены) + { + const BankProfileInfo freshApp = BankProfileDAO::getApplication(device.id, m_appCode); + const QStringList parts = freshApp.fullName.split(' ', Qt::SkipEmptyParts); + + // ISO 4217 numeric currency codes + const QMap currencyCodes = { + {"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972} + }; + const int currencyCode = currencyCodes.value(freshApp.currency.toUpper(), 643); + + QJsonObject profileBody; + profileBody["first_name"] = parts.value(0); + profileBody["middle_name"] = parts.size() >= 3 ? parts.value(1) : ""; + profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1); + profileBody["bank_name"] = m_appCode; + // Телефон: убираем пробелы, оставляем "+" + const QString phone = freshApp.phone.simplified().remove(' '); + profileBody["phone_number"] = phone; + profileBody["balance"] = QString::number(qRound64(totalAmount * 100)); + profileBody["state"] = (freshApp.status == "active") ? "enabled" : "disabled"; + profileBody["device_id"] = device.apiId; + profileBody["currency"] = currencyCode; + ns.setPayload(QJsonDocument(profileBody).toJson()); + + if (freshApp.bankProfileId.isEmpty()) { + ns.addExtra("app_id", freshApp.id); + ns.postBankProfile(); + // Проверяем что bankProfileId сохранился + const BankProfileInfo afterPost = BankProfileDAO::getApplication(device.id, m_appCode); + if (afterPost.bankProfileId.isEmpty()) { + const QString err = "postBankProfile failed for " + m_deviceId; + qWarning() << "[GetCardInfo]" << err; + AppLogger::log("ozon/GetCardInfo", m_deviceId, err, + AdbUtils::captureScreenshotBytes(m_deviceId), + "FETCH_PROFILE"); + m_error = err; + emit finishedWithResult(m_error); + return; + } + qDebug() << "[GetCardInfo] postBankProfile done"; + } else { + ns.addExtra("bank_profile_id", freshApp.bankProfileId); + ns.patchBankProfile(); + qDebug() << "[GetCardInfo] patchBankProfile done"; + } + } + + // Для каждой карты переходим на её экран и получаем полный номер через OCR + for (int i = 0; i < cards.size(); ++i) { + const MaterialInfo &card = cards[i]; + + // Если передан конкретный lastNumbers — обрабатываем только его + if (!m_lastNumbers.isEmpty() && card.lastNumbers != m_lastNumbers) continue; + + qDebug() << "[GetCardInfo] Getting card number for" << card.lastNumbers; + + // Если номер карты уже есть в БД — сумма уже обновлена через upsert, навигация не нужна + MaterialInfo existing = MaterialDAO::findAppAccount(device.id, m_appCode, card.lastNumbers); + if (existing.id != -1 && !existing.cardNumber.isEmpty()) { + qDebug() << "[GetCardInfo] Card" << card.lastNumbers << "already has number, skipping"; + continue; + } + + // Тапаем по кнопке конкретной карты — каждая открывает свой экран + Node cardBtn = xmlScreenParser.findCardButtonByLastNumbers(card.lastNumbers); + if (cardBtn.isEmpty()) { + qWarning() << "[GetCardInfo] Card button not found for" << card.lastNumbers; + continue; + } + + AdbUtils::makeTap(m_deviceId, cardBtn.x(), cardBtn.y()); + + // Скроллим WebView вверх, пока "Показать" не окажется в верхней трети экрана. + // На устройствах с короткой высотой (например, SM-A165F, h=2340) кнопка "Показать" + // находится за bottom navigation, и тап уходит в нав-бар. + const int targetTopY = device.screenHeight / 3; + for (int scrollAttempt = 0; scrollAttempt < 6; ++scrollAttempt) { + QThread::msleep(scrollAttempt == 0 ? 2000 : 600); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + + Node candidate = xmlScreenParser.findButtonNode("Показать", false); + if (candidate.isEmpty()) { + if (xmlScreenParser.isHomeScreen()) { + cardBtn = xmlScreenParser.findCardButtonByLastNumbers(card.lastNumbers); + if (!cardBtn.isEmpty()) { + qDebug() << "[GetCardInfo] Still on home before scroll, re-tapping card" << card.lastNumbers; + AdbUtils::makeTap(m_deviceId, cardBtn.x(), cardBtn.y()); + } + } + continue; + } + if (candidate.y() <= targetTopY) { + qDebug() << "[GetCardInfo] 'Показать' already at top y=" << candidate.y(); + break; + } + const int swipeFromY = candidate.y(); + const int swipeToY = device.screenHeight / 6; + qDebug() << "[GetCardInfo] Scrolling 'Показать' up:" << swipeFromY << "->" << swipeToY; + AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2, swipeFromY, + device.screenWidth / 2, swipeToY); + } + + // Ждём экран карты и стабильной геометрии блока реквизитов. + // Без этого тап по "Показать" во время reflow уезжает ниже — в "Защиту от мошенников". + // Стабильность: "Показать" совпадает по y1 с "Реквизиты карты" И координаты не меняются в двух подряд дампах. + Node showBtn; + int prevShowY = -1; + for (int attempt = 0; attempt < 10; ++attempt) { + QThread::msleep(attempt == 0 ? 2000 : 500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + + Node candidate = xmlScreenParser.findButtonNode("Показать", false); + Node requisites = xmlScreenParser.findTextNode("Реквизиты карты"); + + if (candidate.isEmpty() || requisites.isEmpty()) { + // Если всё ещё на домашнем экране — тап по карточке не сработал, повторяем + if (xmlScreenParser.isHomeScreen()) { + cardBtn = xmlScreenParser.findCardButtonByLastNumbers(card.lastNumbers); + if (!cardBtn.isEmpty()) { + qDebug() << "[GetCardInfo] Still on home screen, re-tapping card" << card.lastNumbers; + AdbUtils::makeTap(m_deviceId, cardBtn.x(), cardBtn.y()); + } + } + prevShowY = -1; + qDebug() << "[GetCardInfo] Requisites not ready, attempt" << attempt + 1 << "/ 10"; + continue; + } + + // "Показать" должен быть в одной строке с "Реквизиты карты" + if (qAbs(candidate.y1() - requisites.y1()) > 10) { + prevShowY = -1; + qDebug() << "[GetCardInfo] 'Показать' not aligned with 'Реквизиты карты', waiting reflow"; + continue; + } + + // Y должна совпасть в двух подряд дампах (layout устаканился) + if (prevShowY >= 0 && qAbs(candidate.y() - prevShowY) <= 5) { + showBtn = candidate; + break; + } + prevShowY = candidate.y(); + qDebug() << "[GetCardInfo] 'Показать' seen at y=" << candidate.y() << ", waiting for stable frame"; + } + + if (showBtn.isEmpty()) { + qWarning() << "[GetCardInfo] 'Показать' not stable for card" << card.lastNumbers; + AdbUtils::goBack(m_deviceId); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + continue; + } + + qDebug() << "[GetCardInfo] 'Показать' at" << showBtn.x() << showBtn.y(); + AdbUtils::makeTap(m_deviceId, showBtn.x(), showBtn.y()); + + // Ждём появления полного номера карты в XML дампе + QString cardNumber; + for (int attempt = 0; attempt < 10; ++attempt) { + QThread::msleep(2000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + cardNumber = xmlScreenParser.findFullCardNumber(); + if (!cardNumber.isEmpty()) { + qDebug() << "[GetCardInfo] Card number found on attempt" << attempt + 1; + break; + } + + // Закрываем баннер "Понятно" и повторно нажимаем "Показать" + Node bannerBtn = xmlScreenParser.findTextNode("Понятно"); + if (!bannerBtn.isEmpty()) { + qDebug() << "[GetCardInfo] Banner 'Понятно' detected, closing..."; + AdbUtils::makeTap(m_deviceId, bannerBtn.x(), bannerBtn.y()); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + Node retryShowBtn = xmlScreenParser.findButtonNode("Показать", false); + if (!retryShowBtn.isEmpty()) { + qDebug() << "[GetCardInfo] Re-tapping 'Показать' after banner close"; + AdbUtils::makeTap(m_deviceId, retryShowBtn.x(), retryShowBtn.y()); + } + continue; + } + + // "Подождите" — загрузка в процессе, просто ждём + if (!xmlScreenParser.findButtonNode("Подождите", false).isEmpty()) { + qDebug() << "[GetCardInfo] 'Подождите' visible, waiting... attempt" << attempt + 1 << "/ 10"; + continue; + } + + // "Скрыть" видна — номер должен быть раскрыт, но парсер не нашёл + if (!xmlScreenParser.findButtonNode("Скрыть", false).isEmpty()) { + qDebug() << "[GetCardInfo] 'Скрыть' visible but card number not parsed, attempt" << attempt + 1 << "/ 10"; + continue; + } + + // Повторно нажимаем "Показать" если тап не сработал + Node retryShowBtn = xmlScreenParser.findButtonNode("Показать", false); + if (!retryShowBtn.isEmpty()) { + qDebug() << "[GetCardInfo] Re-tapping 'Показать', attempt" << attempt + 1 << "/ 10"; + AdbUtils::makeTap(m_deviceId, retryShowBtn.x(), retryShowBtn.y()); + } else { + qDebug() << "[GetCardInfo] Card number not visible, attempt" << attempt + 1 << "/ 10"; + } + } + + if (!cardNumber.isEmpty()) { + qDebug() << "[GetCardInfo] Card" << card.lastNumbers << "full number:" << cardNumber; + // Апсёртим запись только теперь — с полным номером карты. + MaterialInfo toSave = card; + toSave.cardNumber = cardNumber; + toSave.updateTime = QDateTime::currentDateTimeUtc(); + if (!MaterialDAO::upsertAccount(toSave)) { + qWarning() << "[GetCardInfo] Failed to upsert account for card" << card.lastNumbers; + } else { + qDebug() << "[GetCardInfo] Upserted account:" << card.lastNumbers + << card.description << card.amount; + } + } else { + const QString err = "Card number not found for " + card.lastNumbers; + qWarning() << "[GetCardInfo]" << err; + // Без полного номера материал в БД не создаём. + AppLogger::log("ozon/GetCardInfo", m_deviceId, err, + AdbUtils::captureScreenshotBytes(m_deviceId), + "FETCH_PROFILE"); + } + + // Возвращаемся на домашний экран для следующей карты + if (i < cards.size() - 1) { + AdbUtils::goBack(m_deviceId); + QThread::msleep(2000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + } + } + + // 2.5. Достаём номер счёта (общий для всех карт): Основной счёт → Посмотреть реквизиты + // ВРЕМЕННО ОТКЛЮЧЕНО + // QString accountNumber; + // { + // bool needFetch = false; + // for (const MaterialInfo &c : cards) { + // const MaterialInfo dbCard = MaterialDAO::findAppAccount(device.id, m_appCode, c.lastNumbers); + // if (dbCard.id != -1 && dbCard.accountNumber.isEmpty()) { needFetch = true; break; } + // } + // + // if (needFetch) { + // // Возвращаемся на главный экран — критерий именно по наличию кнопки + // // "Основной счёт ... ₽". isHomeScreen() для Озона нестрогий + // // (Перевести/Пополнить присутствуют и на других экранах). + // Node mainAcc; + // for (int attempt = 0; attempt < 6; ++attempt) { + // xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + // mainAcc = xmlScreenParser.findMainAccountButton(); + // if (mainAcc.x() > 0) break; + // AdbUtils::goBack(m_deviceId); + // QThread::msleep(1500); + // } + // + // if (mainAcc.x() > 0) { + // AdbUtils::makeTap(m_deviceId, mainAcc.x(), mainAcc.y()); + // QThread::msleep(2000); + // + // // Скроллим WebView, пока "Посмотреть реквизиты" не окажется в верхней половине, + // // чтобы тап не ушёл в системный нав-бар. + // Node showReq; + // const int targetMaxY = device.screenHeight / 2; + // for (int attempt = 0; attempt < 6; ++attempt) { + // xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + // Node candidate = xmlScreenParser.findShowRequisitesButton(); + // if (candidate.x() > 0 && candidate.y() <= targetMaxY) { + // showReq = candidate; + // break; + // } + // AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2, + // device.screenHeight * 2 / 3, + // device.screenWidth / 2, + // device.screenHeight / 3); + // QThread::msleep(800); + // } + // + // if (showReq.x() > 0) { + // AdbUtils::makeTap(m_deviceId, showReq.x(), showReq.y()); + // QThread::msleep(2000); + // + // bool reqScreen = false; + // for (int attempt = 0; attempt < 6; ++attempt) { + // xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + // if (xmlScreenParser.isAccountRequisitesScreen()) { + // reqScreen = true; + // break; + // } + // QThread::msleep(1000); + // } + // + // if (reqScreen) { + // for (int attempt = 0; attempt < 5; ++attempt) { + // xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + // accountNumber = xmlScreenParser.parseAccountNumberFromRequisites(); + // if (!accountNumber.isEmpty()) break; + // QThread::msleep(800); + // } + // qDebug() << "[GetCardInfo] accountNumber:" << accountNumber; + // } else { + // qWarning() << "[GetCardInfo] Account requisites screen not detected"; + // } + // } else { + // qWarning() << "[GetCardInfo] 'Посмотреть реквизиты' not reached"; + // } + // } else { + // qWarning() << "[GetCardInfo] 'Основной счёт' button not found on home"; + // } + // + // // Возврат на главный — критерий по кнопке "Основной счёт ... ₽" + // for (int attempt = 0; attempt < 5; ++attempt) { + // xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + // if (xmlScreenParser.findMainAccountButton().x() > 0) break; + // AdbUtils::goBack(m_deviceId); + // QThread::msleep(1500); + // } + // + // // Записываем номер счёта во все материалы device/app + // if (!accountNumber.isEmpty()) { + // for (const MaterialInfo &c : cards) { + // MaterialInfo upd = MaterialDAO::findAppAccount(device.id, m_appCode, c.lastNumbers); + // if (upd.id == -1) continue; + // upd.accountNumber = accountNumber; + // upd.updateTime = QDateTime::currentDateTimeUtc(); + // if (!MaterialDAO::upsertAccount(upd)) { + // qWarning() << "[GetCardInfo] Failed to save accountNumber for" << upd.lastNumbers; + // } + // } + // } + // } + // } + + // 3. Отправляем материалы (карты) — после OCR, когда номера карт уже в БД + const BankProfileInfo updatedApp = BankProfileDAO::getApplication(device.id, m_appCode); + if (!updatedApp.bankProfileId.isEmpty()) { + for (const MaterialInfo &card : cards) { + const MaterialInfo dbCard = MaterialDAO::findAppAccount(device.id, m_appCode, card.lastNumbers); + if (dbCard.cardNumber.isEmpty()) continue; + if (!dbCard.materialId.isEmpty()) { + qDebug() << "[GetCardInfo] material already exists for card" << dbCard.lastNumbers << ", skipping"; + continue; + } + + const QMap currencyCodes = { + {"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972} + }; + QJsonObject materialBody; + materialBody["bank_profile_id"] = updatedApp.bankProfileId; + materialBody["card_number"] = dbCard.cardNumber; + materialBody["account_number"] = dbCard.accountNumber; + materialBody["name"] = updatedApp.fullName; + materialBody["state"] = materialStatusToString(dbCard.status) == "active" ? "enabled" : "disabled"; + materialBody["currency"] = currencyCodes.value(dbCard.currency.toUpper(), 643); + ns.setPayload(QJsonDocument(materialBody).toJson()); + ns.addExtra("account_id", dbCard.id); + ns.postMaterial(); + qDebug() << "[GetCardInfo] postMaterial done for card" << dbCard.lastNumbers; + } + } else { + qWarning() << "[GetCardInfo] bankProfileId empty, skipping materials"; + } + + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); +} + +} // namespace Ozon diff --git a/android/ozon/GetCardInfoScript.h b/android/ozon/GetCardInfoScript.h new file mode 100644 index 0000000..b671535 --- /dev/null +++ b/android/ozon/GetCardInfoScript.h @@ -0,0 +1,32 @@ +#pragma once + +#include "CommonScript.h" + +namespace Ozon { + +class GetCardInfoScript final : public CommonScript { + Q_OBJECT + +public: + // lastNumbers — последние 4 цифры карты (например "9865"). + // Если пусто — берём первую найденную карту на домашнем экране. + explicit GetCardInfoScript( + QString deviceId, + QString appCode, + QString lastNumbers, + QObject *parent = nullptr + ); + + ~GetCardInfoScript() override; + +protected: + void doStart() override; + +private: + QString m_deviceId; + const QString m_appCode; + const QString m_lastNumbers; + QString m_error; +}; + +} // namespace Ozon diff --git a/android/ozon/GetLastTransactionsScript.cpp b/android/ozon/GetLastTransactionsScript.cpp new file mode 100644 index 0000000..6ac50c6 --- /dev/null +++ b/android/ozon/GetLastTransactionsScript.cpp @@ -0,0 +1,963 @@ +#include "GetLastTransactionsScript.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "adb/AdbUtils.h" +#include "dump/TaskDumpRecorder.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "dao/TransactionDAO.h" +#include "time/DateUtils.h" +#include "AppLogger.h" + +#include +static QString recognizeTextFromImage(const QByteArray &pngData) { + auto *api = new tesseract::TessBaseAPI(); + if (api->Init(QCoreApplication::applicationDirPath().toUtf8().constData(), "rus+eng")) { + qWarning() << "[OCR] Failed to init Tesseract"; + delete api; + return {}; + } + + Pix *pix = pixReadMem(reinterpret_cast(pngData.constData()), + static_cast(pngData.size())); + if (!pix) { + qWarning() << "[OCR] Failed to read image"; + api->End(); + delete api; + return {}; + } + + Pix *gray = pixConvertRGBToGray(pix, 0.0f, 0.0f, 0.0f); + pixDestroy(&pix); + if (!gray) { + qWarning() << "[OCR] Failed to convert to grayscale"; + api->End(); + delete api; + return {}; + } + + api->SetImage(gray); + api->SetSourceResolution(300); + char *text = api->GetUTF8Text(); + QString result = QString::fromUtf8(text).trimmed(); + delete[] text; + pixDestroy(&gray); + api->End(); + delete api; + + return result; +} + +static QString extractTransactionId(const QString &ocrText) { + // ID может быть на одной строке: "ID операции B606513042211200000120011700501" + // или с дефисами: "ID операции po-3140eb01-002c-5000-8000-033396b637e1" + // или разбит на 2 строки: "ID ... po-3140eb01-002c-5000-8000-033396b\n ... 637e1" + QRegularExpression reId(R"(ID\s+операции\s*=?\s*([A-Za-z0-9\-]+(?:[^\S\n]+[A-Za-z0-9\-]+)?))"); + if (auto m = reId.match(ocrText); m.hasMatch()) { + QString id = m.captured(1); + id.remove(' '); + id.remove(QChar(0x00A0)); + return id; + } + + QRegularExpression reRef(R"(№\s*(Ф-[\d-]+))"); + if (auto m = reRef.match(ocrText); m.hasMatch()) return m.captured(1); + + return {}; +} + +namespace Ozon { + +GetLastTransactionsScript::GetLastTransactionsScript( + QString deviceId, + QString appCode, + int maxCount, + double searchAmount, + QDateTime searchTime, + QString materialId, + QObject *parent +) : CommonScript(parent), + m_deviceId(std::move(deviceId)), + m_appCode(std::move(appCode)), + m_maxCount(maxCount), + m_searchAmount(searchAmount), + m_searchTime(std::move(searchTime)), + m_materialId(std::move(materialId)) { +} + +GetLastTransactionsScript::~GetLastTransactionsScript() = default; + +void GetLastTransactionsScript::doStart() { + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); + if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); + if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + const TaskDumpMeta dumpMeta{m_materialId, m_searchAmount, {}, {}}; + TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error); + + if (!navigateToTransactionList(device, app)) { + emit finishedWithResult(m_error); + return; + } + + // Находим accountId + int accountId = -1; + const QList accounts = MaterialDAO::getAccounts(device.id, m_appCode); + if (!accounts.isEmpty()) { + accountId = accounts.first().id; + } else { + qWarning() << "[GetLastTransactions] No account found for" << device.id << m_appCode; + } + + // Выбор режима: поиск конкретной транзакции или сканирование последних + if (qAbs(m_searchAmount) > 0.01) { + searchTransaction(device, accountId); + } else { + scanRecentTransactions(device, accountId); + } + + emit finishedWithResult(m_error); +} + +bool GetLastTransactionsScript::navigateToTransactionList(const DeviceInfo &device, const BankProfileInfo &app) { + // Хелпер: если на экране ошибка загрузки ("Обновить"), тапаем один раз + auto tryRefreshIfError = [&]() { + // Кнопка "Обновить" — Button с content-desc="Обновить" (text пустой) + Node refreshBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Обновить")); + if (refreshBtn.x() > 0 && refreshBtn.y() > 0) { + qDebug() << "[GetLastTransactions] Page load error, tapping 'Обновить' at" + << refreshBtn.x() << refreshBtn.y(); + AdbUtils::makeTap(m_deviceId, refreshBtn.x(), refreshBtn.y()); + QThread::msleep(5000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + }; + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + tryRefreshIfError(); + + // Закрываем рекламный bottom sheet + if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) { + AdbUtils::makeTap(m_deviceId, adNode.x(), adNode.y()); + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + if (Node stillAd = xmlScreenParser.tryToFindAdBanner(); !stillAd.isEmpty()) { + AdbUtils::goBack(m_deviceId); + QThread::msleep(1000); + } + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Если уже на экране операций — скроллим наверх и готово + if (xmlScreenParser.isTransactionListScreen()) { + qDebug() << "[GetLastTransactions] Already on transaction list screen, scrolling to top"; + // Несколько быстрых свайпов вверх чтобы вернуться к началу списка + for (int i = 0; i < 5; ++i) { + AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2, + device.screenHeight / 3, + device.screenWidth / 2, + device.screenHeight * 2 / 3, 200); + QThread::msleep(500); + } + QThread::msleep(1000); + return true; + } + + // Если не на домашнем экране — перезапускаем + if (!xmlScreenParser.isHomeScreen()) { + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, + device.screenWidth, device.screenHeight)) { + m_error = "Cannot reach home screen: " + device.name + " (" + m_deviceId + ")"; + qWarning() << m_error; + m_resultScreenshot = AdbUtils::captureScreenshotBytes(m_deviceId); + if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot); + AppLogger::log("ozon/GetLastTransactions", m_deviceId, m_error, {}, "FETCH_TRANSACTIONS"); + AdbUtils::tryToKillApplication(m_deviceId, app.package); + return false; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + tryRefreshIfError(); + } + + // Закрываем баннеры на главной + tryToCloseAllBannersOnHomePage(m_deviceId, device.screenWidth, device.screenHeight); + QThread::msleep(500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Скроллим до "Все" и тапаем (быстрый fling чтобы пропустить баннеры) + const int navBarTop = device.screenHeight - (device.screenHeight / 10); + Node allBtn; + for (int i = 0; i < 10; ++i) { + if (QThread::currentThread()->isInterruptionRequested()) return false; + const int x = device.screenWidth / 2; + AdbUtils::makeSwipe(m_deviceId, x, device.screenHeight - 200, x, 200, 100); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + + allBtn = xmlScreenParser.findButtonNode("Все", false); + if (!allBtn.isEmpty() && allBtn.y() > 0 && allBtn.y() < navBarTop) break; + allBtn = {}; + } + + if (allBtn.isEmpty()) { + m_error = "Button 'Все' not found: " + m_deviceId; + qWarning() << m_error; + return false; + } + + AdbUtils::makeTap(m_deviceId, allBtn.x(), allBtn.y()); + QThread::msleep(2000); + + // Ждём экран "Операции" + for (int attempt = 0; attempt < 10; ++attempt) { + if (QThread::currentThread()->isInterruptionRequested()) return false; + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + if (xmlScreenParser.isTransactionListScreen()) return true; + QThread::msleep(1500); + } + + m_error = "Transaction list screen not loaded: " + m_deviceId; + qWarning() << m_error; + return false; +} + +void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int accountId) { + // Скриншот списка транзакций — отправим при ошибке, удалим при успехе + const QByteArray listScreenshot = AdbUtils::captureScreenshotBytes(m_deviceId); + + // Время в Ozon показывается в таймзоне устройства, а не в MSK + const QString tzName = AdbUtils::getDeviceTimezone(m_deviceId); + QTimeZone deviceTz(tzName.toUtf8()); + if (!deviceTz.isValid()) deviceTz = QTimeZone("Europe/Moscow"); + const int maxScrolls = 30; + // Реальный y верхней границы нижнего нав-холдера. Если в XML есть + // ru.ozon.fintech.finance:id/main_activity_bottom_navigation_holder, + // используем его y1; иначе грубая оценка ~10% снизу. + auto detectNavBarTop = [&]() -> int { + for (const Node &n : xmlScreenParser.getNodes()) { + if (n.resourceId == "ru.ozon.fintech.finance:id/main_activity_bottom_navigation_holder" + && n.height() > 0) { + return n.y1(); + } + } + return device.screenHeight - (device.screenHeight / 10); + }; + int navBarTop = detectNavBarTop(); + + // Целевая дата в таймзоне устройства для поиска заголовка дня в списке + const QDateTime searchLocal = m_searchTime.toTimeZone(deviceTz); + const QDate searchDate = searchLocal.date(); + + qDebug() << "[FetchTransactions] Searching: device=" << m_deviceId + << "amount=" << m_searchAmount + << "time=" << m_searchTime.toString(Qt::ISODate) + << "tz=" << tzName + << "date(local)=" << searchDate.toString("dd.MM.yyyy") + << "screen=" << device.screenWidth << "x" << device.screenHeight; + + // ── Шаг 1: скроллим до нужной даты ────────────────────────────── + static const QMap monthMap = { + {"января", 1}, {"февраля", 2}, {"марта", 3}, {"апреля", 4}, + {"мая", 5}, {"июня", 6}, {"июля", 7}, {"августа", 8}, + {"сентября", 9}, {"октября", 10}, {"ноября", 11}, {"декабря", 12} + }; + static const QRegularExpression dateRx(R"((\d{1,2})\s+(\S+),\s+\S+)"); + + auto parseDateHeader = [&](const QString &t) -> QDate { + const QDate today = QDateTime::currentDateTimeUtc().toTimeZone(deviceTz).date(); + if (t.compare("Сегодня", Qt::CaseInsensitive) == 0) return today; + if (t.compare("Вчера", Qt::CaseInsensitive) == 0) return today.addDays(-1); + const auto match = dateRx.match(t); + if (!match.hasMatch()) return {}; + const int day = match.captured(1).toInt(); + const int month = monthMap.value(match.captured(2), 0); + if (month == 0) return {}; + return QDate(searchDate.year(), month, day); + }; + + // Ozon WebView сворачивает заголовки дат вне вьюпорта в один y (0 или общий + // верхний y). Несколько разных дат на одной y-позиции — pile-up скрытых + // заголовков, их надо считать невидимыми. + auto computeDateYCount = [&]() -> QMap { + QMap cnt; + for (const Node &node : xmlScreenParser.getNodes()) { + if (node.className != "android.widget.TextView") continue; + if (node.y() <= 0) continue; + if (parseDateHeader(node.text.trimmed()).isValid()) cnt[node.y()]++; + } + return cnt; + }; + + auto hasDateOnScreen = [&]() -> bool { + const QMap yCount = computeDateYCount(); + for (const Node &node : xmlScreenParser.getNodes()) { + if (node.className != "android.widget.TextView") continue; + if (node.y() <= 0) continue; + if (yCount.value(node.y(), 0) != 1) continue; + if (parseDateHeader(node.text.trimmed()) == searchDate) return true; + } + return false; + }; + + auto hasOlderDateOnScreen = [&]() -> bool { + const QMap yCount = computeDateYCount(); + for (const Node &node : xmlScreenParser.getNodes()) { + if (node.className != "android.widget.TextView") continue; + if (node.y() <= 0) continue; + if (yCount.value(node.y(), 0) != 1) continue; + const QDate d = parseDateHeader(node.text.trimmed()); + if (d.isValid() && d < searchDate) return true; + } + return false; + }; + + // Ждём пока WebView раскроет accessibility tree (иначе dump пустой) + for (int wait = 0; wait < 5; ++wait) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + if (xmlScreenParser.findTransactionButtons(1).size() > 0) break; + if (hasDateOnScreen()) break; + qDebug() << "[FetchTransactions] Waiting for WebView content..." << (wait + 1); + QThread::msleep(2000); + } + + // Скроллим к нужной дате, запоминаем количество скроллов + int scrollsToDate = 0; + bool dateFound = false; + for (int scroll = 0; scroll < maxScrolls; ++scroll) { + if (QThread::currentThread()->isInterruptionRequested()) { + m_error = "Interrupted"; + return; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + if (hasDateOnScreen()) { + dateFound = true; + scrollsToDate = scroll; + qDebug() << "[FetchTransactions] Found target date on screen after" << scroll << "scrolls"; + break; + } + if (hasOlderDateOnScreen()) { + qDebug() << "[FetchTransactions] Passed target date, not found in list"; + break; + } + AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2, + device.screenHeight * 2 / 3, device.screenWidth / 2, + device.screenHeight / 3, 600); + QThread::msleep(1500); + } + + if (!dateFound) { + m_error = "Date not found in transaction list: " + searchDate.toString("dd.MM.yyyy"); + qWarning() << "[FetchTransactions]" << m_error; + m_resultScreenshot = listScreenshot.isEmpty() + ? AdbUtils::captureScreenshotBytes(m_deviceId) : listScreenshot; + if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot); + return; + } + + // Хелпер: находит Y-диапазон [dateY, nextDateY) для нужной даты на экране. + // Кнопки транзакций между этими Y принадлежат нужному дню. + // Возвращает специальные коды через dateY: + // dateY >= 0 : header виден на этом Y (нормальный случай) + // dateY == 0 : header выше viewport, есть более старая видимая дата + // dateY == -1 : header не виден / лежит в нижней sticky-пачке + // (скролл вниз, чтобы target выехал в видимую часть) + // dateY == -2 : header в верхней sticky-пачке (несколько дат свёрнуто + // над viewport) — скролл вверх, чтобы пачка распалась + auto findDateYRange = [&]() -> QPair { + const QMap yCount = computeDateYCount(); + auto isAnchored = [&](int y) { return y > 0 && yCount.value(y, 0) == 1; }; + auto isPiled = [&](int y) { return y > 0 && yCount.value(y, 0) > 1; }; + const int viewportMid = device.screenHeight / 2; + + int dateY = -1; + int nextDateY = device.screenHeight; + int targetPileY = -1; + bool foundOlderDate = false; + for (const Node &node : xmlScreenParser.getNodes()) { + if (node.className != "android.widget.TextView") continue; + const QDate d = parseDateHeader(node.text.trimmed()); + if (!d.isValid()) continue; + if (d == searchDate) { + if (isAnchored(node.y())) dateY = node.y(); + else if (isPiled(node.y())) targetPileY = node.y(); + } else if (d < searchDate) { + foundOlderDate = true; + if (isAnchored(node.y()) + && (dateY < 0 || node.y() > dateY) + && node.y() < nextDateY) { + nextDateY = node.y(); + } + } + } + // Target в sticky-пачке: пачка над viewport (малый Y) → секция уже + // прокручена выше, надо вернуться (dateY = -2). Пачка снизу viewport + // (большой Y) → target ещё не доехал до экрана, скролл вниз (dateY = -1). + if (dateY < 0 && targetPileY >= 0) { + dateY = (targetPileY < viewportMid) ? -2 : -1; + } + // Target-header не виден, но видна более старая дата → target ушёл вверх: + // всё выше nextDateY принадлежит нашему дню. + if (dateY < 0 && nextDateY < device.screenHeight && targetPileY < 0) { + dateY = 0; + } + qDebug() << "[FetchTransactions] findDateYRange: dateY=" << dateY + << "nextDateY=" << nextDateY << "foundOlderDate=" << foundOlderDate + << "targetPileY=" << targetPileY; + return {dateY, nextDateY}; + }; + + // ── Шаг 2: тапаем только по кнопкам под заголовком нужной даты ─── + QSet checkedDetails; + int noProgressCount = 0; // Счётчик итераций без прогресса (новых unchecked кнопок) + + for (int attempt = 0; attempt < 15; ++attempt) { + if (QThread::currentThread()->isInterruptionRequested()) { + m_error = "Interrupted"; + return; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + + // Логируем все заголовки дат на экране + { + QStringList datesOnScreen; + for (const Node &node : xmlScreenParser.getNodes()) { + if (node.className != "android.widget.TextView") continue; + const QDate d = parseDateHeader(node.text.trimmed()); + if (d.isValid()) { + datesOnScreen << QString("%1@y=%2").arg(d.toString("dd.MM")).arg(node.y()); + } + } + qDebug() << "[FetchTransactions] attempt=" << attempt << "dates on screen:" << datesOnScreen; + } + + // Находим Y-диапазон заголовка нужной даты + const auto [dateHeaderY, nextDateHeaderY] = findDateYRange(); + if (dateHeaderY == -2) { + // Target застрял в sticky-пачке (Ozon стекает заголовки выше viewport + // в одну Y-позицию). Ниже пачки есть anchored более старая дата → + // секцию target уже проскроллили мимо. Скроллим обратно вверх, + // чтобы target распался из пачки и занял реальный Y. + qDebug() << "[FetchTransactions] Target piled in sticky band — scrolling up to detach"; + AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2, + device.screenHeight / 3, + device.screenWidth / 2, + device.screenHeight * 2 / 3, 600); + QThread::msleep(1500); + continue; + } + if (dateHeaderY < 0) { + // Target-header не виден и ни одной более старой даты не видно → + // target скорее всего ниже вьюпорта. Скроллим вниз (swipe вверх). + qDebug() << "[FetchTransactions] Target not visible, no older date — scrolling down"; + AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2, + device.screenHeight * 2 / 3, + device.screenWidth / 2, + device.screenHeight / 3, 500); + QThread::msleep(1500); + continue; + } + if (dateHeaderY == 0 && nextDateHeaderY < device.screenHeight) { + // Target-header ушёл вверх, видна только более старая дата. + // Кнопки выше nextDateHeaderY могут принадлежать промежуточным + // (пропущенным) датам — матчить их опасно. Скроллим обратно вверх. + qDebug() << "[FetchTransactions] Target scrolled above — scrolling up to recover"; + AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2, + device.screenHeight / 3, + device.screenWidth / 2, + device.screenHeight * 2 / 3, 600); + QThread::msleep(1500); + continue; + } + + QList txButtons = xmlScreenParser.findTransactionButtons(20); + qDebug() << "[FetchTransactions] Found" << txButtons.size() << "buttons, dateY range:" + << dateHeaderY << "-" << nextDateHeaderY; + + bool tapped = false; + bool hasNewButton = false; // Есть ли новая (ещё не проверенная) кнопка с нужной суммой + int buttonsInRange = 0; // Кнопки внутри Y-диапазона нужной даты + for (int bi = 0; bi < txButtons.size(); ++bi) { + if (QThread::currentThread()->isInterruptionRequested()) { + m_error = "Interrupted"; + return; + } + + // Пропускаем кнопки вне Y-диапазона нужной даты. + // Используем y1 (верх кнопки), а не центр — длинные кнопки могут + // заходить под нав-бар, но всё равно tappable (тапнем по верхней части). + const int btnY1 = txButtons[bi].y1(); + const int btnY = txButtons[bi].y(); + if (btnY1 <= dateHeaderY || btnY1 >= nextDateHeaderY) continue; + if (btnY1 <= 0 || btnY1 >= navBarTop) continue; + ++buttonsInRange; + + const TransactionInfo parsed = xmlScreenParser.parseTransactionButtonText(txButtons[bi].text); + qDebug() << "[FetchTransactions] Button" << bi << "y=" << btnY + << "parsed amount=" << parsed.amount << "search=" << m_searchAmount + << "text=" << txButtons[bi].text.left(60); + if (qAbs(parsed.amount - m_searchAmount) > 0.01) continue; + + const Node &visibleBtn = txButtons[bi]; + // Зажимаем тап-Y на видимую часть кнопки: если кнопка заходит под + // нав-бар, центр окажется на нав-баре и тап улетит мимо. + const int safeMaxY = navBarTop - 20; + int tapY = visibleBtn.y(); + if (tapY > safeMaxY) { + tapY = qMax(visibleBtn.y1() + 30, safeMaxY); + } + qDebug() << "[FetchTransactions] Amount match:" << parsed.amount + << "at" << visibleBtn.x() << tapY + << "(btn y1=" << visibleBtn.y1() << "y2=" << visibleBtn.y2() + << "navBarTop=" << navBarTop + << "dateY:" << dateHeaderY << "-" << nextDateHeaderY << ")"; + + // Тапаем и ждём деталей + AdbUtils::makeTap(m_deviceId, visibleBtn.x(), tapY); + QThread::msleep(2000); + + TransactionInfo detail; + for (int da = 0; da < 10; ++da) { + if (QThread::currentThread()->isInterruptionRequested()) { + m_error = "Interrupted"; + return; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + detail = xmlScreenParser.parseTransactionDetail(); + if (!detail.bankTime.isEmpty()) break; + if (xmlScreenParser.isTransactionListScreen()) { + AdbUtils::makeTap(m_deviceId, visibleBtn.x(), visibleBtn.y()); + } + QThread::msleep(1500); + } + + if (detail.bankTime.isEmpty()) { + qWarning() << "[FetchTransactions] Could not load detail, going back"; + AdbUtils::goBack(m_deviceId); + QThread::msleep(1000); + continue; + } + + // Парсим дату из деталей + QString dtStr = detail.bankTime; + for (int ci = 0; ci < dtStr.size(); ++ci) { + const QChar ch = dtStr.at(ci); + if (ch != ' ' && ch.category() == QChar::Separator_Space) dtStr[ci] = ' '; + } + QDateTime txTime = QDateTime::fromString(dtStr, "dd.MM.yyyy, HH:mm"); + if (!txTime.isValid()) txTime = QDateTime::fromString(dtStr, "dd.MM.yyyy,HH:mm"); + if (txTime.isValid()) txTime.setTimeZone(deviceTz); + + // Пропускаем если уже проверяли эту транзакцию + if (checkedDetails.contains(dtStr)) { + qDebug() << "[FetchTransactions] Already checked:" << dtStr << "- skipping"; + AdbUtils::goBack(m_deviceId); + QThread::msleep(500); + continue; + } + checkedDetails.insert(dtStr); + tapped = true; + hasNewButton = true; + + // Проверяем что транзакция принадлежит нужной дате (защита от широкого Y-диапазона) + if (txTime.isValid()) { + const QDate txLocalDate = txTime.toTimeZone(deviceTz).date(); + if (txLocalDate != searchDate) { + qDebug() << "[FetchTransactions] Wrong date in detail:" << dtStr + << "expected date" << searchDate.toString("dd.MM.yyyy") + << "- button belongs to different date, skipping"; + AdbUtils::goBack(m_deviceId); + QThread::msleep(1000); + continue; + } + } + + // Проверяем время (±10 минут) + if (txTime.isValid() && m_searchTime.isValid()) { + const qint64 diffSecs = qAbs(txTime.toUTC().secsTo(m_searchTime.toUTC())); + if (diffSecs > 600) { + qDebug() << "[FetchTransactions] Time mismatch:" << dtStr + << "expected" << searchLocal.toString("dd.MM.yyyy, HH:mm") + << "diff=" << diffSecs << "sec"; + AdbUtils::goBack(m_deviceId); + QThread::msleep(1000); + continue; + } + } + + // Нашли! + qDebug() << "[FetchTransactions] FOUND transaction: device=" << m_deviceId + << "amount=" << parsed.amount << "time=" << dtStr + << "name=" << detail.name; + + // Скриншот экрана деталей транзакции — отдаём в EventHandler. + // Раньше дополнительно тапали "Документы" + OCR ради ID операции, + // но это медленно и нестабильно. Теперь полагаемся на дату+время+ + // сумму как идентификатор. + m_resultScreenshot = AdbUtils::captureScreenshotBytes(m_deviceId); + + QString externalId = detail.bankTrExternalId; + + // Сохраняем + if (accountId != -1) { + TransactionInfo tx; + tx.accountId = accountId; + tx.amount = parsed.amount; + tx.bankName = m_appCode; + tx.bankTime = dtStr; + tx.bankTrExternalId = externalId.isEmpty() + ? QString("ozon_") + QString(dtStr).remove(',').trimmed().replace(' ', '_') + : externalId; + tx.status = TransactionStatus::Complete; + tx.timestamp = DateUtils::getUtcNow(); + if (txTime.isValid()) tx.completeTime = txTime; + if (!detail.name.isEmpty()) tx.name = detail.name; + if (!detail.description.isEmpty()) tx.description = detail.description; + if (!detail.phone.isEmpty()) { + QString cleanPhone; + for (const QChar &ch : detail.phone) + if (ch.isDigit()) cleanPhone.append(ch); + tx.phone = cleanPhone; + tx.type = TransactionType::Phone; + } + TransactionDAO::insertTransactionIfNotExists(tx); + } + + // Отдаём результат в EventHandler через сигнал (как Dushanbe) + { + const QString finalExternalId = externalId.isEmpty() + ? QString("ozon_") + QString(dtStr).remove(',').trimmed().replace(' ', '_') + : externalId; + + QJsonObject txJson; + txJson["bank_transaction_id"] = finalExternalId; + txJson["amount"] = static_cast(parsed.amount * 100); + txJson["currency"] = 643; // RUB + txJson["transaction_type"] = "bank"; + QString participator = detail.phone; + if (participator.isEmpty()) participator = detail.name; + txJson["bank_participator_id"] = participator; + txJson["status"] = "completed"; + QJsonObject extraObj; + extraObj["fee"] = 0; + txJson["extra"] = extraObj; + txJson["created_at"] = txTime.isValid() + ? txTime.toUTC().toString(Qt::ISODate) : QJsonValue(QJsonValue::Null); + + qDebug() << "[FetchTransactions] Emitting parsed tx:" + << QJsonDocument(txJson).toJson(QJsonDocument::Compact); + emit transactionParsed(txJson); + } + + if (!m_resultScreenshot.isEmpty()) + emit screenshotReady(m_resultScreenshot); + + AdbUtils::goBack(m_deviceId); + QThread::msleep(1000); + AdbUtils::goBack(m_deviceId); + QThread::msleep(1000); + return; // успех + } + + // Отслеживаем прогресс: "без прогресса" только если были кнопки с СОВПАДАЮЩЕЙ + // суммой (tapped), но все уже checked. Если суммы не совпали — скроллим дальше, + // нужная кнопка может быть ниже. + if (!hasNewButton && tapped) { + ++noProgressCount; + if (noProgressCount >= 5) { + qDebug() << "[FetchTransactions] No progress after" << noProgressCount + << "attempts with matching amount — giving up"; + break; + } + } else if (hasNewButton) { + noProgressCount = 0; + } + + // Скроллим если не нашли нужную транзакцию + if (!tapped || !hasNewButton) { + // Признак того, что под фолдом ещё есть не раскрытые транзакции + // (Ozon хранит off-screen-row как android.view.View с height=0). + const bool moreBelow = xmlScreenParser.hasCollapsedTransactionsBelow(device.screenHeight); + qDebug() << "[FetchTransactions] Scrolling (tapped=" << tapped + << "hasNew=" << hasNewButton << "inRange=" << buttonsInRange + << "noProgress=" << noProgressCount + << "dateHeaderY=" << dateHeaderY + << "nextDateHeaderY=" << nextDateHeaderY + << "moreBelow=" << moreBelow << ")"; + + const int cx = device.screenWidth / 2; + if (dateHeaderY == 0 && nextDateHeaderY < device.screenHeight) { + // Target-header ушёл вверх — свайпим вниз чтоб вернуть его. + AdbUtils::makeSwipe(m_deviceId, cx, + device.screenHeight * 2 / 5, cx, + device.screenHeight * 3 / 4, 600); + QThread::msleep(1500); + } else { + // Скролл вниз. Ozon WebView ведёт себя странно: ОДИН медленный + // свайп иногда игнорируется (видимо, расценивается как hold). + // Стратегия: серия из 3 коротких быстрых свайпов внутри + // безопасной зоны (выше нав-бара). Это надёжно триггерит + // нативный fling в WebView. + // safeBottom — верхняя граница нав-бара минус запас. + // Когда target-секция уже у нижнего края (dateHeaderY >= safeBottom), + // зона "внутри секции" схлопывается — свайпаем фиксированным + // диапазоном относительно safeBottom, чтобы вытащить контент + // секции из-под нав-бара. + const int safeBottom = navBarTop - 30; + int amplitude; + if (dateHeaderY > 0 && dateHeaderY < safeBottom - 100) { + // Header виден достаточно высоко — скроллим внутри его секции. + const int safeTop = qMax(dateHeaderY + 80, device.screenHeight / 4); + amplitude = qMin(safeBottom - safeTop, 700); + } else { + // Header у самого низа или ниже нав-бара — большой свайп. + amplitude = qMin(safeBottom - device.screenHeight / 4, 700); + } + amplitude = qMax(amplitude, 300); // минимум 300px чтобы было ощутимо + qDebug() << "[FetchTransactions] Triple-swipe scroll:" + << "safeBottom=" << safeBottom << "amplitude=" << amplitude + << "(dateHeaderY=" << dateHeaderY << " moreBelow=" << moreBelow << ")"; + for (int i = 0; i < 3; ++i) { + AdbUtils::makeSwipe(m_deviceId, cx, safeBottom, + cx, safeBottom - amplitude, 250); + QThread::msleep(350); + } + QThread::msleep(800); + } + } + } + + m_error = "Transaction not found: device=" + m_deviceId + + " amount=" + QString::number(m_searchAmount, 'f', 2) + + " date=" + searchDate.toString("dd.MM.yyyy"); + qWarning() << "[FetchTransactions]" << m_error; + // Отправляем скриншот списка транзакций, сделанный до начала поиска + m_resultScreenshot = listScreenshot.isEmpty() + ? AdbUtils::captureScreenshotBytes(m_deviceId) : listScreenshot; + if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot); +} + +void GetLastTransactionsScript::scanRecentTransactions(const DeviceInfo &device, int accountId) { + const QString tzName = AdbUtils::getDeviceTimezone(m_deviceId); + QTimeZone deviceTz(tzName.toUtf8()); + if (!deviceTz.isValid()) deviceTz = QTimeZone("Europe/Moscow"); + const int navBarTop = device.screenHeight - (device.screenHeight / 10); + + // Ждём появления транзакций + QList txButtons; + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + txButtons = xmlScreenParser.findTransactionButtons(8); + if (!txButtons.isEmpty()) break; + QThread::msleep(1500); + } + + qDebug() << "[GetLastTransactions] Found" << txButtons.size() << "transaction buttons"; + + QList transactions; + for (int i = 0; i < txButtons.size(); ++i) { + const Node &btn = txButtons[i]; + + // Скроллим до кнопки если за экраном + if (btn.y() <= 0 || btn.y() >= navBarTop) { + for (int s = 0; s < 5; ++s) { + const int x = device.screenWidth / 2; + const int offset = device.screenHeight / 4; + AdbUtils::makeSwipe(m_deviceId, x, device.screenHeight - offset, x, offset); + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + txButtons = xmlScreenParser.findTransactionButtons(8); + if (i < txButtons.size() && txButtons[i].y() > 0 && txButtons[i].y() < navBarTop) break; + } + if (i >= txButtons.size()) break; + } + + const Node &visibleBtn = txButtons[i]; + TransactionInfo tx = xmlScreenParser.parseTransactionButtonText(visibleBtn.text); + + AdbUtils::makeTap(m_deviceId, visibleBtn.x(), visibleBtn.y()); + QThread::msleep(2000); + + // Ждём загрузки деталей + TransactionInfo detail; + for (int attempt = 0; attempt < 10; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + detail = xmlScreenParser.parseTransactionDetail(); + if (!detail.bankTime.isEmpty()) break; + if (xmlScreenParser.isTransactionListScreen()) { + AdbUtils::makeTap(m_deviceId, visibleBtn.x(), visibleBtn.y()); + } + QThread::msleep(1500); + } + + tx.bankName = m_appCode; + tx.bankTime = detail.bankTime; + tx.bankTrExternalId = detail.bankTrExternalId; + + // Если ID не найден в XML — пробуем через "Документы" + OCR + if (tx.bankTrExternalId.isEmpty()) { + Node docBtn = xmlScreenParser.findButtonNode("Документы", false); + if (!docBtn.isEmpty()) { + AdbUtils::makeTap(m_deviceId, docBtn.x(), docBtn.y()); + QThread::msleep(2000); + bool docLoaded = false; + for (int da = 0; da < 15; ++da) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + if (!xmlScreenParser.findNodeByContentDesc("Поделиться").isEmpty()) { + docLoaded = true; + break; + } + QThread::msleep(1500); + } + if (docLoaded) { + QThread::msleep(500); + const QByteArray screenshot = AdbUtils::captureScreenshotBytes(m_deviceId); + if (!screenshot.isEmpty()) { + const QString ocrText = recognizeTextFromImage(screenshot); + if (!ocrText.isEmpty()) { + tx.bankTrExternalId = extractTransactionId(ocrText); + qDebug() << "[GetLastTransactions] OCR extracted ID:" << tx.bankTrExternalId; + } + } + } + AdbUtils::goBack(m_deviceId); + QThread::msleep(1000); + } + } + + tx.status = TransactionStatus::Complete; + tx.timestamp = DateUtils::getUtcNow(); + if (!detail.name.isEmpty()) tx.name = detail.name; + if (!detail.phone.isEmpty()) { + QString cleanPhone; + for (const QChar &ch : detail.phone) + if (ch.isDigit()) cleanPhone.append(ch); + tx.phone = cleanPhone; + tx.type = TransactionType::Phone; + } + + // Парсим время + if (!tx.bankTime.isEmpty()) { + QString dtStr = tx.bankTime; + for (int ci = 0; ci < dtStr.size(); ++ci) { + const QChar ch = dtStr.at(ci); + if (ch != ' ' && ch.category() == QChar::Separator_Space) dtStr[ci] = ' '; + } + tx.bankTime = dtStr; + QDateTime parsed = QDateTime::fromString(dtStr, "dd.MM.yyyy, HH:mm"); + if (!parsed.isValid()) parsed = QDateTime::fromString(dtStr, "dd.MM.yyyy,HH:mm"); + if (parsed.isValid()) { + parsed.setTimeZone(deviceTz); + tx.completeTime = parsed; + } + } + + // Фильтр по времени (24ч) если нет maxCount + if (m_maxCount <= 0 && tx.completeTime.isValid()) { + const qint64 ageSecs = tx.completeTime.toUTC().secsTo(QDateTime::currentDateTimeUtc()); + if (ageSecs > 24 * 3600) { + AdbUtils::goBack(m_deviceId); + QThread::msleep(1000); + break; + } + } + + // Сохраняем + if (accountId != -1) { + tx.accountId = accountId; + if (TransactionDAO::insertTransactionIfNotExists(tx)) { + // Скриншот чека + Node docBtn = xmlScreenParser.findButtonNode("Документы", false); + if (!docBtn.isEmpty()) { + AdbUtils::makeTap(m_deviceId, docBtn.x(), docBtn.y()); + QThread::msleep(2000); + + bool docLoaded = false; + for (int da = 0; da < 20; ++da) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + if (!xmlScreenParser.findNodeByContentDesc("Поделиться").isEmpty()) { + docLoaded = true; break; + } + QThread::msleep(1500); + } + + if (docLoaded) { + QThread::msleep(500); + QProcess screencapProc; + screencapProc.start("adb", QStringList() << "-s" << m_deviceId << "exec-out" << "screencap" << "-p"); + screencapProc.waitForFinished(5000); + const QByteArray screenshot = screencapProc.readAllStandardOutput(); + if (!screenshot.isEmpty()) { + TransactionInfo saved = TransactionDAO::findByBankTimeAndAmount( + accountId, tx.bankName, tx.bankTime, tx.amount); + if (saved.id != -1) { + TransactionDAO::updateReceiptImage(saved.id, screenshot); + const QString ocrText = recognizeTextFromImage(screenshot); + if (!ocrText.isEmpty()) { + const QString txId = extractTransactionId(ocrText); + if (!txId.isEmpty()) { + TransactionDAO::updateBankTrExternalId(saved.id, txId); + } + } + } + } + } + AdbUtils::goBack(m_deviceId); + QThread::msleep(1000); + } + } + } + + transactions.append(tx); + + if (m_maxCount > 0 && transactions.size() >= m_maxCount) { + AdbUtils::goBack(m_deviceId); + QThread::msleep(1000); + break; + } + + AdbUtils::goBack(m_deviceId); + QThread::msleep(1500); + + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + if (xmlScreenParser.isTransactionListScreen()) break; + QThread::msleep(1000); + } + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + txButtons = xmlScreenParser.findTransactionButtons(8); + } +} + +} // namespace Ozon diff --git a/android/ozon/GetLastTransactionsScript.h b/android/ozon/GetLastTransactionsScript.h new file mode 100644 index 0000000..c7557af --- /dev/null +++ b/android/ozon/GetLastTransactionsScript.h @@ -0,0 +1,51 @@ +#pragma once +#include "CommonScript.h" +#include +#include +#include "db/DeviceInfo.h" +#include "db/BankProfileInfo.h" + +namespace Ozon { + +class GetLastTransactionsScript final : public CommonScript { + Q_OBJECT + +signals: + void transactionParsed(const QJsonObject &tx); + +public: + explicit GetLastTransactionsScript( + QString deviceId, + QString appCode, + int maxCount = 0, + double searchAmount = 0, + QDateTime searchTime = {}, + QString materialId = {}, + QObject *parent = nullptr + ); + + ~GetLastTransactionsScript() override; + +protected: + void doStart() override; + +private: + // Навигация до экрана "Операции", возвращает true если успешно + bool navigateToTransactionList(const DeviceInfo &device, const BankProfileInfo &app); + + // Сканирование последних транзакций (для UI: 24ч или maxCount) + void scanRecentTransactions(const DeviceInfo &device, int accountId); + + // Поиск конкретной транзакции по сумме и времени (для задач FETCH_TRANSACTIONS) + void searchTransaction(const DeviceInfo &device, int accountId); + + QString m_deviceId; + const QString m_appCode; + const int m_maxCount; // 0 = фильтр по 24ч, >0 = N последних + const double m_searchAmount; // сумма в рублях (0 = не искать конкретную) + const QDateTime m_searchTime; // примерное время (UTC) + const QString m_materialId; // UUID задачи (для сохранения дампов) + QString m_error; +}; + +} // namespace Ozon diff --git a/android/ozon/GetProfileInfoScript.cpp b/android/ozon/GetProfileInfoScript.cpp new file mode 100644 index 0000000..0273976 --- /dev/null +++ b/android/ozon/GetProfileInfoScript.cpp @@ -0,0 +1,207 @@ +#include "GetProfileInfoScript.h" + +#include +#include +#include +#include +#include + +#include "adb/AdbUtils.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "dump/TaskDumpRecorder.h" +#include "net/NetworkService.h" +#include "AppLogger.h" + +namespace Ozon { + +GetProfileInfoScript::GetProfileInfoScript( + QString deviceId, + QString appCode, + QObject *parent +) : CommonScript(parent), + m_deviceId(std::move(deviceId)), m_appCode(std::move(appCode)) { +} + +GetProfileInfoScript::~GetProfileInfoScript() = default; + +void GetProfileInfoScript::doStart() { + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + const TaskDumpMeta dumpMeta{}; + TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error); + + // Читаем текущий экран + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Закрываем рекламный bottom sheet, если открыт (home_ad_1) + if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) { + qDebug() << "[GetProfileInfo] Ad banner detected, closing..."; + AdbUtils::makeTap(m_deviceId, adNode.x(), adNode.y()); + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + + // Свежий дамп перед проверкой домашнего экрана + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Если мы не на домашнем экране — перезапускаем приложение и вводим пин-код + if (!xmlScreenParser.isHomeScreen()) { + qDebug() << "[GetProfileInfo] Not on home screen, restarting app..."; + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, + device.screenWidth, device.screenHeight)) { + m_error = "Cannot reach home screen: " + device.name + " (" + m_deviceId + ") " + m_appCode; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + // После runAppAndGoToHomeScreen xml уже обновлён внутри метода, + // но делаем свежий дамп, чтобы убедиться + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + } + + // Закрываем баннеры на главной, если есть + tryToCloseAllBannersOnHomePage(m_deviceId, device.screenWidth, device.screenHeight); + QThread::msleep(500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + // Парсим баланс с домашнего экрана + const QList parsedCards = xmlScreenParser.parseCardsOnHomeScreen(); + double parsedBalance = parsedCards.isEmpty() ? 0.0 : parsedCards[0].amount; + qDebug() << "[GetProfileInfo] parseCardsOnHomeScreen returned" + << parsedCards.size() << "cards, balance:" << parsedBalance; + // Fallback: если парсинг не нашёл баланс, берём из materials + if (parsedBalance <= 0.0) { + const QList accounts = MaterialDAO::getAccounts(device.id, m_appCode); + for (const auto &acc : accounts) { + if (acc.amount > 0.0) { parsedBalance = acc.amount; break; } + } + qDebug() << "[GetProfileInfo] Fallback balance from materials:" << parsedBalance; + } + if (parsedBalance > 0.0) { + BankProfileDAO::updateAmount(app.id, parsedBalance); + } + + // Находим имя пользователя в верхней части домашнего экрана и тапаем по нему + Node userNameNode = xmlScreenParser.findUserNameOnHomeScreen(device.screenHeight); + if (userNameNode.isEmpty()) { + m_error = "Cannot find user name on home screen: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + qDebug() << "[GetProfileInfo] Tapping on user name:" << userNameNode.text + << "at" << userNameNode.x() << userNameNode.y(); + AdbUtils::makeTap(m_deviceId, userNameNode.x(), userNameNode.y()); + + // Ждём загрузки страницы профиля + QThread::msleep(2500); + + // Читаем XML профиля и проверяем что мы действительно там + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + if (xmlScreenParser.isProfileScreen()) { + break; + } + QThread::msleep(1000); + } + + if (!xmlScreenParser.isProfileScreen()) { + m_error = "Profile screen not detected after tap: " + m_deviceId; + qWarning() << m_error; + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } + + // Извлекаем полное имя и телефон + const QString fullName = xmlScreenParser.parseProfileFullName(); + QString phone = xmlScreenParser.parseProfilePhone(); + phone.remove(' '); + phone.remove(QChar(0x00A0)); + + qDebug() << "[GetProfileInfo] fullName:" << fullName << "phone:" << phone; + + if (!BankProfileDAO::updateProfileInfo(app.id, fullName, phone)) { + m_error = "Failed to save profile info to DB: " + m_deviceId; + qWarning() << m_error; + } + + // Отправляем bank_profile на сервер — используем свежепарсенные данные + const BankProfileInfo freshApp = BankProfileDAO::getApplication(device.id, m_appCode); + + const QMap currencyCodes = { + {"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972} + }; + const int currencyCode = currencyCodes.value(freshApp.currency.toUpper(), 643); + + const QStringList parts = fullName.split(' ', Qt::SkipEmptyParts); + QJsonObject profileBody; + profileBody["first_name"] = parts.value(0); + profileBody["middle_name"] = parts.size() >= 3 ? parts.value(1) : ""; + profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1); + profileBody["bank_name"] = m_appCode; + profileBody["phone_number"] = phone; + profileBody["balance"] = QString::number(qRound64(parsedBalance * 100)); + profileBody["state"] = (freshApp.status == "active") ? "enabled" : "disabled"; + profileBody["device_id"] = device.apiId; + profileBody["currency"] = currencyCode; + + NetworkService ns; + ns.setDeviceId(m_deviceId); + ns.setPayload(QJsonDocument(profileBody).toJson()); + + if (freshApp.bankProfileId.isEmpty()) { + ns.addExtra("app_id", freshApp.id); + ns.postBankProfile(); + const BankProfileInfo afterPost = BankProfileDAO::getApplication(device.id, m_appCode); + if (afterPost.bankProfileId.isEmpty()) { + const QString err = "postBankProfile failed for " + m_deviceId; + qWarning() << "[GetProfileInfo]" << err; + AppLogger::log("ozon/GetProfileInfo", m_deviceId, err, + AdbUtils::captureScreenshotBytes(m_deviceId), + "FETCH_PROFILE"); + m_error = err; + emit finishedWithResult(m_error); + return; + } + qDebug() << "[GetProfileInfo] postBankProfile done"; + } else { + ns.addExtra("bank_profile_id", freshApp.bankProfileId); + ns.patchBankProfile(); + qDebug() << "[GetProfileInfo] patchBankProfile done"; + } + + // Отправляем данные аккаунтов на сервер если нет material_id + const QList allAccounts = MaterialDAO::getAccounts(device.id, m_appCode); + QList> accountsToPost; + for (const auto &acc : allAccounts) { + if (acc.materialId.isEmpty()) { + accountsToPost.append({acc, Node()}); + } + } + if (!accountsToPost.isEmpty()) { + postAccountsData(accountsToPost); + } + + // Возвращаемся на домашний экран + AdbUtils::goBack(m_deviceId); + QThread::msleep(1000); + + emit finishedWithResult(m_error); +} + +} // namespace Ozon diff --git a/android/ozon/GetProfileInfoScript.h b/android/ozon/GetProfileInfoScript.h new file mode 100644 index 0000000..eb715a1 --- /dev/null +++ b/android/ozon/GetProfileInfoScript.h @@ -0,0 +1,27 @@ +#pragma once +#include "CommonScript.h" + +namespace Ozon { + +class GetProfileInfoScript final : public CommonScript { + Q_OBJECT + +public: + ~GetProfileInfoScript() override; + + explicit GetProfileInfoScript( + QString deviceId, + QString appCode, + QObject *parent = nullptr + ); + +protected: + void doStart() override; + +private: + QString m_deviceId; + const QString m_appCode; + QString m_error; +}; + +} // namespace Ozon diff --git a/android/ozon/LoginAndCheckAccountsScript.cpp b/android/ozon/LoginAndCheckAccountsScript.cpp new file mode 100644 index 0000000..258c7d5 --- /dev/null +++ b/android/ozon/LoginAndCheckAccountsScript.cpp @@ -0,0 +1,133 @@ +#include "LoginAndCheckAccountsScript.h" + +#include + +#include "adb/AdbUtils.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "dump/TaskDumpRecorder.h" +#include "AppLogger.h" +#include "GetCardInfoScript.h" + +namespace Ozon { + +LoginAndCheckAccountsScript::LoginAndCheckAccountsScript( + QString deviceId, + QString appCode, + QObject *parent, + bool fetchProfileOnly, + bool pinCheckOnly +) : CommonScript(parent), + m_deviceId(std::move(deviceId)), + m_appCode(std::move(appCode)), + m_fetchProfileOnly(fetchProfileOnly), + m_pinCheckOnly(pinCheckOnly) { +} + +LoginAndCheckAccountsScript::~LoginAndCheckAccountsScript() = default; + + +void LoginAndCheckAccountsScript::doStart() { + // m_deviceId = ADB serial, device.id = android_id (для БД) + DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.id.isEmpty()) device = DeviceDAO::findByAdbSerial(m_deviceId); if (!device.adbSerial.isEmpty()) m_deviceId = device.adbSerial; + const BankProfileInfo app = BankProfileDAO::getApplication(device.id, m_appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_deviceId + " " + m_appCode; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + const TaskDumpMeta dumpMeta{}; + TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + + if (!runAppAndGoToHomeScreen(m_deviceId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { + m_error = "Cant open the home screen: " + device.name + " (" + m_deviceId + ") " + m_appCode; + qWarning() << m_error; + m_resultScreenshot = AdbUtils::captureScreenshotBytes(m_deviceId); + if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot); + BankProfileDAO::updateComment(app.id, + "Не прошла проверка " + QDateTime::currentDateTime().toString("dd.MM.yyyy HH:mm:ss")); + + AdbUtils::tryToKillApplication(m_deviceId, app.package); + emit finishedWithResult(m_error); + return; + } else { + if (m_pinCheckOnly) { + emit finishedWithResult(m_error); + return; + } + + // Скриншот домашнего экрана + m_resultScreenshot = AdbUtils::captureScreenshotBytes(m_deviceId); + if (!m_resultScreenshot.isEmpty()) + emit screenshotReady(m_resultScreenshot); + + // Парсим баланс с домашнего экрана + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + const QList parsedCards = xmlScreenParser.parseCardsOnHomeScreen(); + double parsedBalance = parsedCards.isEmpty() ? 0.0 : parsedCards[0].amount; + qDebug() << "[LoginAndCheck] Parsed balance:" << parsedBalance << "cards:" << parsedCards.size(); + if (parsedBalance > 0.0) { + BankProfileDAO::updateAmount(app.id, parsedBalance); + } + + // Освежаем amount только для уже существующих материалов (cardNumber/materialId + // и прочие поля сохраняем). Новые карты здесь не создаём — запись появится + // только после успешного OCR полного номера в GetCardInfoScript. + // Если ниже goToAllProducts + parseAccountsInfo отработают — они уточнят + // per-card сумму поверх. + for (const MaterialInfo &card : parsedCards) { + const MaterialInfo existing = MaterialDAO::findAppAccount(device.id, m_appCode, card.lastNumbers); + if (existing.id == -1) continue; + MaterialInfo refresh = existing; + refresh.amount = card.amount; + refresh.updateTime = QDateTime::currentDateTimeUtc(); + if (!MaterialDAO::upsertAccount(refresh)) { + qWarning() << "[LoginAndCheck] Failed to refresh amount for card" << card.lastNumbers; + } else { + qDebug() << "[LoginAndCheck] Refreshed amount for card" + << card.lastNumbers << "to" << card.amount; + } + } + + // Создаём/обновляем bank profile на сервере + if (const QString err = createBankProfileIfNeeded(device.id, m_appCode, parsedBalance); !err.isEmpty()) { + m_error = err; + emit finishedWithResult(m_error); + return; + } + + if (goToAllProducts(m_deviceId, device.screenWidth, device.screenHeight)) { + QList > accounts = xmlScreenParser.parseAccountsInfo(); + for (auto &[account, node]: accounts) { + account.deviceId = device.id; + account.appCode = m_appCode; + if (!MaterialDAO::upsertAccount(account)) { + m_error = "Not updated: " + account.lastNumbers; + qCritical() << m_error; + } + } + + if (!m_fetchProfileOnly) { + postAccountsData(accounts); + } + } else { + qWarning() << "Cant go to all products: " << m_deviceId << " " << m_appCode; + } + + if (!m_fetchProfileOnly) { + // Запускаем сканирование баланса, данных карт и отправку на сервер (синхронно) + GetCardInfoScript cardScript(m_deviceId, m_appCode, ""); + cardScript.start(); + } + } + + emit finishedWithResult(m_error); +} + +} // namespace Ozon \ No newline at end of file diff --git a/android/ozon/LoginAndCheckAccountsScript.h b/android/ozon/LoginAndCheckAccountsScript.h new file mode 100644 index 0000000..08368cb --- /dev/null +++ b/android/ozon/LoginAndCheckAccountsScript.h @@ -0,0 +1,31 @@ +#pragma once +#include "CommonScript.h" + +namespace Ozon { + +class LoginAndCheckAccountsScript final : public CommonScript { + Q_OBJECT + +public: + ~LoginAndCheckAccountsScript() override; + + explicit LoginAndCheckAccountsScript( + QString deviceId, + QString appCode, + QObject *parent = nullptr, + bool fetchProfileOnly = false, + bool pinCheckOnly = false + ); + +protected: + void doStart() override; + +private: + QString m_deviceId; + const QString m_appCode; + const bool m_fetchProfileOnly; + const bool m_pinCheckOnly; + QString m_error; +}; + +} // namespace Ozon \ No newline at end of file diff --git a/android/ozon/PayByCardScript.cpp b/android/ozon/PayByCardScript.cpp new file mode 100644 index 0000000..a19db03 --- /dev/null +++ b/android/ozon/PayByCardScript.cpp @@ -0,0 +1,486 @@ +#include "PayByCardScript.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "adb/AdbUtils.h" +#include "dump/TaskDumpRecorder.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "dao/TransactionDAO.h" +#include "time/DateUtils.h" + +static QString recognizeTextFromImage(const QByteArray &pngData) { + auto *api = new tesseract::TessBaseAPI(); + if (api->Init(QCoreApplication::applicationDirPath().toUtf8().constData(), "rus+eng")) { + qWarning() << "[OCR] Failed to init Tesseract"; + delete api; + return {}; + } + + Pix *pix = pixReadMem(reinterpret_cast(pngData.constData()), + static_cast(pngData.size())); + if (!pix) { + qWarning() << "[OCR] Failed to read image"; + api->End(); + delete api; + return {}; + } + + api->SetImage(pix); + char *text = api->GetUTF8Text(); + QString result = QString::fromUtf8(text).trimmed(); + delete[] text; + pixDestroy(&pix); + api->End(); + delete api; + + return result; +} + +static QString extractTransactionId(const QString &ocrText) { + // "ID операции B606513042211200000120011700501" + // "ID операции po-3140eb01-002c-5000-8000-033396b637e1" + // или разбит на 2 строки + QRegularExpression reId(R"(ID\s+операции\s*=?\s*([A-Za-zА-Яа-яЁё0-9\-]+(?:[^\S\n]+[A-Za-zА-Яа-яЁё0-9\-]+)?))"); + if (auto m = reId.match(ocrText); m.hasMatch()) { + QString id = m.captured(1); + id.remove(' '); + id.remove(QChar(0x00A0)); + return id; + } + + // "№ Ф-2026-25235663" + QRegularExpression reRef(R"(№\s*(Ф-[\d-]+))"); + if (auto m = reRef.match(ocrText); m.hasMatch()) return m.captured(1); + + return {}; +} + +namespace Ozon { + +PayByCardScript::PayByCardScript( + MaterialInfo account, + QString cardNumber, + const double amount, + QObject *parent +) : CommonScript(parent), + m_account(std::move(account)), m_cardNumber(std::move(cardNumber)), + m_amount(amount) { +} + +PayByCardScript::~PayByCardScript() = default; + +void PayByCardScript::doStart() { + const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId); + const BankProfileInfo app = BankProfileDAO::getApplication(m_account.deviceId, m_account.appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_account.deviceId; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + const QString &adbId = device.adbSerial; + const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber}; + TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_CARD", adbId, dumpMeta, &m_error); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); + + // Закрываем рекламный bottom sheet + if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) { + qDebug() << "[PayByCard] Ad banner detected, closing..."; + AdbUtils::makeTap(adbId, adNode.x(), adNode.y()); + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); + } + + // Если не на домашнем экране — перезапускаем + if (!xmlScreenParser.isHomeScreen()) { + qDebug() << "[PayByCard] Not on home screen, restarting app..."; + if (!runAppAndGoToHomeScreen(adbId, app.package, app.pinCode, + device.screenWidth, device.screenHeight)) { + m_error = "Cannot reach home screen: " + device.name + " (" + device.id + ")"; + qWarning() << m_error; + AdbUtils::tryToKillApplication(adbId, app.package); + emit finishedWithResult(m_error); + return; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); + } + + // Закрываем баннеры + tryToCloseAllBannersOnHomePage(adbId, device.screenWidth, device.screenHeight); + QThread::msleep(500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); + + // Закрываем баннер Google Play "Доступно обновление" + if (Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(device.screenWidth, device.screenHeight); !gpNode.isEmpty()) { + qDebug() << "[PayByCard] Google Play update banner detected, closing..."; + AdbUtils::makeTap(adbId, gpNode.x(), gpNode.y()); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); + } + + // Закрываем баннер "Заказать карту" — тапаем сверху экрана + if (Node orderCardNode = xmlScreenParser.findTextNode("Заказать карту"); !orderCardNode.isEmpty()) { + qDebug() << "[PayByCard] 'Заказать карту' banner detected, closing..."; + AdbUtils::makeTap(adbId, device.screenWidth / 2, device.screenHeight / 20); + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); + } + + makePayment(device.adbSerial, app.package, app.pinCode, app.phone, device.screenWidth, device.screenHeight); + + emit finishedWithResult(m_error); +} + +void PayByCardScript::makePayment( + const QString &deviceId, + const QString &packageName, + const QString &pinCode, + const QString &bankProfilePhone, + const int width, + const int height +) { + auto checkApp = [&]() -> bool { + if (!isAppRunning(deviceId, packageName)) { + m_error = "Приложение было закрыто"; + qWarning() << "[PayByCard]" << m_error; + return false; + } + return true; + }; + // 1. Нажимаем "Перевести" на домашнем экране + Node transferBtn = xmlScreenParser.findButtonNode("Перевести", false); + if (transferBtn.isEmpty()) { + m_error = "Кнопка 'Перевести' не найдена на домашнем экране"; + qWarning() << "[PayByCard]" << m_error; + return; + } + + qDebug() << "[PayByCard] Tapping 'Перевести'..."; + AdbUtils::makeTap(deviceId, transferBtn.x(), transferBtn.y()); + QThread::msleep(2000); + + // 2. Ждём экран "Платежи и переводы" И прогрузки кнопки "По номеру карты" + Node cardBtn; + bool paymentsPageLoaded = false; + bool titleSeen = false; + { + FreezeDetector freezeCheck; + for (int attempt = 0; attempt < 10; ++attempt) { + if (!checkApp()) return; + const QString xml = AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter); + xmlScreenParser.parseAndSaveXml(xml); + closeGooglePlayBannerIfVisible(deviceId, width, height); + if (freezeCheck.noticeAndCheckFrozen(xml)) { + qWarning() << "[PayByCard] screen frozen waiting Платежи и переводы"; + break; + } + Node title = xmlScreenParser.findTextNode("Платежи и переводы"); + if (!title.isEmpty()) titleSeen = true; + cardBtn = xmlScreenParser.findButtonNode("По номеру карты", false); + if (!title.isEmpty() && !cardBtn.isEmpty()) { + paymentsPageLoaded = true; + break; + } + qDebug() << "[PayByCard] Waiting for 'Платежи и переводы' + 'По номеру карты', attempt" << attempt + 1 << "/ 10"; + QThread::msleep(1500); + } + } + + if (!paymentsPageLoaded) { + m_error = titleSeen + ? "Кнопка 'По номеру карты' не найдена на экране платежей" + : "Экран 'Платежи и переводы' не загрузился"; + qWarning() << "[PayByCard]" << m_error; + return; + } + + // 3. Нажимаем "По номеру карты" в секции "ДРУГИЕ СПОСОБЫ" + qDebug() << "[PayByCard] Tapping 'По номеру карты'..."; + AdbUtils::makeTap(deviceId, cardBtn.x(), cardBtn.y()); + QThread::msleep(2000); + + // 4. Ждём экран "Перевод по номеру карты" + bool cardScreenLoaded = false; + { + FreezeDetector freezeCheck; + for (int attempt = 0; attempt < 10; ++attempt) { + if (!checkApp()) return; + const QString xml = AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter); + xmlScreenParser.parseAndSaveXml(xml); + closeGooglePlayBannerIfVisible(deviceId, width, height); + if (freezeCheck.noticeAndCheckFrozen(xml)) { + qWarning() << "[PayByCard] screen frozen waiting 'Перевод по номеру карты'"; + break; + } + Node title = xmlScreenParser.findTextNode("Перевод по номеру карты"); + if (!title.isEmpty()) { + cardScreenLoaded = true; + break; + } + qDebug() << "[PayByCard] Waiting for 'Перевод по номеру карты', attempt" << attempt + 1 << "/ 10"; + QThread::msleep(1500); + } + } + + if (!cardScreenLoaded) { + m_error = "Экран 'Перевод по номеру карты' не загрузился"; + qWarning() << "[PayByCard]" << m_error; + return; + } + + qDebug() << "[PayByCard] Card payment screen loaded successfully"; + + // 5. Вводим номер карты в поле "Номер карты получателя" + Node cardInput; + for (int attempt = 0; attempt < 10; ++attempt) { + if (!checkApp()) return; + cardInput = xmlScreenParser.findEditTextByHint("Номер карты получателя"); + if (cardInput.isEmpty()) { + // На части сборок Ozon WebView hint у поля PAN пустой, а лейбла + // "Номер карты получателя" на экране нет (есть только "КУДА"). + // Поле PAN всегда внутри элемента resource-id="movable-pan-iframe". + cardInput = xmlScreenParser.findPanCardNumberInput(); + } + if (!cardInput.isEmpty()) break; + qDebug() << "[PayByCard] Waiting for 'Номер карты получателя' input, attempt" << attempt + 1 << "/ 10"; + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + } + + if (cardInput.isEmpty()) { + m_error = "Поле 'Номер карты получателя' не найдено"; + qWarning() << "[PayByCard]" << m_error; + return; + } + + AdbUtils::makeTap(deviceId, cardInput.x(), cardInput.y()); + QThread::msleep(500); + AdbUtils::inputText(deviceId, m_cardNumber); + QThread::msleep(2000); + + // Закрываем системную клавиатуру — она перекрывает поле суммы и кнопку + // "Перевести" внизу, а на TECNO ещё мешает Ozon WebView дорендерить AT. + AdbUtils::hideKeyboard(deviceId); + QThread::msleep(800); + + // 6. Ждём распознавания карты и вводим сумму + Node sumInput; + for (int attempt = 0; attempt < 10; ++attempt) { + if (!checkApp()) return; + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + sumInput = xmlScreenParser.findEditTextByHint("Сумма от"); + if (!sumInput.isEmpty()) break; + + // Fallback для устройств, где Ozon WebView не отдаёт hint (TECNO). + // На экране "Перевод по номеру карты" всегда 2 EditText в DOM-порядке: + // карта (сверху), сумма (снизу). Оба имеют resource-id="input___xxxx", + // поэтому отличать по rid нельзя — берём второй по порядку обхода XML. + QList editTexts; + for (const Node &n : xmlScreenParser.findAllNodes()) { + if (n.className != "android.widget.EditText") continue; + if (n.height() <= 0 || n.height() > height * 4 / 5) continue; + editTexts.append(n); + } + if (editTexts.size() >= 2) { + sumInput = editTexts[1]; + } + if (!sumInput.isEmpty()) break; + + qDebug() << "[PayByCard] Waiting for 'Сумма от' input, attempt" << attempt + 1 << "/ 10"; + QThread::msleep(1500); + } + + if (sumInput.isEmpty()) { + m_error = "Поле ввода суммы не найдено"; + qWarning() << "[PayByCard]" << m_error; + return; + } + + AdbUtils::makeTap(deviceId, sumInput.x(), sumInput.y()); + QThread::msleep(500); + AdbUtils::inputText(deviceId, QString::number(static_cast(m_amount))); + QThread::msleep(1000); + + qDebug() << "[PayByCard] Card number and amount entered"; + + // Закрываем системную софт-клавиатуру — иначе она перекрывает кнопку "Перевести" + // внизу экрана (bounds схлопываются до нулевой высоты). + AdbUtils::hideKeyboard(deviceId); + QThread::msleep(800); + + // 7. Ждём расчёта комиссии (текст после "Комиссия" перестаёт быть "Рассчитаем...") + double fee = 0; + bool feeCalculated = false; + for (int attempt = 0; attempt < 15; ++attempt) { + if (!checkApp()) return; + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + + bool foundKomissiya = false; + for (const Node &node : xmlScreenParser.findAllNodes()) { + if (node.className == "android.widget.TextView" && node.text.trimmed() == "Комиссия") { + foundKomissiya = true; + continue; + } + if (foundKomissiya && node.className == "android.widget.TextView" && !node.text.isEmpty()) { + const QString feeText = node.text.trimmed(); + if (feeText.startsWith("Рассчитаем")) { + foundKomissiya = false; + break; // ещё не рассчитана + } + // Парсим "45 ₽" → 45.0 + QString digits = feeText; + digits.remove(QRegularExpression("[^0-9,\\.]")); + digits.replace(',', '.'); + fee = digits.toDouble(); + feeCalculated = true; + qDebug() << "[PayByCard] Fee parsed:" << fee << "from text:" << feeText; + break; + } + } + if (feeCalculated) break; + qDebug() << "[PayByCard] Waiting for fee calculation, attempt" << attempt + 1 << "/ 15"; + QThread::msleep(1500); + } + + if (!feeCalculated) { + qWarning() << "[PayByCard] Fee not calculated after timeout, proceeding with fee=0"; + } + + // 8. Нажимаем "Перевести" (или "Перевести X ₽") + // Ретраим, пока bounds кнопки не будут ненулевыми — клавиатура могла всплыть + // заново (напр. если расчёт комиссии сфокусировал поле суммы). + Node transferBtn2; + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + transferBtn2 = xmlScreenParser.findButtonNode("Перевести", true); + if (!transferBtn2.isEmpty() && transferBtn2.height() > 0) break; + qDebug() << "[PayByCard] Waiting for tappable 'Перевести', attempt" << attempt + 1 << "/ 5" + << "(bounds collapsed — клавиатура ещё видна?)"; + AdbUtils::hideKeyboard(deviceId); + QThread::msleep(1000); + } + if (transferBtn2.isEmpty() || transferBtn2.height() == 0) { + m_error = "Кнопка 'Перевести' не найдена или перекрыта клавиатурой"; + qWarning() << "[PayByCard]" << m_error; + return; + } + + qDebug() << "[PayByCard] Tapping transfer button:" << transferBtn2.text; + AdbUtils::makeTap(deviceId, transferBtn2.x(), transferBtn2.y()); + QThread::msleep(3000); + + // 8.5. Проверяем экран ввода PIN-кода + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + if (const QList pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН"); + !pinNodes.isEmpty()) { + qDebug() << "[PayByCard] PIN code screen detected after confirm, entering PIN..."; + inputPinCode(deviceId, pinNodes); + QThread::msleep(3000); + } + + // 9. Ждём результат: "Успешно" или "Не удалось" + FreezeDetector resultFreeze; + for (int attempt = 0; attempt < 15; ++attempt) { + if (!checkApp()) return; + const QString xml = AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter); + xmlScreenParser.parseAndSaveXml(xml); + closeGooglePlayBannerIfVisible(deviceId, width, height); + if (resultFreeze.noticeAndCheckFrozen(xml)) { + qWarning() << "[PayByCard] screen frozen while waiting payment result"; + break; + } + + // Если PIN-код появился позже — вводим + if (const QList pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН"); + !pinNodes.isEmpty()) { + qDebug() << "[PayByCard] PIN code screen detected (retry), entering PIN..."; + inputPinCode(deviceId, pinNodes); + QThread::msleep(3000); + continue; + } + + if (!xmlScreenParser.findTextNode("Успешно").isEmpty()) { + qDebug() << "[PayByCard] Payment successful!"; + m_error.clear(); + + // Сохраняем транзакцию в БД + TransactionInfo tx; + tx.accountId = m_account.id; + tx.amount = -m_amount; + tx.fee = fee; + tx.phone = m_cardNumber; + tx.bankName = "ozon"; + tx.type = TransactionType::Card; + tx.status = TransactionStatus::Complete; + tx.timestamp = DateUtils::getUtcNow(); + tx.completeTime = QDateTime::currentDateTime().toTimeZone(QTimeZone("Europe/Moscow")); + tx.bankTime = tx.completeTime.toString("dd.MM.yyyy, HH:mm"); + const int txId = TransactionDAO::insertTransaction(tx); + qDebug() << "[PayByCard] Saved transaction to DB, id:" << txId << "fee:" << fee; + + // Скриншот экрана "Успешно" — отдаём как receipt-image и в EventHandler. + // Раньше дополнительно тапали "Документы" + ждали "Поделиться" + OCR + // ради bank_transaction_id, но это медленно (до 30 сек ожидания) и + // нестабильно. Теперь полагаемся на fallback-ID. + QString externalId; + if (txId != -1) { + const QByteArray screenshot = AdbUtils::captureScreenshotBytes(deviceId); + if (!screenshot.isEmpty()) { + m_resultScreenshot = screenshot; + emit screenshotReady(m_resultScreenshot); + TransactionDAO::updateReceiptImage(txId, screenshot); + qDebug() << "[PayByCard] Receipt saved to DB, txId:" << txId; + } + } + + if (externalId.isEmpty() && txId != -1) { + externalId = "ozon_" + bankProfilePhone + "_" + QString::number(QDateTime::currentSecsSinceEpoch()); + qDebug() << "[PayByCard] Generated fallback transaction ID:" << externalId; + } + if (!externalId.isEmpty() && txId != -1) { + TransactionDAO::updateBankTrExternalId(txId, externalId); + qDebug() << "[PayByCard] Transaction ID:" << externalId; + } + + AdbUtils::goBack(deviceId); + QThread::msleep(1000); + AdbUtils::goBack(deviceId); + QThread::msleep(1000); + return; + } + + if (!xmlScreenParser.findTextNode("Не удалось").isEmpty()) { + m_error = "Перевод не удался (экран 'Не удалось')"; + qWarning() << "[PayByCard]" << m_error; + AdbUtils::goBack(deviceId); + QThread::msleep(1000); + AdbUtils::goBack(deviceId); + QThread::msleep(1000); + return; + } + + qDebug() << "[PayByCard] Waiting for payment result, attempt" << attempt + 1 << "/ 15"; + QThread::msleep(2000); + } + + m_error = "Не дождались результата перевода"; + qWarning() << "[PayByCard]" << m_error; +} + +} // namespace Ozon diff --git a/android/ozon/PayByCardScript.h b/android/ozon/PayByCardScript.h new file mode 100644 index 0000000..8e630b0 --- /dev/null +++ b/android/ozon/PayByCardScript.h @@ -0,0 +1,38 @@ +#pragma once +#include "CommonScript.h" + +namespace Ozon { + +class PayByCardScript final : public CommonScript { + Q_OBJECT + +public: + ~PayByCardScript() override; + + PayByCardScript( + MaterialInfo account, + QString cardNumber, + double amount, + QObject *parent = nullptr + ); + +protected: + void doStart() override; + +private: + const MaterialInfo m_account; + const QString m_cardNumber; + const double m_amount; + QString m_error; + + void makePayment( + const QString &deviceId, + const QString &packageName, + const QString &pinCode, + const QString &bankProfilePhone, + int width, + int height + ); +}; + +} // namespace Ozon diff --git a/android/ozon/PayByPhoneScript.cpp b/android/ozon/PayByPhoneScript.cpp new file mode 100644 index 0000000..0099b14 --- /dev/null +++ b/android/ozon/PayByPhoneScript.cpp @@ -0,0 +1,657 @@ +#include "PayByPhoneScript.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include "adb/AdbUtils.h" +#include "dump/TaskDumpRecorder.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "dao/TransactionDAO.h" +#include "time/DateUtils.h" + +#include +static QString recognizeTextFromImage(const QByteArray &pngData) { + auto *api = new tesseract::TessBaseAPI(); + if (api->Init(QCoreApplication::applicationDirPath().toUtf8().constData(), "rus+eng")) { + qWarning() << "[OCR] Failed to init Tesseract"; + delete api; + return {}; + } + + Pix *pix = pixReadMem(reinterpret_cast(pngData.constData()), + static_cast(pngData.size())); + if (!pix) { + qWarning() << "[OCR] Failed to read image"; + api->End(); + delete api; + return {}; + } + + api->SetImage(pix); + char *text = api->GetUTF8Text(); + QString result = QString::fromUtf8(text).trimmed(); + delete[] text; + pixDestroy(&pix); + api->End(); + delete api; + + return result; +} + +static QString extractTransactionId(const QString &ocrText) { + // "ID операции B606513042211200000120011700501" + // "ID операции po-3140eb01-002c-5000-8000-033396b637e1" + // или разбит на 2 строки + QRegularExpression reId(R"(ID\s+операции\s*=?\s*([A-Za-zА-Яа-яЁё0-9\-]+(?:[^\S\n]+[A-Za-zА-Яа-яЁё0-9\-]+)?))"); + if (auto m = reId.match(ocrText); m.hasMatch()) { + QString id = m.captured(1); + id.remove(' '); + id.remove(QChar(0x00A0)); + return id; + } + + // "№ Ф-2026-25235663" + QRegularExpression reRef(R"(№\s*(Ф-[\d-]+))"); + if (auto m = reRef.match(ocrText); m.hasMatch()) return m.captured(1); + + return {}; +} + +namespace Ozon { + +static QString sanitizeBankName(const QString &raw) { + // "Озон банк (Ozon)" -> "Озон банк"; убираем содержимое круглых скобок + // вместе с предшествующими пробелами и обрезаем края. + QString s = raw; + s.remove(QRegularExpression(R"(\s*\([^)]*\))")); + return s.trimmed(); +} + +PayByPhoneScript::PayByPhoneScript( + MaterialInfo account, + QString phone, + QString bankName, + const double amount, + QObject *parent +) : CommonScript(parent), + m_account(std::move(account)), m_phone(std::move(phone)), + m_bankName(sanitizeBankName(bankName)), m_amount(amount) { +} + +PayByPhoneScript::~PayByPhoneScript() = default; + +void PayByPhoneScript::doStart() { + const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId); + const BankProfileInfo app = BankProfileDAO::getApplication(m_account.deviceId, m_account.appCode); + + if (device.id.isEmpty() || app.id == -1) { + m_error = "Device or app not found: " + m_account.deviceId; + qCritical() << m_error; + emit finishedWithResult(m_error); + return; + } + + const QString &adbId = device.adbSerial; + const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}}; + TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_PHONE", adbId, dumpMeta, &m_error); + + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); + + // Закрываем рекламный bottom sheet + if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) { + qDebug() << "[PayByPhone] Ad banner detected, closing..."; + AdbUtils::makeTap(adbId, adNode.x(), adNode.y()); + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); + } + + // Если не на домашнем экране — перезапускаем + if (!xmlScreenParser.isHomeScreen()) { + qDebug() << "[PayByPhone] Not on home screen, restarting app..."; + if (!runAppAndGoToHomeScreen(adbId, app.package, app.pinCode, + device.screenWidth, device.screenHeight)) { + m_error = "Cannot reach home screen: " + device.name + " (" + device.id + ")"; + qWarning() << m_error; + AdbUtils::tryToKillApplication(adbId, app.package); + emit finishedWithResult(m_error); + return; + } + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); + } + + // Закрываем баннеры + tryToCloseAllBannersOnHomePage(adbId, device.screenWidth, device.screenHeight); + QThread::msleep(500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); + + // Закрываем баннер Google Play "Доступно обновление" + if (Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(device.screenWidth, device.screenHeight); !gpNode.isEmpty()) { + qDebug() << "[PayByPhone] Google Play update banner detected, closing..."; + AdbUtils::makeTap(adbId, gpNode.x(), gpNode.y()); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); + } + + // Закрываем баннер "Заказать карту" — тапаем сверху экрана + if (Node orderCardNode = xmlScreenParser.findTextNode("Заказать карту"); !orderCardNode.isEmpty()) { + qDebug() << "[PayByPhone] 'Заказать карту' banner detected, closing..."; + AdbUtils::makeTap(adbId, device.screenWidth / 2, device.screenHeight / 20); + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); + } + + makePayment(device.adbSerial, app.package, app.pinCode, app.phone, device.screenWidth, device.screenHeight); + + emit finishedWithResult(m_error); +} + +void PayByPhoneScript::makePayment( + const QString &deviceId, + const QString &packageName, + const QString &pinCode, + const QString &bankProfilePhone, + const int width, + const int height +) { + // Макрос-проверка: приложение живо? + auto checkApp = [&]() -> bool { + if (!isAppRunning(deviceId, packageName)) { + m_error = "Приложение было закрыто"; + qWarning() << "[PayByPhone]" << m_error; + return false; + } + return true; + }; + // 1. Нажимаем "Перевести" на домашнем экране. + // Внимание: поверх правой части кнопки сидит referral-бейдж "badge icon за друга" + // (bounds типа [550,90][602,140]), который перекрывает центр кнопки. Тап в центр + // открывает промо "До 3 000 за рекомендацию". Тапаем в левую четверть, чтобы + // гарантированно попасть мимо бейджа. + Node transferBtn = xmlScreenParser.findButtonNode("Перевести", false); + if (transferBtn.isEmpty()) { + m_error = "Кнопка 'Перевести' не найдена на домашнем экране"; + qWarning() << "[PayByPhone]" << m_error; + return; + } + + // Тапаем в нижнюю часть кнопки (там, где визуально надпись "Перевести"), + // ниже бейджа "за друга" (он заканчивается на y≈140). + const int transferTapX = transferBtn.x(); + const int transferTapY = transferBtn.y1() + transferBtn.height() * 4 / 5; + qDebug() << "[PayByPhone] Tapping 'Перевести' at" << transferTapX << "," << transferTapY + << "(below 'за друга' badge)"; + AdbUtils::makeTap(deviceId, transferTapX, transferTapY); + QThread::msleep(2000); + + // 2. Ждём экран "Платежи и переводы" с полем "Телефон получателя" + bool paymentsPageLoaded = false; + for (int attempt = 0; attempt < 10; ++attempt) { + if (!checkApp()) return; + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + Node phoneField = xmlScreenParser.findFirstEditText(); + if (!phoneField.isEmpty()) { + paymentsPageLoaded = true; + break; + } + qDebug() << "[PayByPhone] Waiting for payments page, attempt" << attempt + 1 << "/ 10"; + QThread::msleep(1500); + } + + if (!paymentsPageLoaded) { + m_error = "Экран 'Платежи и переводы' не загрузился"; + qWarning() << "[PayByPhone]" << m_error; + return; + } + + // 3. Тапаем на поле "Телефон получателя" чтобы перейти на экран "Кому перевести" + Node phoneFieldTap = xmlScreenParser.findFirstEditText(); + qDebug() << "[PayByPhone] Tapping 'Телефон получателя'..."; + AdbUtils::makeTap(deviceId, phoneFieldTap.x(), phoneFieldTap.y()); + QThread::msleep(2000); + + // 4. Ждём экран "Кому перевести" + bool phoneScreenLoaded = false; + for (int attempt = 0; attempt < 10; ++attempt) { + if (!checkApp()) return; + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + + // Закрываем диалог "Разрешить доступ к контактам" + Node notNowBtn = xmlScreenParser.findButtonNode("Не сейчас", false); + if (!notNowBtn.isEmpty()) { + qDebug() << "[PayByPhone] Contacts permission dialog detected, tapping 'Не сейчас'..."; + AdbUtils::makeTap(deviceId, notNowBtn.x(), notNowBtn.y()); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + } + + Node title = xmlScreenParser.findTextNode("Кому перевести"); + if (!title.isEmpty()) { + phoneScreenLoaded = true; + break; + } + qDebug() << "[PayByPhone] Waiting for 'Кому перевести', attempt" << attempt + 1 << "/ 10"; + QThread::msleep(1500); + } + + if (!phoneScreenLoaded) { + m_error = "Экран 'Кому перевести' не загрузился"; + qWarning() << "[PayByPhone]" << m_error; + return; + } + + // 5. Тапаем на EditText и вводим номер телефона + Node phoneInput; + for (int attempt = 0; attempt < 10; ++attempt) { + if (!checkApp()) return; + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + phoneInput = xmlScreenParser.findFirstEditText(); + if (!phoneInput.isEmpty()) break; + qDebug() << "[PayByPhone] Waiting for phone input field, attempt" << attempt + 1 << "/ 10"; + QThread::msleep(1500); + } + if (phoneInput.isEmpty()) { + m_error = "Поле ввода телефона не найдено"; + qWarning() << "[PayByPhone]" << m_error; + return; + } + + AdbUtils::makeTap(deviceId, phoneInput.x(), phoneInput.y()); + QThread::msleep(500); + AdbUtils::inputText(deviceId, m_phone); + QThread::msleep(2000); + + // 6. Ждём экран "По номеру телефона", обрабатывая промежуточные экраны + bool sumScreenLoaded = false; + bool bankSelected = false; + bool otherBankClicked = false; + FreezeDetector freezeCheck; + for (int attempt = 0; attempt < 20; ++attempt) { + if (!checkApp()) return; + const QString currentDumpXml = AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter); + xmlScreenParser.parseAndSaveXml(currentDumpXml); + closeGooglePlayBannerIfVisible(deviceId, width, height); + + if (freezeCheck.noticeAndCheckFrozen(currentDumpXml)) { + qWarning() << "[PayByPhone] 4 identical dumps in a row — screen frozen, aborting"; + break; + } + + // Проверяем: уже на экране суммы? + Node sumTitle = xmlScreenParser.findTextNode("По номеру телефона"); + if (!sumTitle.isEmpty() && sumTitle.y() < height / 7) { + sumScreenLoaded = true; + break; + } + + // Ищем кнопку с номером телефона (новый перевод / последние переводы) + QString phoneDigits = m_phone; + phoneDigits.remove(QRegularExpression("[^0-9]")); + bool foundPhoneBtn = false; + for (const Node &btn : xmlScreenParser.findAllButtons()) { + QString btnDigits = btn.text; + btnDigits.remove(QRegularExpression("[^0-9]")); + if (btnDigits.contains(phoneDigits) || phoneDigits.contains(btnDigits.right(10))) { + qDebug() << "[PayByPhone] Found button with phone number:" << btn.text; + AdbUtils::makeTap(deviceId, btn.x(), btn.y()); + QThread::msleep(2000); + foundPhoneBtn = true; + break; + } + } + if (foundPhoneBtn) continue; + + // Экран выбора банка: ищем "Другой банк" (текст) или EditText с hint "Название банка" + if (!bankSelected) { + // Вариант 1: кнопка "Другой банк" (content-desc="other-bank-item", text пустой). + // На устройствах с маленьким экраном кнопка лежит в конце виртуализированного + // списка и приходит с bounds=[0,0][0,0] — Node::isEmpty это не ловит, тап + // улетает в (0,0). Скроллим список вниз, пока bounds не станут ненулевыми + // (после первого скролла кнопка обычно материализуется у нижнего края, но + // тап по ней работает — это её настоящая позиция в layout). + auto findOtherBank = [&]() -> Node { + for (const Node &n : xmlScreenParser.findAllNodes()) { + if (n.contentDesc.contains("other-bank-item")) return n; + } + return {}; + }; + + bool foundOtherBank = false; + Node node = findOtherBank(); + for (int scrollAttempt = 0; + !node.isEmpty() && (node.width() == 0 || node.height() == 0) && scrollAttempt < 6; + ++scrollAttempt) { + qDebug() << "[PayByPhone] 'Другой банк' has collapsed bounds, scrolling list down, attempt" + << scrollAttempt + 1 << "/ 6"; + AdbUtils::makeSwipe(deviceId, width / 2, height * 3 / 4, width / 2, height / 4); + QThread::msleep(800); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + node = findOtherBank(); + } + + if (!node.isEmpty() && node.width() > 0 && node.height() > 0) { + qDebug() << "[PayByPhone] Tapping 'Другой банк' (other-bank-item) at" + << node.x() << "," << node.y(); + AdbUtils::makeTap(deviceId, node.x(), node.y()); + QThread::msleep(2000); + foundOtherBank = true; + otherBankClicked = true; + } + if (foundOtherBank) continue; + + // Вариант 2: уже на экране поиска банка (EditText) — только после клика на "Другой банк" + if (otherBankClicked) { + // После перехода на экран поиска банка поле ввода прячется под верхним + // тулбаром (рендерится Svelte WebView'ом). Свайпаем сверху вниз, чтобы + // подтянуть содержимое и сделать поле физически видимым/активным. + qDebug() << "[PayByPhone] Scrolling up to reveal hidden bank search input..."; + AdbUtils::makeSwipe(deviceId, width / 2, height / 4, width / 2, height * 3 / 4); + QThread::msleep(800); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + + Node bankInput = xmlScreenParser.findFirstEditText(); + if (!bankInput.isEmpty()) { + qDebug() << "[PayByPhone] Bank search screen detected, entering bank name..."; + + // Переключаем IME ДО тапа по полю — иначе смена IME после тапа + // закроет системную клавиатуру и WebView резайзится, из-за чего + // сохранённые координаты bankInput уезжают и повторный тап мимо. + if (!AdbUtils::enableAdbIMEKeyboard(deviceId)) { + m_error = "Не удалось активировать ADB Keyboard (см. логи 'ime list -s' / default_input_method)"; + qWarning() << "[PayByPhone]" << m_error; + return; + } + QThread::msleep(800); + + // Перечитываем XML — layout с ADBKeyboard (без UI) отличается от layout + // с системной клавиатурой. Берём актуальные bounds поля. + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + Node freshBankInput = xmlScreenParser.findFirstEditText(); + if (!freshBankInput.isEmpty()) bankInput = freshBankInput; + + AdbUtils::makeTap(deviceId, bankInput.x(), bankInput.y()); + QThread::msleep(500); + AdbUtils::inputAdbIMEText(deviceId, m_bankName); + QThread::msleep(2500); + + // Верифицируем, что текст действительно попал в поле + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + Node verifyInput = xmlScreenParser.findFirstEditText(); + if (verifyInput.text.isEmpty()) { + qWarning() << "[PayByPhone] Поле банка пустое после inputAdbIMEText — ретраим"; + AdbUtils::makeTap(deviceId, bankInput.x(), bankInput.y()); + QThread::msleep(500); + AdbUtils::inputAdbIMEText(deviceId, m_bankName); + QThread::msleep(2500); + } + + AdbUtils::disableAdbIMEKeyboard(deviceId); + qDebug() << "[PayByPhone] Entered bank name via ADB IME:" << m_bankName; + QThread::msleep(2000); + + // Ищем банк в списке и кликаем по нему (кнопка с числовым content-desc) + bool bankFound = false; + for (int ba = 0; ba < 10; ++ba) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + + // Сначала проверяем, что поле поиска реально содержит введённый текст — + // иначе мы тапаем по первому попавшемуся банку из несфильтрованного списка. + Node searchField = xmlScreenParser.findFirstEditText(); + if (searchField.text.trimmed().isEmpty()) { + qDebug() << "[PayByPhone] Search field still empty, waiting for filter, attempt" << ba + 1 << "/ 10"; + QThread::msleep(1500); + continue; + } + + for (const Node &node : xmlScreenParser.findAllNodes()) { + if (node.className == "android.widget.Button" + && !node.contentDesc.isEmpty() + && node.contentDesc != "base-go-back-button" + && node.contentDesc != "native-toolbar-close-button" + && node.contentDesc != "clear-input-icon") { + const int tapX = node.x1() + node.width() / 4; + qDebug() << "[PayByPhone] Found bank in list, tapping:" << node.contentDesc << "tapX:" << tapX << "y:" << node.y(); + AdbUtils::makeTap(deviceId, tapX, node.y()); + bankFound = true; + break; + } + } + if (bankFound) break; + qDebug() << "[PayByPhone] Waiting for bank list, attempt" << ba + 1 << "/ 10"; + QThread::msleep(1500); + } + + if (!bankFound) { + m_error = "Банк '" + m_bankName + "' не найден в списке"; + qWarning() << "[PayByPhone]" << m_error; + return; + } + + bankSelected = true; + QThread::msleep(2000); + continue; + } + } // otherBankClicked + } + + qDebug() << "[PayByPhone] Waiting for 'По номеру телефона', attempt" << attempt + 1 << "/ 20"; + QThread::msleep(1500); + } + + if (!sumScreenLoaded) { + m_error = "Экран ввода суммы не загрузился"; + qWarning() << "[PayByPhone]" << m_error; + return; + } + + // 7. Вводим сумму + // На экране "По номеру телефона" обычно 2 EditText: сумма (сверху) и + // сообщение получателю (снизу). Оба имеют rid="input___xxxx" в Svelte UI. + // Берём верхний (минимальный y1) — это amount. + Node sumInput; + for (int attempt = 0; attempt < 10; ++attempt) { + if (!checkApp()) return; + QList candidateInputs; + for (const Node &n : xmlScreenParser.findAllNodes()) { + if (n.className != "android.widget.EditText") continue; + if (n.height() <= 0 || n.height() > height * 4 / 5) continue; + if (!n.resourceId.startsWith("input___")) continue; + candidateInputs.append(n); + } + if (!candidateInputs.isEmpty()) { + std::sort(candidateInputs.begin(), candidateInputs.end(), + [](const Node &a, const Node &b) { return a.y1() < b.y1(); }); + sumInput = candidateInputs.first(); // верхний = amount + break; + } + // Старая логика — если нет ни одного "input___": берём любой EditText. + sumInput = xmlScreenParser.findFirstEditText(); + if (!sumInput.isEmpty()) break; + + qDebug() << "[PayByPhone] Waiting for sum input field, attempt" << attempt + 1 << "/ 10"; + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + } + if (sumInput.isEmpty()) { + m_error = "Поле ввода суммы не найдено"; + qWarning() << "[PayByPhone]" << m_error; + return; + } + + AdbUtils::makeTap(deviceId, sumInput.x(), sumInput.y()); + QThread::msleep(500); + AdbUtils::inputText(deviceId, QString::number(static_cast(m_amount))); + QThread::msleep(1000); + + // Закрываем системную софт-клавиатуру, иначе она перекрывает "Продолжить" + // (кнопка уезжает за нижнюю границу web_view_holder и имеет нулевую высоту). + AdbUtils::hideKeyboard(deviceId); + QThread::msleep(800); + + // 6. Нажимаем "Продолжить" (ретраим, пока bounds перестанут быть нулевыми) + Node continueBtn; + for (int attempt = 0; attempt < 5; ++attempt) { + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + continueBtn = xmlScreenParser.findButtonNode("Продолжить", false); + if (!continueBtn.isEmpty() && continueBtn.height() > 0) break; + qDebug() << "[PayByPhone] Waiting for tappable 'Продолжить', attempt" << attempt + 1 << "/ 5" + << "(bounds collapsed — клавиатура ещё видна?)"; + AdbUtils::hideKeyboard(deviceId); + QThread::msleep(1000); + } + if (continueBtn.isEmpty() || continueBtn.height() == 0) { + m_error = "Кнопка 'Продолжить' не найдена или перекрыта клавиатурой"; + qWarning() << "[PayByPhone]" << m_error; + return; + } + + AdbUtils::makeTap(deviceId, continueBtn.x(), continueBtn.y()); + QThread::msleep(2000); + + // 7. Ждём экран "Подтвердите перевод" с кнопкой "Перевести ..." + bool confirmScreenLoaded = false; + for (int attempt = 0; attempt < 10; ++attempt) { + if (!checkApp()) return; + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + Node confirmTitle = xmlScreenParser.findTextNode("Подтвердите перевод"); + if (!confirmTitle.isEmpty()) { + confirmScreenLoaded = true; + break; + } + qDebug() << "[PayByPhone] Waiting for 'Подтвердите перевод', attempt" << attempt + 1 << "/ 10"; + QThread::msleep(1500); + } + + if (!confirmScreenLoaded) { + m_error = "Экран подтверждения не загрузился"; + qWarning() << "[PayByPhone]" << m_error; + return; + } + + // 8. Нажимаем "Перевести ..." (ищем по первому слову) + Node transferConfirmBtn; + for (int attempt = 0; attempt < 10; ++attempt) { + if (!checkApp()) return; + transferConfirmBtn = xmlScreenParser.findButtonNode("Перевести", true); + if (!transferConfirmBtn.isEmpty()) break; + qDebug() << "[PayByPhone] Waiting for 'Перевести ...' button, attempt" << attempt + 1 << "/ 10"; + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + } + if (transferConfirmBtn.isEmpty()) { + m_error = "Кнопка 'Перевести ...' не найдена на экране подтверждения"; + qWarning() << "[PayByPhone]" << m_error; + return; + } + + qDebug() << "[PayByPhone] Tapping confirm button:" << transferConfirmBtn.text; + AdbUtils::makeTap(deviceId, transferConfirmBtn.x(), transferConfirmBtn.y()); + QThread::msleep(3000); + + // 8.5. Проверяем экран ввода PIN-кода + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + if (const QList pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН"); + !pinNodes.isEmpty()) { + qDebug() << "[PayByPhone] PIN code screen detected after confirm, entering PIN..."; + inputPinCode(deviceId, pinNodes); + QThread::msleep(3000); + } + + // 9. Ждём результат: "Успешно" или "Не удалось" + for (int attempt = 0; attempt < 15; ++attempt) { + if (!checkApp()) return; + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + + // Если PIN-код появился позже — вводим + if (const QList pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН"); + !pinNodes.isEmpty()) { + qDebug() << "[PayByPhone] PIN code screen detected (retry), entering PIN..."; + inputPinCode(deviceId, pinNodes); + QThread::msleep(3000); + continue; + } + + if (!xmlScreenParser.findTextNode("Успешно").isEmpty()) { + qDebug() << "[PayByPhone] Payment successful!"; + m_error.clear(); + + // Сохраняем транзакцию в БД + TransactionInfo tx; + tx.accountId = m_account.id; + tx.amount = -m_amount; + tx.phone = m_phone; + tx.bankName = "ozon"; + tx.type = TransactionType::Phone; + tx.status = TransactionStatus::Complete; + tx.timestamp = DateUtils::getUtcNow(); + tx.completeTime = QDateTime::currentDateTime().toTimeZone(QTimeZone("Europe/Moscow")); + tx.bankTime = tx.completeTime.toString("dd.MM.yyyy, HH:mm"); + const int txId = TransactionDAO::insertTransaction(tx); + qDebug() << "[PayByPhone] Saved transaction to DB, id:" << txId; + + // Скриншот экрана "Успешно" — отдаём как receipt-image и в EventHandler. + // Раньше дополнительно тапали "Документы" + ждали "Поделиться" + OCR + // ради bank_transaction_id, но это медленно (до 30 сек ожидания) и + // нестабильно. Теперь полагаемся на fallback-ID из phone+timestamp. + QString externalId; + if (txId != -1) { + const QByteArray screenshot = AdbUtils::captureScreenshotBytes(deviceId); + if (!screenshot.isEmpty()) { + m_resultScreenshot = screenshot; + emit screenshotReady(m_resultScreenshot); + TransactionDAO::updateReceiptImage(txId, screenshot); + qDebug() << "[PayByPhone] Receipt saved to DB, txId:" << txId; + } + } + + if (externalId.isEmpty() && txId != -1) { + externalId = "ozon_" + bankProfilePhone + "_" + QString::number(QDateTime::currentSecsSinceEpoch()); + qDebug() << "[PayByPhone] Generated fallback transaction ID:" << externalId; + } + if (!externalId.isEmpty() && txId != -1) { + TransactionDAO::updateBankTrExternalId(txId, externalId); + qDebug() << "[PayByPhone] Transaction ID:" << externalId; + } + + AdbUtils::goBack(deviceId); + QThread::msleep(1000); + AdbUtils::goBack(deviceId); + QThread::msleep(1000); + return; + } + + if (!xmlScreenParser.findTextNode("Не удалось").isEmpty()) { + m_error = "Перевод не удался (экран 'Не удалось')"; + qWarning() << "[PayByPhone]" << m_error; + AdbUtils::goBack(deviceId); + QThread::msleep(1000); + AdbUtils::goBack(deviceId); + QThread::msleep(1000); + return; + } + + qDebug() << "[PayByPhone] Waiting for payment result, attempt" << attempt + 1 << "/ 15"; + QThread::msleep(2000); + } + + m_error = "Не дождались результата перевода"; + qWarning() << "[PayByPhone]" << m_error; +} + +} // namespace Ozon diff --git a/android/ozon/PayByPhoneScript.h b/android/ozon/PayByPhoneScript.h new file mode 100644 index 0000000..27cd548 --- /dev/null +++ b/android/ozon/PayByPhoneScript.h @@ -0,0 +1,40 @@ +#pragma once +#include "CommonScript.h" + +namespace Ozon { + +class PayByPhoneScript final : public CommonScript { + Q_OBJECT + +public: + ~PayByPhoneScript() override; + + PayByPhoneScript( + MaterialInfo account, + QString phone, + QString bankName, + double amount, + QObject *parent = nullptr + ); + +protected: + void doStart() override; + +private: + const MaterialInfo m_account; + const QString m_phone; + const QString m_bankName; + const double m_amount; + QString m_error; + + void makePayment( + const QString &deviceId, + const QString &packageName, + const QString &pinCode, + const QString &bankProfilePhone, + int width, + int height + ); +}; + +} // namespace Ozon \ No newline at end of file diff --git a/android/ozon/ScreenXmlParser.cpp b/android/ozon/ScreenXmlParser.cpp new file mode 100644 index 0000000..0d1e12b --- /dev/null +++ b/android/ozon/ScreenXmlParser.cpp @@ -0,0 +1,1719 @@ +#include "ScreenXmlParser.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "adb/AdbUtils.h" +#include "android/xml/Node.h" +#include "db/MaterialInfo.h" +#include "db/TransactionInfo.h" +#include "time/DateUtils.h" + +namespace Ozon { + +ScreenXmlParser::ScreenXmlParser(QObject *parent) : QObject(parent) { +} + +ScreenXmlParser::~ScreenXmlParser() = default; + +static const QRegularExpression boundsRegex(R"(\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\])"); +static const QRegularExpression onlyDigitsRegex(R"([^0-9])"); +static const QRegularExpression onlyDigitsAndDotRegex(R"([^0-9.])"); + +static const QRegularExpression rePhone(R"raw((\+7\(?\d{3}\)?[- ]?\d{3}[- ]?\d{2}[- ]?\d{2}))raw"); +static const QRegularExpression reFromBank(R"raw(из (.+?) на)raw"); +static const QRegularExpression reToBank(R"raw(в (.+?) через)raw"); +static const QRegularExpression reSbpId(R"(СБП(?: С2С)? ([A-Z0-9]{30,}))"); +static const QRegularExpression reIdAlt(R"(ID перевода (\d+))"); +static const QRegularExpression reDate(R"((?:Дата операции|дата) (\d{2}\.\d{2}\.\d{4}(?: \d{2}:\d{2}:\d{2})?))"); +static const QRegularExpression reSender(R"(от ([^\(]+))"); +static const QRegularExpression reNameAlt(R"(Перевод ([А-Яа-яЁё\s\*]+) в)"); + +static const QRegularExpression reCardData(R"(цифрами\s+(\d\s\d\s\d\s\d).*?Сумма:\s([\d\s]+)\sруб)"); + +Node ScreenXmlParser::tryToFindBannerOrDialog(const int width, const int height) { + if (m_nodes.isEmpty()) return {}; + // 1. Ищем push banner - он перекрывает весь экран + Node first = m_nodes[0]; + for (const Node &node: m_nodes) { + // Если это однозначно, что пуш-баннер + if (node.resourceId == "aft-popup-pushNotification-iconClose" && node.className == "android.widget.Button") { + return node; + } + // Если есть кнопка "Позже" + if (node.text.trimmed() == "Позже" && node.className == "android.widget.Button") { + return node; + } + // Если нашли пустую кнпоку + if (node.className == "android.widget.Button" && node.clickable + && node.text.isEmpty() && node.contentDesc.isEmpty()) { + // + if (node.x() > width / 2 && node.y() < width / 2) { + return node; + } + } + } + return {}; +} + +/** + * Смотрим что есть текс "Мы заботимся о вашей безопасности" и + * кнопка "Отложить" + * @return координаты кнопки для закрытия + */ +Node ScreenXmlParser::tryToFindSecurityBanner() { + Node text; + Node closeBtn; + bool hasBiometryScreen = false; + Node notNowNode; + + for (const Node &node: m_nodes) { + if (node.text.trimmed() == "Мы заботимся о вашей безопасности" && node.className == "android.app.Dialog") { + text = node; + } + if (node.text.trimmed() == "Отложить" && node.className == "android.widget.Button") { + closeBtn = node; + } + // Экран биометрии "Быстрый вход" — нажимаем "Не сейчас" + if (node.text.trimmed() == "Быстрый вход") { + hasBiometryScreen = true; + } + if (node.text.trimmed() == "Не сейчас") { + notNowNode = node; + } + } + + if (!text.isEmpty() && !closeBtn.isEmpty()) { + return closeBtn; + } + if (hasBiometryScreen && !notNowNode.isEmpty()) { + return notNowNode; + } + return {}; +} + +Node ScreenXmlParser::tryToFindSelectBottomSheet() { + Node closeBtn; + for (const Node &node: m_nodes) { + if ((node.text.trimmed() == "Отмена" || node.text.trimmed() == "Отложить") && node.className == + "android.widget.Button") { + closeBtn = node; + } + } + // Не нужно проверять все, если хотя бы одна координата есть + if (!closeBtn.isEmpty()) { + return closeBtn; + } + return {}; +} + +Node ScreenXmlParser::findTextNode(const QString &text) { + for (const Node &node: m_nodes) { + if (node.className == "android.widget.TextView" && node.text.trimmed() == text) { + return node; + } + } + return {}; +} + +Node ScreenXmlParser::findTextNode(const QString &text, const int x1, const int y1, const int x2, const int y2) { + for (const Node &node: m_nodes) { + if (node.className == "android.widget.TextView" && node.text.trimmed() == text + && node.x() >= x1 && node.x() <= x2 && node.y() >= y1 && node.y() <= y2) { + return node; + } + } + return {}; +} + +Node ScreenXmlParser::findFirstEditText() { + for (const Node &node: m_nodes) { + if (node.className == "android.widget.EditText" && node.focusable) { + return node; + } + } + return {}; +} + +Node ScreenXmlParser::findEditTextByHint(const QString &hintPrefix) { + for (const Node &node : m_nodes) { + if (node.className == "android.widget.EditText" && node.hint.contains(hintPrefix)) { + return node; + } + } + // Некоторые устройства/прошивки (напр. Infinix) в uiautomator-дампе не отдают + // атрибут hint для EditText внутри WebView. В этом случае ищем TextView-label + // с нужным текстом и ближайший к нему EditText с горизонтальным перекрытием. + Node label; + for (const Node &node : m_nodes) { + if (node.className == "android.widget.TextView" && node.text.contains(hintPrefix)) { + label = node; + break; + } + } + if (label.isEmpty()) return {}; + + Node best; + int bestDy = INT_MAX; + for (const Node &node : m_nodes) { + if (node.className != "android.widget.EditText") continue; + const bool hOverlap = node.x1() < label.x2() && node.x2() > label.x1(); + if (!hOverlap) continue; + const int dy = std::abs(node.y() - label.y()); + if (dy < bestDy) { + bestDy = dy; + best = node; + } + } + return best; +} + +static QDomElement findPanIframeElement(const QDomElement &root) { + if (root.isNull()) return {}; + if (root.attribute("resource-id") == "movable-pan-iframe") return root; + for (QDomElement child = root.firstChildElement(); !child.isNull(); child = child.nextSiblingElement()) { + QDomElement found = findPanIframeElement(child); + if (!found.isNull()) return found; + } + return {}; +} + +static QDomElement findFirstEditTextDescendant(const QDomElement &root) { + if (root.isNull()) return {}; + if (root.attribute("class") == "android.widget.EditText") return root; + for (QDomElement child = root.firstChildElement(); !child.isNull(); child = child.nextSiblingElement()) { + QDomElement found = findFirstEditTextDescendant(child); + if (!found.isNull()) return found; + } + return {}; +} + +Node ScreenXmlParser::findPanCardNumberInput() { + const QDomElement iframe = findPanIframeElement(m_xml); + if (iframe.isNull()) return {}; + const QDomElement edit = findFirstEditTextDescendant(iframe); + if (edit.isNull()) return {}; + + Node node; + if (QRegularExpressionMatch m = boundsRegex.match(edit.attribute("bounds")); m.hasMatch()) { + node.setBounds(m.captured(1).toInt(), m.captured(2).toInt(), + m.captured(3).toInt(), m.captured(4).toInt()); + } + node.className = edit.attribute("class"); + node.text = edit.attribute("text"); + node.contentDesc = edit.attribute("content-desc"); + node.resourceId = edit.attribute("resource-id"); + node.hint = edit.attribute("hint"); + node.clickable = edit.attribute("clickable") == "true"; + node.focusable = edit.attribute("focusable") == "true"; + return node; +} + +Node ScreenXmlParser::findButtonNode(const QString &text, const bool contains) { + for (const Node &node: m_nodes) { + if (contains) { + if (node.className == "android.widget.Button" && node.text.contains(text)) { + return node; + } + } else { + if (node.className == "android.widget.Button" && node.text.trimmed() == text) { + return node; + } + } + } + return {}; +} + +QList ScreenXmlParser::findAllButtons() { + QList buttons; + for (const Node &node : m_nodes) { + if (node.className == "android.widget.Button" && !node.text.isEmpty()) { + buttons.append(node); + } + } + return buttons; +} + +const QList &ScreenXmlParser::findAllNodes() const { + return m_nodes; +} + +Node ScreenXmlParser::findNodeByContentDesc(const QString &contentDesc) { + for (const Node &node: m_nodes) { + if (node.clickable && node.contentDesc.trimmed() == contentDesc) { + return node; + } + } + return {}; +} + +bool ScreenXmlParser::isHomeScreen() { + // Домашний экран Ozon Банка: одновременно присутствуют кнопки "Перевести" и "Пополнить" + bool hasTransfer = false; + bool hasTopUp = false; + for (const Node &node: m_nodes) { + const QString t = node.text.trimmed(); + if (t == "Перевести") hasTransfer = true; + if (t == "Пополнить") hasTopUp = true; + if (hasTransfer && hasTopUp) return true; + } + return false; +} + +Node ScreenXmlParser::tryToFindAdBanner() { + // Рекламный bottom sheet (home_ad_1) определяется по наличию кликабельного + // фонового оверлея "touch_outside" и самого bottom sheet контейнера. + bool hasBottomSheet = false; + Node touchOutside; + for (const Node &node: m_nodes) { + if (node.resourceId == "ru.ozon.fintech.finance:id/design_bottom_sheet") { + hasBottomSheet = true; + } + if (node.resourceId == "ru.ozon.fintech.finance:id/touch_outside" && node.clickable) { + touchOutside = node; + } + } + if (hasBottomSheet && !touchOutside.isEmpty()) { + return touchOutside; + } + return {}; +} + +Node ScreenXmlParser::tryToFindSystemPermissionDenyButton() { + for (const Node &node : m_nodes) { + if (node.resourceId == "com.android.permissioncontroller:id/permission_deny_button" + && node.clickable) { + return node; + } + } + for (const Node &node : m_nodes) { + if (node.clickable && node.text.trimmed() == QString::fromUtf8("ЗАПРЕТИТЬ")) { + return node; + } + } + return {}; +} + +Node ScreenXmlParser::tryToFindGooglePlayBanner(const int screenWidth, const int screenHeight) { + // Баннер Google Play "Доступно обновление" — ищем кнопку закрытия по content-desc + for (const Node &node : m_nodes) { + if (node.contentDesc.contains("Закрыть диалоговое окно") && node.clickable) { + return node; + } + } + // Альтернативно — по content-desc "Доступно обновление" + for (const Node &node : m_nodes) { + if (node.contentDesc.contains("Доступно обновление")) { + // Тапаем в верхнюю часть экрана (выше баннера), чтобы закрыть + Node tapNode; + const int cx = screenWidth / 2; + const int cy = screenHeight / 20; + tapNode.setBounds(cx, cy, cx + 1, cy + 1); + return tapNode; + } + } + return {}; +} + +bool ScreenXmlParser::isProfileScreen() { + // Страница профиля Ozon: есть заголовок "Аккаунт" и кнопка "Выйти" + bool hasTitle = false; + bool hasExit = false; + for (const Node &node: m_nodes) { + const QString t = node.text.trimmed(); + if (t == "Аккаунт") hasTitle = true; + if (t == "Выйти") hasExit = true; + if (hasTitle && hasExit) return true; + } + return false; +} + +Node ScreenXmlParser::findUserNameOnHomeScreen(const int screenHeight) { + // Имя пользователя — TextView в верхней части домашнего экрана (y < screenHeight/5). + // Тап по его координатам делегируется родительскому кликабельному View. + static const QSet knownLabels = { + "Финансы", "Платежи", "Выгода", "Чат", "Ещё" + }; + for (const Node &node: m_nodes) { + if (node.className == "android.widget.TextView" + && !node.text.trimmed().isEmpty() + && node.y() > 0 + && node.y() < screenHeight / 5 + && !knownLabels.contains(node.text.trimmed())) { + return node; + } + } + return {}; +} + +QString ScreenXmlParser::parseProfileFullName() { + // Полное имя — первый TextView ниже заголовка "Аккаунт", + // который не является известным элементом UI и не содержит спецсимволов. + static const QSet uiLabels = { + "Аккаунт", "Мои документы", "Паспорт и ИНН", + "Уровень счёта Лимиты и тарифы", "Повысить", + "Изменить код-пароль", "Изменить кодовое слово", + "Использовать данные биометрии", "Выйти", + "Оформите подписку", + "QR-код для пополнений Принимайте чаевые и донаты", + "Добавить Почта" + }; + + // Находим y-координату заголовка "Аккаунт" + int titleY = -1; + for (const Node &node : m_nodes) { + if (node.className == "android.widget.TextView" + && node.text.trimmed() == "Аккаунт" + && node.width() > 50) { + titleY = node.y(); + break; + } + } + if (titleY < 0) return {}; + + // Находим y-координату блока с телефоном (ограничение снизу) + int phoneY = INT_MAX; + for (const Node &node : m_nodes) { + if (node.text.contains("Номер телефона")) { + phoneY = node.y(); + break; + } + } + + // Первый подходящий TextView между заголовком и телефоном + for (const Node &node : m_nodes) { + if (node.className == "android.widget.TextView" + && !node.text.trimmed().isEmpty() + && node.y() > titleY + && node.y() < phoneY + && !uiLabels.contains(node.text.trimmed())) { + return node.text.trimmed(); + } + } + return {}; +} + +Node ScreenXmlParser::findCardButtonOnHomeScreen() { + // Карточка на домашнем экране — Button, текст которого заканчивается на 4 цифры. + // Формат может быть "9865", "•• 9865", "**** 9865" и т.п. + static const QRegularExpression endsWithFourDigits(R"(\d{4}\s*$)"); + for (const Node &node : m_nodes) { + if (node.className == "android.widget.Button" + && node.clickable + && endsWithFourDigits.match(node.text.trimmed()).hasMatch()) { + qDebug() << "[findCardButton] Found:" << node.text << "at" << node.x() << node.y(); + return node; + } + } + // Диагностика: выводим все clickable Button'ы чтобы понять что есть на экране + qDebug() << "[findCardButton] Not found. All clickable Buttons:"; + for (const Node &node : m_nodes) { + if (node.className == "android.widget.Button" && node.clickable) { + qDebug() << " text=" << node.text << "resId=" << node.resourceId; + } + } + return {}; +} + +Node ScreenXmlParser::findCardButtonByLastNumbers(const QString &lastNumbers) { + for (const Node &node : m_nodes) { + if (node.className != "android.widget.Button" || !node.clickable) continue; + const QString t = node.text.trimmed(); + if (!t.endsWith(lastNumbers)) continue; + // Отсекаем строки из ленты переводов (см. parseCardsOnHomeScreen) + if (t.contains(QString::fromUtf8("Переводы")) + || t.contains(QStringLiteral("**")) + || t.contains(QChar(0x2212)) || t.contains(QChar('+'))) { + qDebug() << "[findCardBtn] Skipping transfer-feed row for lastNumbers" + << lastNumbers << "text=" << node.text; + continue; + } + qDebug() << "[findCardBtn] Found by lastNumbers" << lastNumbers + << "text=" << node.text << "at" << node.x() << node.y(); + return node; + } + qWarning() << "[findCardBtn] Not found for lastNumbers=" << lastNumbers + << ". Clickable buttons:"; + for (const Node &node : m_nodes) { + if (node.className == "android.widget.Button" && node.clickable) + qWarning() << " text=" << node.text; + } + return {}; +} + +Node ScreenXmlParser::findMainAccountButton() { + for (const Node &node : m_nodes) { + if (node.className != "android.widget.Button") continue; + QString t = node.text; + t.replace(QChar(0x00A0), ' ').replace(QChar(0x202F), ' '); + if (t.contains("Основной", Qt::CaseInsensitive) && t.contains("₽")) { + return node; + } + } + return {}; +} + +Node ScreenXmlParser::findShowRequisitesButton() { + for (const Node &node : m_nodes) { + if (node.text.trimmed() == QString::fromUtf8("Посмотреть реквизиты")) { + return node; + } + } + return {}; +} + +bool ScreenXmlParser::isAccountRequisitesScreen() { + for (const Node &node : m_nodes) { + if (node.text.trimmed() == QString::fromUtf8("Реквизиты счёта")) { + return true; + } + } + return false; +} + +QString ScreenXmlParser::parseAccountNumberFromRequisites() { + // text="Номер счёта получателя 40914810100039708263" + // Префикс может варьироваться по регистру/пробелам — берём после первого совпадения цифр в конце. + static const QRegularExpression tailDigits(R"((\d{15,}))"); + static const QString prefix = QString::fromUtf8("Номер счёта получателя"); + for (const Node &node : m_nodes) { + const QString t = node.text; + if (!t.contains(prefix)) continue; + const QRegularExpressionMatch m = tailDigits.match(t); + if (m.hasMatch()) return m.captured(1); + } + return {}; +} + +QList ScreenXmlParser::parseCardsOnHomeScreen() { + // "Основной счёт X ₽" — общая кнопка с балансом для всех карт. + // Карты — отдельные кнопки с текстом из 4 цифр (могут быть любые non-breaking spaces). + // Сумма из "Основной счёт" проставляется на все карты. + + static const QRegularExpression endsWithFourDigits(R"(\d{4}\s*$)"); + + // 1. Ищем кнопку "Основной счёт X ₽" — содержит "Основной" и "₽" + double commonAmount = 0.0; + for (const Node &node : m_nodes) { + if (node.className != "android.widget.Button") continue; + QString t = node.text; + t.replace(QChar(0x00A0), ' ').replace(QChar(0x202F), ' '); + if (!t.contains("Основной", Qt::CaseInsensitive) || !t.contains("₽")) continue; + qDebug() << "[parseCards] Found balance button, text:" << t; + const QRegularExpressionMatch m = QRegularExpression( + R"(([\d\s]+(?:[.,]\d+)?)\s*₽)" + ).match(t); + if (m.hasMatch()) { + QString amtStr = m.captured(1).remove(' ').replace(',', '.'); + commonAmount = amtStr.toDouble(); + qDebug() << "[parseCards] Parsed amount:" << commonAmount; + break; + } + qWarning() << "[parseCards] Amount regex not matched in:" << t; + } + if (commonAmount == 0.0) + qWarning() << "[parseCards] Balance button not found or amount=0"; + + // 2. Собираем все кнопки карт (текст заканчивается на 4 цифры). + // Записи из ленты переводов выглядят как кнопки и тоже заканчиваются на 4 цифры + // ("Евгений Дмитриевич Р. − 100 ₽ Переводы • Карта **1092") — отсеиваем по маркерам. + QList cards; + for (const Node &node : m_nodes) { + if (node.className != "android.widget.Button" || !node.clickable) continue; + QString t = node.text.trimmed(); + if (!endsWithFourDigits.match(t).hasMatch()) continue; + + // Маркеры записи из ленты переводов: + // - слово "Переводы" + // - маска "**" перед цифрами + // - знак суммы транзакции (минус U+2212 или плюс) + if (t.contains(QString::fromUtf8("Переводы")) + || t.contains(QStringLiteral("**")) + || t.contains(QChar(0x2212)) || t.contains(QChar('+'))) { + qDebug() << "[parseCards] Skipping transfer-feed row:" << t; + continue; + } + + MaterialInfo acc; + acc.lastNumbers = t.right(4); + acc.amount = commonAmount; + acc.currency = "RUB"; + acc.description = "Виртуальная карта"; + qDebug() << "[parseCards] Card found: lastNumbers=" << acc.lastNumbers + << "text=" << node.text << "amount=" << commonAmount; + cards.append(acc); + } + + qDebug() << "[parseCards] Total cards found:" << cards.size(); + return cards; +} + +QString ScreenXmlParser::parseProfilePhone() { + // Телефон — Button с текстом вида "+7 953 803 83 18 Номер телефона" + for (const Node &node: m_nodes) { + if (node.text.contains("Номер телефона")) { + const int idx = node.text.indexOf("Номер телефона"); + QString phone = node.text.left(idx).trimmed(); + phone.remove(' '); + phone.remove(QChar(0x00A0)); + return phone; + } + } + return {}; +} + +QList ScreenXmlParser::findPinCodeNodes(const QString &pinCode) { + const QString cleanPin = QString(pinCode).remove(' '); + if (cleanPin.isEmpty()) { + qWarning() << "[findPinCodeNodes] pinCode is empty — check bank_profiles.pin_code in DB"; + return {}; + } + // Способ 1: ищем кликабельные ноды с текстом-цифрой (0-9) + // Поддерживаем форматы: "1", "key 1", и т.п. + QMap digitMap; + static const QRegularExpression digitRx(R"(^(?:key\s+)?(\d)$)", QRegularExpression::CaseInsensitiveOption); + for (const Node &node : m_nodes) { + if (!node.clickable || node.width() <= 0 || node.height() <= 0) continue; + const QString t = node.text.trimmed(); + if (const auto match = digitRx.match(t); match.hasMatch()) { + digitMap[match.captured(1)] = node; + } + } + + qDebug() << "[findPinCodeNodes] digitMap size:" << digitMap.size() + << "keys:" << digitMap.keys(); + if (digitMap.size() >= 10) { + QList result; + for (const QChar &c : cleanPin) { + const QString d = QString(c); + if (digitMap.contains(d)) { + result.append(digitMap[d]); + } else { + qWarning() << "[findPinCodeNodes] digit not found:" << d; + return {}; + } + } + return result; + } + + // Способ 2 (fallback): группируем clickable-ноды похожего размера, + // ищем сетку 4x3 по паттерну (не привязываясь к координатам экрана) + constexpr int SIZE_TOLERANCE = 10; // допуск в пикселях + + // Собираем все clickable ноды с положительным размером + QList allClickable; + for (const Node &node : m_nodes) { + if (!node.clickable || node.width() <= 0 || node.height() <= 0) continue; + allClickable.append(node); + } + + // Группируем с допуском: для каждой ноды проверяем, подходит ли она к существующей группе + auto fuzzyGroup = [&](bool squareOnly) -> QList { + QList> groups; + QList> groupSizes; // эталонный размер каждой группы + + for (const Node &node : allClickable) { + if (squareOnly && std::abs(node.width() - node.height()) > SIZE_TOLERANCE) + continue; + bool placed = false; + for (int g = 0; g < groups.size(); ++g) { + if (std::abs(node.width() - groupSizes[g].first) <= SIZE_TOLERANCE && + std::abs(node.height() - groupSizes[g].second) <= SIZE_TOLERANCE) { + groups[g].append(node); + placed = true; + break; + } + } + if (!placed) { + groups.append({node}); + groupSizes.append({node.width(), node.height()}); + } + } + + QList best; + for (const auto &grp : groups) { + if (grp.size() >= 10 && (best.size() < 10 || grp.size() < best.size())) + best = grp; + } + return best; + }; + + // Сначала квадратные, потом любые + QList candidates = fuzzyGroup(true); + qDebug() << "[findPinCodeNodes] fallback square candidates:" << candidates.size(); + if (candidates.size() < 10) { + candidates = fuzzyGroup(false); + qDebug() << "[findPinCodeNodes] fallback any candidates:" << candidates.size(); + } + + if (candidates.size() < 10) { + // Дебаг: выводим все clickable ноды и их размеры + qWarning() << "[findPinCodeNodes] grid not found. Total clickable nodes:" << allClickable.size(); + for (const Node &n : allClickable) { + qDebug() << " clickable:" << n.width() << "x" << n.height() + << "pos:" << n.x() << "," << n.y() + << "class:" << n.className + << "text:" << n.text.left(30); + } + return {}; + } + + // Сортируем по Y, затем группируем в строки + std::sort(candidates.begin(), candidates.end(), [](const Node &a, const Node &b) { + return a.y() < b.y(); + }); + + QList> rows; + for (const Node &node : candidates) { + bool placed = false; + for (QList &row : rows) { + if (std::abs(node.y() - row.first().y()) < row.first().height()) { + row.append(node); + placed = true; + break; + } + } + if (!placed) rows.append({node}); + } + + for (QList &row : rows) { + std::sort(row.begin(), row.end(), [](const Node &a, const Node &b) { + return a.x() < b.x(); + }); + } + + // Строки по 3 — основные ряды (1-9), строки по 1-3 в конце — ряд с "0" + QList> fullRows; // строки по 3 кнопки + QList> shortRows; // строки с 1-2 кнопками (последний ряд) + for (const QList &row : rows) { + if (row.size() == 3) fullRows.append(row); + else if (row.size() >= 1 && row.size() <= 2) shortRows.append(row); + } + + if (fullRows.size() < 3) { + qWarning() << "[findPinCodeNodes] grid not found: fullRows=" << fullRows.size() + << "candidates=" << candidates.size(); + return {}; + } + + const QStringList digits19 = {"1", "2", "3", "4", "5", "6", "7", "8", "9"}; + int idx = 0; + for (int r = 0; r < fullRows.size() && idx < digits19.size(); ++r) { + for (const Node &n : fullRows[r]) { + if (idx < digits19.size()) digitMap[digits19[idx++]] = n; + } + } + // "0" — в последнем ряду: если 3 кнопки — средняя, если 1 — единственная + if (!shortRows.isEmpty()) { + const auto &lastRow = shortRows.last(); + if (lastRow.size() >= 2) + digitMap["0"] = lastRow[1]; // средняя кнопка + else + digitMap["0"] = lastRow[0]; // единственная кнопка + } else if (fullRows.size() >= 4) { + // fallback: "0" — средняя кнопка последнего полного ряда + digitMap["0"] = fullRows.last()[1]; + } + + QList result; + for (const QChar &c : cleanPin) { + if (const QString d = QString(c); digitMap.contains(d)) { + result.append(digitMap[d]); + } else { + qWarning() << "[findPinCodeNodes] fallback: digit not in digitMap:" << d; + } + } + qDebug() << "[findPinCodeNodes] fallback result size:" << result.size() + << "digitMap size:" << digitMap.size(); + return result; +} + +QList ScreenXmlParser::tryToFindPinCode(const QString &pinCode, const QString &title) { + bool isPinScreen = false; + for (const Node &node: m_nodes) { + // Баннер — пин-код не вводить + if (node.text.trimmed() == "Отложить" && node.className == "android.widget.Button") { + return {}; + } + // В Ozon PIN экран определяется по ссылке "Не помню код-пароль" + if (node.text.trimmed() == "Не помню код-пароль") { + isPinScreen = true; + } + // Fallback: другие банки используют title текст + if (!title.isEmpty() && node.text.trimmed() == title) { + isPinScreen = true; + } + } + if (isPinScreen) { + return findPinCodeNodes(pinCode); + } + return {}; +} + +QList ScreenXmlParser::getLast2DaysTransactions() { + QList nodes; + for (const Node &node: m_nodes) { + if (node.className == "android.widget.Button" && node.text.contains("рубл") + && (node.text.contains("Минус") || node.text.contains("Плюс"))) { + nodes.append(node); + } + } + return nodes; +} + +/** +* + */ +void ScreenXmlParser::parseAndSaveXml(const QString &xml) { + QDomDocument doc; + doc.setContent(xml); + m_xml = doc.documentElement(); + m_nodes.clear(); + + QXmlStreamReader reader(xml); + while (!reader.atEnd()) { + reader.readNext(); + if (reader.isStartElement() && reader.name() == "node") { + Node node = Node(); + if (reader.attributes().hasAttribute("bounds")) { + QString bounds = reader.attributes().value("bounds").toString(); + if (QRegularExpressionMatch match = boundsRegex.match(bounds); match.hasMatch()) { + node.setBounds( + match.captured(1).toInt(), + match.captured(2).toInt(), + match.captured(3).toInt(), + match.captured(4).toInt() + ); + } + } + if (reader.attributes().hasAttribute("text")) { + node.text = reader.attributes().value("text").toString(); + } + + if (reader.attributes().hasAttribute("content-desc")) { + node.contentDesc = reader.attributes().value("content-desc").toString(); + } + + if (reader.attributes().hasAttribute("class")) { + node.className = reader.attributes().value("class").toString(); + } + + if (reader.attributes().hasAttribute("clickable")) { + node.clickable = reader.attributes().value("clickable").toString() == "true"; + } + if (reader.attributes().hasAttribute("focusable")) { + node.focusable = reader.attributes().value("focusable").toString() == "true"; + } + if (reader.attributes().hasAttribute("resource-id")) { + node.resourceId = reader.attributes().value("resource-id").toString(); + } + if (reader.attributes().hasAttribute("hint")) { + node.hint = reader.attributes().value("hint").toString(); + } + + m_nodes.append(node); + } + } +} + +QString getDayOfWeekAsString(const int day_of_week) { + switch (day_of_week) { + case 1: return "понедельник"; + case 2: return "вторник"; + case 3: return "среда"; + case 4: return "четверг"; + case 5: return "пятница"; + case 6: return "суббота"; + case 7: return "воскресенье"; + default: return ""; + } +} + +QString getMonthAsString(const int month) { + switch (month) { + case 1: return "января"; + case 2: return "февраля"; + case 3: return "марта"; + case 4: return "апреля"; + case 5: return "мая"; + case 6: return "июня"; + case 7: return "июля"; + case 8: return "августа"; + case 9: return "сентября"; + case 10: return "октября"; + case 11: return "ноября"; + case 12: return "декабря"; + default: return ""; + } +} + +QDomElement findSubElement(const QDomElement &elem, const QString &index) { + if (elem.hasChildNodes()) { + QDomNodeList subInfoList = elem.childNodes(); + for (QDomNode subInfoNode: subInfoList) { + if (!subInfoNode.isElement()) continue; + QDomElement subInfo = subInfoNode.toElement(); + if (subInfo.attribute("index") == index) { + return subInfo; + } + } + } + return {}; +} + +QString findSubInfo(const QDomElement &elem, const QString &index) { + QDomElement subInfo = findSubElement(elem, index); + if (!subInfo.isNull()) { + return subInfo.attribute("text"); + } + return ""; +} + +bool hasTimeComponent(const QString &datetimeStr) { + QDateTime dt = QDateTime::fromString(datetimeStr, "dd.MM.yyyy HH:mm:ss"); + if (!dt.isValid()) { + return false; // Строка не соответствует формату даты и времени + } + return dt.time() != QTime(0, 0, 0); // Проверка, что время не равно 00:00:00 +} + +QString transformBankDateTime(const QString &date) { + QDateTime dt; + if (date.contains(",")) { + // 10.05.2025, 15:03 + dt = QDateTime::fromString(date, "dd.MM.yyyy, HH:mm"); + } else { + dt = QDateTime::fromString(date, "dd.MM.yyyy HH:mm:ss"); + } + if (!dt.isValid()) { + dt = QDateTime::fromString(date, "dd.MM.yyyy HH:mm"); + } + if (!dt.isValid()) { + return date; // не распарсили + } + return dt.toString("dd.MM.yyyy HH:mm:ss"); +} + +void parseTransferInfo(const QString &text, TransactionInfo &info) { + if (const auto m = reSender.match(text); m.hasMatch()) { + info.name = m.captured(1).trimmed(); + } else if (const auto m1 = reNameAlt.match(text); m1.hasMatch()) { + info.name = m1.captured(1).trimmed(); + } + + if (const auto m = rePhone.match(text); m.hasMatch()) { + info.phone = m.captured(1).trimmed().remove(onlyDigitsRegex); + } + + if (const auto m = reFromBank.match(text); m.hasMatch()) { + if (const QString bank = m.captured(1).trimmed(); !bank.isEmpty()) { + info.bankName = bank; + } + } else if (const auto m1 = reToBank.match(text); m1.hasMatch()) { + if (const QString bank = m1.captured(1).trimmed(); !bank.isEmpty()) { + info.bankName = bank; + } + } + + if (const auto m = reSbpId.match(text); m.hasMatch()) { + info.bankTrExternalId = m.captured(1); + } else if (const auto m1 = reIdAlt.match(text); m1.hasMatch()) { + info.bankTrExternalId = m1.captured(1); + } + + if (const auto m = reDate.match(text); m.hasMatch()) { + const QString date = m.captured(1); + // если не пустая и дата у нас с 00:00 + // if (hasTimeComponent(date)) { + info.bankTime = transformBankDateTime(date); + // } + } +} + +void traverseNodes(const QDomNode &node) { + if (node.isElement()) { + QDomElement elem = node.toElement(); + + if (node.isElement()) { + QDomElement elem = node.toElement(); + + if (elem.tagName() == "node" && + elem.attribute("class") == "android.widget.Button" && + elem.attribute("text") == "Детали операции") { + // Все что ниже деталей операции + QDomNodeList detailInfo = elem.parentNode().childNodes(); + + + for (int j = 0; j < detailInfo.count(); ++j) { + QDomNode info = detailInfo.at(j); + if (!info.isElement()) continue; + QDomElement infoElement = info.toElement(); + QString index = infoElement.attribute("index"); + switch (index.toInt()) { + case 2: + qDebug() << "Дата операции:" << findSubInfo(infoElement, "1"); + break; + case 5: + qDebug() << "Название:" << findSubInfo(infoElement, "1"); + qDebug() << "-------------------------------------"; + // parseTransfer(findSubInfo(infoElement)); + break; + case 7: + qDebug() << "Комиссия:" << findSubInfo(infoElement, "1"); + break; + default: + qDebug() << "Прочее:" << findSubInfo(infoElement, "1"); + break; + } + + /** + Node: "2" "" + ... Node: "0" "Дата операции" + ... Node: "1" "30.04.2025 17:46:29" + Node: "5" "" + ... Node: "0" "Название" + ... Node: "1" "DBO TRANSFER RSHB INTERNET-BANK" + Node: "7" "" + ... Node: "0" "Комиссия" + ... Node: "1" "50 ₽" + */ + + // if (sibElem.hasChildNodes()) { + // QDomNodeList sibChildren = sibElem.childNodes(); + // for (int k = 0; k < sibChildren.count(); ++k) { + // QDomNode sibChild = sibChildren.at(k); + // if (!sibChild.isElement()) continue; + // QDomElement sibChildElem = sibChild.toElement(); + // qDebug() << "... Node:" + // << sibChildElem.attribute("index") + // << sibChildElem.attribute("text"); + // // << sibChildElem.attribute("class"); + // + // + // if (sibChildElem.hasChildNodes()) { + // QDomNodeList sibChildren2 = sibChildElem.childNodes(); + // for (int k = 0; k < sibChildren2.count(); ++k) { + // QDomNode sibChild2 = sibChildren2.at(k); + // if (!sibChild2.isElement()) continue; + // QDomElement sibChildElem2 = sibChild2.toElement(); + // + // qDebug() << "...... Node:" + // << sibChildElem2.attribute("index") + // << sibChildElem2.attribute("text"); + // // << sibChildElem2.attribute("class"); + // + // if (sibChildElem2.hasChildNodes()) { + // QDomNodeList sibChildren3 = sibChildElem2.childNodes(); + // for (int k = 0; k < sibChildren3.count(); ++k) { + // QDomNode sibChild3 = sibChildren3.at(k); + // if (!sibChild3.isElement()) continue; + // QDomElement sibChildElem3 = sibChild3.toElement(); + // + // qDebug() << "......... Node:" + // << sibChildElem3.attribute("index") + // << sibChildElem3.attribute("text"); + // // << sibChildElem2.attribute("class"); + // } + // } + // } + // } + // } + // } + } + } + } + } + + // Рекурсивно обходим всех детей + QDomNode child = node.firstChild(); + while (!child.isNull()) { + traverseNodes(child); + child = child.nextSibling(); + } +} + +QDomNode findNode(const QDomNode &node, const QString &text) { + if (node.isElement()) { + QString nodeText = node.toElement().attribute("text"); + if (nodeText.toLower().startsWith(text)) { + return node; + } + } + + + QDomNode child = node.firstChild(); + while (!child.isNull()) { + QDomNode result = findNode(child, text); + if (!result.isNull()) { + return result; + } + child = child.nextSibling(); + } + return {}; +} + +float extractAmounts(const QString &text) { + // Разрешаем пробелы внутри чисел, захватываем "рубль", "рубля", "рублей" и т.д. + // QRegularExpression re(R"((Плюс|Минус)\s+([\d\s]+)\s+рубл\w*\s+(\d+)\s+копе\w*)"); + QRegularExpression re( + R"((Плюс|Минус)\s+([\d\s]+)\s+рубл\w*\s+(\d+)\s+копе\w*)", + QRegularExpression::UseUnicodePropertiesOption + ); + + QRegularExpressionMatch match = re.match(text); + if (match.hasMatch()) { + QString signStr = match.captured(1); // "Плюс" или "Минус" + QString rublesStr = match.captured(2).remove(' '); // "1 000" → "1000" + int rubles = rublesStr.toInt(); + int kopecks = match.captured(3).toInt(); + + int totalKopecks = rubles * 100 + kopecks; + if (signStr == "Минус") { + totalKopecks *= -1; + } + + return totalKopecks / 100.0f; + } + return 0; +} + +QString beforeAmount(const QString &text) { + int index = text.indexOf(QRegularExpression("Плюс|Минус")); + if (index != -1) { + return text.left(index).trimmed(); + } + return text; // если не найдено — вернуть всю строку +} + +double parseAmount(const QString &amountAsString) { + QString numberString = amountAsString; + numberString.remove(QRegularExpression(R"([^\dточка])")); + numberString.replace("точка", "."); + double amount = numberString.toDouble(); + + if (amountAsString.startsWith("-")) { + amount *= -1; + } + + return amount; +} + +QDomElement findElementWithListView(const QDomElement &parent) { + QDomElement element = parent.firstChildElement(); + while (!element.isNull()) { + QDomElement child = element.firstChildElement(); + while (!child.isNull()) { + if (child.hasAttribute("class") && child.attribute("class") == "android.widget.ListView") { + return child; + } + child = child.nextSiblingElement(); + } + + QDomElement found = findElementWithListView(element); + if (!found.isNull()) { + return found; + } + element = element.nextSiblingElement(); + } + return {}; +} + +QList ScreenXmlParser::parseTodayAndYesterdayReportHistory() { + QList transactions; + + QDomElement child = findElementWithListView(m_xml); + QDomNodeList listView = findElementWithListView(m_xml).childNodes(); + qDebug() << listView.count(); + if (!listView.isEmpty()) { + QString date; + for (QDomNode node: listView) { + if (node.hasChildNodes()) { + QString text; + if (node.childNodes().count() > 1) { + date = node.firstChildElement().attribute("text"); + text = node.lastChildElement().attribute("text"); + } else if (node.childNodes().count() == 1) { + text = node.firstChildElement().attribute("text"); + } + + TransactionInfo transaction; + if (text.startsWith("Перевод")) { + QRegularExpression re(R"([+-] \d{1,3}(?: \d{3})* точка \d{2} Рубль$)"); + QRegularExpressionMatch match = re.match(text); + if (match.hasMatch()) { + QString amount = match.captured(0); + transaction.amount = parseAmount(amount.trimmed()); + text.chop(amount.length()); + } + transaction.description = text.trimmed(); + parseTransferInfo(text, transaction); + if (transaction.bankTime.isEmpty()) { + transaction.bankTime = transformBankDateTime(date); + } + } else { + // карта + if (text.contains("без учета комиссии")) { + QRegularExpression re(R"( без учета комиссии -\d{1,3}(?: \d{3})* точка \d{2} Рубль$)"); + QRegularExpressionMatch match = re.match(text); + if (match.hasMatch()) { + QString fee = match.captured(0); + text.chop(fee.length()); + transaction.fee = parseAmount(fee.replace("без учета комиссии", "").trimmed()); + } + } + QRegularExpression re(R"([+-] \d{1,3}(?: \d{3})* точка \d{2} Рубль$)"); + QRegularExpressionMatch match = re.match(text); + if (match.hasMatch()) { + QString amount = match.captured(0); + transaction.amount = parseAmount(amount.trimmed()); + text.chop(amount.length()); + } + transaction.description = text.trimmed(); + if (transaction.bankTime.isEmpty()) { + transaction.bankTime = transformBankDateTime(date); + } + } + transactions.append(transaction); + } + } + } else { + qDebug() << "ListView is empty"; + } + return transactions; +} + +QList ScreenXmlParser::parseTodayAndYesterdayHistory() { + QDate today = DateUtils::getMoscowNow().date(); + QDate yesterday = today.addDays(-1); + QDate beforeYesterday = today.addDays(-2); + + + QString todayTitle = QString("сегодня, %1").arg(getDayOfWeekAsString(today.dayOfWeek())); + QString yesterdayTitle = QString("вчера, %1").arg(getDayOfWeekAsString(yesterday.dayOfWeek())); + QString beforeYesterdayTitle = QString("%1 %2, %3").arg( + QString::number(beforeYesterday.day()), + getMonthAsString(beforeYesterday.month()), + getDayOfWeekAsString(beforeYesterday.dayOfWeek()) + ); + + QDomNode todayNode = findNode(m_xml, todayTitle); + QDomNode yesterdayNode = findNode(m_xml, yesterdayTitle); + + QDomNode parentNode; + if (!yesterdayNode.isNull()) { + parentNode = yesterdayNode.parentNode(); + } else if (!todayNode.isNull()) { + parentNode = todayNode.parentNode(); + } + + + QList transactions; + if (!parentNode.isNull()) { + QDate date = {}; + for (QDomNode transactionNode: parentNode.childNodes()) { + QString transactionShortText = transactionNode.toElement().attribute("text"); + + const QString titleLowerCase = transactionShortText.toLower(); + if (titleLowerCase.startsWith(beforeYesterdayTitle)) { + // только сегодня и вчера парсим, позавчера не трогаем + break; + } + if (titleLowerCase.startsWith(todayTitle)) { + date = today; + transactionShortText = transactionShortText.mid(todayTitle.length() + 1); + } + if (titleLowerCase.startsWith(yesterdayTitle)) { + date = yesterday; + transactionShortText = transactionShortText.mid(yesterdayTitle.length() + 1); + } + + if (transactionShortText.contains("ЕСПП")) { + continue; + } + + float amount = extractAmounts(transactionShortText); + QString shortDesc = beforeAmount(transactionShortText); + + TransactionInfo info; + info.amount = amount; + info.bankTime = date.toString("dd.MM.yyyy"); + info.description = shortDesc; + + transactions.append(info); + } + } + return transactions; +} + +void parse2daysHistory(const QDomNode &node) { + QDate today = DateUtils::getMoscowNow().date(); + QDate tomorrow = today.addDays(-1); + QDate after2Days = today.addDays(-2); + + QString todayTitle = QString("сегодня, %1").arg(getDayOfWeekAsString(today.dayOfWeek())); + QDomNode todayNode = findNode(node, todayTitle); + if (todayNode.isNull()) { + } else { + qDebug() << "todayNode: " << todayNode.toElement().attribute("text"); + } + + QString tomorrowTitle = QString("вчера, %1").arg(getDayOfWeekAsString(tomorrow.dayOfWeek())); + QDomNode tomorrowNode = findNode(node, tomorrowTitle); + if (tomorrowNode.isNull()) { + } else { + qDebug() << "tomorrowNode: " << tomorrowNode.toElement().attribute("text"); + } + + // 30 апреля, Среда + QString after2DaysTitle = QString("%1 %2, %3").arg( + QString::number(after2Days.day()), + getMonthAsString(after2Days.month()), + getDayOfWeekAsString(after2Days.dayOfWeek())); + QDomNode after2DaysNode = findNode(node, after2DaysTitle); + if (after2DaysNode.isNull()) { + qDebug() << "No after2DaysNode"; + } else { + qDebug() << "after2DaysNode: " << after2DaysNode.toElement().attribute("text"); + } + + QDomNode parentNode; + if (!todayNode.isNull() || !tomorrowNode.isNull() || !after2DaysNode.isNull()) { + if (!todayNode.isNull()) { + parentNode = todayNode.parentNode(); + } + if (!tomorrowNode.isNull()) { + parentNode = tomorrowNode.parentNode(); + } + if (!after2DaysNode.isNull()) { + parentNode = after2DaysNode.parentNode(); + } + } + + + if (!parentNode.isNull()) { + for (QDomNode child_node: parentNode.childNodes()) { + QString child_text = child_node.toElement().attribute("text"); + if (child_text.toLower().startsWith(todayTitle)) { + qDebug() << "=== Today: " << todayTitle; + qDebug() << "node: " << beforeAmount(child_text.mid(todayTitle.length() + 1)); + qDebug() << "--> sum: " << QString::number(extractAmounts(child_text), 'f', 2); + // qDebug() << ""; + continue; + } + if (child_text.toLower().startsWith(tomorrowTitle)) { + qDebug() << "=== Yesterday: " << todayTitle; + qDebug() << "node: " << beforeAmount(child_text.mid(tomorrowTitle.length() + 1)); + qDebug() << "--> sum: " << QString::number(extractAmounts(child_text), 'f', 2); + // qDebug() << ""; + continue; + } + if (child_text.toLower().startsWith(after2DaysTitle)) { + qDebug() << "=== Before yesterday: " << todayTitle; + qDebug() << "node: " << beforeAmount(child_text.mid(after2DaysTitle.length() + 1)); + qDebug() << "--> sum: " << QString::number(extractAmounts(child_text), 'f', 2); + // qDebug() << ""; + continue; + } + + qDebug() << "node: " << beforeAmount(child_node.toElement().attribute("text")); + qDebug() << "--> sum: " << + QString::number(extractAmounts(child_node.toElement().attribute("text")), 'f', 2); + // qDebug() << ""; + } + } + + + // QString d = today.day() > 9 ? QString::number(today.day()) : "0" + QString::number(today.day()); + // QString m = today.month() > 9 ? QString::number(today.month()) : "0" + QString::number(today.month()); + // QString y = QString::number(today.year()); + // QString w = getDayOfWeekAsString(today.dayOfWeek()); + // QString mm = getMonthAsString(today.month()); + // qDebug() << "Date:" << d << m << y << w << mm; + + + // QString d2 = after2Days.day() > 9 ? QString::number(after2Days.day()) : "0" + QString::number(after2Days.day()); + // QString m2 = after2Days.month() > 9 + // ? QString::number(after2Days.month()) + // : "0" + QString::number(after2Days.month()); + // QString y2 = QString::number(after2Days.year()); + // QString w2 = getDayOfWeekAsString(after2Days.dayOfWeek()); + // QString mm2 = getMonthAsString(after2Days.month()); + // qDebug() << "Date:" << d2 << m2 << y2 << w2 << mm2; +} + +TransactionInfo parseExpandedInfoHistory(const QDomElement &root) { + if (root.isElement()) { + QDomElement elem = root.toElement(); + if (elem.tagName() == "node" + && elem.attribute("class") == "android.widget.Button" + && elem.attribute("text") == "Детали операции" + ) { + TransactionInfo transaction; + // Все что ниже деталей операции + QDomNodeList detailInfo = elem.parentNode().childNodes(); + + for (int i = 0; i < detailInfo.count(); ++i) { + QDomNode info = detailInfo.at(i); + if (!info.isElement()) continue; + + switch (QDomElement e = info.toElement(); e.attribute("index").toInt()) { + case 2: + // Дата операции + transaction.bankTime = transformBankDateTime(findSubInfo(e, "1")); + break; + case 5: + transaction.description = findSubInfo(e, "1"); + parseTransferInfo(transaction.description, transaction); + break; + case 7: + // Комиссия + transaction.fee = findSubInfo(e, "1").remove(onlyDigitsRegex).toFloat(); + break; + default: + // qDebug() << "Прочее:" << findSubInfo(e); + break; + } + } + return transaction; + } + } + + // Рекурсивно обходим всех детей + QDomNode child = root.firstChild(); + while (!child.isNull()) { + if (TransactionInfo info = parseExpandedInfoHistory(child.toElement()); !info.bankTime.isEmpty()) { + return info; + } + child = child.nextSibling(); + } + + return {}; +} + +TransactionInfo ScreenXmlParser::parseTransactionInfoHistory(int screenWidth, int screenHeight) { + // 1. Распарсить описание транзакции + TransactionInfo transaction = parseExpandedInfoHistory(m_xml); + // 1. Найти статус: Исполнен | Операция исполнена + transaction.status = TransactionStatus::Wait; + Node status1 = findTextNode("Исполнен", 0, 0, screenWidth, screenHeight / 2); + Node status2 = findTextNode("Операция исполнена", 0, 0, screenWidth, screenHeight / 2); + if (!status1.isEmpty() || !status2.isEmpty()) { + transaction.status = TransactionStatus::Complete; + } + return transaction; +} + +TransactionInfo parseExpandedInfo(const QDomElement &root) { + if (root.isElement()) { + QDomElement elem = root.toElement(); + if (elem.tagName() == "node" + && elem.attribute("class") == "android.widget.Button" + && elem.attribute("text") == "Детали операции" + ) { + TransactionInfo transaction; + // Все что ниже деталей операции + QDomNodeList detailInfo = elem.parentNode().childNodes(); + + for (int i = 0; i < detailInfo.count(); ++i) { + QDomNode info = detailInfo.at(i); + if (!info.isElement()) continue; + + QDomElement infoElement = info.toElement(); + QString index = infoElement.attribute("index"); + QDomElement ee; + switch (index.toInt()) { + case 3: + transaction.bankTime = transformBankDateTime(findSubInfo(infoElement, "1")); + break; + case 4: + ee = findSubElement(infoElement, "0"); + ee = findSubElement(ee, "1"); + transaction.phone = findSubInfo(ee, "0").remove(onlyDigitsRegex); + break; + case 5: + transaction.name = findSubInfo(infoElement, "1"); + break; + case 6: + transaction.bankName = findSubInfo(infoElement, "1"); + break; + case 9: + transaction.fee = findSubInfo(infoElement, "1").remove(onlyDigitsAndDotRegex).toFloat(); + break; + case 10: + ee = findSubElement(infoElement, "1"); + transaction.bankTrExternalId = findSubInfo(ee, "1"); + break; + default: + break; + } + } + return transaction; + } + } + + // Рекурсивно обходим всех детей + QDomNode child = root.firstChild(); + while (!child.isNull()) { + if (TransactionInfo info = parseExpandedInfo(child.toElement()); !info.bankTime.isEmpty()) { + return info; + } + child = child.nextSibling(); + } + + return {}; +} + +TransactionInfo ScreenXmlParser::parseTransactionInfo(int screenWidth, int screenHeight) { + // 1. Распарсить описание транзакции + TransactionInfo transaction = parseExpandedInfo(m_xml); + // 1. Найти статус: Исполнен | Операция исполнена + transaction.status = TransactionStatus::Wait; + Node status1 = findTextNode("Исполнен", 0, 0, screenWidth, screenHeight / 2); + Node status2 = findTextNode("Операция исполнена", 0, 0, screenWidth, screenHeight / 2); + if (!status1.isEmpty() || !status2.isEmpty()) { + transaction.status = TransactionStatus::Complete; + } + return transaction; +} + + +QList > ScreenXmlParser::parseAccountsInfo() { + QList > accounts; + for (const Node &node: m_nodes) { + const QString text = node.text; + + // Проверяем только нужные строки + if (!text.contains("Номер карты с последними цифрами")) { + continue; + } + QRegularExpressionMatch match = reCardData.match(text); + if (!match.hasMatch()) { + continue; + } + + MaterialInfo info; + info.lastNumbers = match.captured(1).remove(" "); + info.amount = match.captured(2).remove(" ").toFloat(); + info.description = text.mid(0, text.indexOf("Номер карты")); + info.updateTime = DateUtils::getUtcNow(); + + accounts.append(qMakePair(info, node)); + } + + return accounts; +} + +QString ScreenXmlParser::findFullCardNumber() { + static const QRegularExpression cardNumberRegex(R"(^\d{4}[\s\x{00A0}\x{202F}]\d{4}[\s\x{00A0}\x{202F}]\d{4}[\s\x{00A0}\x{202F}]\d{4}$)"); + for (const Node &node : m_nodes) { + if (node.className == "android.widget.TextView") { + const QString text = node.text.trimmed(); + if (cardNumberRegex.match(text).hasMatch()) { + QString result = text; + result.remove(QChar(' ')).remove(QChar(0x00A0)).remove(QChar(0x202F)); + return result; + } + } + } + return {}; +} + +bool ScreenXmlParser::isTransactionListScreen() { + // Экран "Операции": WebView с text="Операции" + наличие кнопок фильтров + for (const Node &node : m_nodes) { + if (node.className == "android.webkit.WebView" && node.text.trimmed() == "Операции") { + return true; + } + } + return false; +} + +QList ScreenXmlParser::findTransactionButtons(const int maxCount) { + const QChar rubleSign(0x20BD); // ₽ + QList buttons; + + // В новом Svelte-UI Ozon ноды устроены так: Button-обёртка с пустым text, + // а сам текст транзакции живёт во вложенной android.view.View с теми же + // bounds. В старом UI текст был прямо в Button. Поддерживаем оба варианта: + // сначала сматчим Button с непустым text, затем пройдёмся по пустым + // Button-ам и подтянем текст из перекрывающейся View с ₽-знаком. + auto isSummary = [](const QString &t) { + return t.startsWith("Расходы") || t.startsWith("Доходы"); + }; + + for (const Node &node : m_nodes) { + if (node.className != "android.widget.Button") continue; + const QString text = node.text.trimmed(); + if (text.isEmpty()) { + // Ищем View с тем же bounds (или внутри Button-bounds), содержащую ₽. + // У транзакционной строки высота ~170px — отсекаем мелкие декоры. + if (node.height() < 80 || node.height() > 260) continue; + QString viewText; + for (const Node &v : m_nodes) { + if (v.className != "android.view.View") continue; + if (!v.text.contains(rubleSign)) continue; + if (v.y1() < node.y1() || v.y2() > node.y2()) continue; + if (v.x1() < node.x1() || v.x2() > node.x2()) continue; + viewText = v.text.trimmed(); + break; + } + if (viewText.isEmpty() || isSummary(viewText)) continue; + // Возвращаем Button с подставленным текстом, чтобы вызывающий код + // (parseTransactionButtonText / makeTap по координатам) работал + // прозрачно для обоих UI-вариантов. + Node enriched = node; + enriched.text = viewText; + buttons.append(enriched); + } else { + if (!text.contains(rubleSign)) continue; + if (isSummary(text)) continue; + buttons.append(node); + } + if (buttons.size() >= maxCount) break; + } + return buttons; +} + +bool ScreenXmlParser::hasCollapsedTransactionsBelow(const int screenHeight) { + const QChar rubleSign(0x20BD); + // Off-screen строка в Ozon WebView выглядит так: bounds=[0,Y][W,Y] (height=0) + // на нижнем краю viewport. Текст транзакции лежит во внутреннем + // android.widget.Button (внешний wrapper-View имеет пустой text). + // Ищем именно Button-ы с h=0 и ₽ в тексте — в этом случае и количество + // совпадает с числом скрытых строк. + const int bottomHalf = screenHeight / 2; + for (const Node &node : m_nodes) { + if (node.className != "android.widget.Button") continue; + if (node.height() != 0) continue; + if (node.y1() < bottomHalf) continue; + const QString text = node.text.trimmed(); + if (text.isEmpty() || !text.contains(rubleSign)) continue; + if (text.startsWith("Расходы") || text.startsWith("Доходы")) continue; + return true; + } + return false; +} + +TransactionInfo ScreenXmlParser::parseTransactionButtonText(const QString &text) { + const QChar rubleSign(0x20BD); // ₽ + const QChar unicodeMinus(0x2212); // − + const QChar nbsp(0x00A0); // non-breaking space + + TransactionInfo info; + + const int rubleIdx = text.indexOf(rubleSign); + if (rubleIdx < 0) return info; + + int signIdx = -1; + bool isExpense = false; + for (int i = rubleIdx - 1; i >= 0; --i) { + const QChar ch = text.at(i); + if (ch == '+') { signIdx = i; isExpense = false; break; } + if (ch == '-' || ch == unicodeMinus) { signIdx = i; isExpense = true; break; } + } + if (signIdx < 0) return info; + + info.description = text.left(signIdx).trimmed(); + + QString amountStr = text.mid(signIdx + 1, rubleIdx - signIdx - 1).trimmed(); + amountStr.remove(QChar(' ')); + amountStr.remove(nbsp); + amountStr.replace(',', '.'); + info.amount = amountStr.toDouble(); + if (isExpense) info.amount = -info.amount; + + return info; +} + +TransactionInfo ScreenXmlParser::parseTransactionDetail() { + TransactionInfo info; + + for (const Node &node : m_nodes) { + if (node.className != "android.widget.TextView") continue; + const QString text = node.text.trimmed(); + if (text.isEmpty()) continue; + + // Дата+время: "15.01.2026, 15:55" + if (info.bankTime.isEmpty() && text.contains('.') && text.contains(':') && text.length() <= 25) { + info.bankTime = text; + continue; + } + + // Телефон: "+7 (985) 553-28-96" + if (info.phone.isEmpty() && text.startsWith('+') && text.contains('(')) { + info.phone = text; + continue; + } + } + + // Имя отправителя — TextView прямо перед телефоном (по y-координате чуть выше) + if (!info.phone.isEmpty()) { + // Ищем ноду телефона и берём ближайший TextView выше + int phoneY = -1; + for (const Node &node : m_nodes) { + if (node.className == "android.widget.TextView" && node.text.trimmed() == info.phone) { + phoneY = node.y(); + break; + } + } + if (phoneY > 0) { + int bestY = -1; + for (const Node &node : m_nodes) { + if (node.className != "android.widget.TextView") continue; + const QString text = node.text.trimmed(); + if (text.isEmpty() || text == info.bankTime || text == info.phone) continue; + if (node.y() > 0 && node.y() < phoneY && node.y() > bestY) { + bestY = node.y(); + info.name = text; + } + } + } + } + + return info; +} + +void ScreenXmlParser::test() { + // TransactionInfo info2 = parseExpandedInfo(m_xml); + // qDebug().noquote().nospace() << convertTransactionToString(info2); + // return; + + QFile file("assets/rshb/all_products.xml"); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qDebug() << "Cannot open file"; + } + + QDomDocument doc; + if (!doc.setContent(&file)) { + qDebug() << "Failed to parse XML"; + } + file.close(); + + qDebug() << "Start parsing XML:"; + + // Номер карты с последними цифрами + QDomElement root = doc.documentElement(); + parseAndSaveXml(doc.toString()); + // for (const MaterialInfo &info: parseAccountsInfo()) { + // qDebug().noquote().nospace() << convertAccountToString(info); + // } + // TransactionInfo info = parseExpandedInfo(root); + // qDebug().noquote().nospace() << convertTransactionToString(info); + // ЕСПП +} + +} // namespace Ozon \ No newline at end of file diff --git a/android/ozon/ScreenXmlParser.h b/android/ozon/ScreenXmlParser.h new file mode 100644 index 0000000..b6f0089 --- /dev/null +++ b/android/ozon/ScreenXmlParser.h @@ -0,0 +1,135 @@ +#pragma once + +#include +#include +#include "android/xml/Node.h" +#include "db/MaterialInfo.h" +#include "db/TransactionInfo.h" + +namespace Ozon { + +class ScreenXmlParser final : public QObject { + Q_OBJECT + +public: + explicit ScreenXmlParser(QObject *parent = nullptr); + + ~ScreenXmlParser() override; + + Node tryToFindBannerOrDialog(int width, int height); + + Node tryToFindSecurityBanner(); + + Node tryToFindSelectBottomSheet(); + + Node findTextNode(const QString &text); + + Node findTextNode(const QString &text, int x1, int y1, int x2, int y2); + + Node findFirstEditText(); + + Node findEditTextByHint(const QString &hintPrefix); + + // Возвращает EditText внутри Ozon "movable-pan-iframe" — поле номера карты + // получателя на экране "Перевод по номеру карты". Нужно как fallback, когда + // WebView не отдаёт hint и на экране нет TextView-лейбла "Номер карты + // получателя" (лейбл — просто "КУДА"). + Node findPanCardNumberInput(); + + Node findButtonNode(const QString &text, bool contains); + + QList findAllButtons(); + + const QList &findAllNodes() const; + + Node findNodeByContentDesc(const QString &contentDesc); + + bool isHomeScreen(); + + Node tryToFindAdBanner(); + + Node tryToFindGooglePlayBanner(int screenWidth, int screenHeight); + + // Находит кнопку "ЗАПРЕТИТЬ" в системном диалоге запроса разрешения Android + // (com.google.android.permissioncontroller). Возвращает пустой Node если диалог не виден. + Node tryToFindSystemPermissionDenyButton(); + + // Returns a Button node whose text is exactly 4 digits (card last numbers) + Node findCardButtonOnHomeScreen(); + + // Finds a card button whose text ends with the given last 4 digits + Node findCardButtonByLastNumbers(const QString &lastNumbers); + + // Returns all cards visible on the home screen as MaterialInfo stubs + // (lastNumbers, description, amount filled; id=-1, cardNumber empty) + QList parseCardsOnHomeScreen(); + + bool isProfileScreen(); + + Node findUserNameOnHomeScreen(int screenHeight); + + QString parseProfileFullName(); + + QString parseProfilePhone(); + + QList tryToFindPinCode(const QString &pinCode, const QString &title); + + QList getLast2DaysTransactions(); + + void parseAndSaveXml(const QString &xml); + + TransactionInfo parseTransactionInfoHistory(int screenWidth, int screenHeight); + TransactionInfo parseTransactionInfo(int screenWidth, int screenHeight); + + QList parseTodayAndYesterdayReportHistory(); + + QList parseTodayAndYesterdayHistory(); + + + QList > parseAccountsInfo(); + + // Finds a TextView with full card number (format "XXXX XXXX XXXX XXXX") + QString findFullCardNumber(); + + // Кнопка "Основной счёт X ₽" на главном экране + Node findMainAccountButton(); + + // Кнопка/текст "Посмотреть реквизиты" на экране "Основной счёт" + Node findShowRequisitesButton(); + + // Экран "Реквизиты счёта" — заголовок text="Реквизиты счёта" + bool isAccountRequisitesScreen(); + + // Парсит "Номер счёта получателя <20 цифр>" из текстовой ноды + QString parseAccountNumberFromRequisites(); + + bool isTransactionListScreen(); + + // Возвращает кнопки транзакций на экране "Операции" (до maxCount штук) + QList findTransactionButtons(int maxCount); + + // Ozon WebView рендерит tappable-Button только для видимых транзакций. + // Off-screen элементы остаются в accessibility-tree как android.view.View + // с "схлопнутыми" bounds (height == 0). Если такие View с ₽-знаком есть — + // значит в списке осталось что прокручивать. Используется для детекции + // того, что скролл не завершён. + bool hasCollapsedTransactionsBelow(int screenHeight); + + // Парсит имя и сумму из текста кнопки: "NAME + 150 ₽ CATEGORY" + TransactionInfo parseTransactionButtonText(const QString &text); + + // Парсит детали транзакции: дату, имя отправителя, телефон + TransactionInfo parseTransactionDetail(); + + void test(); + + [[nodiscard]] const QList &getNodes() const { return m_nodes; } + +private: + QList m_nodes; + QDomElement m_xml; + + QList findPinCodeNodes(const QString &pinCode); +}; + +} // namespace Ozon \ No newline at end of file diff --git a/arc.ico b/arc.ico new file mode 100644 index 0000000..59b0e72 Binary files /dev/null and b/arc.ico differ diff --git a/arc.png b/arc.png new file mode 100644 index 0000000..a0be21c Binary files /dev/null and b/arc.png differ diff --git a/assets/ADBKeyboard.apk b/assets/ADBKeyboard.apk new file mode 100644 index 0000000..6e54ba9 Binary files /dev/null and b/assets/ADBKeyboard.apk differ diff --git a/assets/alpha_contact_permission.xml b/assets/alpha_contact_permission.xml new file mode 100644 index 0000000..0855e70 --- /dev/null +++ b/assets/alpha_contact_permission.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/black/accounts.png b/assets/black/accounts.png new file mode 100644 index 0000000..d33cf8a Binary files /dev/null and b/assets/black/accounts.png differ diff --git a/assets/black/accounts.xml b/assets/black/accounts.xml new file mode 100644 index 0000000..8e5ab51 --- /dev/null +++ b/assets/black/accounts.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/black/home_screen.png b/assets/black/home_screen.png new file mode 100644 index 0000000..dfb18f6 Binary files /dev/null and b/assets/black/home_screen.png differ diff --git a/assets/black/home_screen.xml b/assets/black/home_screen.xml new file mode 100644 index 0000000..dcf8876 --- /dev/null +++ b/assets/black/home_screen.xml @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/black/pay/pay_confirm.png b/assets/black/pay/pay_confirm.png new file mode 100644 index 0000000..7658b4d Binary files /dev/null and b/assets/black/pay/pay_confirm.png differ diff --git a/assets/black/pay/pay_confirm.xml b/assets/black/pay/pay_confirm.xml new file mode 100644 index 0000000..03f40a9 --- /dev/null +++ b/assets/black/pay/pay_confirm.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/black/pay/pay_input_card_number.png b/assets/black/pay/pay_input_card_number.png new file mode 100644 index 0000000..711dc07 Binary files /dev/null and b/assets/black/pay/pay_input_card_number.png differ diff --git a/assets/black/pay/pay_input_card_number.xml b/assets/black/pay/pay_input_card_number.xml new file mode 100644 index 0000000..402bdbf --- /dev/null +++ b/assets/black/pay/pay_input_card_number.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/black/pay/pay_input_sum.png b/assets/black/pay/pay_input_sum.png new file mode 100644 index 0000000..8f0f2b5 Binary files /dev/null and b/assets/black/pay/pay_input_sum.png differ diff --git a/assets/black/pay/pay_input_sum.xml b/assets/black/pay/pay_input_sum.xml new file mode 100644 index 0000000..0cbe2db --- /dev/null +++ b/assets/black/pay/pay_input_sum.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/black/pay/pay_success.png b/assets/black/pay/pay_success.png new file mode 100644 index 0000000..8c5d151 Binary files /dev/null and b/assets/black/pay/pay_success.png differ diff --git a/assets/black/pay/pay_success.xml b/assets/black/pay/pay_success.xml new file mode 100644 index 0000000..c3d77d5 --- /dev/null +++ b/assets/black/pay/pay_success.xml @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/black/pay/pay_success_document.png b/assets/black/pay/pay_success_document.png new file mode 100644 index 0000000..f592a1b Binary files /dev/null and b/assets/black/pay/pay_success_document.png differ diff --git a/assets/black/pay/pay_success_document.xml b/assets/black/pay/pay_success_document.xml new file mode 100644 index 0000000..edf5198 --- /dev/null +++ b/assets/black/pay/pay_success_document.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/black/pay/pay_transfer_confirm.png b/assets/black/pay/pay_transfer_confirm.png new file mode 100644 index 0000000..d21a9c4 Binary files /dev/null and b/assets/black/pay/pay_transfer_confirm.png differ diff --git a/assets/black/pay/pay_transfer_confirm.xml b/assets/black/pay/pay_transfer_confirm.xml new file mode 100644 index 0000000..1825f61 --- /dev/null +++ b/assets/black/pay/pay_transfer_confirm.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/black/pin_code.png b/assets/black/pin_code.png new file mode 100644 index 0000000..b3647ff Binary files /dev/null and b/assets/black/pin_code.png differ diff --git a/assets/black/pin_code.xml b/assets/black/pin_code.xml new file mode 100644 index 0000000..72c21dc --- /dev/null +++ b/assets/black/pin_code.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/black/profile_page.png b/assets/black/profile_page.png new file mode 100644 index 0000000..9d55147 Binary files /dev/null and b/assets/black/profile_page.png differ diff --git a/assets/black/profile_page.xml b/assets/black/profile_page.xml new file mode 100644 index 0000000..0911197 --- /dev/null +++ b/assets/black/profile_page.xml @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/black/profile_page_2.png b/assets/black/profile_page_2.png new file mode 100644 index 0000000..708799b Binary files /dev/null and b/assets/black/profile_page_2.png differ diff --git a/assets/black/profile_page_2.xml b/assets/black/profile_page_2.xml new file mode 100644 index 0000000..14df9dc --- /dev/null +++ b/assets/black/profile_page_2.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/black/side_menu.png b/assets/black/side_menu.png new file mode 100644 index 0000000..8e05b93 Binary files /dev/null and b/assets/black/side_menu.png differ diff --git a/assets/black/side_menu.xml b/assets/black/side_menu.xml new file mode 100644 index 0000000..d94bd68 --- /dev/null +++ b/assets/black/side_menu.xml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/black/transactions_loading_screen.xml b/assets/black/transactions_loading_screen.xml new file mode 100644 index 0000000..2934fee --- /dev/null +++ b/assets/black/transactions_loading_screen.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/black/transactions_screen.png b/assets/black/transactions_screen.png new file mode 100644 index 0000000..418d406 Binary files /dev/null and b/assets/black/transactions_screen.png differ diff --git a/assets/black/transactions_screen.xml b/assets/black/transactions_screen.xml new file mode 100644 index 0000000..a4415a0 --- /dev/null +++ b/assets/black/transactions_screen.xml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/capture.sh b/assets/capture.sh new file mode 100755 index 0000000..2230c0c --- /dev/null +++ b/assets/capture.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Проверка аргумента +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +NAME="$1" +DEVICE_ID="6edc4a47" + +# Снимок UI Automator +adb -s "$DEVICE_ID" shell 'uiautomator dump /sdcard/uidump.xml' && \ +adb -s "$DEVICE_ID" pull /sdcard/uidump.xml "${NAME}.xml" && \ +adb -s "$DEVICE_ID" shell 'rm /sdcard/uidump.xml' + +# Скриншот экрана +adb -s "$DEVICE_ID" shell 'screencap -p /sdcard/screen.png' && \ +adb -s "$DEVICE_ID" pull /sdcard/screen.png "${NAME}.png" && \ +adb -s "$DEVICE_ID" shell 'rm /sdcard/screen.png' + diff --git a/assets/dushanbe/3.2.33/card_detail.jpg b/assets/dushanbe/3.2.33/card_detail.jpg new file mode 100644 index 0000000..da9231e Binary files /dev/null and b/assets/dushanbe/3.2.33/card_detail.jpg differ diff --git a/assets/dushanbe/3.2.33/card_detail.xml b/assets/dushanbe/3.2.33/card_detail.xml new file mode 100644 index 0000000..1f84e9e --- /dev/null +++ b/assets/dushanbe/3.2.33/card_detail.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/card_transfer_confirm.jpg b/assets/dushanbe/3.2.33/card_transfer_confirm.jpg new file mode 100644 index 0000000..aa5cd06 Binary files /dev/null and b/assets/dushanbe/3.2.33/card_transfer_confirm.jpg differ diff --git a/assets/dushanbe/3.2.33/card_transfer_confirm.xml b/assets/dushanbe/3.2.33/card_transfer_confirm.xml new file mode 100644 index 0000000..a3a46f6 --- /dev/null +++ b/assets/dushanbe/3.2.33/card_transfer_confirm.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/card_transfer_confirm_2.jpg b/assets/dushanbe/3.2.33/card_transfer_confirm_2.jpg new file mode 100644 index 0000000..821cf9e Binary files /dev/null and b/assets/dushanbe/3.2.33/card_transfer_confirm_2.jpg differ diff --git a/assets/dushanbe/3.2.33/card_transfer_confirm_2.xml b/assets/dushanbe/3.2.33/card_transfer_confirm_2.xml new file mode 100644 index 0000000..be76870 --- /dev/null +++ b/assets/dushanbe/3.2.33/card_transfer_confirm_2.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/card_transfer_input.jpg b/assets/dushanbe/3.2.33/card_transfer_input.jpg new file mode 100644 index 0000000..9921626 Binary files /dev/null and b/assets/dushanbe/3.2.33/card_transfer_input.jpg differ diff --git a/assets/dushanbe/3.2.33/card_transfer_input.xml b/assets/dushanbe/3.2.33/card_transfer_input.xml new file mode 100644 index 0000000..f6b537f --- /dev/null +++ b/assets/dushanbe/3.2.33/card_transfer_input.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/card_transfer_success.jpg b/assets/dushanbe/3.2.33/card_transfer_success.jpg new file mode 100644 index 0000000..595c632 Binary files /dev/null and b/assets/dushanbe/3.2.33/card_transfer_success.jpg differ diff --git a/assets/dushanbe/3.2.33/card_transfer_success.xml b/assets/dushanbe/3.2.33/card_transfer_success.xml new file mode 100644 index 0000000..4b62c7f --- /dev/null +++ b/assets/dushanbe/3.2.33/card_transfer_success.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/card_transfer_tx_detail.jpg b/assets/dushanbe/3.2.33/card_transfer_tx_detail.jpg new file mode 100644 index 0000000..5721895 Binary files /dev/null and b/assets/dushanbe/3.2.33/card_transfer_tx_detail.jpg differ diff --git a/assets/dushanbe/3.2.33/card_transfer_tx_detail.xml b/assets/dushanbe/3.2.33/card_transfer_tx_detail.xml new file mode 100644 index 0000000..465c902 --- /dev/null +++ b/assets/dushanbe/3.2.33/card_transfer_tx_detail.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/cards_screen.jpg b/assets/dushanbe/3.2.33/cards_screen.jpg new file mode 100644 index 0000000..0ff9ccb Binary files /dev/null and b/assets/dushanbe/3.2.33/cards_screen.jpg differ diff --git a/assets/dushanbe/3.2.33/cards_screen.xml b/assets/dushanbe/3.2.33/cards_screen.xml new file mode 100644 index 0000000..e7b77cb --- /dev/null +++ b/assets/dushanbe/3.2.33/cards_screen.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/history_screen.jpg b/assets/dushanbe/3.2.33/history_screen.jpg new file mode 100644 index 0000000..bd6145b Binary files /dev/null and b/assets/dushanbe/3.2.33/history_screen.jpg differ diff --git a/assets/dushanbe/3.2.33/history_screen.xml b/assets/dushanbe/3.2.33/history_screen.xml new file mode 100644 index 0000000..75852e7 --- /dev/null +++ b/assets/dushanbe/3.2.33/history_screen.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/home_screen.jpg b/assets/dushanbe/3.2.33/home_screen.jpg new file mode 100644 index 0000000..9520cb2 Binary files /dev/null and b/assets/dushanbe/3.2.33/home_screen.jpg differ diff --git a/assets/dushanbe/3.2.33/home_screen.xml b/assets/dushanbe/3.2.33/home_screen.xml new file mode 100644 index 0000000..6deef18 --- /dev/null +++ b/assets/dushanbe/3.2.33/home_screen.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/launch_screen.jpg b/assets/dushanbe/3.2.33/launch_screen.jpg new file mode 100644 index 0000000..1959e7e Binary files /dev/null and b/assets/dushanbe/3.2.33/launch_screen.jpg differ diff --git a/assets/dushanbe/3.2.33/launch_screen.xml b/assets/dushanbe/3.2.33/launch_screen.xml new file mode 100644 index 0000000..f9e1e85 --- /dev/null +++ b/assets/dushanbe/3.2.33/launch_screen.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/phone_transfer_confirm.jpg b/assets/dushanbe/3.2.33/phone_transfer_confirm.jpg new file mode 100644 index 0000000..18b37db Binary files /dev/null and b/assets/dushanbe/3.2.33/phone_transfer_confirm.jpg differ diff --git a/assets/dushanbe/3.2.33/phone_transfer_confirm.xml b/assets/dushanbe/3.2.33/phone_transfer_confirm.xml new file mode 100644 index 0000000..8fdb3db --- /dev/null +++ b/assets/dushanbe/3.2.33/phone_transfer_confirm.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/phone_transfer_input.jpg b/assets/dushanbe/3.2.33/phone_transfer_input.jpg new file mode 100644 index 0000000..97752f9 Binary files /dev/null and b/assets/dushanbe/3.2.33/phone_transfer_input.jpg differ diff --git a/assets/dushanbe/3.2.33/phone_transfer_input.xml b/assets/dushanbe/3.2.33/phone_transfer_input.xml new file mode 100644 index 0000000..e69407b --- /dev/null +++ b/assets/dushanbe/3.2.33/phone_transfer_input.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/phone_transfer_success.jpg b/assets/dushanbe/3.2.33/phone_transfer_success.jpg new file mode 100644 index 0000000..af35587 Binary files /dev/null and b/assets/dushanbe/3.2.33/phone_transfer_success.jpg differ diff --git a/assets/dushanbe/3.2.33/phone_transfer_success.xml b/assets/dushanbe/3.2.33/phone_transfer_success.xml new file mode 100644 index 0000000..b45518d --- /dev/null +++ b/assets/dushanbe/3.2.33/phone_transfer_success.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/side_menu.jpg b/assets/dushanbe/3.2.33/side_menu.jpg new file mode 100644 index 0000000..b3f5ee8 Binary files /dev/null and b/assets/dushanbe/3.2.33/side_menu.jpg differ diff --git a/assets/dushanbe/3.2.33/side_menu.xml b/assets/dushanbe/3.2.33/side_menu.xml new file mode 100644 index 0000000..69a71e5 --- /dev/null +++ b/assets/dushanbe/3.2.33/side_menu.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/transfers_screen.jpg b/assets/dushanbe/3.2.33/transfers_screen.jpg new file mode 100644 index 0000000..cf1420f Binary files /dev/null and b/assets/dushanbe/3.2.33/transfers_screen.jpg differ diff --git a/assets/dushanbe/3.2.33/transfers_screen.xml b/assets/dushanbe/3.2.33/transfers_screen.xml new file mode 100644 index 0000000..2ed8618 --- /dev/null +++ b/assets/dushanbe/3.2.33/transfers_screen.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/3.2.33/tx_detail.jpg b/assets/dushanbe/3.2.33/tx_detail.jpg new file mode 100644 index 0000000..d55a1cd Binary files /dev/null and b/assets/dushanbe/3.2.33/tx_detail.jpg differ diff --git a/assets/dushanbe/3.2.33/tx_detail.xml b/assets/dushanbe/3.2.33/tx_detail.xml new file mode 100644 index 0000000..5a0096d --- /dev/null +++ b/assets/dushanbe/3.2.33/tx_detail.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dushanbe/card_detail.png b/assets/dushanbe/card_detail.png new file mode 100644 index 0000000..ee5f9a6 Binary files /dev/null and b/assets/dushanbe/card_detail.png differ diff --git a/assets/dushanbe/card_detail.xml b/assets/dushanbe/card_detail.xml new file mode 100644 index 0000000..e138aaf --- /dev/null +++ b/assets/dushanbe/card_detail.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/card_detail_2.png b/assets/dushanbe/card_detail_2.png new file mode 100644 index 0000000..6acb18e Binary files /dev/null and b/assets/dushanbe/card_detail_2.png differ diff --git a/assets/dushanbe/card_detail_2.xml b/assets/dushanbe/card_detail_2.xml new file mode 100644 index 0000000..395c68c --- /dev/null +++ b/assets/dushanbe/card_detail_2.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/card_transfer_confirm.png b/assets/dushanbe/card_transfer_confirm.png new file mode 100644 index 0000000..855fcbe Binary files /dev/null and b/assets/dushanbe/card_transfer_confirm.png differ diff --git a/assets/dushanbe/card_transfer_confirm.xml b/assets/dushanbe/card_transfer_confirm.xml new file mode 100644 index 0000000..172de9b --- /dev/null +++ b/assets/dushanbe/card_transfer_confirm.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/card_transfer_confirm_detail.png b/assets/dushanbe/card_transfer_confirm_detail.png new file mode 100644 index 0000000..fb7f313 Binary files /dev/null and b/assets/dushanbe/card_transfer_confirm_detail.png differ diff --git a/assets/dushanbe/card_transfer_confirm_detail.xml b/assets/dushanbe/card_transfer_confirm_detail.xml new file mode 100644 index 0000000..17ff53c --- /dev/null +++ b/assets/dushanbe/card_transfer_confirm_detail.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/card_transfer_input.png b/assets/dushanbe/card_transfer_input.png new file mode 100644 index 0000000..88fa6c9 Binary files /dev/null and b/assets/dushanbe/card_transfer_input.png differ diff --git a/assets/dushanbe/card_transfer_input.xml b/assets/dushanbe/card_transfer_input.xml new file mode 100644 index 0000000..3f04567 --- /dev/null +++ b/assets/dushanbe/card_transfer_input.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/card_transfer_success.png b/assets/dushanbe/card_transfer_success.png new file mode 100644 index 0000000..03b7385 Binary files /dev/null and b/assets/dushanbe/card_transfer_success.png differ diff --git a/assets/dushanbe/card_transfer_success.xml b/assets/dushanbe/card_transfer_success.xml new file mode 100644 index 0000000..7c9e25a --- /dev/null +++ b/assets/dushanbe/card_transfer_success.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/card_transfer_tx_detail.png b/assets/dushanbe/card_transfer_tx_detail.png new file mode 100644 index 0000000..394b2be Binary files /dev/null and b/assets/dushanbe/card_transfer_tx_detail.png differ diff --git a/assets/dushanbe/card_transfer_tx_detail.xml b/assets/dushanbe/card_transfer_tx_detail.xml new file mode 100644 index 0000000..9d15d7b --- /dev/null +++ b/assets/dushanbe/card_transfer_tx_detail.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/cards_screen.png b/assets/dushanbe/cards_screen.png new file mode 100644 index 0000000..fbc6b5c Binary files /dev/null and b/assets/dushanbe/cards_screen.png differ diff --git a/assets/dushanbe/cards_screen.xml b/assets/dushanbe/cards_screen.xml new file mode 100644 index 0000000..c7f0c49 --- /dev/null +++ b/assets/dushanbe/cards_screen.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/history_screen.png b/assets/dushanbe/history_screen.png new file mode 100644 index 0000000..29e189d Binary files /dev/null and b/assets/dushanbe/history_screen.png differ diff --git a/assets/dushanbe/history_screen.xml b/assets/dushanbe/history_screen.xml new file mode 100644 index 0000000..f178473 --- /dev/null +++ b/assets/dushanbe/history_screen.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/home_screen.png b/assets/dushanbe/home_screen.png new file mode 100644 index 0000000..1676921 Binary files /dev/null and b/assets/dushanbe/home_screen.png differ diff --git a/assets/dushanbe/home_screen.xml b/assets/dushanbe/home_screen.xml new file mode 100644 index 0000000..6da6324 --- /dev/null +++ b/assets/dushanbe/home_screen.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/launch_screen.png b/assets/dushanbe/launch_screen.png new file mode 100644 index 0000000..65d8a33 Binary files /dev/null and b/assets/dushanbe/launch_screen.png differ diff --git a/assets/dushanbe/launch_screen.xml b/assets/dushanbe/launch_screen.xml new file mode 100644 index 0000000..2dc70d5 --- /dev/null +++ b/assets/dushanbe/launch_screen.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/phone_transfer_confirm.png b/assets/dushanbe/phone_transfer_confirm.png new file mode 100644 index 0000000..dd97ac9 Binary files /dev/null and b/assets/dushanbe/phone_transfer_confirm.png differ diff --git a/assets/dushanbe/phone_transfer_confirm.xml b/assets/dushanbe/phone_transfer_confirm.xml new file mode 100644 index 0000000..2e492f8 --- /dev/null +++ b/assets/dushanbe/phone_transfer_confirm.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/phone_transfer_input.png b/assets/dushanbe/phone_transfer_input.png new file mode 100644 index 0000000..b3248a1 Binary files /dev/null and b/assets/dushanbe/phone_transfer_input.png differ diff --git a/assets/dushanbe/phone_transfer_input.xml b/assets/dushanbe/phone_transfer_input.xml new file mode 100644 index 0000000..84e92c1 --- /dev/null +++ b/assets/dushanbe/phone_transfer_input.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/phone_transfer_success.png b/assets/dushanbe/phone_transfer_success.png new file mode 100644 index 0000000..7013b24 Binary files /dev/null and b/assets/dushanbe/phone_transfer_success.png differ diff --git a/assets/dushanbe/phone_transfer_success.xml b/assets/dushanbe/phone_transfer_success.xml new file mode 100644 index 0000000..baa36a2 --- /dev/null +++ b/assets/dushanbe/phone_transfer_success.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/side_menu.png b/assets/dushanbe/side_menu.png new file mode 100644 index 0000000..16fcfd8 Binary files /dev/null and b/assets/dushanbe/side_menu.png differ diff --git a/assets/dushanbe/side_menu.xml b/assets/dushanbe/side_menu.xml new file mode 100644 index 0000000..fab331e --- /dev/null +++ b/assets/dushanbe/side_menu.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/transfers_screen.png b/assets/dushanbe/transfers_screen.png new file mode 100644 index 0000000..9cc040d Binary files /dev/null and b/assets/dushanbe/transfers_screen.png differ diff --git a/assets/dushanbe/transfers_screen.xml b/assets/dushanbe/transfers_screen.xml new file mode 100644 index 0000000..30ddbf1 --- /dev/null +++ b/assets/dushanbe/transfers_screen.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/dushanbe/tx_detail.png b/assets/dushanbe/tx_detail.png new file mode 100644 index 0000000..be66fdd Binary files /dev/null and b/assets/dushanbe/tx_detail.png differ diff --git a/assets/dushanbe/tx_detail.xml b/assets/dushanbe/tx_detail.xml new file mode 100644 index 0000000..e919b60 --- /dev/null +++ b/assets/dushanbe/tx_detail.xml @@ -0,0 +1 @@ +UI hierchary dumped to: /dev/tty diff --git a/assets/error.png b/assets/error.png new file mode 100644 index 0000000..e10d2ce Binary files /dev/null and b/assets/error.png differ diff --git a/assets/ozon/ask_biometry.png b/assets/ozon/ask_biometry.png new file mode 100644 index 0000000..2f7ba83 Binary files /dev/null and b/assets/ozon/ask_biometry.png differ diff --git a/assets/ozon/ask_biometry.xml b/assets/ozon/ask_biometry.xml new file mode 100644 index 0000000..fc8bfed --- /dev/null +++ b/assets/ozon/ask_biometry.xml @@ -0,0 +1,450 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/banner_ipoteka.png b/assets/ozon/banner_ipoteka.png new file mode 100644 index 0000000..f437b28 Binary files /dev/null and b/assets/ozon/banner_ipoteka.png differ diff --git a/assets/ozon/banner_ipoteka.xml b/assets/ozon/banner_ipoteka.xml new file mode 100644 index 0000000..ae74225 --- /dev/null +++ b/assets/ozon/banner_ipoteka.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ozon/card_banner.png b/assets/ozon/card_banner.png new file mode 100644 index 0000000..78622d9 Binary files /dev/null and b/assets/ozon/card_banner.png differ diff --git a/assets/ozon/card_banner.xml b/assets/ozon/card_banner.xml new file mode 100644 index 0000000..c015962 --- /dev/null +++ b/assets/ozon/card_banner.xml @@ -0,0 +1,746 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/card_screen.png b/assets/ozon/card_screen.png new file mode 100644 index 0000000..fffacb6 Binary files /dev/null and b/assets/ozon/card_screen.png differ diff --git a/assets/ozon/card_screen.xml b/assets/ozon/card_screen.xml new file mode 100644 index 0000000..a656a13 --- /dev/null +++ b/assets/ozon/card_screen.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/card_screen_hide.png b/assets/ozon/card_screen_hide.png new file mode 100644 index 0000000..8c23944 Binary files /dev/null and b/assets/ozon/card_screen_hide.png differ diff --git a/assets/ozon/card_screen_hide.xml b/assets/ozon/card_screen_hide.xml new file mode 100644 index 0000000..d40d81f --- /dev/null +++ b/assets/ozon/card_screen_hide.xml @@ -0,0 +1,821 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/card_screen_open.png b/assets/ozon/card_screen_open.png new file mode 100644 index 0000000..be5b92b Binary files /dev/null and b/assets/ozon/card_screen_open.png differ diff --git a/assets/ozon/card_screen_open.xml b/assets/ozon/card_screen_open.xml new file mode 100644 index 0000000..2355089 --- /dev/null +++ b/assets/ozon/card_screen_open.xml @@ -0,0 +1,872 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/google_play.png b/assets/ozon/google_play.png new file mode 100644 index 0000000..427dd62 Binary files /dev/null and b/assets/ozon/google_play.png differ diff --git a/assets/ozon/google_play.xml b/assets/ozon/google_play.xml new file mode 100644 index 0000000..2cc0562 --- /dev/null +++ b/assets/ozon/google_play.xml @@ -0,0 +1,283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/home_ad_1.png b/assets/ozon/home_ad_1.png new file mode 100644 index 0000000..a9ab1f5 Binary files /dev/null and b/assets/ozon/home_ad_1.png differ diff --git a/assets/ozon/home_ad_1.xml b/assets/ozon/home_ad_1.xml new file mode 100644 index 0000000..1d9c436 --- /dev/null +++ b/assets/ozon/home_ad_1.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ozon/home_page.png b/assets/ozon/home_page.png new file mode 100644 index 0000000..a955747 Binary files /dev/null and b/assets/ozon/home_page.png differ diff --git a/assets/ozon/home_page.xml b/assets/ozon/home_page.xml new file mode 100644 index 0000000..f2a7a37 --- /dev/null +++ b/assets/ozon/home_page.xml @@ -0,0 +1,1507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/home_page_2.png b/assets/ozon/home_page_2.png new file mode 100644 index 0000000..7cc4ae0 Binary files /dev/null and b/assets/ozon/home_page_2.png differ diff --git a/assets/ozon/home_page_2.xml b/assets/ozon/home_page_2.xml new file mode 100644 index 0000000..d553587 --- /dev/null +++ b/assets/ozon/home_page_2.xml @@ -0,0 +1,1552 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/last_transactions.png b/assets/ozon/last_transactions.png new file mode 100644 index 0000000..d4b530f Binary files /dev/null and b/assets/ozon/last_transactions.png differ diff --git a/assets/ozon/last_transactions.xml b/assets/ozon/last_transactions.xml new file mode 100644 index 0000000..a96240b --- /dev/null +++ b/assets/ozon/last_transactions.xml @@ -0,0 +1,1105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/pay/confirm_payment.png b/assets/ozon/pay/confirm_payment.png new file mode 100644 index 0000000..e8133f9 Binary files /dev/null and b/assets/ozon/pay/confirm_payment.png differ diff --git a/assets/ozon/pay/confirm_payment.xml b/assets/ozon/pay/confirm_payment.xml new file mode 100644 index 0000000..2918a8a --- /dev/null +++ b/assets/ozon/pay/confirm_payment.xml @@ -0,0 +1,539 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/pay/confirm_payment_error.png b/assets/ozon/pay/confirm_payment_error.png new file mode 100644 index 0000000..f45651c Binary files /dev/null and b/assets/ozon/pay/confirm_payment_error.png differ diff --git a/assets/ozon/pay/confirm_payment_error.xml b/assets/ozon/pay/confirm_payment_error.xml new file mode 100644 index 0000000..c187dca --- /dev/null +++ b/assets/ozon/pay/confirm_payment_error.xml @@ -0,0 +1,698 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/pay/confirm_payment_success.png b/assets/ozon/pay/confirm_payment_success.png new file mode 100644 index 0000000..8555d5e Binary files /dev/null and b/assets/ozon/pay/confirm_payment_success.png differ diff --git a/assets/ozon/pay/confirm_payment_success.xml b/assets/ozon/pay/confirm_payment_success.xml new file mode 100644 index 0000000..cbbff82 --- /dev/null +++ b/assets/ozon/pay/confirm_payment_success.xml @@ -0,0 +1,780 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/pay/pay_buy_phone.png b/assets/ozon/pay/pay_buy_phone.png new file mode 100644 index 0000000..90dacb2 Binary files /dev/null and b/assets/ozon/pay/pay_buy_phone.png differ diff --git a/assets/ozon/pay/pay_buy_phone.xml b/assets/ozon/pay/pay_buy_phone.xml new file mode 100644 index 0000000..3aff34d --- /dev/null +++ b/assets/ozon/pay/pay_buy_phone.xml @@ -0,0 +1,512 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/pay/pay_buy_phone_check_data.png b/assets/ozon/pay/pay_buy_phone_check_data.png new file mode 100644 index 0000000..53b4732 Binary files /dev/null and b/assets/ozon/pay/pay_buy_phone_check_data.png differ diff --git a/assets/ozon/pay/pay_buy_phone_check_data.xml b/assets/ozon/pay/pay_buy_phone_check_data.xml new file mode 100644 index 0000000..418e793 --- /dev/null +++ b/assets/ozon/pay/pay_buy_phone_check_data.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ozon/pay/pay_buy_phone_default_bank.png b/assets/ozon/pay/pay_buy_phone_default_bank.png new file mode 100644 index 0000000..145f9fd Binary files /dev/null and b/assets/ozon/pay/pay_buy_phone_default_bank.png differ diff --git a/assets/ozon/pay/pay_buy_phone_default_bank.xml b/assets/ozon/pay/pay_buy_phone_default_bank.xml new file mode 100644 index 0000000..0b19d91 --- /dev/null +++ b/assets/ozon/pay/pay_buy_phone_default_bank.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ozon/pay/pay_buy_phone_enter_sum.png b/assets/ozon/pay/pay_buy_phone_enter_sum.png new file mode 100644 index 0000000..f4df1e0 Binary files /dev/null and b/assets/ozon/pay/pay_buy_phone_enter_sum.png differ diff --git a/assets/ozon/pay/pay_buy_phone_enter_sum.xml b/assets/ozon/pay/pay_buy_phone_enter_sum.xml new file mode 100644 index 0000000..9be6d28 --- /dev/null +++ b/assets/ozon/pay/pay_buy_phone_enter_sum.xml @@ -0,0 +1,641 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/pay/pay_buy_phone_select_bank.png b/assets/ozon/pay/pay_buy_phone_select_bank.png new file mode 100644 index 0000000..2699522 Binary files /dev/null and b/assets/ozon/pay/pay_buy_phone_select_bank.png differ diff --git a/assets/ozon/pay/pay_buy_phone_select_bank.xml b/assets/ozon/pay/pay_buy_phone_select_bank.xml new file mode 100644 index 0000000..381780d --- /dev/null +++ b/assets/ozon/pay/pay_buy_phone_select_bank.xml @@ -0,0 +1,1290 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/pay/pay_buy_phone_select_bank_1.png b/assets/ozon/pay/pay_buy_phone_select_bank_1.png new file mode 100644 index 0000000..64f7b61 Binary files /dev/null and b/assets/ozon/pay/pay_buy_phone_select_bank_1.png differ diff --git a/assets/ozon/pay/pay_buy_phone_select_bank_1.xml b/assets/ozon/pay/pay_buy_phone_select_bank_1.xml new file mode 100644 index 0000000..3dd8103 --- /dev/null +++ b/assets/ozon/pay/pay_buy_phone_select_bank_1.xml @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/pay/pay_buy_phone_sent_success.png b/assets/ozon/pay/pay_buy_phone_sent_success.png new file mode 100644 index 0000000..09ec8fd Binary files /dev/null and b/assets/ozon/pay/pay_buy_phone_sent_success.png differ diff --git a/assets/ozon/pay/pay_buy_phone_sent_success.xml b/assets/ozon/pay/pay_buy_phone_sent_success.xml new file mode 100644 index 0000000..06bd5b5 --- /dev/null +++ b/assets/ozon/pay/pay_buy_phone_sent_success.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ozon/pay/pay_by_card.png b/assets/ozon/pay/pay_by_card.png new file mode 100644 index 0000000..f1c0c78 Binary files /dev/null and b/assets/ozon/pay/pay_by_card.png differ diff --git a/assets/ozon/pay/pay_by_card.xml b/assets/ozon/pay/pay_by_card.xml new file mode 100644 index 0000000..3e18cf8 --- /dev/null +++ b/assets/ozon/pay/pay_by_card.xml @@ -0,0 +1,820 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/pay/pay_by_card_enter_sum.png b/assets/ozon/pay/pay_by_card_enter_sum.png new file mode 100644 index 0000000..cf5a218 Binary files /dev/null and b/assets/ozon/pay/pay_by_card_enter_sum.png differ diff --git a/assets/ozon/pay/pay_by_card_enter_sum.xml b/assets/ozon/pay/pay_by_card_enter_sum.xml new file mode 100644 index 0000000..21e36cf --- /dev/null +++ b/assets/ozon/pay/pay_by_card_enter_sum.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ozon/pay/pay_by_card_success.png b/assets/ozon/pay/pay_by_card_success.png new file mode 100644 index 0000000..4e1dce4 Binary files /dev/null and b/assets/ozon/pay/pay_by_card_success.png differ diff --git a/assets/ozon/pay/pay_by_card_success.xml b/assets/ozon/pay/pay_by_card_success.xml new file mode 100644 index 0000000..31214a4 --- /dev/null +++ b/assets/ozon/pay/pay_by_card_success.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ozon/pay/pay_to_new_number.png b/assets/ozon/pay/pay_to_new_number.png new file mode 100644 index 0000000..a4629b3 Binary files /dev/null and b/assets/ozon/pay/pay_to_new_number.png differ diff --git a/assets/ozon/pay/pay_to_new_number.xml b/assets/ozon/pay/pay_to_new_number.xml new file mode 100644 index 0000000..15ed612 --- /dev/null +++ b/assets/ozon/pay/pay_to_new_number.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ozon/pay/pay_to_old_number.png b/assets/ozon/pay/pay_to_old_number.png new file mode 100644 index 0000000..7d9553a Binary files /dev/null and b/assets/ozon/pay/pay_to_old_number.png differ diff --git a/assets/ozon/pay/pay_to_old_number.xml b/assets/ozon/pay/pay_to_old_number.xml new file mode 100644 index 0000000..7c22ca8 --- /dev/null +++ b/assets/ozon/pay/pay_to_old_number.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ozon/payments_page.png b/assets/ozon/payments_page.png new file mode 100644 index 0000000..9b6c113 Binary files /dev/null and b/assets/ozon/payments_page.png differ diff --git a/assets/ozon/payments_page.xml b/assets/ozon/payments_page.xml new file mode 100644 index 0000000..72c8cb5 --- /dev/null +++ b/assets/ozon/payments_page.xml @@ -0,0 +1,1492 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/pin_code.png b/assets/ozon/pin_code.png new file mode 100644 index 0000000..6a34cb4 Binary files /dev/null and b/assets/ozon/pin_code.png differ diff --git a/assets/ozon/pin_code.xml b/assets/ozon/pin_code.xml new file mode 100644 index 0000000..62e5eda --- /dev/null +++ b/assets/ozon/pin_code.xml @@ -0,0 +1,1631 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/profile.png b/assets/ozon/profile.png new file mode 100644 index 0000000..ca4c371 Binary files /dev/null and b/assets/ozon/profile.png differ diff --git a/assets/ozon/profile.xml b/assets/ozon/profile.xml new file mode 100644 index 0000000..ec1afd6 --- /dev/null +++ b/assets/ozon/profile.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ozon/select_bank.png b/assets/ozon/select_bank.png new file mode 100644 index 0000000..7840c19 Binary files /dev/null and b/assets/ozon/select_bank.png differ diff --git a/assets/ozon/select_bank.xml b/assets/ozon/select_bank.xml new file mode 100644 index 0000000..a149a99 --- /dev/null +++ b/assets/ozon/select_bank.xml @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/transaction_banner.png b/assets/ozon/transaction_banner.png new file mode 100644 index 0000000..af62a55 Binary files /dev/null and b/assets/ozon/transaction_banner.png differ diff --git a/assets/ozon/transaction_banner.xml b/assets/ozon/transaction_banner.xml new file mode 100644 index 0000000..fece037 --- /dev/null +++ b/assets/ozon/transaction_banner.xml @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/transaction_document.png b/assets/ozon/transaction_document.png new file mode 100644 index 0000000..00281a0 Binary files /dev/null and b/assets/ozon/transaction_document.png differ diff --git a/assets/ozon/transaction_document.xml b/assets/ozon/transaction_document.xml new file mode 100644 index 0000000..9196524 --- /dev/null +++ b/assets/ozon/transaction_document.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/transaction_info.png b/assets/ozon/transaction_info.png new file mode 100644 index 0000000..5c916f5 Binary files /dev/null and b/assets/ozon/transaction_info.png differ diff --git a/assets/ozon/transaction_info.xml b/assets/ozon/transaction_info.xml new file mode 100644 index 0000000..f0589ac --- /dev/null +++ b/assets/ozon/transaction_info.xml @@ -0,0 +1,502 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/ozon/transaction_info_spb.png b/assets/ozon/transaction_info_spb.png new file mode 100644 index 0000000..7e6f092 Binary files /dev/null and b/assets/ozon/transaction_info_spb.png differ diff --git a/assets/ozon/transaction_info_spb.xml b/assets/ozon/transaction_info_spb.xml new file mode 100644 index 0000000..b1e79e9 --- /dev/null +++ b/assets/ozon/transaction_info_spb.xml @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/visualizer/index.html b/assets/visualizer/index.html new file mode 100644 index 0000000..90dc36f --- /dev/null +++ b/assets/visualizer/index.html @@ -0,0 +1,28 @@ + + + + + UI Dump Viewer + + + +
+ + +
+
+
+ screenshot +
+
+
+ + + diff --git a/assets/visualizer/script.js b/assets/visualizer/script.js new file mode 100644 index 0000000..c27511f --- /dev/null +++ b/assets/visualizer/script.js @@ -0,0 +1,70 @@ +let dumpWidth = 1080; +let dumpHeight = 1920; + +document.getElementById('xmlInput').addEventListener('change', handleXml); +document.getElementById('imgInput').addEventListener('change', handleImage); + +function handleImage(event) { + const file = event.target.files[0]; + if (!file) return; + + const url = URL.createObjectURL(file); + document.getElementById('screenshot').src = url; +} + +async function handleXml(event) { + const file = event.target.files[0]; + if (!file) return; + + const text = await file.text(); + const parser = new DOMParser(); + const xml = parser.parseFromString(text, "text/xml"); + + const nodes = Array.from(xml.querySelectorAll("node")); + const canvas = document.getElementById("canvas"); + canvas.innerHTML = ""; + + // Автоопределение размеров дампа + let maxX = 0, maxY = 0; + for (const node of nodes) { + const bounds = node.getAttribute("bounds"); + const match = bounds && bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/); + if (!match) continue; + + const [, , , x2, y2] = match.map(Number); + if (x2 > maxX) maxX = x2; + if (y2 > maxY) maxY = y2; + } + dumpWidth = maxX; + dumpHeight = maxY; + + // Масштабирование по высоте экрана с отступами + const padding = 100; + const displayHeight = window.innerHeight - padding; + const scale = displayHeight / dumpHeight; + const displayWidth = dumpWidth * scale; + + // Установка размеров canvas + canvas.style.width = `${displayWidth}px`; + canvas.style.height = `${displayHeight}px`; + + for (const node of nodes) { + const bounds = node.getAttribute("bounds"); + const match = bounds && bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/); + if (!match) continue; + + const [, x1, y1, x2, y2] = match.map(Number); + + const div = document.createElement("div"); + div.className = "rect"; + div.style.left = `${x1 * scale}px`; + div.style.top = `${y1 * scale}px`; + div.style.width = `${(x2 - x1) * scale}px`; + div.style.height = `${(y2 - y1) * scale}px`; + + const text = (node.getAttribute("text") || '').trim(); + if (text) div.innerText = text; + + canvas.appendChild(div); + } +} diff --git a/assets/visualizer/style.css b/assets/visualizer/style.css new file mode 100644 index 0000000..492c800 --- /dev/null +++ b/assets/visualizer/style.css @@ -0,0 +1,72 @@ +body, html { + margin: 0; + height: 100%; + font-family: sans-serif; + background-color: #f4f4f4; /* вернули светлый фон */ + color: #333; +} + +#layout { + display: flex; + height: 100vh; +} + +#sidebar { + width: 250px; + padding: 20px; + background: #ffffff; /* светлая боковая панель */ + border-right: 1px solid #ccc; + box-sizing: border-box; +} + +#sidebar h3, label { + color: #333; +} + +#sidebar input[type="file"] { + background: #fff; + color: #000; + border: 1px solid #ccc; + padding: 6px; + margin-top: 4px; + width: 100%; +} + +#content { + display: flex; + flex: 1; + align-items: center; + justify-content: space-around; + overflow: auto; + padding: 50px 20px; + box-sizing: border-box; + gap: 20px; + background-color: #f4f4f4; +} + +#canvas { + position: relative; + background: #2a2a2a; /* серый фон области дампа */ + border: 1px solid #444; + flex-shrink: 0; +} + +#screenshot-container { + flex-shrink: 0; +} + +#screenshot { + max-height: calc(100vh - 100px); + border: 1px solid #ccc; +} + +.rect { + position: absolute; + border: 1px dashed #ff9800; + background: transparent; + color: #ff9800; + font-size: 11px; + font-weight: bold; +} + + diff --git a/build_x64.bat b/build_x64.bat new file mode 100644 index 0000000..4c7cf0e --- /dev/null +++ b/build_x64.bat @@ -0,0 +1,85 @@ +@echo off +setlocal enabledelayedexpansion + +set PROJECT_DIR=%~dp0 +set BUILD_DIR=%PROJECT_DIR%build_x64 +set RELEASE_DIR=%BUILD_DIR%\Release +set QT_DIR=C:\Qt\6.9.0\msvc2022_64 +set WINDEPLOYQT=%QT_DIR%\bin\windeployqt.exe +set CONFIG_FILE=%PROJECT_DIR%config.ini +set TG_BOT_TOKEN=8256716069:AAFgZRB_0Y6KTpqFQmCxIlZiWdY5m2dR2D8 +set TG_CHAT_ID=-5177086220 + +for /f "tokens=2 delims==" %%V in ('findstr /b /i "version=" "%CONFIG_FILE%"') do set OLD_VERSION=%%V +set CURRENT_VERSION=%OLD_VERSION% + +if "%~1"=="-up-version" ( + echo === [0] Incrementing build version === + for /f "tokens=1-3 delims=." %%A in ("!OLD_VERSION!") do ( + set MAJOR=%%A + set MINOR=%%B + set /a PATCH=%%C+1 + ) + set CURRENT_VERSION=!MAJOR!.!MINOR!.!PATCH! + powershell -NoProfile -Command "(Get-Content '!CONFIG_FILE!') -replace '^version=!OLD_VERSION!$', 'version=!CURRENT_VERSION!' | Set-Content '!CONFIG_FILE!'" + echo Version: !OLD_VERSION! -^> !CURRENT_VERSION! +) else ( + echo Version: !CURRENT_VERSION! +) + +set ZIP_NAME=ARC_x64_!CURRENT_VERSION!.zip + +echo === [1/5] Configuring CMake for x64 Release === +if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%" +cd /d "%BUILD_DIR%" +cmake .. -G "Visual Studio 17 2022" -A x64 -DCMAKE_PREFIX_PATH="%QT_DIR%" -DCMAKE_BUILD_TYPE=Release +if %ERRORLEVEL% neq 0 ( + echo ERROR: CMake configure failed + exit /b 1 +) + +echo === [2/5] Building Release === +cmake --build . --config Release +if %ERRORLEVEL% neq 0 ( + echo ERROR: Build failed + exit /b 1 +) + +echo === [3/5] Deploying Qt dependencies === +"%WINDEPLOYQT%" "%RELEASE_DIR%\ARC.exe" +if %ERRORLEVEL% neq 0 ( + echo ERROR: windeployqt failed + exit /b 1 +) + +echo === [4/5] Creating zip archive === +cd /d "%PROJECT_DIR%" +if exist "%ZIP_NAME%" del "%ZIP_NAME%" +powershell -NoProfile -Command "Compress-Archive -Path '%RELEASE_DIR%\*' -DestinationPath '%ZIP_NAME%' -Force" +if %ERRORLEVEL% neq 0 ( + echo ERROR: Failed to create zip archive + exit /b 1 +) + +echo === [5/5] Uploading and sending link to Telegram === +for /f "delims=" %%B in ('powershell -NoProfile -Command "[guid]::NewGuid().ToString('N').Substring(0,16)"') do set BIN_ID=arc-%%B +curl -s -H "Content-Type: application/octet-stream" -X POST --data-binary "@%PROJECT_DIR%%ZIP_NAME%" "https://filebin.net/!BIN_ID!/%ZIP_NAME%" > nul +if %ERRORLEVEL% neq 0 ( + echo ERROR: Failed to upload archive + exit /b 1 +) +set DOWNLOAD_URL=https://filebin.net/!BIN_ID!/%ZIP_NAME% +echo Upload URL: !DOWNLOAD_URL! + +curl -s -X POST "https://api.telegram.org/bot%TG_BOT_TOKEN%/sendMessage" -d "chat_id=%TG_CHAT_ID%" -d "text=ARC x64 v!CURRENT_VERSION!: !DOWNLOAD_URL!" +if %ERRORLEVEL% neq 0 ( + echo ERROR: Failed to send message to Telegram + exit /b 1 +) + +echo. +echo === Build complete! === +echo Version: %NEW_VERSION% +echo Archive: %PROJECT_DIR%%ZIP_NAME% +echo Download: !DOWNLOAD_URL! +endlocal diff --git a/build_x64/ALL_BUILD.vcxproj b/build_x64/ALL_BUILD.vcxproj new file mode 100644 index 0000000..1b82f15 --- /dev/null +++ b/build_x64/ALL_BUILD.vcxproj @@ -0,0 +1,185 @@ + + + + ARM64 + + + false + + + + Debug + x64 + + + Release + x64 + + + MinSizeRel + x64 + + + RelWithDebInfo + x64 + + + + {E0AF0033-C4E9-3BF4-A17D-12B2127E85D4} + Win32Proj + 10.0.22621.0 + x64 + ALL_BUILD + NoUpgrade + + + + Utility + MultiByte + v143 + + + Utility + MultiByte + v143 + + + Utility + MultiByte + v143 + + + Utility + MultiByte + v143 + + + + + + + + + + <_ProjectFileVersion>10.0.20506.1 + $(Platform)\$(Configuration)\$(ProjectName)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + + + + %(AdditionalIncludeDirectories) + $(ProjectDir)/$(IntDir) + %(Filename).h + %(Filename).tlb + %(Filename)_i.c + %(Filename)_p.c + + + + + %(AdditionalIncludeDirectories) + $(ProjectDir)/$(IntDir) + %(Filename).h + %(Filename).tlb + %(Filename)_i.c + %(Filename)_p.c + + + + + %(AdditionalIncludeDirectories) + $(ProjectDir)/$(IntDir) + %(Filename).h + %(Filename).tlb + %(Filename)_i.c + %(Filename)_p.c + + + + + %(AdditionalIncludeDirectories) + $(ProjectDir)/$(IntDir) + %(Filename).h + %(Filename).tlb + %(Filename)_i.c + %(Filename)_p.c + + + + + Always + Building Custom Rule C:/Users/User/CLionProjects/ARCDeskProject/CMakeLists.txt + setlocal +"C:\Program Files\CMake\bin\cmake.exe" -SC:/Users/User/CLionProjects/ARCDeskProject -BC:/Users/User/CLionProjects/ARCDeskProject/build_x64 --check-stamp-file C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/generate.stamp +if %errorlevel% neq 0 goto :cmEnd +:cmEnd +endlocal & call :cmErrorLevel %errorlevel% & goto :cmDone +:cmErrorLevel +exit /b %1 +:cmDone +if %errorlevel% neq 0 goto :VCEnd + C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCXXInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCheckCompilerFlagCommonPatterns.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCommonLanguageInclude.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeFindDependencyMacro.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeGenericSystem.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeInitializeConfigs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeLanguageInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeRCInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeSystemSpecificInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeSystemSpecificInitialize.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckCXXCompilerFlag.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckCXXSourceCompiles.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckIncludeFileCXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckLibraryExists.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\CMakeCommonCompilerMacros.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindCURL.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindLibArchive.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindPackageHandleStandardArgs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindPackageMessage.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindThreads.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindVulkan.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindZLIB.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\GNUInstallDirs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CMakeCXXLinkerInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CMakeCommonLinkerInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckCompilerFlag.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckFlagCommonConfig.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckSourceCompiles.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Linker\MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Linker\MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Linker\Windows-MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Linker\Windows-MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-Initialize.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\WindowsPaths.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\SelectLibraryConfigurations.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\FindWrapAtomic.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\FindWrapVulkanHeaders.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Config.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigExtras.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Dependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Targets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6VersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtFeature.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtFeatureCommon.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtInstallPaths.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicAndroidHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicAppleHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicCMakeHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicCMakeVersionHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicDependencyHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicExternalProjectHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicFinalizerHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicFindPackageHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicGitHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicPluginHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicPluginHelpers_v2.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomAttributionHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomCpeHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomDepHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomFileHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomGenerationHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomLicenseHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomOpsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomPurlHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomPythonHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomQtEntityHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomSystemDepHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicTargetHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicTestHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicToolHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicWalkLibsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicWindowsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigExtras.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreMacros.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsMacros.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateVersionlessAliasTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaConfig-version.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeCXXCompiler.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeRCCompiler.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeSystem.cmake;%(AdditionalInputs) + C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\generate.stamp + false + Building Custom Rule C:/Users/User/CLionProjects/ARCDeskProject/CMakeLists.txt + setlocal +"C:\Program Files\CMake\bin\cmake.exe" -SC:/Users/User/CLionProjects/ARCDeskProject -BC:/Users/User/CLionProjects/ARCDeskProject/build_x64 --check-stamp-file C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/generate.stamp +if %errorlevel% neq 0 goto :cmEnd +:cmEnd +endlocal & call :cmErrorLevel %errorlevel% & goto :cmDone +:cmErrorLevel +exit /b %1 +:cmDone +if %errorlevel% neq 0 goto :VCEnd + C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCXXInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCheckCompilerFlagCommonPatterns.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCommonLanguageInclude.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeFindDependencyMacro.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeGenericSystem.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeInitializeConfigs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeLanguageInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeRCInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeSystemSpecificInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeSystemSpecificInitialize.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckCXXCompilerFlag.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckCXXSourceCompiles.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckIncludeFileCXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckLibraryExists.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\CMakeCommonCompilerMacros.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindCURL.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindLibArchive.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindPackageHandleStandardArgs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindPackageMessage.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindThreads.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindVulkan.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindZLIB.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\GNUInstallDirs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CMakeCXXLinkerInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CMakeCommonLinkerInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckCompilerFlag.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckFlagCommonConfig.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckSourceCompiles.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Linker\MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Linker\MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Linker\Windows-MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Linker\Windows-MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-Initialize.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\WindowsPaths.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\SelectLibraryConfigurations.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\FindWrapAtomic.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\FindWrapVulkanHeaders.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Config.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigExtras.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Dependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Targets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6VersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtFeature.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtFeatureCommon.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtInstallPaths.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicAndroidHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicAppleHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicCMakeHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicCMakeVersionHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicDependencyHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicExternalProjectHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicFinalizerHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicFindPackageHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicGitHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicPluginHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicPluginHelpers_v2.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomAttributionHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomCpeHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomDepHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomFileHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomGenerationHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomLicenseHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomOpsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomPurlHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomPythonHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomQtEntityHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomSystemDepHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicTargetHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicTestHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicToolHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicWalkLibsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicWindowsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigExtras.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreMacros.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsMacros.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateVersionlessAliasTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaConfig-version.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeCXXCompiler.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeRCCompiler.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeSystem.cmake;%(AdditionalInputs) + C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\generate.stamp + false + Building Custom Rule C:/Users/User/CLionProjects/ARCDeskProject/CMakeLists.txt + setlocal +"C:\Program Files\CMake\bin\cmake.exe" -SC:/Users/User/CLionProjects/ARCDeskProject -BC:/Users/User/CLionProjects/ARCDeskProject/build_x64 --check-stamp-file C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/generate.stamp +if %errorlevel% neq 0 goto :cmEnd +:cmEnd +endlocal & call :cmErrorLevel %errorlevel% & goto :cmDone +:cmErrorLevel +exit /b %1 +:cmDone +if %errorlevel% neq 0 goto :VCEnd + C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCXXInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCheckCompilerFlagCommonPatterns.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCommonLanguageInclude.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeFindDependencyMacro.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeGenericSystem.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeInitializeConfigs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeLanguageInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeRCInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeSystemSpecificInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeSystemSpecificInitialize.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckCXXCompilerFlag.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckCXXSourceCompiles.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckIncludeFileCXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckLibraryExists.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\CMakeCommonCompilerMacros.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindCURL.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindLibArchive.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindPackageHandleStandardArgs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindPackageMessage.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindThreads.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindVulkan.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindZLIB.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\GNUInstallDirs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CMakeCXXLinkerInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CMakeCommonLinkerInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckCompilerFlag.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckFlagCommonConfig.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckSourceCompiles.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Linker\MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Linker\MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Linker\Windows-MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Linker\Windows-MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-Initialize.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\WindowsPaths.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\SelectLibraryConfigurations.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\FindWrapAtomic.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\FindWrapVulkanHeaders.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Config.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigExtras.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Dependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Targets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6VersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtFeature.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtFeatureCommon.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtInstallPaths.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicAndroidHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicAppleHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicCMakeHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicCMakeVersionHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicDependencyHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicExternalProjectHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicFinalizerHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicFindPackageHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicGitHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicPluginHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicPluginHelpers_v2.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomAttributionHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomCpeHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomDepHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomFileHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomGenerationHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomLicenseHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomOpsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomPurlHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomPythonHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomQtEntityHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomSystemDepHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicTargetHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicTestHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicToolHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicWalkLibsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicWindowsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigExtras.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreMacros.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsMacros.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateVersionlessAliasTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaConfig-version.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeCXXCompiler.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeRCCompiler.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeSystem.cmake;%(AdditionalInputs) + C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\generate.stamp + false + Building Custom Rule C:/Users/User/CLionProjects/ARCDeskProject/CMakeLists.txt + setlocal +"C:\Program Files\CMake\bin\cmake.exe" -SC:/Users/User/CLionProjects/ARCDeskProject -BC:/Users/User/CLionProjects/ARCDeskProject/build_x64 --check-stamp-file C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/generate.stamp +if %errorlevel% neq 0 goto :cmEnd +:cmEnd +endlocal & call :cmErrorLevel %errorlevel% & goto :cmDone +:cmErrorLevel +exit /b %1 +:cmDone +if %errorlevel% neq 0 goto :VCEnd + C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCXXInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCheckCompilerFlagCommonPatterns.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCommonLanguageInclude.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeFindDependencyMacro.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeGenericSystem.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeInitializeConfigs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeLanguageInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeRCInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeSystemSpecificInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeSystemSpecificInitialize.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckCXXCompilerFlag.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckCXXSourceCompiles.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckIncludeFileCXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckLibraryExists.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\CMakeCommonCompilerMacros.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindCURL.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindLibArchive.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindPackageHandleStandardArgs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindPackageMessage.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindThreads.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindVulkan.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindZLIB.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\GNUInstallDirs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CMakeCXXLinkerInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CMakeCommonLinkerInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckCompilerFlag.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckFlagCommonConfig.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckSourceCompiles.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Linker\MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Linker\MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Linker\Windows-MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Linker\Windows-MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-Initialize.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\WindowsPaths.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\SelectLibraryConfigurations.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\FindWrapAtomic.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\FindWrapVulkanHeaders.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Config.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigExtras.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Dependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Targets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6VersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtFeature.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtFeatureCommon.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtInstallPaths.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicAndroidHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicAppleHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicCMakeHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicCMakeVersionHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicDependencyHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicExternalProjectHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicFinalizerHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicFindPackageHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicGitHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicPluginHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicPluginHelpers_v2.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomAttributionHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomCpeHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomDepHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomFileHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomGenerationHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomLicenseHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomOpsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomPurlHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomPythonHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomQtEntityHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomSystemDepHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicTargetHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicTestHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicToolHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicWalkLibsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicWindowsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigExtras.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreMacros.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsMacros.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateVersionlessAliasTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaConfig-version.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeCXXCompiler.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeRCCompiler.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeSystem.cmake;%(AdditionalInputs) + C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\generate.stamp + false + + + + + + + + {4D99233C-723D-3DF3-8D31-21835B437C32} + ZERO_CHECK + false + Never + + + {0DFFB647-077E-3BCD-9EA4-BDCE49AC4298} + ARC + + + + + + \ No newline at end of file diff --git a/build_x64/ALL_BUILD.vcxproj.filters b/build_x64/ALL_BUILD.vcxproj.filters new file mode 100644 index 0000000..9f70252 --- /dev/null +++ b/build_x64/ALL_BUILD.vcxproj.filters @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/build_x64/CMakeFiles/4.0.2/CMakeCXXCompiler.cmake b/build_x64/CMakeFiles/4.0.2/CMakeCXXCompiler.cmake new file mode 100644 index 0000000..decb528 --- /dev/null +++ b/build_x64/CMakeFiles/4.0.2/CMakeCXXCompiler.cmake @@ -0,0 +1,104 @@ +set(CMAKE_CXX_COMPILER "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.43.34808/bin/Hostarm64/x64/cl.exe") +set(CMAKE_CXX_COMPILER_ARG1 "") +set(CMAKE_CXX_COMPILER_ID "MSVC") +set(CMAKE_CXX_COMPILER_VERSION "19.43.34810.0") +set(CMAKE_CXX_COMPILER_VERSION_INTERNAL "") +set(CMAKE_CXX_COMPILER_WRAPPER "") +set(CMAKE_CXX_STANDARD_COMPUTED_DEFAULT "14") +set(CMAKE_CXX_EXTENSIONS_COMPUTED_DEFAULT "OFF") +set(CMAKE_CXX_STANDARD_LATEST "23") +set(CMAKE_CXX_COMPILE_FEATURES "cxx_std_98;cxx_template_template_parameters;cxx_std_11;cxx_alias_templates;cxx_alignas;cxx_alignof;cxx_attributes;cxx_auto_type;cxx_constexpr;cxx_decltype;cxx_decltype_incomplete_return_types;cxx_default_function_template_args;cxx_defaulted_functions;cxx_defaulted_move_initializers;cxx_delegating_constructors;cxx_deleted_functions;cxx_enum_forward_declarations;cxx_explicit_conversions;cxx_extended_friend_declarations;cxx_extern_templates;cxx_final;cxx_func_identifier;cxx_generalized_initializers;cxx_inheriting_constructors;cxx_inline_namespaces;cxx_lambdas;cxx_local_type_template_args;cxx_long_long_type;cxx_noexcept;cxx_nonstatic_member_init;cxx_nullptr;cxx_override;cxx_range_for;cxx_raw_string_literals;cxx_reference_qualified_functions;cxx_right_angle_brackets;cxx_rvalue_references;cxx_sizeof_member;cxx_static_assert;cxx_strong_enums;cxx_thread_local;cxx_trailing_return_types;cxx_unicode_literals;cxx_uniform_initialization;cxx_unrestricted_unions;cxx_user_literals;cxx_variadic_macros;cxx_variadic_templates;cxx_std_14;cxx_aggregate_default_initializers;cxx_attribute_deprecated;cxx_binary_literals;cxx_contextual_conversions;cxx_decltype_auto;cxx_digit_separators;cxx_generic_lambdas;cxx_lambda_init_captures;cxx_relaxed_constexpr;cxx_return_type_deduction;cxx_variable_templates;cxx_std_17;cxx_std_20;cxx_std_23") +set(CMAKE_CXX98_COMPILE_FEATURES "cxx_std_98;cxx_template_template_parameters") +set(CMAKE_CXX11_COMPILE_FEATURES "cxx_std_11;cxx_alias_templates;cxx_alignas;cxx_alignof;cxx_attributes;cxx_auto_type;cxx_constexpr;cxx_decltype;cxx_decltype_incomplete_return_types;cxx_default_function_template_args;cxx_defaulted_functions;cxx_defaulted_move_initializers;cxx_delegating_constructors;cxx_deleted_functions;cxx_enum_forward_declarations;cxx_explicit_conversions;cxx_extended_friend_declarations;cxx_extern_templates;cxx_final;cxx_func_identifier;cxx_generalized_initializers;cxx_inheriting_constructors;cxx_inline_namespaces;cxx_lambdas;cxx_local_type_template_args;cxx_long_long_type;cxx_noexcept;cxx_nonstatic_member_init;cxx_nullptr;cxx_override;cxx_range_for;cxx_raw_string_literals;cxx_reference_qualified_functions;cxx_right_angle_brackets;cxx_rvalue_references;cxx_sizeof_member;cxx_static_assert;cxx_strong_enums;cxx_thread_local;cxx_trailing_return_types;cxx_unicode_literals;cxx_uniform_initialization;cxx_unrestricted_unions;cxx_user_literals;cxx_variadic_macros;cxx_variadic_templates") +set(CMAKE_CXX14_COMPILE_FEATURES "cxx_std_14;cxx_aggregate_default_initializers;cxx_attribute_deprecated;cxx_binary_literals;cxx_contextual_conversions;cxx_decltype_auto;cxx_digit_separators;cxx_generic_lambdas;cxx_lambda_init_captures;cxx_relaxed_constexpr;cxx_return_type_deduction;cxx_variable_templates") +set(CMAKE_CXX17_COMPILE_FEATURES "cxx_std_17") +set(CMAKE_CXX20_COMPILE_FEATURES "cxx_std_20") +set(CMAKE_CXX23_COMPILE_FEATURES "cxx_std_23") +set(CMAKE_CXX26_COMPILE_FEATURES "") + +set(CMAKE_CXX_PLATFORM_ID "Windows") +set(CMAKE_CXX_SIMULATE_ID "") +set(CMAKE_CXX_COMPILER_FRONTEND_VARIANT "MSVC") +set(CMAKE_CXX_COMPILER_APPLE_SYSROOT "") +set(CMAKE_CXX_SIMULATE_VERSION "") +set(CMAKE_CXX_COMPILER_ARCHITECTURE_ID x64) + +set(MSVC_CXX_ARCHITECTURE_ID x64) + +set(CMAKE_AR "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.43.34808/bin/Hostarm64/x64/lib.exe") +set(CMAKE_CXX_COMPILER_AR "") +set(CMAKE_RANLIB ":") +set(CMAKE_CXX_COMPILER_RANLIB "") +set(CMAKE_LINKER "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.43.34808/bin/Hostarm64/x64/link.exe") +set(CMAKE_LINKER_LINK "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.43.34808/bin/Hostarm64/x64/link.exe") +set(CMAKE_LINKER_LLD "lld-link") +set(CMAKE_CXX_COMPILER_LINKER "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.43.34808/bin/HostArm64/x64/link.exe") +set(CMAKE_CXX_COMPILER_LINKER_ID "MSVC") +set(CMAKE_CXX_COMPILER_LINKER_VERSION 14.43.34810.0) +set(CMAKE_CXX_COMPILER_LINKER_FRONTEND_VARIANT MSVC) +set(CMAKE_MT "CMAKE_MT-NOTFOUND") +set(CMAKE_TAPI "") +set(CMAKE_COMPILER_IS_GNUCXX ) +set(CMAKE_CXX_COMPILER_LOADED 1) +set(CMAKE_CXX_COMPILER_WORKS TRUE) +set(CMAKE_CXX_ABI_COMPILED TRUE) + +set(CMAKE_CXX_COMPILER_ENV_VAR "CXX") + +set(CMAKE_CXX_COMPILER_ID_RUN 1) +set(CMAKE_CXX_SOURCE_FILE_EXTENSIONS C;M;c++;cc;cpp;cxx;m;mm;mpp;CPP;ixx;cppm;ccm;cxxm;c++m) +set(CMAKE_CXX_IGNORE_EXTENSIONS inl;h;hpp;HPP;H;o;O;obj;OBJ;def;DEF;rc;RC) + +foreach (lang IN ITEMS C OBJC OBJCXX) + if (CMAKE_${lang}_COMPILER_ID_RUN) + foreach(extension IN LISTS CMAKE_${lang}_SOURCE_FILE_EXTENSIONS) + list(REMOVE_ITEM CMAKE_CXX_SOURCE_FILE_EXTENSIONS ${extension}) + endforeach() + endif() +endforeach() + +set(CMAKE_CXX_LINKER_PREFERENCE 30) +set(CMAKE_CXX_LINKER_PREFERENCE_PROPAGATES 1) +set(CMAKE_CXX_LINKER_DEPFILE_SUPPORTED ) +set(CMAKE_LINKER_PUSHPOP_STATE_SUPPORTED ) +set(CMAKE_CXX_LINKER_PUSHPOP_STATE_SUPPORTED ) + +# Save compiler ABI information. +set(CMAKE_CXX_SIZEOF_DATA_PTR "8") +set(CMAKE_CXX_COMPILER_ABI "") +set(CMAKE_CXX_BYTE_ORDER "LITTLE_ENDIAN") +set(CMAKE_CXX_LIBRARY_ARCHITECTURE "") + +if(CMAKE_CXX_SIZEOF_DATA_PTR) + set(CMAKE_SIZEOF_VOID_P "${CMAKE_CXX_SIZEOF_DATA_PTR}") +endif() + +if(CMAKE_CXX_COMPILER_ABI) + set(CMAKE_INTERNAL_PLATFORM_ABI "${CMAKE_CXX_COMPILER_ABI}") +endif() + +if(CMAKE_CXX_LIBRARY_ARCHITECTURE) + set(CMAKE_LIBRARY_ARCHITECTURE "") +endif() + +set(CMAKE_CXX_CL_SHOWINCLUDES_PREFIX "") +if(CMAKE_CXX_CL_SHOWINCLUDES_PREFIX) + set(CMAKE_CL_SHOWINCLUDES_PREFIX "${CMAKE_CXX_CL_SHOWINCLUDES_PREFIX}") +endif() + + + + + +set(CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES "") +set(CMAKE_CXX_IMPLICIT_LINK_LIBRARIES "") +set(CMAKE_CXX_IMPLICIT_LINK_DIRECTORIES "") +set(CMAKE_CXX_IMPLICIT_LINK_FRAMEWORK_DIRECTORIES "") +set(CMAKE_CXX_COMPILER_CLANG_RESOURCE_DIR "") + +set(CMAKE_CXX_COMPILER_IMPORT_STD "") +### Imported target for C++23 standard library +set(CMAKE_CXX23_COMPILER_IMPORT_STD_NOT_FOUND_MESSAGE "Unsupported generator: Visual Studio 17 2022") + + + diff --git a/build_x64/CMakeFiles/4.0.2/CMakeDetermineCompilerABI_CXX.bin b/build_x64/CMakeFiles/4.0.2/CMakeDetermineCompilerABI_CXX.bin new file mode 100644 index 0000000..a77deb7 Binary files /dev/null and b/build_x64/CMakeFiles/4.0.2/CMakeDetermineCompilerABI_CXX.bin differ diff --git a/build_x64/CMakeFiles/4.0.2/CMakeRCCompiler.cmake b/build_x64/CMakeFiles/4.0.2/CMakeRCCompiler.cmake new file mode 100644 index 0000000..0f61961 --- /dev/null +++ b/build_x64/CMakeFiles/4.0.2/CMakeRCCompiler.cmake @@ -0,0 +1,6 @@ +set(CMAKE_RC_COMPILER "rc") +set(CMAKE_RC_COMPILER_ARG1 "") +set(CMAKE_RC_COMPILER_LOADED 1) +set(CMAKE_RC_SOURCE_FILE_EXTENSIONS rc;RC) +set(CMAKE_RC_OUTPUT_EXTENSION .res) +set(CMAKE_RC_COMPILER_ENV_VAR "RC") diff --git a/build_x64/CMakeFiles/4.0.2/CMakeSystem.cmake b/build_x64/CMakeFiles/4.0.2/CMakeSystem.cmake new file mode 100644 index 0000000..88ce365 --- /dev/null +++ b/build_x64/CMakeFiles/4.0.2/CMakeSystem.cmake @@ -0,0 +1,15 @@ +set(CMAKE_HOST_SYSTEM "Windows-10.0.26200") +set(CMAKE_HOST_SYSTEM_NAME "Windows") +set(CMAKE_HOST_SYSTEM_VERSION "10.0.26200") +set(CMAKE_HOST_SYSTEM_PROCESSOR "AMD64") + + + +set(CMAKE_SYSTEM "Windows-10.0.26200") +set(CMAKE_SYSTEM_NAME "Windows") +set(CMAKE_SYSTEM_VERSION "10.0.26200") +set(CMAKE_SYSTEM_PROCESSOR "AMD64") + +set(CMAKE_CROSSCOMPILING "FALSE") + +set(CMAKE_SYSTEM_LOADED 1) diff --git a/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/CMakeCXXCompilerId.cpp b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/CMakeCXXCompilerId.cpp new file mode 100644 index 0000000..94d4310 --- /dev/null +++ b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/CMakeCXXCompilerId.cpp @@ -0,0 +1,920 @@ +/* This source file must have a .cpp extension so that all C++ compilers + recognize the extension without flags. Borland does not know .cxx for + example. */ +#ifndef __cplusplus +# error "A C compiler has been selected for C++." +#endif + +#if !defined(__has_include) +/* If the compiler does not have __has_include, pretend the answer is + always no. */ +# define __has_include(x) 0 +#endif + + +/* Version number components: V=Version, R=Revision, P=Patch + Version date components: YYYY=Year, MM=Month, DD=Day */ + +#if defined(__INTEL_COMPILER) || defined(__ICC) +# define COMPILER_ID "Intel" +# if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +# endif +# if defined(__GNUC__) +# define SIMULATE_ID "GNU" +# endif + /* __INTEL_COMPILER = VRP prior to 2021, and then VVVV for 2021 and later, + except that a few beta releases use the old format with V=2021. */ +# if __INTEL_COMPILER < 2021 || __INTEL_COMPILER == 202110 || __INTEL_COMPILER == 202111 +# define COMPILER_VERSION_MAJOR DEC(__INTEL_COMPILER/100) +# define COMPILER_VERSION_MINOR DEC(__INTEL_COMPILER/10 % 10) +# if defined(__INTEL_COMPILER_UPDATE) +# define COMPILER_VERSION_PATCH DEC(__INTEL_COMPILER_UPDATE) +# else +# define COMPILER_VERSION_PATCH DEC(__INTEL_COMPILER % 10) +# endif +# else +# define COMPILER_VERSION_MAJOR DEC(__INTEL_COMPILER) +# define COMPILER_VERSION_MINOR DEC(__INTEL_COMPILER_UPDATE) + /* The third version component from --version is an update index, + but no macro is provided for it. */ +# define COMPILER_VERSION_PATCH DEC(0) +# endif +# if defined(__INTEL_COMPILER_BUILD_DATE) + /* __INTEL_COMPILER_BUILD_DATE = YYYYMMDD */ +# define COMPILER_VERSION_TWEAK DEC(__INTEL_COMPILER_BUILD_DATE) +# endif +# if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +# endif +# if defined(__GNUC__) +# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) +# elif defined(__GNUG__) +# define SIMULATE_VERSION_MAJOR DEC(__GNUG__) +# endif +# if defined(__GNUC_MINOR__) +# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) +# endif +# if defined(__GNUC_PATCHLEVEL__) +# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +# endif + +#elif (defined(__clang__) && defined(__INTEL_CLANG_COMPILER)) || defined(__INTEL_LLVM_COMPILER) +# define COMPILER_ID "IntelLLVM" +#if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +#endif +#if defined(__GNUC__) +# define SIMULATE_ID "GNU" +#endif +/* __INTEL_LLVM_COMPILER = VVVVRP prior to 2021.2.0, VVVVRRPP for 2021.2.0 and + * later. Look for 6 digit vs. 8 digit version number to decide encoding. + * VVVV is no smaller than the current year when a version is released. + */ +#if __INTEL_LLVM_COMPILER < 1000000L +# define COMPILER_VERSION_MAJOR DEC(__INTEL_LLVM_COMPILER/100) +# define COMPILER_VERSION_MINOR DEC(__INTEL_LLVM_COMPILER/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__INTEL_LLVM_COMPILER % 10) +#else +# define COMPILER_VERSION_MAJOR DEC(__INTEL_LLVM_COMPILER/10000) +# define COMPILER_VERSION_MINOR DEC(__INTEL_LLVM_COMPILER/100 % 100) +# define COMPILER_VERSION_PATCH DEC(__INTEL_LLVM_COMPILER % 100) +#endif +#if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +#endif +#if defined(__GNUC__) +# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) +#elif defined(__GNUG__) +# define SIMULATE_VERSION_MAJOR DEC(__GNUG__) +#endif +#if defined(__GNUC_MINOR__) +# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) +#endif +#if defined(__GNUC_PATCHLEVEL__) +# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +#endif + +#elif defined(__PATHCC__) +# define COMPILER_ID "PathScale" +# define COMPILER_VERSION_MAJOR DEC(__PATHCC__) +# define COMPILER_VERSION_MINOR DEC(__PATHCC_MINOR__) +# if defined(__PATHCC_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__PATHCC_PATCHLEVEL__) +# endif + +#elif defined(__BORLANDC__) && defined(__CODEGEARC_VERSION__) +# define COMPILER_ID "Embarcadero" +# define COMPILER_VERSION_MAJOR HEX(__CODEGEARC_VERSION__>>24 & 0x00FF) +# define COMPILER_VERSION_MINOR HEX(__CODEGEARC_VERSION__>>16 & 0x00FF) +# define COMPILER_VERSION_PATCH DEC(__CODEGEARC_VERSION__ & 0xFFFF) + +#elif defined(__BORLANDC__) +# define COMPILER_ID "Borland" + /* __BORLANDC__ = 0xVRR */ +# define COMPILER_VERSION_MAJOR HEX(__BORLANDC__>>8) +# define COMPILER_VERSION_MINOR HEX(__BORLANDC__ & 0xFF) + +#elif defined(__WATCOMC__) && __WATCOMC__ < 1200 +# define COMPILER_ID "Watcom" + /* __WATCOMC__ = VVRR */ +# define COMPILER_VERSION_MAJOR DEC(__WATCOMC__ / 100) +# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) +# if (__WATCOMC__ % 10) > 0 +# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) +# endif + +#elif defined(__WATCOMC__) +# define COMPILER_ID "OpenWatcom" + /* __WATCOMC__ = VVRP + 1100 */ +# define COMPILER_VERSION_MAJOR DEC((__WATCOMC__ - 1100) / 100) +# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) +# if (__WATCOMC__ % 10) > 0 +# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) +# endif + +#elif defined(__SUNPRO_CC) +# define COMPILER_ID "SunPro" +# if __SUNPRO_CC >= 0x5100 + /* __SUNPRO_CC = 0xVRRP */ +# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_CC>>12) +# define COMPILER_VERSION_MINOR HEX(__SUNPRO_CC>>4 & 0xFF) +# define COMPILER_VERSION_PATCH HEX(__SUNPRO_CC & 0xF) +# else + /* __SUNPRO_CC = 0xVRP */ +# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_CC>>8) +# define COMPILER_VERSION_MINOR HEX(__SUNPRO_CC>>4 & 0xF) +# define COMPILER_VERSION_PATCH HEX(__SUNPRO_CC & 0xF) +# endif + +#elif defined(__HP_aCC) +# define COMPILER_ID "HP" + /* __HP_aCC = VVRRPP */ +# define COMPILER_VERSION_MAJOR DEC(__HP_aCC/10000) +# define COMPILER_VERSION_MINOR DEC(__HP_aCC/100 % 100) +# define COMPILER_VERSION_PATCH DEC(__HP_aCC % 100) + +#elif defined(__DECCXX) +# define COMPILER_ID "Compaq" + /* __DECCXX_VER = VVRRTPPPP */ +# define COMPILER_VERSION_MAJOR DEC(__DECCXX_VER/10000000) +# define COMPILER_VERSION_MINOR DEC(__DECCXX_VER/100000 % 100) +# define COMPILER_VERSION_PATCH DEC(__DECCXX_VER % 10000) + +#elif defined(__IBMCPP__) && defined(__COMPILER_VER__) +# define COMPILER_ID "zOS" + /* __IBMCPP__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMCPP__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMCPP__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMCPP__ % 10) + +#elif defined(__open_xl__) && defined(__clang__) +# define COMPILER_ID "IBMClang" +# define COMPILER_VERSION_MAJOR DEC(__open_xl_version__) +# define COMPILER_VERSION_MINOR DEC(__open_xl_release__) +# define COMPILER_VERSION_PATCH DEC(__open_xl_modification__) +# define COMPILER_VERSION_TWEAK DEC(__open_xl_ptf_fix_level__) +# define COMPILER_VERSION_INTERNAL_STR __clang_version__ + + +#elif defined(__ibmxl__) && defined(__clang__) +# define COMPILER_ID "XLClang" +# define COMPILER_VERSION_MAJOR DEC(__ibmxl_version__) +# define COMPILER_VERSION_MINOR DEC(__ibmxl_release__) +# define COMPILER_VERSION_PATCH DEC(__ibmxl_modification__) +# define COMPILER_VERSION_TWEAK DEC(__ibmxl_ptf_fix_level__) + + +#elif defined(__IBMCPP__) && !defined(__COMPILER_VER__) && __IBMCPP__ >= 800 +# define COMPILER_ID "XL" + /* __IBMCPP__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMCPP__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMCPP__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMCPP__ % 10) + +#elif defined(__IBMCPP__) && !defined(__COMPILER_VER__) && __IBMCPP__ < 800 +# define COMPILER_ID "VisualAge" + /* __IBMCPP__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMCPP__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMCPP__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMCPP__ % 10) + +#elif defined(__NVCOMPILER) +# define COMPILER_ID "NVHPC" +# define COMPILER_VERSION_MAJOR DEC(__NVCOMPILER_MAJOR__) +# define COMPILER_VERSION_MINOR DEC(__NVCOMPILER_MINOR__) +# if defined(__NVCOMPILER_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__NVCOMPILER_PATCHLEVEL__) +# endif + +#elif defined(__PGI) +# define COMPILER_ID "PGI" +# define COMPILER_VERSION_MAJOR DEC(__PGIC__) +# define COMPILER_VERSION_MINOR DEC(__PGIC_MINOR__) +# if defined(__PGIC_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__PGIC_PATCHLEVEL__) +# endif + +#elif defined(__clang__) && defined(__cray__) +# define COMPILER_ID "CrayClang" +# define COMPILER_VERSION_MAJOR DEC(__cray_major__) +# define COMPILER_VERSION_MINOR DEC(__cray_minor__) +# define COMPILER_VERSION_PATCH DEC(__cray_patchlevel__) +# define COMPILER_VERSION_INTERNAL_STR __clang_version__ + + +#elif defined(_CRAYC) +# define COMPILER_ID "Cray" +# define COMPILER_VERSION_MAJOR DEC(_RELEASE_MAJOR) +# define COMPILER_VERSION_MINOR DEC(_RELEASE_MINOR) + +#elif defined(__TI_COMPILER_VERSION__) +# define COMPILER_ID "TI" + /* __TI_COMPILER_VERSION__ = VVVRRRPPP */ +# define COMPILER_VERSION_MAJOR DEC(__TI_COMPILER_VERSION__/1000000) +# define COMPILER_VERSION_MINOR DEC(__TI_COMPILER_VERSION__/1000 % 1000) +# define COMPILER_VERSION_PATCH DEC(__TI_COMPILER_VERSION__ % 1000) + +#elif defined(__CLANG_FUJITSU) +# define COMPILER_ID "FujitsuClang" +# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) +# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) +# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) +# define COMPILER_VERSION_INTERNAL_STR __clang_version__ + + +#elif defined(__FUJITSU) +# define COMPILER_ID "Fujitsu" +# if defined(__FCC_version__) +# define COMPILER_VERSION __FCC_version__ +# elif defined(__FCC_major__) +# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) +# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) +# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) +# endif +# if defined(__fcc_version) +# define COMPILER_VERSION_INTERNAL DEC(__fcc_version) +# elif defined(__FCC_VERSION) +# define COMPILER_VERSION_INTERNAL DEC(__FCC_VERSION) +# endif + + +#elif defined(__ghs__) +# define COMPILER_ID "GHS" +/* __GHS_VERSION_NUMBER = VVVVRP */ +# ifdef __GHS_VERSION_NUMBER +# define COMPILER_VERSION_MAJOR DEC(__GHS_VERSION_NUMBER / 100) +# define COMPILER_VERSION_MINOR DEC(__GHS_VERSION_NUMBER / 10 % 10) +# define COMPILER_VERSION_PATCH DEC(__GHS_VERSION_NUMBER % 10) +# endif + +#elif defined(__TASKING__) +# define COMPILER_ID "Tasking" + # define COMPILER_VERSION_MAJOR DEC(__VERSION__/1000) + # define COMPILER_VERSION_MINOR DEC(__VERSION__ % 100) +# define COMPILER_VERSION_INTERNAL DEC(__VERSION__) + +#elif defined(__ORANGEC__) +# define COMPILER_ID "OrangeC" +# define COMPILER_VERSION_MAJOR DEC(__ORANGEC_MAJOR__) +# define COMPILER_VERSION_MINOR DEC(__ORANGEC_MINOR__) +# define COMPILER_VERSION_PATCH DEC(__ORANGEC_PATCHLEVEL__) + +#elif defined(__SCO_VERSION__) +# define COMPILER_ID "SCO" + +#elif defined(__ARMCC_VERSION) && !defined(__clang__) +# define COMPILER_ID "ARMCC" +#if __ARMCC_VERSION >= 1000000 + /* __ARMCC_VERSION = VRRPPPP */ + # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/1000000) + # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 100) + # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) +#else + /* __ARMCC_VERSION = VRPPPP */ + # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/100000) + # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 10) + # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) +#endif + + +#elif defined(__clang__) && defined(__apple_build_version__) +# define COMPILER_ID "AppleClang" +# if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +# endif +# define COMPILER_VERSION_MAJOR DEC(__clang_major__) +# define COMPILER_VERSION_MINOR DEC(__clang_minor__) +# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) +# if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +# endif +# define COMPILER_VERSION_TWEAK DEC(__apple_build_version__) + +#elif defined(__clang__) && defined(__ARMCOMPILER_VERSION) +# define COMPILER_ID "ARMClang" + # define COMPILER_VERSION_MAJOR DEC(__ARMCOMPILER_VERSION/1000000) + # define COMPILER_VERSION_MINOR DEC(__ARMCOMPILER_VERSION/10000 % 100) + # define COMPILER_VERSION_PATCH DEC(__ARMCOMPILER_VERSION/100 % 100) +# define COMPILER_VERSION_INTERNAL DEC(__ARMCOMPILER_VERSION) + +#elif defined(__clang__) && defined(__ti__) +# define COMPILER_ID "TIClang" + # define COMPILER_VERSION_MAJOR DEC(__ti_major__) + # define COMPILER_VERSION_MINOR DEC(__ti_minor__) + # define COMPILER_VERSION_PATCH DEC(__ti_patchlevel__) +# define COMPILER_VERSION_INTERNAL DEC(__ti_version__) + +#elif defined(__clang__) +# define COMPILER_ID "Clang" +# if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +# endif +# define COMPILER_VERSION_MAJOR DEC(__clang_major__) +# define COMPILER_VERSION_MINOR DEC(__clang_minor__) +# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) +# if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +# endif + +#elif defined(__LCC__) && (defined(__GNUC__) || defined(__GNUG__) || defined(__MCST__)) +# define COMPILER_ID "LCC" +# define COMPILER_VERSION_MAJOR DEC(__LCC__ / 100) +# define COMPILER_VERSION_MINOR DEC(__LCC__ % 100) +# if defined(__LCC_MINOR__) +# define COMPILER_VERSION_PATCH DEC(__LCC_MINOR__) +# endif +# if defined(__GNUC__) && defined(__GNUC_MINOR__) +# define SIMULATE_ID "GNU" +# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) +# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) +# if defined(__GNUC_PATCHLEVEL__) +# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +# endif +# endif + +#elif defined(__GNUC__) || defined(__GNUG__) +# define COMPILER_ID "GNU" +# if defined(__GNUC__) +# define COMPILER_VERSION_MAJOR DEC(__GNUC__) +# else +# define COMPILER_VERSION_MAJOR DEC(__GNUG__) +# endif +# if defined(__GNUC_MINOR__) +# define COMPILER_VERSION_MINOR DEC(__GNUC_MINOR__) +# endif +# if defined(__GNUC_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +# endif + +#elif defined(_MSC_VER) +# define COMPILER_ID "MSVC" + /* _MSC_VER = VVRR */ +# define COMPILER_VERSION_MAJOR DEC(_MSC_VER / 100) +# define COMPILER_VERSION_MINOR DEC(_MSC_VER % 100) +# if defined(_MSC_FULL_VER) +# if _MSC_VER >= 1400 + /* _MSC_FULL_VER = VVRRPPPPP */ +# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 100000) +# else + /* _MSC_FULL_VER = VVRRPPPP */ +# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 10000) +# endif +# endif +# if defined(_MSC_BUILD) +# define COMPILER_VERSION_TWEAK DEC(_MSC_BUILD) +# endif + +#elif defined(_ADI_COMPILER) +# define COMPILER_ID "ADSP" +#if defined(__VERSIONNUM__) + /* __VERSIONNUM__ = 0xVVRRPPTT */ +# define COMPILER_VERSION_MAJOR DEC(__VERSIONNUM__ >> 24 & 0xFF) +# define COMPILER_VERSION_MINOR DEC(__VERSIONNUM__ >> 16 & 0xFF) +# define COMPILER_VERSION_PATCH DEC(__VERSIONNUM__ >> 8 & 0xFF) +# define COMPILER_VERSION_TWEAK DEC(__VERSIONNUM__ & 0xFF) +#endif + +#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) +# define COMPILER_ID "IAR" +# if defined(__VER__) && defined(__ICCARM__) +# define COMPILER_VERSION_MAJOR DEC((__VER__) / 1000000) +# define COMPILER_VERSION_MINOR DEC(((__VER__) / 1000) % 1000) +# define COMPILER_VERSION_PATCH DEC((__VER__) % 1000) +# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) +# elif defined(__VER__) && (defined(__ICCAVR__) || defined(__ICCRX__) || defined(__ICCRH850__) || defined(__ICCRL78__) || defined(__ICC430__) || defined(__ICCRISCV__) || defined(__ICCV850__) || defined(__ICC8051__) || defined(__ICCSTM8__)) +# define COMPILER_VERSION_MAJOR DEC((__VER__) / 100) +# define COMPILER_VERSION_MINOR DEC((__VER__) - (((__VER__) / 100)*100)) +# define COMPILER_VERSION_PATCH DEC(__SUBVERSION__) +# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) +# endif + + +/* These compilers are either not known or too old to define an + identification macro. Try to identify the platform and guess that + it is the native compiler. */ +#elif defined(__hpux) || defined(__hpua) +# define COMPILER_ID "HP" + +#else /* unknown compiler */ +# define COMPILER_ID "" +#endif + +/* Construct the string literal in pieces to prevent the source from + getting matched. Store it in a pointer rather than an array + because some compilers will just produce instructions to fill the + array rather than assigning a pointer to a static array. */ +char const* info_compiler = "INFO" ":" "compiler[" COMPILER_ID "]"; +#ifdef SIMULATE_ID +char const* info_simulate = "INFO" ":" "simulate[" SIMULATE_ID "]"; +#endif + +#ifdef __QNXNTO__ +char const* qnxnto = "INFO" ":" "qnxnto[]"; +#endif + +#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) +char const *info_cray = "INFO" ":" "compiler_wrapper[CrayPrgEnv]"; +#endif + +#define STRINGIFY_HELPER(X) #X +#define STRINGIFY(X) STRINGIFY_HELPER(X) + +/* Identify known platforms by name. */ +#if defined(__linux) || defined(__linux__) || defined(linux) +# define PLATFORM_ID "Linux" + +#elif defined(__MSYS__) +# define PLATFORM_ID "MSYS" + +#elif defined(__CYGWIN__) +# define PLATFORM_ID "Cygwin" + +#elif defined(__MINGW32__) +# define PLATFORM_ID "MinGW" + +#elif defined(__APPLE__) +# define PLATFORM_ID "Darwin" + +#elif defined(_WIN32) || defined(__WIN32__) || defined(WIN32) +# define PLATFORM_ID "Windows" + +#elif defined(__FreeBSD__) || defined(__FreeBSD) +# define PLATFORM_ID "FreeBSD" + +#elif defined(__NetBSD__) || defined(__NetBSD) +# define PLATFORM_ID "NetBSD" + +#elif defined(__OpenBSD__) || defined(__OPENBSD) +# define PLATFORM_ID "OpenBSD" + +#elif defined(__sun) || defined(sun) +# define PLATFORM_ID "SunOS" + +#elif defined(_AIX) || defined(__AIX) || defined(__AIX__) || defined(__aix) || defined(__aix__) +# define PLATFORM_ID "AIX" + +#elif defined(__hpux) || defined(__hpux__) +# define PLATFORM_ID "HP-UX" + +#elif defined(__HAIKU__) +# define PLATFORM_ID "Haiku" + +#elif defined(__BeOS) || defined(__BEOS__) || defined(_BEOS) +# define PLATFORM_ID "BeOS" + +#elif defined(__QNX__) || defined(__QNXNTO__) +# define PLATFORM_ID "QNX" + +#elif defined(__tru64) || defined(_tru64) || defined(__TRU64__) +# define PLATFORM_ID "Tru64" + +#elif defined(__riscos) || defined(__riscos__) +# define PLATFORM_ID "RISCos" + +#elif defined(__sinix) || defined(__sinix__) || defined(__SINIX__) +# define PLATFORM_ID "SINIX" + +#elif defined(__UNIX_SV__) +# define PLATFORM_ID "UNIX_SV" + +#elif defined(__bsdos__) +# define PLATFORM_ID "BSDOS" + +#elif defined(_MPRAS) || defined(MPRAS) +# define PLATFORM_ID "MP-RAS" + +#elif defined(__osf) || defined(__osf__) +# define PLATFORM_ID "OSF1" + +#elif defined(_SCO_SV) || defined(SCO_SV) || defined(sco_sv) +# define PLATFORM_ID "SCO_SV" + +#elif defined(__ultrix) || defined(__ultrix__) || defined(_ULTRIX) +# define PLATFORM_ID "ULTRIX" + +#elif defined(__XENIX__) || defined(_XENIX) || defined(XENIX) +# define PLATFORM_ID "Xenix" + +#elif defined(__WATCOMC__) +# if defined(__LINUX__) +# define PLATFORM_ID "Linux" + +# elif defined(__DOS__) +# define PLATFORM_ID "DOS" + +# elif defined(__OS2__) +# define PLATFORM_ID "OS2" + +# elif defined(__WINDOWS__) +# define PLATFORM_ID "Windows3x" + +# elif defined(__VXWORKS__) +# define PLATFORM_ID "VxWorks" + +# else /* unknown platform */ +# define PLATFORM_ID +# endif + +#elif defined(__INTEGRITY) +# if defined(INT_178B) +# define PLATFORM_ID "Integrity178" + +# else /* regular Integrity */ +# define PLATFORM_ID "Integrity" +# endif + +# elif defined(_ADI_COMPILER) +# define PLATFORM_ID "ADSP" + +#else /* unknown platform */ +# define PLATFORM_ID + +#endif + +/* For windows compilers MSVC and Intel we can determine + the architecture of the compiler being used. This is because + the compilers do not have flags that can change the architecture, + but rather depend on which compiler is being used +*/ +#if defined(_WIN32) && defined(_MSC_VER) +# if defined(_M_IA64) +# define ARCHITECTURE_ID "IA64" + +# elif defined(_M_ARM64EC) +# define ARCHITECTURE_ID "ARM64EC" + +# elif defined(_M_X64) || defined(_M_AMD64) +# define ARCHITECTURE_ID "x64" + +# elif defined(_M_IX86) +# define ARCHITECTURE_ID "X86" + +# elif defined(_M_ARM64) +# define ARCHITECTURE_ID "ARM64" + +# elif defined(_M_ARM) +# if _M_ARM == 4 +# define ARCHITECTURE_ID "ARMV4I" +# elif _M_ARM == 5 +# define ARCHITECTURE_ID "ARMV5I" +# else +# define ARCHITECTURE_ID "ARMV" STRINGIFY(_M_ARM) +# endif + +# elif defined(_M_MIPS) +# define ARCHITECTURE_ID "MIPS" + +# elif defined(_M_SH) +# define ARCHITECTURE_ID "SHx" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__WATCOMC__) +# if defined(_M_I86) +# define ARCHITECTURE_ID "I86" + +# elif defined(_M_IX86) +# define ARCHITECTURE_ID "X86" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) +# if defined(__ICCARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__ICCRX__) +# define ARCHITECTURE_ID "RX" + +# elif defined(__ICCRH850__) +# define ARCHITECTURE_ID "RH850" + +# elif defined(__ICCRL78__) +# define ARCHITECTURE_ID "RL78" + +# elif defined(__ICCRISCV__) +# define ARCHITECTURE_ID "RISCV" + +# elif defined(__ICCAVR__) +# define ARCHITECTURE_ID "AVR" + +# elif defined(__ICC430__) +# define ARCHITECTURE_ID "MSP430" + +# elif defined(__ICCV850__) +# define ARCHITECTURE_ID "V850" + +# elif defined(__ICC8051__) +# define ARCHITECTURE_ID "8051" + +# elif defined(__ICCSTM8__) +# define ARCHITECTURE_ID "STM8" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__ghs__) +# if defined(__PPC64__) +# define ARCHITECTURE_ID "PPC64" + +# elif defined(__ppc__) +# define ARCHITECTURE_ID "PPC" + +# elif defined(__ARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__x86_64__) +# define ARCHITECTURE_ID "x64" + +# elif defined(__i386__) +# define ARCHITECTURE_ID "X86" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__clang__) && defined(__ti__) +# if defined(__ARM_ARCH) +# define ARCHITECTURE_ID "ARM" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__TI_COMPILER_VERSION__) +# if defined(__TI_ARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__MSP430__) +# define ARCHITECTURE_ID "MSP430" + +# elif defined(__TMS320C28XX__) +# define ARCHITECTURE_ID "TMS320C28x" + +# elif defined(__TMS320C6X__) || defined(_TMS320C6X) +# define ARCHITECTURE_ID "TMS320C6x" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +# elif defined(__ADSPSHARC__) +# define ARCHITECTURE_ID "SHARC" + +# elif defined(__ADSPBLACKFIN__) +# define ARCHITECTURE_ID "Blackfin" + +#elif defined(__TASKING__) + +# if defined(__CTC__) || defined(__CPTC__) +# define ARCHITECTURE_ID "TriCore" + +# elif defined(__CMCS__) +# define ARCHITECTURE_ID "MCS" + +# elif defined(__CARM__) || defined(__CPARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__CARC__) +# define ARCHITECTURE_ID "ARC" + +# elif defined(__C51__) +# define ARCHITECTURE_ID "8051" + +# elif defined(__CPCP__) +# define ARCHITECTURE_ID "PCP" + +# else +# define ARCHITECTURE_ID "" +# endif + +#else +# define ARCHITECTURE_ID +#endif + +/* Convert integer to decimal digit literals. */ +#define DEC(n) \ + ('0' + (((n) / 10000000)%10)), \ + ('0' + (((n) / 1000000)%10)), \ + ('0' + (((n) / 100000)%10)), \ + ('0' + (((n) / 10000)%10)), \ + ('0' + (((n) / 1000)%10)), \ + ('0' + (((n) / 100)%10)), \ + ('0' + (((n) / 10)%10)), \ + ('0' + ((n) % 10)) + +/* Convert integer to hex digit literals. */ +#define HEX(n) \ + ('0' + ((n)>>28 & 0xF)), \ + ('0' + ((n)>>24 & 0xF)), \ + ('0' + ((n)>>20 & 0xF)), \ + ('0' + ((n)>>16 & 0xF)), \ + ('0' + ((n)>>12 & 0xF)), \ + ('0' + ((n)>>8 & 0xF)), \ + ('0' + ((n)>>4 & 0xF)), \ + ('0' + ((n) & 0xF)) + +/* Construct a string literal encoding the version number. */ +#ifdef COMPILER_VERSION +char const* info_version = "INFO" ":" "compiler_version[" COMPILER_VERSION "]"; + +/* Construct a string literal encoding the version number components. */ +#elif defined(COMPILER_VERSION_MAJOR) +char const info_version[] = { + 'I', 'N', 'F', 'O', ':', + 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','[', + COMPILER_VERSION_MAJOR, +# ifdef COMPILER_VERSION_MINOR + '.', COMPILER_VERSION_MINOR, +# ifdef COMPILER_VERSION_PATCH + '.', COMPILER_VERSION_PATCH, +# ifdef COMPILER_VERSION_TWEAK + '.', COMPILER_VERSION_TWEAK, +# endif +# endif +# endif + ']','\0'}; +#endif + +/* Construct a string literal encoding the internal version number. */ +#ifdef COMPILER_VERSION_INTERNAL +char const info_version_internal[] = { + 'I', 'N', 'F', 'O', ':', + 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','_', + 'i','n','t','e','r','n','a','l','[', + COMPILER_VERSION_INTERNAL,']','\0'}; +#elif defined(COMPILER_VERSION_INTERNAL_STR) +char const* info_version_internal = "INFO" ":" "compiler_version_internal[" COMPILER_VERSION_INTERNAL_STR "]"; +#endif + +/* Construct a string literal encoding the version number components. */ +#ifdef SIMULATE_VERSION_MAJOR +char const info_simulate_version[] = { + 'I', 'N', 'F', 'O', ':', + 's','i','m','u','l','a','t','e','_','v','e','r','s','i','o','n','[', + SIMULATE_VERSION_MAJOR, +# ifdef SIMULATE_VERSION_MINOR + '.', SIMULATE_VERSION_MINOR, +# ifdef SIMULATE_VERSION_PATCH + '.', SIMULATE_VERSION_PATCH, +# ifdef SIMULATE_VERSION_TWEAK + '.', SIMULATE_VERSION_TWEAK, +# endif +# endif +# endif + ']','\0'}; +#endif + +/* Construct the string literal in pieces to prevent the source from + getting matched. Store it in a pointer rather than an array + because some compilers will just produce instructions to fill the + array rather than assigning a pointer to a static array. */ +char const* info_platform = "INFO" ":" "platform[" PLATFORM_ID "]"; +char const* info_arch = "INFO" ":" "arch[" ARCHITECTURE_ID "]"; + + + +#define CXX_STD_98 199711L +#define CXX_STD_11 201103L +#define CXX_STD_14 201402L +#define CXX_STD_17 201703L +#define CXX_STD_20 202002L +#define CXX_STD_23 202302L + +#if defined(__INTEL_COMPILER) && defined(_MSVC_LANG) +# if _MSVC_LANG > CXX_STD_17 +# define CXX_STD _MSVC_LANG +# elif _MSVC_LANG == CXX_STD_17 && defined(__cpp_aggregate_paren_init) +# define CXX_STD CXX_STD_20 +# elif _MSVC_LANG > CXX_STD_14 && __cplusplus > CXX_STD_17 +# define CXX_STD CXX_STD_20 +# elif _MSVC_LANG > CXX_STD_14 +# define CXX_STD CXX_STD_17 +# elif defined(__INTEL_CXX11_MODE__) && defined(__cpp_aggregate_nsdmi) +# define CXX_STD CXX_STD_14 +# elif defined(__INTEL_CXX11_MODE__) +# define CXX_STD CXX_STD_11 +# else +# define CXX_STD CXX_STD_98 +# endif +#elif defined(_MSC_VER) && defined(_MSVC_LANG) +# if _MSVC_LANG > __cplusplus +# define CXX_STD _MSVC_LANG +# else +# define CXX_STD __cplusplus +# endif +#elif defined(__NVCOMPILER) +# if __cplusplus == CXX_STD_17 && defined(__cpp_aggregate_paren_init) +# define CXX_STD CXX_STD_20 +# else +# define CXX_STD __cplusplus +# endif +#elif defined(__INTEL_COMPILER) || defined(__PGI) +# if __cplusplus == CXX_STD_11 && defined(__cpp_namespace_attributes) +# define CXX_STD CXX_STD_17 +# elif __cplusplus == CXX_STD_11 && defined(__cpp_aggregate_nsdmi) +# define CXX_STD CXX_STD_14 +# else +# define CXX_STD __cplusplus +# endif +#elif (defined(__IBMCPP__) || defined(__ibmxl__)) && defined(__linux__) +# if __cplusplus == CXX_STD_11 && defined(__cpp_aggregate_nsdmi) +# define CXX_STD CXX_STD_14 +# else +# define CXX_STD __cplusplus +# endif +#elif __cplusplus == 1 && defined(__GXX_EXPERIMENTAL_CXX0X__) +# define CXX_STD CXX_STD_11 +#else +# define CXX_STD __cplusplus +#endif + +const char* info_language_standard_default = "INFO" ":" "standard_default[" +#if CXX_STD > CXX_STD_23 + "26" +#elif CXX_STD > CXX_STD_20 + "23" +#elif CXX_STD > CXX_STD_17 + "20" +#elif CXX_STD > CXX_STD_14 + "17" +#elif CXX_STD > CXX_STD_11 + "14" +#elif CXX_STD >= CXX_STD_11 + "11" +#else + "98" +#endif +"]"; + +const char* info_language_extensions_default = "INFO" ":" "extensions_default[" +#if (defined(__clang__) || defined(__GNUC__) || defined(__xlC__) || \ + defined(__TI_COMPILER_VERSION__)) && \ + !defined(__STRICT_ANSI__) + "ON" +#else + "OFF" +#endif +"]"; + +/*--------------------------------------------------------------------------*/ + +int main(int argc, char* argv[]) +{ + int require = 0; + require += info_compiler[argc]; + require += info_platform[argc]; + require += info_arch[argc]; +#ifdef COMPILER_VERSION_MAJOR + require += info_version[argc]; +#endif +#if defined(COMPILER_VERSION_INTERNAL) || defined(COMPILER_VERSION_INTERNAL_STR) + require += info_version_internal[argc]; +#endif +#ifdef SIMULATE_ID + require += info_simulate[argc]; +#endif +#ifdef SIMULATE_VERSION_MAJOR + require += info_simulate_version[argc]; +#endif +#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) + require += info_cray[argc]; +#endif + require += info_language_standard_default[argc]; + require += info_language_extensions_default[argc]; + (void)argv; + return require; +} diff --git a/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/CompilerIdCXX.exe b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/CompilerIdCXX.exe new file mode 100644 index 0000000..08f1926 Binary files /dev/null and b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/CompilerIdCXX.exe differ diff --git a/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/CompilerIdCXX.vcxproj b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/CompilerIdCXX.vcxproj new file mode 100644 index 0000000..2fa859e --- /dev/null +++ b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/CompilerIdCXX.vcxproj @@ -0,0 +1,72 @@ + + + + + Debug + x64 + + + + {CAE07175-D007-4FC3-BFE8-47B392814159} + CompilerIdCXX + Win32Proj + + + 10.0.22621.0 + + + + + + + + + ARM64 + + + Application + v143 + + MultiByte + + + + + + + <_ProjectFileVersion>10.0.30319.1 + .\ + $(Configuration)\ + false + + + + Disabled + %(PreprocessorDefinitions) + false + EnableFastChecks + MultiThreadedDebugDLL + + + TurnOffAllWarnings + + + + + + false + Console + + + + for %%i in (cl.exe) do %40echo CMAKE_CXX_COMPILER=%%~$PATH:i + + + + + + + + + + diff --git a/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CMakeCXXCompilerId.obj b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CMakeCXXCompilerId.obj new file mode 100644 index 0000000..ab6e287 Binary files /dev/null and b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CMakeCXXCompilerId.obj differ diff --git a/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.exe.recipe b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.exe.recipe new file mode 100644 index 0000000..d2bfdaf --- /dev/null +++ b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.exe.recipe @@ -0,0 +1,11 @@ + + + + + C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CompilerIdCXX\CompilerIdCXX.exe + + + + + + \ No newline at end of file diff --git a/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/CL.command.1.tlog b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/CL.command.1.tlog new file mode 100644 index 0000000..99a3660 Binary files /dev/null and b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/CL.command.1.tlog differ diff --git a/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/CL.read.1.tlog b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/CL.read.1.tlog new file mode 100644 index 0000000..849abc8 Binary files /dev/null and b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/CL.read.1.tlog differ diff --git a/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/CL.write.1.tlog b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/CL.write.1.tlog new file mode 100644 index 0000000..c2db9ca Binary files /dev/null and b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/CL.write.1.tlog differ diff --git a/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/Cl.items.tlog b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/Cl.items.tlog new file mode 100644 index 0000000..fec64a1 --- /dev/null +++ b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/Cl.items.tlog @@ -0,0 +1 @@ +C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CompilerIdCXX\CMakeCXXCompilerId.cpp;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CompilerIdCXX\Debug\CMakeCXXCompilerId.obj diff --git a/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/CompilerIdCXX.lastbuildstate b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/CompilerIdCXX.lastbuildstate new file mode 100644 index 0000000..5fde88b --- /dev/null +++ b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/CompilerIdCXX.lastbuildstate @@ -0,0 +1,2 @@ +PlatformToolSet=v143:VCToolArchitecture=NativeARM64:VCToolsVersion=14.43.34808:TargetPlatformVersion=10.0.22621.0: +Debug|x64|C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CompilerIdCXX\| diff --git a/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/link.command.1.tlog b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/link.command.1.tlog new file mode 100644 index 0000000..66e12c8 Binary files /dev/null and b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/link.command.1.tlog differ diff --git a/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/link.read.1.tlog b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/link.read.1.tlog new file mode 100644 index 0000000..ea60f57 Binary files /dev/null and b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/link.read.1.tlog differ diff --git a/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/link.secondary.1.tlog b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/link.secondary.1.tlog new file mode 100644 index 0000000..4e31bf3 --- /dev/null +++ b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/link.secondary.1.tlog @@ -0,0 +1 @@ +^C:\USERS\USER\CLIONPROJECTS\ARCDESKPROJECT\BUILD_X64\CMAKEFILES\4.0.2\COMPILERIDCXX\DEBUG\CMAKECXXCOMPILERID.OBJ diff --git a/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/link.write.1.tlog b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/link.write.1.tlog new file mode 100644 index 0000000..76e5200 Binary files /dev/null and b/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/Debug/CompilerIdCXX.tlog/link.write.1.tlog differ diff --git a/build_x64/CMakeFiles/4.0.2/VCTargetsPath.txt b/build_x64/CMakeFiles/4.0.2/VCTargetsPath.txt new file mode 100644 index 0000000..513c277 --- /dev/null +++ b/build_x64/CMakeFiles/4.0.2/VCTargetsPath.txt @@ -0,0 +1 @@ +C:/Program Files/Microsoft Visual Studio/2022/Community/MSBuild/Microsoft/VC/v170 diff --git a/build_x64/CMakeFiles/4.0.2/VCTargetsPath.vcxproj b/build_x64/CMakeFiles/4.0.2/VCTargetsPath.vcxproj new file mode 100644 index 0000000..761b03c --- /dev/null +++ b/build_x64/CMakeFiles/4.0.2/VCTargetsPath.vcxproj @@ -0,0 +1,31 @@ + + + + + Debug + x64 + + + + {F3FC6D86-508D-3FB1-96D2-995F08B142EC} + Win32Proj + x64 + 10.0.22621.0 + + + + ARM64 + + + Utility + MultiByte + v143 + + + + + echo VCTargetsPath=$(VCTargetsPath) + + + + diff --git a/build_x64/CMakeFiles/4.0.2/VCTargetsPath/x64/Debug/VCTargetsPath.recipe b/build_x64/CMakeFiles/4.0.2/VCTargetsPath/x64/Debug/VCTargetsPath.recipe new file mode 100644 index 0000000..b8ffe23 --- /dev/null +++ b/build_x64/CMakeFiles/4.0.2/VCTargetsPath/x64/Debug/VCTargetsPath.recipe @@ -0,0 +1,11 @@ + + + + + C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\x64\Debug\VCTargetsPath + + + + + + \ No newline at end of file diff --git a/build_x64/CMakeFiles/4.0.2/VCTargetsPath/x64/Debug/VCTargetsPath.tlog/VCTargetsPath.lastbuildstate b/build_x64/CMakeFiles/4.0.2/VCTargetsPath/x64/Debug/VCTargetsPath.tlog/VCTargetsPath.lastbuildstate new file mode 100644 index 0000000..a1e97bd --- /dev/null +++ b/build_x64/CMakeFiles/4.0.2/VCTargetsPath/x64/Debug/VCTargetsPath.tlog/VCTargetsPath.lastbuildstate @@ -0,0 +1,2 @@ +PlatformToolSet=v143:VCToolArchitecture=NativeARM64:VCToolsVersion=14.43.34808:TargetPlatformVersion=10.0.22621.0: +Debug|x64|C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\| diff --git a/build_x64/CMakeFiles/99218e3165732949c3bd031198cbc842/generate.stamp.rule b/build_x64/CMakeFiles/99218e3165732949c3bd031198cbc842/generate.stamp.rule new file mode 100644 index 0000000..2d3998c --- /dev/null +++ b/build_x64/CMakeFiles/99218e3165732949c3bd031198cbc842/generate.stamp.rule @@ -0,0 +1 @@ +# generated from CMake diff --git a/build_x64/CMakeFiles/CMakeConfigureLog.yaml b/build_x64/CMakeFiles/CMakeConfigureLog.yaml new file mode 100644 index 0000000..65de76e --- /dev/null +++ b/build_x64/CMakeFiles/CMakeConfigureLog.yaml @@ -0,0 +1,407 @@ + +--- +events: + - + kind: "message-v1" + backtrace: + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeDetermineSystem.cmake:205 (message)" + - "CMakeLists.txt:2 (project)" + message: | + The system is: Windows - 10.0.26200 - AMD64 + - + kind: "message-v1" + backtrace: + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeDetermineCompilerId.cmake:17 (message)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeDetermineCompilerId.cmake:64 (__determine_compiler_id_test)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeDetermineCXXCompiler.cmake:126 (CMAKE_DETERMINE_COMPILER_ID)" + - "CMakeLists.txt:2 (project)" + message: | + Compiling the CXX compiler identification source file "CMakeCXXCompilerId.cpp" succeeded. + Compiler: + Build flags: + Id flags: + + The output was: + 0 + MSBuild version 17.13.19+0d9f5a35a for .NET Framework + Build started 4/6/2026 8:32:51 PM. + + Project "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\4.0.2\\CompilerIdCXX\\CompilerIdCXX.vcxproj" on node 1 (default targets). + PrepareForBuild: + Creating directory "Debug\\". + Structured output is enabled. The formatting of compiler diagnostics will reflect the error hierarchy. See https://aka.ms/cpp/structured-output for more details. + Creating directory "Debug\\CompilerIdCXX.tlog\\". + InitializeBuildStatus: + Creating "Debug\\CompilerIdCXX.tlog\\unsuccessfulbuild" because "AlwaysCreate" was specified. + Touching "Debug\\CompilerIdCXX.tlog\\unsuccessfulbuild". + ClCompile: + C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC\\14.43.34808\\bin\\HostArm64\\x64\\CL.exe /c /nologo /W0 /WX- /diagnostics:column /Od /D _MBCS /Gm- /EHsc /RTC1 /MDd /GS /fp:precise /Zc:wchar_t /Zc:forScope /Zc:inline /Fo"Debug\\\\" /Fd"Debug\\vc143.pdb" /external:W0 /Gd /TP /FC /errorReport:queue CMakeCXXCompilerId.cpp + CMakeCXXCompilerId.cpp + Link: + C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC\\14.43.34808\\bin\\HostArm64\\x64\\link.exe /ERRORREPORT:QUEUE /OUT:".\\CompilerIdCXX.exe" /INCREMENTAL:NO /NOLOGO kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /MANIFEST /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /manifest:embed /PDB:".\\CompilerIdCXX.pdb" /SUBSYSTEM:CONSOLE /TLBID:1 /DYNAMICBASE /NXCOMPAT /IMPLIB:".\\CompilerIdCXX.lib" /MACHINE:X64 Debug\\CMakeCXXCompilerId.obj + CompilerIdCXX.vcxproj -> C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\4.0.2\\CompilerIdCXX\\CompilerIdCXX.exe + PostBuildEvent: + for %%i in (cl.exe) do @echo CMAKE_CXX_COMPILER=%%~$PATH:i + :VCEnd + CMAKE_CXX_COMPILER=C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC\\14.43.34808\\bin\\Hostarm64\\x64\\cl.exe + FinalizeBuildStatus: + Deleting file "Debug\\CompilerIdCXX.tlog\\unsuccessfulbuild". + Touching "Debug\\CompilerIdCXX.tlog\\CompilerIdCXX.lastbuildstate". + Done Building Project "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\4.0.2\\CompilerIdCXX\\CompilerIdCXX.vcxproj" (default targets). + + Build succeeded. + 0 Warning(s) + 0 Error(s) + + Time Elapsed 00:00:00.60 + + + Compilation of the CXX compiler identification source "CMakeCXXCompilerId.cpp" produced "CompilerIdCXX.exe" + + Compilation of the CXX compiler identification source "CMakeCXXCompilerId.cpp" produced "CompilerIdCXX.vcxproj" + + The CXX compiler identification is MSVC, found in: + C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/4.0.2/CompilerIdCXX/CompilerIdCXX.exe + + - + kind: "try_compile-v1" + backtrace: + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeDetermineCompilerABI.cmake:83 (try_compile)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeTestCXXCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" + - "CMakeLists.txt:2 (project)" + checks: + - "Detecting CXX compiler ABI info" + directories: + source: "C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-qhnq82" + binary: "C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-qhnq82" + cmakeVariables: + CMAKE_CXX_FLAGS: "/DWIN32 /D_WINDOWS /GR /EHsc" + CMAKE_CXX_FLAGS_DEBUG: "/Zi /Ob0 /Od /RTC1" + CMAKE_CXX_SCAN_FOR_MODULES: "OFF" + CMAKE_EXE_LINKER_FLAGS: "/machine:x64" + buildResult: + variable: "CMAKE_CXX_ABI_COMPILED" + cached: true + stdout: | + Change Dir: 'C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-qhnq82' + + Run Build Command(s): "C:/Program Files/Microsoft Visual Studio/2022/Community/MSBuild/Current/Bin/arm64/MSBuild.exe" cmTC_89270.vcxproj /p:Configuration=Debug /p:Platform=x64 /p:VisualStudioVersion=17.0 /v:n + MSBuild version 17.13.19+0d9f5a35a for .NET Framework + Build started 4/6/2026 8:32:52 PM. + + Project "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-qhnq82\\cmTC_89270.vcxproj" on node 1 (default targets). + PrepareForBuild: + Creating directory "cmTC_89270.dir\\Debug\\". + Structured output is enabled. The formatting of compiler diagnostics will reflect the error hierarchy. See https://aka.ms/cpp/structured-output for more details. + Creating directory "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-qhnq82\\Debug\\". + Creating directory "cmTC_89270.dir\\Debug\\cmTC_89270.tlog\\". + InitializeBuildStatus: + Creating "cmTC_89270.dir\\Debug\\cmTC_89270.tlog\\unsuccessfulbuild" because "AlwaysCreate" was specified. + Touching "cmTC_89270.dir\\Debug\\cmTC_89270.tlog\\unsuccessfulbuild". + ClCompile: + C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC\\14.43.34808\\bin\\HostArm64\\x64\\CL.exe /c /Zi /W1 /WX- /diagnostics:column /Od /Ob0 /D _MBCS /D WIN32 /D _WINDOWS /D "CMAKE_INTDIR=\\"Debug\\"" /EHsc /RTC1 /MDd /GS /fp:precise /Zc:wchar_t /Zc:forScope /Zc:inline /GR /Fo"cmTC_89270.dir\\Debug\\\\" /Fd"cmTC_89270.dir\\Debug\\vc143.pdb" /external:W1 /Gd /TP /errorReport:queue "C:\\Program Files\\CMake\\share\\cmake-4.0\\Modules\\CMakeCXXCompilerABI.cpp" + Microsoft (R) C/C++ Optimizing Compiler Version 19.43.34810 for x64 + Copyright (C) Microsoft Corporation. All rights reserved. + cl /c /Zi /W1 /WX- /diagnostics:column /Od /Ob0 /D _MBCS /D WIN32 /D _WINDOWS /D "CMAKE_INTDIR=\\"Debug\\"" /EHsc /RTC1 /MDd /GS /fp:precise /Zc:wchar_t /Zc:forScope /Zc:inline /GR /Fo"cmTC_89270.dir\\Debug\\\\" /Fd"cmTC_89270.dir\\Debug\\vc143.pdb" /external:W1 /Gd /TP /errorReport:queue "C:\\Program Files\\CMake\\share\\cmake-4.0\\Modules\\CMakeCXXCompilerABI.cpp" + CMakeCXXCompilerABI.cpp + Link: + C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC\\14.43.34808\\bin\\HostArm64\\x64\\link.exe /ERRORREPORT:QUEUE /OUT:"C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-qhnq82\\Debug\\cmTC_89270.exe" /INCREMENTAL /ILK:"cmTC_89270.dir\\Debug\\cmTC_89270.ilk" /NOLOGO kernel32.lib user32.lib gdi32.lib winspool.lib shell32.lib ole32.lib oleaut32.lib uuid.lib comdlg32.lib advapi32.lib /MANIFEST /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /manifest:embed /DEBUG /PDB:"C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-qhnq82/Debug/cmTC_89270.pdb" /SUBSYSTEM:CONSOLE /TLBID:1 /DYNAMICBASE /NXCOMPAT /IMPLIB:"C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-qhnq82/Debug/cmTC_89270.lib" /MACHINE:X64 /machine:x64 cmTC_89270.dir\\Debug\\CMakeCXXCompilerABI.obj + cmTC_89270.vcxproj -> C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-qhnq82\\Debug\\cmTC_89270.exe + FinalizeBuildStatus: + Deleting file "cmTC_89270.dir\\Debug\\cmTC_89270.tlog\\unsuccessfulbuild". + Touching "cmTC_89270.dir\\Debug\\cmTC_89270.tlog\\cmTC_89270.lastbuildstate". + Done Building Project "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-qhnq82\\cmTC_89270.vcxproj" (default targets). + + Build succeeded. + 0 Warning(s) + 0 Error(s) + + Time Elapsed 00:00:00.42 + + exitCode: 0 + - + kind: "message-v1" + backtrace: + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeDetermineCompilerABI.cmake:227 (message)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeTestCXXCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" + - "CMakeLists.txt:2 (project)" + message: | + Parsed CXX implicit link information: + link line regex: [^( *|.*[/\\])(ld[0-9]*(|\\.[a-rt-z][a-z]*|\\.s[a-np-z][a-z]*|\\.so[a-z]+)|link\\.exe|lld-link(\\.exe)?|CMAKE_LINK_STARTFILE-NOTFOUND|([^/\\]+-)?ld|collect2)[^/\\]*( |$)] + linker tool regex: [^[ ]*(->|")?[ ]*(([^"]*[/\\])?(ld[0-9]*(|\\.[a-rt-z][a-z]*|\\.s[a-np-z][a-z]*|\\.so[a-z]+)|link\\.exe|lld-link(\\.exe)?))("|,| |$)] + linker tool for 'CXX': C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.43.34808/bin/HostArm64/x64/link.exe + implicit libs: [] + implicit objs: [] + implicit dirs: [] + implicit fwks: [] + + + - + kind: "message-v1" + backtrace: + - "C:/Program Files/CMake/share/cmake-4.0/Modules/Internal/CMakeDetermineLinkerId.cmake:36 (message)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeDetermineCompilerABI.cmake:270 (cmake_determine_linker_id)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeTestCXXCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" + - "CMakeLists.txt:2 (project)" + message: | + Running the CXX compiler's linker: "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.43.34808/bin/HostArm64/x64/link.exe" "-v" + Microsoft (R) Incremental Linker Version 14.43.34810.0 + Copyright (C) Microsoft Corporation. All rights reserved. + - + kind: "try_compile-v1" + backtrace: + - "C:/Program Files/CMake/share/cmake-4.0/Modules/Internal/CheckSourceCompiles.cmake:104 (try_compile)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CheckCXXSourceCompiles.cmake:58 (cmake_check_source_compiles)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/FindThreads.cmake:99 (check_cxx_source_compiles)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/FindThreads.cmake:163 (_threads_check_libc)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeFindDependencyMacro.cmake:78 (find_package)" + - "C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/WebP/WebPConfig.cmake:33 (find_dependency)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeFindDependencyMacro.cmake:78 (find_package)" + - "C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/leptonica/LeptonicaConfig.cmake:29 (find_dependency)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeFindDependencyMacro.cmake:78 (find_package)" + - "C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/tesseract/TesseractConfig.cmake:25 (find_dependency)" + - "CMakeLists.txt:45 (find_package)" + checks: + - "Performing Test CMAKE_HAVE_LIBC_PTHREAD" + directories: + source: "C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-g43k4f" + binary: "C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-g43k4f" + cmakeVariables: + CMAKE_CXX_FLAGS: "/DWIN32 /D_WINDOWS /GR /EHsc" + CMAKE_CXX_FLAGS_DEBUG: "/Zi /Ob0 /Od /RTC1" + CMAKE_EXE_LINKER_FLAGS: "/machine:x64" + buildResult: + variable: "CMAKE_HAVE_LIBC_PTHREAD" + cached: true + stdout: | + Change Dir: 'C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-g43k4f' + + Run Build Command(s): "C:/Program Files/Microsoft Visual Studio/2022/Community/MSBuild/Current/Bin/arm64/MSBuild.exe" cmTC_34b36.vcxproj /p:Configuration=Debug /p:Platform=x64 /p:VisualStudioVersion=17.0 /v:n + MSBuild version 17.13.19+0d9f5a35a for .NET Framework + Build started 4/6/2026 8:32:53 PM. + + Project "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-g43k4f\\cmTC_34b36.vcxproj" on node 1 (default targets). + PrepareForBuild: + Creating directory "cmTC_34b36.dir\\Debug\\". + Structured output is enabled. The formatting of compiler diagnostics will reflect the error hierarchy. See https://aka.ms/cpp/structured-output for more details. + Creating directory "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-g43k4f\\Debug\\". + Creating directory "cmTC_34b36.dir\\Debug\\cmTC_34b36.tlog\\". + InitializeBuildStatus: + Creating "cmTC_34b36.dir\\Debug\\cmTC_34b36.tlog\\unsuccessfulbuild" because "AlwaysCreate" was specified. + Touching "cmTC_34b36.dir\\Debug\\cmTC_34b36.tlog\\unsuccessfulbuild". + ClCompile: + C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC\\14.43.34808\\bin\\HostArm64\\x64\\CL.exe /c /Zi /W1 /WX- /diagnostics:column /Od /Ob0 /D _MBCS /D WIN32 /D _WINDOWS /D CMAKE_HAVE_LIBC_PTHREAD /D "CMAKE_INTDIR=\\"Debug\\"" /EHsc /RTC1 /MDd /GS /fp:precise /Zc:wchar_t /Zc:forScope /Zc:inline /GR /std:c++17 /Fo"cmTC_34b36.dir\\Debug\\\\" /Fd"cmTC_34b36.dir\\Debug\\vc143.pdb" /external:W1 /Gd /TP /errorReport:queue "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-g43k4f\\src.cxx" + Microsoft (R) C/C++ Optimizing Compiler Version 19.43.34810 for x64 + Copyright (C) Microsoft Corporation. All rights reserved. + cl /c /Zi /W1 /WX- /diagnostics:column /Od /Ob0 /D _MBCS /D WIN32 /D _WINDOWS /D CMAKE_HAVE_LIBC_PTHREAD /D "CMAKE_INTDIR=\\"Debug\\"" /EHsc /RTC1 /MDd /GS /fp:precise /Zc:wchar_t /Zc:forScope /Zc:inline /GR /std:c++17 /Fo"cmTC_34b36.dir\\Debug\\\\" /Fd"cmTC_34b36.dir\\Debug\\vc143.pdb" /external:W1 /Gd /TP /errorReport:queue "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-g43k4f\\src.cxx" + src.cxx + C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-g43k4f\\src.cxx(1,10): error C1083: Cannot open include file: 'pthread.h': No such file or directory [C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-g43k4f\\cmTC_34b36.vcxproj] + Done Building Project "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-g43k4f\\cmTC_34b36.vcxproj" (default targets) -- FAILED. + + Build FAILED. + + "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-g43k4f\\cmTC_34b36.vcxproj" (default target) (1) -> + (ClCompile target) -> + C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-g43k4f\\src.cxx(1,10): error C1083: Cannot open include file: 'pthread.h': No such file or directory [C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-g43k4f\\cmTC_34b36.vcxproj] + + 0 Warning(s) + 1 Error(s) + + Time Elapsed 00:00:00.37 + + exitCode: 1 + - + kind: "try_compile-v1" + backtrace: + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CheckLibraryExists.cmake:112 (try_compile)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/FindThreads.cmake:112 (check_library_exists)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/FindThreads.cmake:175 (_threads_check_lib)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeFindDependencyMacro.cmake:78 (find_package)" + - "C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/WebP/WebPConfig.cmake:33 (find_dependency)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeFindDependencyMacro.cmake:78 (find_package)" + - "C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/leptonica/LeptonicaConfig.cmake:29 (find_dependency)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeFindDependencyMacro.cmake:78 (find_package)" + - "C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/tesseract/TesseractConfig.cmake:25 (find_dependency)" + - "CMakeLists.txt:45 (find_package)" + checks: + - "Looking for pthread_create in pthreads" + directories: + source: "C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-oqlsec" + binary: "C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-oqlsec" + cmakeVariables: + CMAKE_CXX_FLAGS: "/DWIN32 /D_WINDOWS /GR /EHsc" + CMAKE_CXX_FLAGS_DEBUG: "/Zi /Ob0 /Od /RTC1" + CMAKE_EXE_LINKER_FLAGS: "/machine:x64" + buildResult: + variable: "CMAKE_HAVE_PTHREADS_CREATE" + cached: true + stdout: | + Change Dir: 'C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-oqlsec' + + Run Build Command(s): "C:/Program Files/Microsoft Visual Studio/2022/Community/MSBuild/Current/Bin/arm64/MSBuild.exe" cmTC_48e26.vcxproj /p:Configuration=Debug /p:Platform=x64 /p:VisualStudioVersion=17.0 /v:n + MSBuild version 17.13.19+0d9f5a35a for .NET Framework + Build started 4/6/2026 8:32:54 PM. + + Project "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-oqlsec\\cmTC_48e26.vcxproj" on node 1 (default targets). + PrepareForBuild: + Creating directory "cmTC_48e26.dir\\Debug\\". + Structured output is enabled. The formatting of compiler diagnostics will reflect the error hierarchy. See https://aka.ms/cpp/structured-output for more details. + Creating directory "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-oqlsec\\Debug\\". + Creating directory "cmTC_48e26.dir\\Debug\\cmTC_48e26.tlog\\". + InitializeBuildStatus: + Creating "cmTC_48e26.dir\\Debug\\cmTC_48e26.tlog\\unsuccessfulbuild" because "AlwaysCreate" was specified. + Touching "cmTC_48e26.dir\\Debug\\cmTC_48e26.tlog\\unsuccessfulbuild". + ClCompile: + C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC\\14.43.34808\\bin\\HostArm64\\x64\\CL.exe /c /Zi /W1 /WX- /diagnostics:column /Od /Ob0 /D _MBCS /D WIN32 /D _WINDOWS /D CHECK_FUNCTION_EXISTS=pthread_create /D "CMAKE_INTDIR=\\"Debug\\"" /EHsc /RTC1 /MDd /GS /fp:precise /Zc:wchar_t /Zc:forScope /Zc:inline /GR /std:c++17 /Fo"cmTC_48e26.dir\\Debug\\\\" /Fd"cmTC_48e26.dir\\Debug\\vc143.pdb" /external:W1 /Gd /TP /errorReport:queue "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-oqlsec\\CheckFunctionExists.cxx" + Microsoft (R) C/C++ Optimizing Compiler Version 19.43.34810 for x64 + Copyright (C) Microsoft Corporation. All rights reserved. + cl /c /Zi /W1 /WX- /diagnostics:column /Od /Ob0 /D _MBCS /D WIN32 /D _WINDOWS /D CHECK_FUNCTION_EXISTS=pthread_create /D "CMAKE_INTDIR=\\"Debug\\"" /EHsc /RTC1 /MDd /GS /fp:precise /Zc:wchar_t /Zc:forScope /Zc:inline /GR /std:c++17 /Fo"cmTC_48e26.dir\\Debug\\\\" /Fd"cmTC_48e26.dir\\Debug\\vc143.pdb" /external:W1 /Gd /TP /errorReport:queue "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-oqlsec\\CheckFunctionExists.cxx" + CheckFunctionExists.cxx + Link: + C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC\\14.43.34808\\bin\\HostArm64\\x64\\link.exe /ERRORREPORT:QUEUE /OUT:"C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-oqlsec\\Debug\\cmTC_48e26.exe" /INCREMENTAL /ILK:"cmTC_48e26.dir\\Debug\\cmTC_48e26.ilk" /NOLOGO pthreads.lib kernel32.lib user32.lib gdi32.lib winspool.lib shell32.lib ole32.lib oleaut32.lib uuid.lib comdlg32.lib advapi32.lib /MANIFEST /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /manifest:embed /DEBUG /PDB:"C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-oqlsec/Debug/cmTC_48e26.pdb" /SUBSYSTEM:CONSOLE /TLBID:1 /DYNAMICBASE /NXCOMPAT /IMPLIB:"C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-oqlsec/Debug/cmTC_48e26.lib" /MACHINE:X64 /machine:x64 cmTC_48e26.dir\\Debug\\CheckFunctionExists.obj + LINK : fatal error LNK1104: cannot open file 'pthreads.lib' [C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-oqlsec\\cmTC_48e26.vcxproj] + Done Building Project "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-oqlsec\\cmTC_48e26.vcxproj" (default targets) -- FAILED. + + Build FAILED. + + "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-oqlsec\\cmTC_48e26.vcxproj" (default target) (1) -> + (Link target) -> + LINK : fatal error LNK1104: cannot open file 'pthreads.lib' [C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-oqlsec\\cmTC_48e26.vcxproj] + + 0 Warning(s) + 1 Error(s) + + Time Elapsed 00:00:00.65 + + exitCode: 1 + - + kind: "try_compile-v1" + backtrace: + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CheckLibraryExists.cmake:112 (try_compile)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/FindThreads.cmake:112 (check_library_exists)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/FindThreads.cmake:176 (_threads_check_lib)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeFindDependencyMacro.cmake:78 (find_package)" + - "C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/WebP/WebPConfig.cmake:33 (find_dependency)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeFindDependencyMacro.cmake:78 (find_package)" + - "C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/leptonica/LeptonicaConfig.cmake:29 (find_dependency)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeFindDependencyMacro.cmake:78 (find_package)" + - "C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/tesseract/TesseractConfig.cmake:25 (find_dependency)" + - "CMakeLists.txt:45 (find_package)" + checks: + - "Looking for pthread_create in pthread" + directories: + source: "C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-4hrlm6" + binary: "C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-4hrlm6" + cmakeVariables: + CMAKE_CXX_FLAGS: "/DWIN32 /D_WINDOWS /GR /EHsc" + CMAKE_CXX_FLAGS_DEBUG: "/Zi /Ob0 /Od /RTC1" + CMAKE_EXE_LINKER_FLAGS: "/machine:x64" + buildResult: + variable: "CMAKE_HAVE_PTHREAD_CREATE" + cached: true + stdout: | + Change Dir: 'C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-4hrlm6' + + Run Build Command(s): "C:/Program Files/Microsoft Visual Studio/2022/Community/MSBuild/Current/Bin/arm64/MSBuild.exe" cmTC_f0446.vcxproj /p:Configuration=Debug /p:Platform=x64 /p:VisualStudioVersion=17.0 /v:n + MSBuild version 17.13.19+0d9f5a35a for .NET Framework + Build started 4/6/2026 8:32:55 PM. + + Project "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-4hrlm6\\cmTC_f0446.vcxproj" on node 1 (default targets). + PrepareForBuild: + Creating directory "cmTC_f0446.dir\\Debug\\". + Structured output is enabled. The formatting of compiler diagnostics will reflect the error hierarchy. See https://aka.ms/cpp/structured-output for more details. + Creating directory "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-4hrlm6\\Debug\\". + Creating directory "cmTC_f0446.dir\\Debug\\cmTC_f0446.tlog\\". + InitializeBuildStatus: + Creating "cmTC_f0446.dir\\Debug\\cmTC_f0446.tlog\\unsuccessfulbuild" because "AlwaysCreate" was specified. + Touching "cmTC_f0446.dir\\Debug\\cmTC_f0446.tlog\\unsuccessfulbuild". + ClCompile: + C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC\\14.43.34808\\bin\\HostArm64\\x64\\CL.exe /c /Zi /W1 /WX- /diagnostics:column /Od /Ob0 /D _MBCS /D WIN32 /D _WINDOWS /D CHECK_FUNCTION_EXISTS=pthread_create /D "CMAKE_INTDIR=\\"Debug\\"" /EHsc /RTC1 /MDd /GS /fp:precise /Zc:wchar_t /Zc:forScope /Zc:inline /GR /std:c++17 /Fo"cmTC_f0446.dir\\Debug\\\\" /Fd"cmTC_f0446.dir\\Debug\\vc143.pdb" /external:W1 /Gd /TP /errorReport:queue "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-4hrlm6\\CheckFunctionExists.cxx" + Microsoft (R) C/C++ Optimizing Compiler Version 19.43.34810 for x64 + Copyright (C) Microsoft Corporation. All rights reserved. + cl /c /Zi /W1 /WX- /diagnostics:column /Od /Ob0 /D _MBCS /D WIN32 /D _WINDOWS /D CHECK_FUNCTION_EXISTS=pthread_create /D "CMAKE_INTDIR=\\"Debug\\"" /EHsc /RTC1 /MDd /GS /fp:precise /Zc:wchar_t /Zc:forScope /Zc:inline /GR /std:c++17 /Fo"cmTC_f0446.dir\\Debug\\\\" /Fd"cmTC_f0446.dir\\Debug\\vc143.pdb" /external:W1 /Gd /TP /errorReport:queue "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-4hrlm6\\CheckFunctionExists.cxx" + CheckFunctionExists.cxx + Link: + C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC\\14.43.34808\\bin\\HostArm64\\x64\\link.exe /ERRORREPORT:QUEUE /OUT:"C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-4hrlm6\\Debug\\cmTC_f0446.exe" /INCREMENTAL /ILK:"cmTC_f0446.dir\\Debug\\cmTC_f0446.ilk" /NOLOGO pthread.lib kernel32.lib user32.lib gdi32.lib winspool.lib shell32.lib ole32.lib oleaut32.lib uuid.lib comdlg32.lib advapi32.lib /MANIFEST /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /manifest:embed /DEBUG /PDB:"C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-4hrlm6/Debug/cmTC_f0446.pdb" /SUBSYSTEM:CONSOLE /TLBID:1 /DYNAMICBASE /NXCOMPAT /IMPLIB:"C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-4hrlm6/Debug/cmTC_f0446.lib" /MACHINE:X64 /machine:x64 cmTC_f0446.dir\\Debug\\CheckFunctionExists.obj + LINK : fatal error LNK1104: cannot open file 'pthread.lib' [C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-4hrlm6\\cmTC_f0446.vcxproj] + Done Building Project "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-4hrlm6\\cmTC_f0446.vcxproj" (default targets) -- FAILED. + + Build FAILED. + + "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-4hrlm6\\cmTC_f0446.vcxproj" (default target) (1) -> + (Link target) -> + LINK : fatal error LNK1104: cannot open file 'pthread.lib' [C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-4hrlm6\\cmTC_f0446.vcxproj] + + 0 Warning(s) + 1 Error(s) + + Time Elapsed 00:00:00.38 + + exitCode: 1 + - + kind: "try_compile-v1" + backtrace: + - "C:/Program Files/CMake/share/cmake-4.0/Modules/Internal/CheckSourceCompiles.cmake:104 (try_compile)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CheckCXXSourceCompiles.cmake:58 (cmake_check_source_compiles)" + - "C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/FindWrapAtomic.cmake:36 (check_cxx_source_compiles)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeFindDependencyMacro.cmake:78 (find_package)" + - "C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicDependencyHelpers.cmake:36 (find_dependency)" + - "C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Core/Qt6CoreDependencies.cmake:35 (_qt_internal_find_third_party_dependencies)" + - "C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Core/Qt6CoreConfig.cmake:45 (include)" + - "C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeFindDependencyMacro.cmake:78 (find_package)" + - "C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicDependencyHelpers.cmake:137 (find_dependency)" + - "C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6NetworkDependencies.cmake:45 (_qt_internal_find_qt_dependencies)" + - "C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6NetworkConfig.cmake:43 (include)" + - "C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/Qt6Config.cmake:218 (find_package)" + - "CMakeLists.txt:53 (find_package)" + checks: + - "Performing Test HAVE_STDATOMIC" + directories: + source: "C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-gsfapo" + binary: "C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-gsfapo" + cmakeVariables: + CMAKE_CXX_FLAGS: "/DWIN32 /D_WINDOWS /GR /EHsc" + CMAKE_CXX_FLAGS_DEBUG: "/Zi /Ob0 /Od /RTC1" + CMAKE_EXE_LINKER_FLAGS: "/machine:x64" + CMAKE_MODULE_PATH: "C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6;C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/3rdparty/extra-cmake-modules/find-modules;C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/3rdparty/kwin" + buildResult: + variable: "HAVE_STDATOMIC" + cached: true + stdout: | + Change Dir: 'C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-gsfapo' + + Run Build Command(s): "C:/Program Files/Microsoft Visual Studio/2022/Community/MSBuild/Current/Bin/arm64/MSBuild.exe" cmTC_c8b44.vcxproj /p:Configuration=Debug /p:Platform=x64 /p:VisualStudioVersion=17.0 /v:n + MSBuild version 17.13.19+0d9f5a35a for .NET Framework + Build started 4/6/2026 8:32:56 PM. + + Project "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-gsfapo\\cmTC_c8b44.vcxproj" on node 1 (default targets). + PrepareForBuild: + Creating directory "cmTC_c8b44.dir\\Debug\\". + Structured output is enabled. The formatting of compiler diagnostics will reflect the error hierarchy. See https://aka.ms/cpp/structured-output for more details. + Creating directory "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-gsfapo\\Debug\\". + Creating directory "cmTC_c8b44.dir\\Debug\\cmTC_c8b44.tlog\\". + InitializeBuildStatus: + Creating "cmTC_c8b44.dir\\Debug\\cmTC_c8b44.tlog\\unsuccessfulbuild" because "AlwaysCreate" was specified. + Touching "cmTC_c8b44.dir\\Debug\\cmTC_c8b44.tlog\\unsuccessfulbuild". + ClCompile: + C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC\\14.43.34808\\bin\\HostArm64\\x64\\CL.exe /c /Zi /W1 /WX- /diagnostics:column /Od /Ob0 /D _MBCS /D WIN32 /D _WINDOWS /D HAVE_STDATOMIC /D "CMAKE_INTDIR=\\"Debug\\"" /EHsc /RTC1 /MDd /GS /fp:precise /Zc:wchar_t /Zc:forScope /Zc:inline /GR /std:c++17 /Fo"cmTC_c8b44.dir\\Debug\\\\" /Fd"cmTC_c8b44.dir\\Debug\\vc143.pdb" /external:W1 /Gd /TP /errorReport:queue "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-gsfapo\\src.cxx" + Microsoft (R) C/C++ Optimizing Compiler Version 19.43.34810 for x64 + Copyright (C) Microsoft Corporation. All rights reserved. + cl /c /Zi /W1 /WX- /diagnostics:column /Od /Ob0 /D _MBCS /D WIN32 /D _WINDOWS /D HAVE_STDATOMIC /D "CMAKE_INTDIR=\\"Debug\\"" /EHsc /RTC1 /MDd /GS /fp:precise /Zc:wchar_t /Zc:forScope /Zc:inline /GR /std:c++17 /Fo"cmTC_c8b44.dir\\Debug\\\\" /Fd"cmTC_c8b44.dir\\Debug\\vc143.pdb" /external:W1 /Gd /TP /errorReport:queue "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-gsfapo\\src.cxx" + src.cxx + Link: + C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC\\14.43.34808\\bin\\HostArm64\\x64\\link.exe /ERRORREPORT:QUEUE /OUT:"C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-gsfapo\\Debug\\cmTC_c8b44.exe" /INCREMENTAL /ILK:"cmTC_c8b44.dir\\Debug\\cmTC_c8b44.ilk" /NOLOGO kernel32.lib user32.lib gdi32.lib winspool.lib shell32.lib ole32.lib oleaut32.lib uuid.lib comdlg32.lib advapi32.lib /MANIFEST /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /manifest:embed /DEBUG /PDB:"C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-gsfapo/Debug/cmTC_c8b44.pdb" /SUBSYSTEM:CONSOLE /TLBID:1 /DYNAMICBASE /NXCOMPAT /IMPLIB:"C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/CMakeScratch/TryCompile-gsfapo/Debug/cmTC_c8b44.lib" /MACHINE:X64 /machine:x64 cmTC_c8b44.dir\\Debug\\src.obj + cmTC_c8b44.vcxproj -> C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-gsfapo\\Debug\\cmTC_c8b44.exe + FinalizeBuildStatus: + Deleting file "cmTC_c8b44.dir\\Debug\\cmTC_c8b44.tlog\\unsuccessfulbuild". + Touching "cmTC_c8b44.dir\\Debug\\cmTC_c8b44.tlog\\cmTC_c8b44.lastbuildstate". + Done Building Project "C:\\Users\\User\\CLionProjects\\ARCDeskProject\\build_x64\\CMakeFiles\\CMakeScratch\\TryCompile-gsfapo\\cmTC_c8b44.vcxproj" (default targets). + + Build succeeded. + 0 Warning(s) + 0 Error(s) + + Time Elapsed 00:00:00.43 + + exitCode: 0 +... diff --git a/build_x64/CMakeFiles/InstallScripts.json b/build_x64/CMakeFiles/InstallScripts.json new file mode 100644 index 0000000..738eee3 --- /dev/null +++ b/build_x64/CMakeFiles/InstallScripts.json @@ -0,0 +1,7 @@ +{ + "InstallScripts" : + [ + "C:/Users/User/CLionProjects/ARCDeskProject/build_x64/cmake_install.cmake" + ], + "Parallel" : false +} diff --git a/build_x64/CMakeFiles/TargetDirectories.txt b/build_x64/CMakeFiles/TargetDirectories.txt new file mode 100644 index 0000000..b0a632d --- /dev/null +++ b/build_x64/CMakeFiles/TargetDirectories.txt @@ -0,0 +1,3 @@ +C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/ARC.dir +C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/ALL_BUILD.dir +C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/ZERO_CHECK.dir diff --git a/build_x64/CMakeFiles/cmake.check_cache b/build_x64/CMakeFiles/cmake.check_cache new file mode 100644 index 0000000..3dccd73 --- /dev/null +++ b/build_x64/CMakeFiles/cmake.check_cache @@ -0,0 +1 @@ +# This file is generated by cmake for dependency checking of the CMakeCache.txt file diff --git a/build_x64/CMakeFiles/generate.stamp b/build_x64/CMakeFiles/generate.stamp new file mode 100644 index 0000000..9b5f49f --- /dev/null +++ b/build_x64/CMakeFiles/generate.stamp @@ -0,0 +1 @@ +# CMake generation timestamp file for this directory. diff --git a/build_x64/CMakeFiles/generate.stamp.depend b/build_x64/CMakeFiles/generate.stamp.depend new file mode 100644 index 0000000..ead0a48 --- /dev/null +++ b/build_x64/CMakeFiles/generate.stamp.depend @@ -0,0 +1,359 @@ +# CMake generation dependency list for this directory. +C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeCXXInformation.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeCheckCompilerFlagCommonPatterns.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeCommonLanguageInclude.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeFindDependencyMacro.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeGenericSystem.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeInitializeConfigs.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeLanguageInformation.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeRCInformation.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeSystemSpecificInformation.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/CMakeSystemSpecificInitialize.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/CheckCXXCompilerFlag.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/CheckCXXSourceCompiles.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/CheckIncludeFileCXX.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/CheckLibraryExists.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/Compiler/CMakeCommonCompilerMacros.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/Compiler/MSVC-CXX.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/Compiler/MSVC.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/FindCURL.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/FindLibArchive.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/FindPackageHandleStandardArgs.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/FindPackageMessage.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/FindThreads.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/FindVulkan.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/FindZLIB.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/GNUInstallDirs.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/Internal/CMakeCXXLinkerInformation.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/Internal/CMakeCommonLinkerInformation.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/Internal/CheckCompilerFlag.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/Internal/CheckFlagCommonConfig.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/Internal/CheckSourceCompiles.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/Linker/MSVC-CXX.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/Linker/MSVC.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/Platform/Linker/Windows-MSVC-CXX.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/Platform/Linker/Windows-MSVC.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/Platform/Windows-Initialize.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/Platform/Windows-MSVC-CXX.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/Platform/Windows-MSVC.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/Platform/Windows.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/Platform/WindowsPaths.cmake +C:/Program Files/CMake/share/cmake-4.0/Modules/SelectLibraryConfigurations.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/FindWrapAtomic.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/FindWrapVulkanHeaders.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/Qt6Config.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/Qt6ConfigExtras.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/Qt6ConfigVersion.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/Qt6ConfigVersionImpl.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/Qt6Dependencies.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/Qt6Targets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/Qt6VersionlessAliasTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtFeature.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtFeatureCommon.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtInstallPaths.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicAndroidHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicAppleHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicCMakeHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicCMakeVersionHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicDependencyHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicExternalProjectHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicFinalizerHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicFindPackageHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicGitHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicPluginHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicPluginHelpers_v2.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicSbomAttributionHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicSbomCpeHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicSbomDepHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicSbomFileHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicSbomGenerationHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicSbomHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicSbomLicenseHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicSbomOpsHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicSbomPurlHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicSbomPythonHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicSbomQtEntityHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicSbomSystemDepHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicTargetHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicTestHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicToolHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicWalkLibsHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6/QtPublicWindowsHelpers.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Core/Qt6CoreAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Core/Qt6CoreConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Core/Qt6CoreConfigExtras.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Core/Qt6CoreConfigVersion.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Core/Qt6CoreConfigVersionImpl.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Core/Qt6CoreDependencies.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Core/Qt6CoreMacros.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Core/Qt6CoreTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Core/Qt6CoreTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Core/Qt6CoreTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Core/Qt6CoreVersionlessAliasTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6CorePrivate/Qt6CorePrivateAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6CorePrivate/Qt6CorePrivateConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6CorePrivate/Qt6CorePrivateConfigVersion.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6CorePrivate/Qt6CorePrivateConfigVersionImpl.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6CorePrivate/Qt6CorePrivateDependencies.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6CorePrivate/Qt6CorePrivateTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6CorePrivate/Qt6CorePrivateVersionlessAliasTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6CoreTools/Qt6CoreToolsAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6CoreTools/Qt6CoreToolsConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6CoreTools/Qt6CoreToolsConfigVersion.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6CoreTools/Qt6CoreToolsConfigVersionImpl.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6CoreTools/Qt6CoreToolsDependencies.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6CoreTools/Qt6CoreToolsTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6CoreTools/Qt6CoreToolsTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6CoreTools/Qt6CoreToolsTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6CoreTools/Qt6CoreToolsVersionlessTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6EntryPointPrivate/Qt6EntryPointPrivateAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6EntryPointPrivate/Qt6EntryPointPrivateConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6EntryPointPrivate/Qt6EntryPointPrivateConfigVersion.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6EntryPointPrivate/Qt6EntryPointPrivateConfigVersionImpl.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6EntryPointPrivate/Qt6EntryPointPrivateTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6EntryPointPrivate/Qt6EntryPointPrivateTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6EntryPointPrivate/Qt6EntryPointPrivateTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6EntryPointPrivate/Qt6EntryPointPrivateVersionlessAliasTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6GuiAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6GuiConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6GuiConfigVersion.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6GuiConfigVersionImpl.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6GuiDependencies.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6GuiPlugins.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6GuiTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6GuiTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6GuiTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6GuiVersionlessAliasTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QGifPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QGifPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QGifPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QGifPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QGifPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QICOPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QICOPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QICOPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QICOPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QICOPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QJpegPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QJpegPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QJpegPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QJpegPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QJpegPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QMinimalIntegrationPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QMinimalIntegrationPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QMinimalIntegrationPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QMinimalIntegrationPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QMinimalIntegrationPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QOffscreenIntegrationPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QOffscreenIntegrationPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QOffscreenIntegrationPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QOffscreenIntegrationPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QOffscreenIntegrationPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QSvgIconPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QSvgIconPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QSvgIconPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QSvgIconPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QSvgIconPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QSvgPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QSvgPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QSvgPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QSvgPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QSvgPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QTuioTouchPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QTuioTouchPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QTuioTouchPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QTuioTouchPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QTuioTouchPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QWindowsDirect2DIntegrationPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QWindowsDirect2DIntegrationPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QWindowsDirect2DIntegrationPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QWindowsDirect2DIntegrationPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QWindowsDirect2DIntegrationPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QWindowsIntegrationPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QWindowsIntegrationPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QWindowsIntegrationPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QWindowsIntegrationPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Gui/Qt6QWindowsIntegrationPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6GuiPrivate/Qt6GuiPrivateAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6GuiPrivate/Qt6GuiPrivateConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6GuiPrivate/Qt6GuiPrivateConfigVersion.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6GuiPrivate/Qt6GuiPrivateConfigVersionImpl.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6GuiPrivate/Qt6GuiPrivateDependencies.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6GuiPrivate/Qt6GuiPrivateTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6GuiPrivate/Qt6GuiPrivateVersionlessAliasTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6GuiTools/Qt6GuiToolsAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6GuiTools/Qt6GuiToolsConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6GuiTools/Qt6GuiToolsConfigVersion.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6GuiTools/Qt6GuiToolsConfigVersionImpl.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6GuiTools/Qt6GuiToolsDependencies.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6GuiTools/Qt6GuiToolsTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6GuiTools/Qt6GuiToolsTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6GuiTools/Qt6GuiToolsTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6GuiTools/Qt6GuiToolsVersionlessTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6NetworkAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6NetworkConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6NetworkConfigVersion.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6NetworkConfigVersionImpl.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6NetworkDependencies.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6NetworkPlugins.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6NetworkTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6NetworkTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6NetworkTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6NetworkVersionlessAliasTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QNLMNIPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QNLMNIPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QNLMNIPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QNLMNIPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QNLMNIPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QSchannelBackendPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QSchannelBackendPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QSchannelBackendPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QSchannelBackendPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QSchannelBackendPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QTlsBackendCertOnlyPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QTlsBackendCertOnlyPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QTlsBackendCertOnlyPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QTlsBackendCertOnlyPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QTlsBackendCertOnlyPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QTlsBackendOpenSSLPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QTlsBackendOpenSSLPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QTlsBackendOpenSSLPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QTlsBackendOpenSSLPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Network/Qt6QTlsBackendOpenSSLPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6NetworkPrivate/Qt6NetworkPrivateAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6NetworkPrivate/Qt6NetworkPrivateConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6NetworkPrivate/Qt6NetworkPrivateConfigVersion.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6NetworkPrivate/Qt6NetworkPrivateConfigVersionImpl.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6NetworkPrivate/Qt6NetworkPrivateDependencies.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6NetworkPrivate/Qt6NetworkPrivateTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6NetworkPrivate/Qt6NetworkPrivateVersionlessAliasTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QIBaseDriverPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QIBaseDriverPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QIBaseDriverPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QIBaseDriverPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QIBaseDriverPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QMimerSQLDriverPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QMimerSQLDriverPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QMimerSQLDriverPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QMimerSQLDriverPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QMimerSQLDriverPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QOCIDriverPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QOCIDriverPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QOCIDriverPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QOCIDriverPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QOCIDriverPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QODBCDriverPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QODBCDriverPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QODBCDriverPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QODBCDriverPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QODBCDriverPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QPSQLDriverPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QPSQLDriverPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QPSQLDriverPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QPSQLDriverPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QPSQLDriverPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QSQLiteDriverPluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QSQLiteDriverPluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QSQLiteDriverPluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QSQLiteDriverPluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6QSQLiteDriverPluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6SqlAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6SqlConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6SqlConfigVersion.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6SqlConfigVersionImpl.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6SqlDependencies.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6SqlPlugins.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6SqlTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6SqlTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6SqlTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Sql/Qt6SqlVersionlessAliasTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6SqlPrivate/Qt6SqlPrivateAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6SqlPrivate/Qt6SqlPrivateConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6SqlPrivate/Qt6SqlPrivateConfigVersion.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6SqlPrivate/Qt6SqlPrivateConfigVersionImpl.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6SqlPrivate/Qt6SqlPrivateDependencies.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6SqlPrivate/Qt6SqlPrivateTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6SqlPrivate/Qt6SqlPrivateVersionlessAliasTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Widgets/Qt6QModernWindowsStylePluginAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Widgets/Qt6QModernWindowsStylePluginConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Widgets/Qt6QModernWindowsStylePluginTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Widgets/Qt6QModernWindowsStylePluginTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Widgets/Qt6QModernWindowsStylePluginTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Widgets/Qt6WidgetsAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Widgets/Qt6WidgetsConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Widgets/Qt6WidgetsConfigVersion.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Widgets/Qt6WidgetsConfigVersionImpl.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Widgets/Qt6WidgetsDependencies.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Widgets/Qt6WidgetsMacros.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Widgets/Qt6WidgetsPlugins.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Widgets/Qt6WidgetsTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Widgets/Qt6WidgetsTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Widgets/Qt6WidgetsTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Widgets/Qt6WidgetsVersionlessAliasTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6WidgetsPrivate/Qt6WidgetsPrivateAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6WidgetsPrivate/Qt6WidgetsPrivateConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6WidgetsPrivate/Qt6WidgetsPrivateConfigVersion.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6WidgetsPrivate/Qt6WidgetsPrivateConfigVersionImpl.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6WidgetsPrivate/Qt6WidgetsPrivateDependencies.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6WidgetsPrivate/Qt6WidgetsPrivateTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6WidgetsPrivate/Qt6WidgetsPrivateVersionlessAliasTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6WidgetsTools/Qt6WidgetsToolsAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6WidgetsTools/Qt6WidgetsToolsConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6WidgetsTools/Qt6WidgetsToolsConfigVersion.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6WidgetsTools/Qt6WidgetsToolsConfigVersionImpl.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6WidgetsTools/Qt6WidgetsToolsDependencies.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6WidgetsTools/Qt6WidgetsToolsTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6WidgetsTools/Qt6WidgetsToolsTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6WidgetsTools/Qt6WidgetsToolsTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6WidgetsTools/Qt6WidgetsToolsVersionlessTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Xml/Qt6XmlAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Xml/Qt6XmlConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Xml/Qt6XmlConfigVersion.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Xml/Qt6XmlConfigVersionImpl.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Xml/Qt6XmlDependencies.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Xml/Qt6XmlTargets-debug.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Xml/Qt6XmlTargets-relwithdebinfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Xml/Qt6XmlTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6Xml/Qt6XmlVersionlessAliasTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6XmlPrivate/Qt6XmlPrivateAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6XmlPrivate/Qt6XmlPrivateConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6XmlPrivate/Qt6XmlPrivateConfigVersion.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6XmlPrivate/Qt6XmlPrivateConfigVersionImpl.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6XmlPrivate/Qt6XmlPrivateDependencies.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6XmlPrivate/Qt6XmlPrivateTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6XmlPrivate/Qt6XmlPrivateVersionlessAliasTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6ZlibPrivate/Qt6ZlibPrivateAdditionalTargetInfo.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6ZlibPrivate/Qt6ZlibPrivateConfig.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6ZlibPrivate/Qt6ZlibPrivateConfigVersion.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6ZlibPrivate/Qt6ZlibPrivateConfigVersionImpl.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6ZlibPrivate/Qt6ZlibPrivateTargets.cmake +C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6ZlibPrivate/Qt6ZlibPrivateVersionlessAliasTargets.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/WebP/WebPConfig.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/WebP/WebPConfigVersion.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/WebP/WebPTargets-debug.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/WebP/WebPTargets-release.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/WebP/WebPTargets.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/curl/CURLConfig.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/curl/CURLConfigVersion.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/curl/CURLTargets-debug.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/curl/CURLTargets-release.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/curl/CURLTargets.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/leptonica/LeptonicaConfig-version.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/leptonica/LeptonicaConfig.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/leptonica/LeptonicaTargets-debug.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/leptonica/LeptonicaTargets-release.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/leptonica/LeptonicaTargets.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/openjpeg/OpenJPEGConfig.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/openjpeg/OpenJPEGConfigVersion.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/openjpeg/OpenJPEGTargets-debug.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/openjpeg/OpenJPEGTargets-release.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/openjpeg/OpenJPEGTargets.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/tesseract/TesseractConfig.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/tesseract/TesseractConfigVersion.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/tesseract/TesseractTargets-debug.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/tesseract/TesseractTargets-release.cmake +C:/Users/User/.vcpkg-clion/vcpkg/installed/x64-windows/share/tesseract/TesseractTargets.cmake +C:/Users/User/CLionProjects/ARCDeskProject/CMakeLists.txt +C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/4.0.2/CMakeCXXCompiler.cmake +C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/4.0.2/CMakeRCCompiler.cmake +C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/4.0.2/CMakeSystem.cmake diff --git a/build_x64/CMakeFiles/generate.stamp.list b/build_x64/CMakeFiles/generate.stamp.list new file mode 100644 index 0000000..19a9122 --- /dev/null +++ b/build_x64/CMakeFiles/generate.stamp.list @@ -0,0 +1 @@ +C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/generate.stamp diff --git a/build_x64/Release/D3Dcompiler_47.dll b/build_x64/Release/D3Dcompiler_47.dll new file mode 100644 index 0000000..56512f5 Binary files /dev/null and b/build_x64/Release/D3Dcompiler_47.dll differ diff --git a/build_x64/Release/Qt6Core.dll b/build_x64/Release/Qt6Core.dll new file mode 100644 index 0000000..954a554 Binary files /dev/null and b/build_x64/Release/Qt6Core.dll differ diff --git a/build_x64/Release/Qt6Gui.dll b/build_x64/Release/Qt6Gui.dll new file mode 100644 index 0000000..04745cb Binary files /dev/null and b/build_x64/Release/Qt6Gui.dll differ diff --git a/build_x64/Release/Qt6Network.dll b/build_x64/Release/Qt6Network.dll new file mode 100644 index 0000000..f3988b9 Binary files /dev/null and b/build_x64/Release/Qt6Network.dll differ diff --git a/build_x64/Release/Qt6Sql.dll b/build_x64/Release/Qt6Sql.dll new file mode 100644 index 0000000..5710512 Binary files /dev/null and b/build_x64/Release/Qt6Sql.dll differ diff --git a/build_x64/Release/Qt6Svg.dll b/build_x64/Release/Qt6Svg.dll new file mode 100644 index 0000000..f0fe0d9 Binary files /dev/null and b/build_x64/Release/Qt6Svg.dll differ diff --git a/build_x64/Release/Qt6Widgets.dll b/build_x64/Release/Qt6Widgets.dll new file mode 100644 index 0000000..e583ef4 Binary files /dev/null and b/build_x64/Release/Qt6Widgets.dll differ diff --git a/build_x64/Release/Qt6Xml.dll b/build_x64/Release/Qt6Xml.dll new file mode 100644 index 0000000..7ed7813 Binary files /dev/null and b/build_x64/Release/Qt6Xml.dll differ diff --git a/build_x64/Release/config.ini b/build_x64/Release/config.ini new file mode 100644 index 0000000..fab9609 --- /dev/null +++ b/build_x64/Release/config.ini @@ -0,0 +1,37 @@ +[apps] +list=ozon, black, dushanbe_city_bank + +[common] +currencies=USD, ARS, RUB, TJS +version=0.0.1 + +[db] +dbPath=./database/app_data.db + +[net] +apiBase=http://93.183.75.134:8005 +pathAuthLogin=/api/v1/auth/login +pathAuthRefresh=/api/v1/auth/refresh_token +pathBankProfile=/api/v1/profile/bank_profile +pathDesktopCreate=/api/v1/desktop/ +pathDevice=/api/v1/device/ +pathDeviceScreenshot=/api/v1/device/add_screenshot/ +pathMaterial=/api/v1/profile/material + +[black] +android_package_name=app.blackwallet.ar +currency=ARS +name=Black +phone_code=54 + +[ozon] +android_package_name=ru.ozon.fintech.finance +currency=RUB +name=Ozon +phone_code=7 + +[dushanbe_city_bank] +android_package_name=tj.dc.next1 +currency=TJS +name=Dushanbe City +phone_code=992 diff --git a/build_x64/Release/eng.traineddata b/build_x64/Release/eng.traineddata new file mode 100644 index 0000000..f4744c2 Binary files /dev/null and b/build_x64/Release/eng.traineddata differ diff --git a/build_x64/Release/generic/qtuiotouchplugin.dll b/build_x64/Release/generic/qtuiotouchplugin.dll new file mode 100644 index 0000000..acdbe38 Binary files /dev/null and b/build_x64/Release/generic/qtuiotouchplugin.dll differ diff --git a/build_x64/Release/iconengines/qsvgicon.dll b/build_x64/Release/iconengines/qsvgicon.dll new file mode 100644 index 0000000..ba4ae1f Binary files /dev/null and b/build_x64/Release/iconengines/qsvgicon.dll differ diff --git a/build_x64/Release/imageformats/qgif.dll b/build_x64/Release/imageformats/qgif.dll new file mode 100644 index 0000000..698ac2a Binary files /dev/null and b/build_x64/Release/imageformats/qgif.dll differ diff --git a/build_x64/Release/imageformats/qico.dll b/build_x64/Release/imageformats/qico.dll new file mode 100644 index 0000000..ae362a3 Binary files /dev/null and b/build_x64/Release/imageformats/qico.dll differ diff --git a/build_x64/Release/imageformats/qjpeg.dll b/build_x64/Release/imageformats/qjpeg.dll new file mode 100644 index 0000000..c35c372 Binary files /dev/null and b/build_x64/Release/imageformats/qjpeg.dll differ diff --git a/build_x64/Release/imageformats/qsvg.dll b/build_x64/Release/imageformats/qsvg.dll new file mode 100644 index 0000000..a7c1894 Binary files /dev/null and b/build_x64/Release/imageformats/qsvg.dll differ diff --git a/build_x64/Release/images/alfabank_icon.png b/build_x64/Release/images/alfabank_icon.png new file mode 100644 index 0000000..40bb199 Binary files /dev/null and b/build_x64/Release/images/alfabank_icon.png differ diff --git a/build_x64/Release/images/birbank_icon.png b/build_x64/Release/images/birbank_icon.png new file mode 100644 index 0000000..531a3fa Binary files /dev/null and b/build_x64/Release/images/birbank_icon.png differ diff --git a/build_x64/Release/images/cogwheel.png b/build_x64/Release/images/cogwheel.png new file mode 100644 index 0000000..23d33ba Binary files /dev/null and b/build_x64/Release/images/cogwheel.png differ diff --git a/build_x64/Release/images/ozon_icon.png b/build_x64/Release/images/ozon_icon.png new file mode 100644 index 0000000..363c802 Binary files /dev/null and b/build_x64/Release/images/ozon_icon.png differ diff --git a/build_x64/Release/images/rshb_icon.png b/build_x64/Release/images/rshb_icon.png new file mode 100644 index 0000000..a5124c1 Binary files /dev/null and b/build_x64/Release/images/rshb_icon.png differ diff --git a/build_x64/Release/images/santanderbank_icon.png b/build_x64/Release/images/santanderbank_icon.png new file mode 100644 index 0000000..4b0d927 Binary files /dev/null and b/build_x64/Release/images/santanderbank_icon.png differ diff --git a/build_x64/Release/images/toss_icon.png b/build_x64/Release/images/toss_icon.png new file mode 100644 index 0000000..efe17c7 Binary files /dev/null and b/build_x64/Release/images/toss_icon.png differ diff --git a/build_x64/Release/images/turn_off.png b/build_x64/Release/images/turn_off.png new file mode 100644 index 0000000..bcce6c6 Binary files /dev/null and b/build_x64/Release/images/turn_off.png differ diff --git a/build_x64/Release/images/turn_on.png b/build_x64/Release/images/turn_on.png new file mode 100644 index 0000000..53b7552 Binary files /dev/null and b/build_x64/Release/images/turn_on.png differ diff --git a/build_x64/Release/networkinformation/qnetworklistmanager.dll b/build_x64/Release/networkinformation/qnetworklistmanager.dll new file mode 100644 index 0000000..43f7759 Binary files /dev/null and b/build_x64/Release/networkinformation/qnetworklistmanager.dll differ diff --git a/build_x64/Release/opengl32sw.dll b/build_x64/Release/opengl32sw.dll new file mode 100644 index 0000000..1bf3581 Binary files /dev/null and b/build_x64/Release/opengl32sw.dll differ diff --git a/build_x64/Release/platforms/qwindows.dll b/build_x64/Release/platforms/qwindows.dll new file mode 100644 index 0000000..5fa94ce Binary files /dev/null and b/build_x64/Release/platforms/qwindows.dll differ diff --git a/build_x64/Release/rus.traineddata b/build_x64/Release/rus.traineddata new file mode 100644 index 0000000..8b71c2d Binary files /dev/null and b/build_x64/Release/rus.traineddata differ diff --git a/build_x64/Release/sqldrivers/qsqlibase.dll b/build_x64/Release/sqldrivers/qsqlibase.dll new file mode 100644 index 0000000..09a0052 Binary files /dev/null and b/build_x64/Release/sqldrivers/qsqlibase.dll differ diff --git a/build_x64/Release/sqldrivers/qsqlite.dll b/build_x64/Release/sqldrivers/qsqlite.dll new file mode 100644 index 0000000..de84cc1 Binary files /dev/null and b/build_x64/Release/sqldrivers/qsqlite.dll differ diff --git a/build_x64/Release/sqldrivers/qsqlmimer.dll b/build_x64/Release/sqldrivers/qsqlmimer.dll new file mode 100644 index 0000000..74a2d9e Binary files /dev/null and b/build_x64/Release/sqldrivers/qsqlmimer.dll differ diff --git a/build_x64/Release/sqldrivers/qsqloci.dll b/build_x64/Release/sqldrivers/qsqloci.dll new file mode 100644 index 0000000..c49072f Binary files /dev/null and b/build_x64/Release/sqldrivers/qsqloci.dll differ diff --git a/build_x64/Release/sqldrivers/qsqlodbc.dll b/build_x64/Release/sqldrivers/qsqlodbc.dll new file mode 100644 index 0000000..5113876 Binary files /dev/null and b/build_x64/Release/sqldrivers/qsqlodbc.dll differ diff --git a/build_x64/Release/sqldrivers/qsqlpsql.dll b/build_x64/Release/sqldrivers/qsqlpsql.dll new file mode 100644 index 0000000..6cf7067 Binary files /dev/null and b/build_x64/Release/sqldrivers/qsqlpsql.dll differ diff --git a/build_x64/Release/styles/qmodernwindowsstyle.dll b/build_x64/Release/styles/qmodernwindowsstyle.dll new file mode 100644 index 0000000..a49f56d Binary files /dev/null and b/build_x64/Release/styles/qmodernwindowsstyle.dll differ diff --git a/build_x64/Release/tls/qcertonlybackend.dll b/build_x64/Release/tls/qcertonlybackend.dll new file mode 100644 index 0000000..53e5b2e Binary files /dev/null and b/build_x64/Release/tls/qcertonlybackend.dll differ diff --git a/build_x64/Release/tls/qschannelbackend.dll b/build_x64/Release/tls/qschannelbackend.dll new file mode 100644 index 0000000..bd9a900 Binary files /dev/null and b/build_x64/Release/tls/qschannelbackend.dll differ diff --git a/build_x64/Release/tools/adb/windows/AdbWinApi.dll b/build_x64/Release/tools/adb/windows/AdbWinApi.dll new file mode 100644 index 0000000..c32abf5 Binary files /dev/null and b/build_x64/Release/tools/adb/windows/AdbWinApi.dll differ diff --git a/build_x64/Release/tools/adb/windows/AdbWinUsbApi.dll b/build_x64/Release/tools/adb/windows/AdbWinUsbApi.dll new file mode 100644 index 0000000..d10d1e1 Binary files /dev/null and b/build_x64/Release/tools/adb/windows/AdbWinUsbApi.dll differ diff --git a/build_x64/Release/tools/adb/windows/adb.exe b/build_x64/Release/tools/adb/windows/adb.exe new file mode 100644 index 0000000..10784b3 Binary files /dev/null and b/build_x64/Release/tools/adb/windows/adb.exe differ diff --git a/build_x64/Release/tools/adb/windows/libwinpthread-1.dll b/build_x64/Release/tools/adb/windows/libwinpthread-1.dll new file mode 100644 index 0000000..f6599c7 Binary files /dev/null and b/build_x64/Release/tools/adb/windows/libwinpthread-1.dll differ diff --git a/build_x64/Release/translations/qt_ar.qm b/build_x64/Release/translations/qt_ar.qm new file mode 100644 index 0000000..32861b8 Binary files /dev/null and b/build_x64/Release/translations/qt_ar.qm differ diff --git a/build_x64/Release/translations/qt_bg.qm b/build_x64/Release/translations/qt_bg.qm new file mode 100644 index 0000000..faeb167 Binary files /dev/null and b/build_x64/Release/translations/qt_bg.qm differ diff --git a/build_x64/Release/translations/qt_ca.qm b/build_x64/Release/translations/qt_ca.qm new file mode 100644 index 0000000..4082e41 Binary files /dev/null and b/build_x64/Release/translations/qt_ca.qm differ diff --git a/build_x64/Release/translations/qt_cs.qm b/build_x64/Release/translations/qt_cs.qm new file mode 100644 index 0000000..459ef26 Binary files /dev/null and b/build_x64/Release/translations/qt_cs.qm differ diff --git a/build_x64/Release/translations/qt_da.qm b/build_x64/Release/translations/qt_da.qm new file mode 100644 index 0000000..4ede24b Binary files /dev/null and b/build_x64/Release/translations/qt_da.qm differ diff --git a/build_x64/Release/translations/qt_de.qm b/build_x64/Release/translations/qt_de.qm new file mode 100644 index 0000000..20d7fd2 Binary files /dev/null and b/build_x64/Release/translations/qt_de.qm differ diff --git a/build_x64/Release/translations/qt_en.qm b/build_x64/Release/translations/qt_en.qm new file mode 100644 index 0000000..937ea3e Binary files /dev/null and b/build_x64/Release/translations/qt_en.qm differ diff --git a/build_x64/Release/translations/qt_es.qm b/build_x64/Release/translations/qt_es.qm new file mode 100644 index 0000000..1a13157 Binary files /dev/null and b/build_x64/Release/translations/qt_es.qm differ diff --git a/build_x64/Release/translations/qt_fa.qm b/build_x64/Release/translations/qt_fa.qm new file mode 100644 index 0000000..aadc0c1 Binary files /dev/null and b/build_x64/Release/translations/qt_fa.qm differ diff --git a/build_x64/Release/translations/qt_fi.qm b/build_x64/Release/translations/qt_fi.qm new file mode 100644 index 0000000..934aecd Binary files /dev/null and b/build_x64/Release/translations/qt_fi.qm differ diff --git a/build_x64/Release/translations/qt_fr.qm b/build_x64/Release/translations/qt_fr.qm new file mode 100644 index 0000000..2e977a4 Binary files /dev/null and b/build_x64/Release/translations/qt_fr.qm differ diff --git a/build_x64/Release/translations/qt_gd.qm b/build_x64/Release/translations/qt_gd.qm new file mode 100644 index 0000000..3fe3841 Binary files /dev/null and b/build_x64/Release/translations/qt_gd.qm differ diff --git a/build_x64/Release/translations/qt_he.qm b/build_x64/Release/translations/qt_he.qm new file mode 100644 index 0000000..95ed0c7 Binary files /dev/null and b/build_x64/Release/translations/qt_he.qm differ diff --git a/build_x64/Release/translations/qt_hr.qm b/build_x64/Release/translations/qt_hr.qm new file mode 100644 index 0000000..4ed06fb Binary files /dev/null and b/build_x64/Release/translations/qt_hr.qm differ diff --git a/build_x64/Release/translations/qt_hu.qm b/build_x64/Release/translations/qt_hu.qm new file mode 100644 index 0000000..291bb89 Binary files /dev/null and b/build_x64/Release/translations/qt_hu.qm differ diff --git a/build_x64/Release/translations/qt_it.qm b/build_x64/Release/translations/qt_it.qm new file mode 100644 index 0000000..3785a6f Binary files /dev/null and b/build_x64/Release/translations/qt_it.qm differ diff --git a/build_x64/Release/translations/qt_ja.qm b/build_x64/Release/translations/qt_ja.qm new file mode 100644 index 0000000..acd2f03 Binary files /dev/null and b/build_x64/Release/translations/qt_ja.qm differ diff --git a/build_x64/Release/translations/qt_ka.qm b/build_x64/Release/translations/qt_ka.qm new file mode 100644 index 0000000..2756e92 Binary files /dev/null and b/build_x64/Release/translations/qt_ka.qm differ diff --git a/build_x64/Release/translations/qt_ko.qm b/build_x64/Release/translations/qt_ko.qm new file mode 100644 index 0000000..20e4661 Binary files /dev/null and b/build_x64/Release/translations/qt_ko.qm differ diff --git a/build_x64/Release/translations/qt_lv.qm b/build_x64/Release/translations/qt_lv.qm new file mode 100644 index 0000000..f88a761 Binary files /dev/null and b/build_x64/Release/translations/qt_lv.qm differ diff --git a/build_x64/Release/translations/qt_nl.qm b/build_x64/Release/translations/qt_nl.qm new file mode 100644 index 0000000..de4e74a Binary files /dev/null and b/build_x64/Release/translations/qt_nl.qm differ diff --git a/build_x64/Release/translations/qt_nn.qm b/build_x64/Release/translations/qt_nn.qm new file mode 100644 index 0000000..506ec45 Binary files /dev/null and b/build_x64/Release/translations/qt_nn.qm differ diff --git a/build_x64/Release/translations/qt_pl.qm b/build_x64/Release/translations/qt_pl.qm new file mode 100644 index 0000000..3c4e03b Binary files /dev/null and b/build_x64/Release/translations/qt_pl.qm differ diff --git a/build_x64/Release/translations/qt_pt_BR.qm b/build_x64/Release/translations/qt_pt_BR.qm new file mode 100644 index 0000000..6fabd0e Binary files /dev/null and b/build_x64/Release/translations/qt_pt_BR.qm differ diff --git a/build_x64/Release/translations/qt_ru.qm b/build_x64/Release/translations/qt_ru.qm new file mode 100644 index 0000000..c1a2286 Binary files /dev/null and b/build_x64/Release/translations/qt_ru.qm differ diff --git a/build_x64/Release/translations/qt_sk.qm b/build_x64/Release/translations/qt_sk.qm new file mode 100644 index 0000000..55a377e Binary files /dev/null and b/build_x64/Release/translations/qt_sk.qm differ diff --git a/build_x64/Release/translations/qt_tr.qm b/build_x64/Release/translations/qt_tr.qm new file mode 100644 index 0000000..efed395 Binary files /dev/null and b/build_x64/Release/translations/qt_tr.qm differ diff --git a/build_x64/Release/translations/qt_uk.qm b/build_x64/Release/translations/qt_uk.qm new file mode 100644 index 0000000..96aeaf9 Binary files /dev/null and b/build_x64/Release/translations/qt_uk.qm differ diff --git a/build_x64/Release/translations/qt_zh_CN.qm b/build_x64/Release/translations/qt_zh_CN.qm new file mode 100644 index 0000000..ba2273c Binary files /dev/null and b/build_x64/Release/translations/qt_zh_CN.qm differ diff --git a/build_x64/Release/translations/qt_zh_TW.qm b/build_x64/Release/translations/qt_zh_TW.qm new file mode 100644 index 0000000..f32a72f Binary files /dev/null and b/build_x64/Release/translations/qt_zh_TW.qm differ diff --git a/build_x64/ZERO_CHECK.vcxproj b/build_x64/ZERO_CHECK.vcxproj new file mode 100644 index 0000000..a9245a4 --- /dev/null +++ b/build_x64/ZERO_CHECK.vcxproj @@ -0,0 +1,179 @@ + + + + ARM64 + + + false + + + + Debug + x64 + + + Release + x64 + + + MinSizeRel + x64 + + + RelWithDebInfo + x64 + + + + {4D99233C-723D-3DF3-8D31-21835B437C32} + Win32Proj + 10.0.22621.0 + x64 + ZERO_CHECK + NoUpgrade + + + + Utility + MultiByte + v143 + + + Utility + MultiByte + v143 + + + Utility + MultiByte + v143 + + + Utility + MultiByte + v143 + + + + + + + + + + <_ProjectFileVersion>10.0.20506.1 + $(Platform)\$(Configuration)\$(ProjectName)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + $(Platform)\$(Configuration)\$(ProjectName)\ + + + + %(AdditionalIncludeDirectories) + $(ProjectDir)/$(IntDir) + %(Filename).h + %(Filename).tlb + %(Filename)_i.c + %(Filename)_p.c + + + + + %(AdditionalIncludeDirectories) + $(ProjectDir)/$(IntDir) + %(Filename).h + %(Filename).tlb + %(Filename)_i.c + %(Filename)_p.c + + + + + %(AdditionalIncludeDirectories) + $(ProjectDir)/$(IntDir) + %(Filename).h + %(Filename).tlb + %(Filename)_i.c + %(Filename)_p.c + + + + + %(AdditionalIncludeDirectories) + $(ProjectDir)/$(IntDir) + %(Filename).h + %(Filename).tlb + %(Filename)_i.c + %(Filename)_p.c + + + + + Always + true + Checking Build System + setlocal +"C:\Program Files\CMake\bin\cmake.exe" -SC:/Users/User/CLionProjects/ARCDeskProject -BC:/Users/User/CLionProjects/ARCDeskProject/build_x64 --check-stamp-list CMakeFiles/generate.stamp.list --vs-solution-file C:/Users/User/CLionProjects/ARCDeskProject/build_x64/ARC.sln +if %errorlevel% neq 0 goto :cmEnd +:cmEnd +endlocal & call :cmErrorLevel %errorlevel% & goto :cmDone +:cmErrorLevel +exit /b %1 +:cmDone +if %errorlevel% neq 0 goto :VCEnd + C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCXXInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCheckCompilerFlagCommonPatterns.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCommonLanguageInclude.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeFindDependencyMacro.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeGenericSystem.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeInitializeConfigs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeLanguageInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeRCInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeSystemSpecificInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeSystemSpecificInitialize.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckCXXCompilerFlag.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckCXXSourceCompiles.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckIncludeFileCXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckLibraryExists.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\CMakeCommonCompilerMacros.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindCURL.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindLibArchive.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindPackageHandleStandardArgs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindPackageMessage.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindThreads.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindVulkan.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindZLIB.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\GNUInstallDirs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CMakeCXXLinkerInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CMakeCommonLinkerInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckCompilerFlag.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckFlagCommonConfig.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckSourceCompiles.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Linker\MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Linker\MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Linker\Windows-MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Linker\Windows-MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-Initialize.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\WindowsPaths.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\SelectLibraryConfigurations.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\FindWrapAtomic.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\FindWrapVulkanHeaders.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Config.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigExtras.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Dependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Targets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6VersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtFeature.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtFeatureCommon.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtInstallPaths.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicAndroidHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicAppleHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicCMakeHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicCMakeVersionHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicDependencyHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicExternalProjectHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicFinalizerHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicFindPackageHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicGitHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicPluginHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicPluginHelpers_v2.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomAttributionHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomCpeHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomDepHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomFileHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomGenerationHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomLicenseHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomOpsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomPurlHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomPythonHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomQtEntityHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomSystemDepHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicTargetHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicTestHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicToolHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicWalkLibsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicWindowsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigExtras.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreMacros.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsMacros.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateVersionlessAliasTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaConfig-version.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets.cmake;C:\Users\User\CLionProjects\ARCDeskProject\CMakeLists.txt;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeCXXCompiler.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeRCCompiler.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeSystem.cmake;%(AdditionalInputs) + C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\generate.stamp + false + true + Checking Build System + setlocal +"C:\Program Files\CMake\bin\cmake.exe" -SC:/Users/User/CLionProjects/ARCDeskProject -BC:/Users/User/CLionProjects/ARCDeskProject/build_x64 --check-stamp-list CMakeFiles/generate.stamp.list --vs-solution-file C:/Users/User/CLionProjects/ARCDeskProject/build_x64/ARC.sln +if %errorlevel% neq 0 goto :cmEnd +:cmEnd +endlocal & call :cmErrorLevel %errorlevel% & goto :cmDone +:cmErrorLevel +exit /b %1 +:cmDone +if %errorlevel% neq 0 goto :VCEnd + C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCXXInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCheckCompilerFlagCommonPatterns.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCommonLanguageInclude.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeFindDependencyMacro.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeGenericSystem.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeInitializeConfigs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeLanguageInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeRCInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeSystemSpecificInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeSystemSpecificInitialize.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckCXXCompilerFlag.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckCXXSourceCompiles.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckIncludeFileCXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckLibraryExists.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\CMakeCommonCompilerMacros.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindCURL.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindLibArchive.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindPackageHandleStandardArgs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindPackageMessage.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindThreads.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindVulkan.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindZLIB.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\GNUInstallDirs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CMakeCXXLinkerInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CMakeCommonLinkerInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckCompilerFlag.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckFlagCommonConfig.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckSourceCompiles.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Linker\MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Linker\MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Linker\Windows-MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Linker\Windows-MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-Initialize.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\WindowsPaths.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\SelectLibraryConfigurations.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\FindWrapAtomic.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\FindWrapVulkanHeaders.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Config.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigExtras.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Dependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Targets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6VersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtFeature.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtFeatureCommon.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtInstallPaths.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicAndroidHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicAppleHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicCMakeHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicCMakeVersionHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicDependencyHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicExternalProjectHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicFinalizerHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicFindPackageHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicGitHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicPluginHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicPluginHelpers_v2.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomAttributionHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomCpeHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomDepHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomFileHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomGenerationHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomLicenseHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomOpsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomPurlHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomPythonHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomQtEntityHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomSystemDepHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicTargetHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicTestHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicToolHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicWalkLibsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicWindowsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigExtras.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreMacros.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsMacros.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateVersionlessAliasTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaConfig-version.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets.cmake;C:\Users\User\CLionProjects\ARCDeskProject\CMakeLists.txt;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeCXXCompiler.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeRCCompiler.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeSystem.cmake;%(AdditionalInputs) + C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\generate.stamp + false + true + Checking Build System + setlocal +"C:\Program Files\CMake\bin\cmake.exe" -SC:/Users/User/CLionProjects/ARCDeskProject -BC:/Users/User/CLionProjects/ARCDeskProject/build_x64 --check-stamp-list CMakeFiles/generate.stamp.list --vs-solution-file C:/Users/User/CLionProjects/ARCDeskProject/build_x64/ARC.sln +if %errorlevel% neq 0 goto :cmEnd +:cmEnd +endlocal & call :cmErrorLevel %errorlevel% & goto :cmDone +:cmErrorLevel +exit /b %1 +:cmDone +if %errorlevel% neq 0 goto :VCEnd + C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCXXInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCheckCompilerFlagCommonPatterns.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCommonLanguageInclude.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeFindDependencyMacro.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeGenericSystem.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeInitializeConfigs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeLanguageInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeRCInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeSystemSpecificInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeSystemSpecificInitialize.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckCXXCompilerFlag.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckCXXSourceCompiles.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckIncludeFileCXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckLibraryExists.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\CMakeCommonCompilerMacros.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindCURL.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindLibArchive.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindPackageHandleStandardArgs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindPackageMessage.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindThreads.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindVulkan.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindZLIB.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\GNUInstallDirs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CMakeCXXLinkerInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CMakeCommonLinkerInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckCompilerFlag.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckFlagCommonConfig.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckSourceCompiles.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Linker\MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Linker\MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Linker\Windows-MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Linker\Windows-MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-Initialize.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\WindowsPaths.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\SelectLibraryConfigurations.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\FindWrapAtomic.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\FindWrapVulkanHeaders.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Config.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigExtras.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Dependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Targets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6VersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtFeature.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtFeatureCommon.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtInstallPaths.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicAndroidHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicAppleHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicCMakeHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicCMakeVersionHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicDependencyHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicExternalProjectHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicFinalizerHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicFindPackageHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicGitHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicPluginHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicPluginHelpers_v2.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomAttributionHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomCpeHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomDepHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomFileHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomGenerationHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomLicenseHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomOpsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomPurlHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomPythonHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomQtEntityHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomSystemDepHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicTargetHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicTestHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicToolHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicWalkLibsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicWindowsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigExtras.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreMacros.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsMacros.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateVersionlessAliasTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaConfig-version.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets.cmake;C:\Users\User\CLionProjects\ARCDeskProject\CMakeLists.txt;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeCXXCompiler.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeRCCompiler.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeSystem.cmake;%(AdditionalInputs) + C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\generate.stamp + false + true + Checking Build System + setlocal +"C:\Program Files\CMake\bin\cmake.exe" -SC:/Users/User/CLionProjects/ARCDeskProject -BC:/Users/User/CLionProjects/ARCDeskProject/build_x64 --check-stamp-list CMakeFiles/generate.stamp.list --vs-solution-file C:/Users/User/CLionProjects/ARCDeskProject/build_x64/ARC.sln +if %errorlevel% neq 0 goto :cmEnd +:cmEnd +endlocal & call :cmErrorLevel %errorlevel% & goto :cmDone +:cmErrorLevel +exit /b %1 +:cmDone +if %errorlevel% neq 0 goto :VCEnd + C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCXXInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCheckCompilerFlagCommonPatterns.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeCommonLanguageInclude.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeFindDependencyMacro.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeGenericSystem.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeInitializeConfigs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeLanguageInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeRCInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeSystemSpecificInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CMakeSystemSpecificInitialize.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckCXXCompilerFlag.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckCXXSourceCompiles.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckIncludeFileCXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\CheckLibraryExists.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\CMakeCommonCompilerMacros.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Compiler\MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindCURL.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindLibArchive.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindPackageHandleStandardArgs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindPackageMessage.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindThreads.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindVulkan.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\FindZLIB.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\GNUInstallDirs.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CMakeCXXLinkerInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CMakeCommonLinkerInformation.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckCompilerFlag.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckFlagCommonConfig.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Internal\CheckSourceCompiles.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Linker\MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Linker\MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Linker\Windows-MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Linker\Windows-MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-Initialize.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-MSVC-CXX.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows-MSVC.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\Windows.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\Platform\WindowsPaths.cmake;C:\Program Files\CMake\share\cmake-4.0\Modules\SelectLibraryConfigurations.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\FindWrapAtomic.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\FindWrapVulkanHeaders.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Config.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigExtras.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6ConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Dependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6Targets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\Qt6VersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtFeature.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtFeatureCommon.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtInstallPaths.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicAndroidHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicAppleHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicCMakeHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicCMakeVersionHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicDependencyHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicExternalProjectHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicFinalizerHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicFindPackageHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicGitHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicPluginHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicPluginHelpers_v2.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomAttributionHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomCpeHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomDepHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomFileHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomGenerationHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomLicenseHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomOpsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomPurlHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomPythonHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomQtEntityHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicSbomSystemDepHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicTargetHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicTestHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicToolHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicWalkLibsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6\QtPublicWindowsHelpers.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigExtras.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreMacros.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Core\Qt6CoreVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CorePrivate\Qt6CorePrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6CoreTools\Qt6CoreToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6EntryPointPrivate\Qt6EntryPointPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6GuiVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QGifPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QICOPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QJpegPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QMinimalIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QOffscreenIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgIconPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QSvgPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QTuioTouchPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsDirect2DIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Gui\Qt6QWindowsIntegrationPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiPrivate\Qt6GuiPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6GuiTools\Qt6GuiToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6NetworkVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QNLMNIPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QSchannelBackendPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendCertOnlyPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Network\Qt6QTlsBackendOpenSSLPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6NetworkPrivate\Qt6NetworkPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QIBaseDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QMimerSQLDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QOCIDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QODBCDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QPSQLDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6QSQLiteDriverPluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Sql\Qt6SqlVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6SqlPrivate\Qt6SqlPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6QModernWindowsStylePluginTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsMacros.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsPlugins.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Widgets\Qt6WidgetsVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsPrivate\Qt6WidgetsPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6WidgetsTools\Qt6WidgetsToolsVersionlessTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets-debug.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets-relwithdebinfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6Xml\Qt6XmlVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateDependencies.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6XmlPrivate\Qt6XmlPrivateVersionlessAliasTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateAdditionalTargetInfo.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfig.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfigVersion.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateConfigVersionImpl.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateTargets.cmake;C:\Qt\6.9.0\msvc2022_64\lib\cmake\Qt6ZlibPrivate\Qt6ZlibPrivateVersionlessAliasTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\WebP\WebPTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\curl\CURLTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaConfig-version.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\leptonica\LeptonicaTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\openjpeg\OpenJPEGTargets.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractConfig.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractConfigVersion.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets-debug.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets-release.cmake;C:\Users\User\.vcpkg-clion\vcpkg\installed\x64-windows\share\tesseract\TesseractTargets.cmake;C:\Users\User\CLionProjects\ARCDeskProject\CMakeLists.txt;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeCXXCompiler.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeRCCompiler.cmake;C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\4.0.2\CMakeSystem.cmake;%(AdditionalInputs) + C:\Users\User\CLionProjects\ARCDeskProject\build_x64\CMakeFiles\generate.stamp + false + + + + + + + + + + + \ No newline at end of file diff --git a/build_x64/ZERO_CHECK.vcxproj.filters b/build_x64/ZERO_CHECK.vcxproj.filters new file mode 100644 index 0000000..8960a7e --- /dev/null +++ b/build_x64/ZERO_CHECK.vcxproj.filters @@ -0,0 +1,13 @@ + + + + + CMake Rules + + + + + {9AC283BE-D891-32D8-8CCA-6BA05672C74C} + + + diff --git a/build_x64/config.ini b/build_x64/config.ini new file mode 100644 index 0000000..935a229 --- /dev/null +++ b/build_x64/config.ini @@ -0,0 +1,37 @@ +[apps] +list=ozon, black, dushanbe_city_bank + +[common] +currencies=USD, ARS, RUB, TJS +version=0.0.2 + +[db] +dbPath=./database/app_data.db + +[net] +apiBase=http://93.183.75.134:8005 +pathAuthLogin=/api/v1/auth/login +pathAuthRefresh=/api/v1/auth/refresh_token +pathBankProfile=/api/v1/profile/bank_profile +pathDesktopCreate=/api/v1/desktop/ +pathDevice=/api/v1/device/ +pathDeviceScreenshot=/api/v1/device/add_screenshot/ +pathMaterial=/api/v1/profile/material + +[black] +android_package_name=app.blackwallet.ar +currency=ARS +name=Black +phone_code=54 + +[ozon] +android_package_name=ru.ozon.fintech.finance +currency=RUB +name=Ozon +phone_code=7 + +[dushanbe_city_bank] +android_package_name=tj.dc.next1 +currency=TJS +name=Dushanbe City +phone_code=992 diff --git a/build_x64/eng.traineddata b/build_x64/eng.traineddata new file mode 100644 index 0000000..f4744c2 Binary files /dev/null and b/build_x64/eng.traineddata differ diff --git a/build_x64/images/alfabank_icon.png b/build_x64/images/alfabank_icon.png new file mode 100644 index 0000000..40bb199 Binary files /dev/null and b/build_x64/images/alfabank_icon.png differ diff --git a/build_x64/images/birbank_icon.png b/build_x64/images/birbank_icon.png new file mode 100644 index 0000000..531a3fa Binary files /dev/null and b/build_x64/images/birbank_icon.png differ diff --git a/build_x64/images/cogwheel.png b/build_x64/images/cogwheel.png new file mode 100644 index 0000000..23d33ba Binary files /dev/null and b/build_x64/images/cogwheel.png differ diff --git a/build_x64/images/ozon_icon.png b/build_x64/images/ozon_icon.png new file mode 100644 index 0000000..363c802 Binary files /dev/null and b/build_x64/images/ozon_icon.png differ diff --git a/build_x64/images/rshb_icon.png b/build_x64/images/rshb_icon.png new file mode 100644 index 0000000..a5124c1 Binary files /dev/null and b/build_x64/images/rshb_icon.png differ diff --git a/build_x64/images/santanderbank_icon.png b/build_x64/images/santanderbank_icon.png new file mode 100644 index 0000000..4b0d927 Binary files /dev/null and b/build_x64/images/santanderbank_icon.png differ diff --git a/build_x64/images/toss_icon.png b/build_x64/images/toss_icon.png new file mode 100644 index 0000000..efe17c7 Binary files /dev/null and b/build_x64/images/toss_icon.png differ diff --git a/build_x64/images/turn_off.png b/build_x64/images/turn_off.png new file mode 100644 index 0000000..bcce6c6 Binary files /dev/null and b/build_x64/images/turn_off.png differ diff --git a/build_x64/images/turn_on.png b/build_x64/images/turn_on.png new file mode 100644 index 0000000..53b7552 Binary files /dev/null and b/build_x64/images/turn_on.png differ diff --git a/build_x64/rus.traineddata b/build_x64/rus.traineddata new file mode 100644 index 0000000..8b71c2d Binary files /dev/null and b/build_x64/rus.traineddata differ diff --git a/build_x64/x64/Release/ALL_BUILD/ALL_BUILD.recipe b/build_x64/x64/Release/ALL_BUILD/ALL_BUILD.recipe new file mode 100644 index 0000000..19ff7ff --- /dev/null +++ b/build_x64/x64/Release/ALL_BUILD/ALL_BUILD.recipe @@ -0,0 +1,17 @@ + + + + + C:\Users\User\CLionProjects\ARCDeskProject\build_x64\x64\Release\ZERO_CHECK + + + C:\Users\User\CLionProjects\ARCDeskProject\build_x64\Release\ARC.exe + + + C:\Users\User\CLionProjects\ARCDeskProject\build_x64\x64\Release\ALL_BUILD + + + + + + \ No newline at end of file diff --git a/build_x64/x64/Release/ALL_BUILD/ALL_BUILD.tlog/ALL_BUILD.lastbuildstate b/build_x64/x64/Release/ALL_BUILD/ALL_BUILD.tlog/ALL_BUILD.lastbuildstate new file mode 100644 index 0000000..dc59056 --- /dev/null +++ b/build_x64/x64/Release/ALL_BUILD/ALL_BUILD.tlog/ALL_BUILD.lastbuildstate @@ -0,0 +1,2 @@ +PlatformToolSet=v143:VCToolArchitecture=NativeARM64:VCToolsVersion=14.43.34808:TargetPlatformVersion=10.0.22621.0: +Release|x64|C:\Users\User\CLionProjects\ARCDeskProject\build_x64\| diff --git a/build_x64/x64/Release/ALL_BUILD/ALL_BUILD.tlog/CustomBuild.command.1.tlog b/build_x64/x64/Release/ALL_BUILD/ALL_BUILD.tlog/CustomBuild.command.1.tlog new file mode 100644 index 0000000..39a3cd1 --- /dev/null +++ b/build_x64/x64/Release/ALL_BUILD/ALL_BUILD.tlog/CustomBuild.command.1.tlog @@ -0,0 +1,10 @@ +^C:\USERS\USER\CLIONPROJECTS\ARCDESKPROJECT\CMAKELISTS.TXT +setlocal +"C:\Program Files\CMake\bin\cmake.exe" -SC:/Users/User/CLionProjects/ARCDeskProject -BC:/Users/User/CLionProjects/ARCDeskProject/build_x64 --check-stamp-file C:/Users/User/CLionProjects/ARCDeskProject/build_x64/CMakeFiles/generate.stamp +if %errorlevel% neq 0 goto :cmEnd +:cmEnd +endlocal & call :cmErrorLevel %errorlevel% & goto :cmDone +:cmErrorLevel +exit /b %1 +:cmDone +if %errorlevel% neq 0 goto :VCEnd diff --git a/build_x64/x64/Release/ALL_BUILD/ALL_BUILD.tlog/CustomBuild.read.1.tlog b/build_x64/x64/Release/ALL_BUILD/ALL_BUILD.tlog/CustomBuild.read.1.tlog new file mode 100644 index 0000000..018d4b7 --- /dev/null +++ b/build_x64/x64/Release/ALL_BUILD/ALL_BUILD.tlog/CustomBuild.read.1.tlog @@ -0,0 +1,358 @@ +^C:\USERS\USER\CLIONPROJECTS\ARCDESKPROJECT\CMAKELISTS.TXT +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKECXXINFORMATION.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKECHECKCOMPILERFLAGCOMMONPATTERNS.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKECOMMONLANGUAGEINCLUDE.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKEFINDDEPENDENCYMACRO.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKEGENERICSYSTEM.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKEINITIALIZECONFIGS.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKELANGUAGEINFORMATION.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKERCINFORMATION.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKESYSTEMSPECIFICINFORMATION.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKESYSTEMSPECIFICINITIALIZE.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CHECKCXXCOMPILERFLAG.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CHECKCXXSOURCECOMPILES.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CHECKINCLUDEFILECXX.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CHECKLIBRARYEXISTS.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\COMPILER\CMAKECOMMONCOMPILERMACROS.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\COMPILER\MSVC-CXX.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\COMPILER\MSVC.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\FINDCURL.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\FINDLIBARCHIVE.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\FINDPACKAGEHANDLESTANDARDARGS.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\FINDPACKAGEMESSAGE.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\FINDTHREADS.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\FINDVULKAN.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\FINDZLIB.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\GNUINSTALLDIRS.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\INTERNAL\CMAKECXXLINKERINFORMATION.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\INTERNAL\CMAKECOMMONLINKERINFORMATION.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\INTERNAL\CHECKCOMPILERFLAG.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\INTERNAL\CHECKFLAGCOMMONCONFIG.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\INTERNAL\CHECKSOURCECOMPILES.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\LINKER\MSVC-CXX.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\LINKER\MSVC.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\PLATFORM\LINKER\WINDOWS-MSVC-CXX.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\PLATFORM\LINKER\WINDOWS-MSVC.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\PLATFORM\WINDOWS-INITIALIZE.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\PLATFORM\WINDOWS-MSVC-CXX.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\PLATFORM\WINDOWS-MSVC.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\PLATFORM\WINDOWS.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\PLATFORM\WINDOWSPATHS.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\SELECTLIBRARYCONFIGURATIONS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\FINDWRAPATOMIC.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\FINDWRAPVULKANHEADERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QT6CONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QT6CONFIGEXTRAS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QT6CONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QT6CONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QT6DEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QT6TARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QT6VERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTFEATURE.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTFEATURECOMMON.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTINSTALLPATHS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICANDROIDHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICAPPLEHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICCMAKEHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICCMAKEVERSIONHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICDEPENDENCYHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICEXTERNALPROJECTHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICFINALIZERHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICFINDPACKAGEHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICGITHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICPLUGINHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICPLUGINHELPERS_V2.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMATTRIBUTIONHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMCPEHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMDEPHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMFILEHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMGENERATIONHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMLICENSEHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMOPSHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMPURLHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMPYTHONHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMQTENTITYHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMSYSTEMDEPHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICTARGETHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICTESTHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICTOOLHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICWALKLIBSHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICWINDOWSHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6COREADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6CORECONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6CORECONFIGEXTRAS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6CORECONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6CORECONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6COREDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6COREMACROS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6CORETARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6CORETARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6CORETARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6COREVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6COREPRIVATE\QT6COREPRIVATEADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6COREPRIVATE\QT6COREPRIVATECONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6COREPRIVATE\QT6COREPRIVATECONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6COREPRIVATE\QT6COREPRIVATECONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6COREPRIVATE\QT6COREPRIVATEDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6COREPRIVATE\QT6COREPRIVATETARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6COREPRIVATE\QT6COREPRIVATEVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORETOOLS\QT6CORETOOLSADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORETOOLS\QT6CORETOOLSCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORETOOLS\QT6CORETOOLSCONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORETOOLS\QT6CORETOOLSCONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORETOOLS\QT6CORETOOLSDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORETOOLS\QT6CORETOOLSTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORETOOLS\QT6CORETOOLSTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORETOOLS\QT6CORETOOLSTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORETOOLS\QT6CORETOOLSVERSIONLESSTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ENTRYPOINTPRIVATE\QT6ENTRYPOINTPRIVATEADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ENTRYPOINTPRIVATE\QT6ENTRYPOINTPRIVATECONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ENTRYPOINTPRIVATE\QT6ENTRYPOINTPRIVATECONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ENTRYPOINTPRIVATE\QT6ENTRYPOINTPRIVATECONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ENTRYPOINTPRIVATE\QT6ENTRYPOINTPRIVATETARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ENTRYPOINTPRIVATE\QT6ENTRYPOINTPRIVATETARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ENTRYPOINTPRIVATE\QT6ENTRYPOINTPRIVATETARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ENTRYPOINTPRIVATE\QT6ENTRYPOINTPRIVATEVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUIADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUICONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUICONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUICONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUIDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUIPLUGINS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUITARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUITARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUITARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUIVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QGIFPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QGIFPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QGIFPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QGIFPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QGIFPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QICOPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QICOPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QICOPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QICOPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QICOPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QJPEGPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QJPEGPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QJPEGPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QJPEGPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QJPEGPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QMINIMALINTEGRATIONPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QMINIMALINTEGRATIONPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QMINIMALINTEGRATIONPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QMINIMALINTEGRATIONPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QMINIMALINTEGRATIONPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QOFFSCREENINTEGRATIONPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QOFFSCREENINTEGRATIONPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QOFFSCREENINTEGRATIONPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QOFFSCREENINTEGRATIONPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QOFFSCREENINTEGRATIONPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGICONPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGICONPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGICONPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGICONPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGICONPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QTUIOTOUCHPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QTUIOTOUCHPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QTUIOTOUCHPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QTUIOTOUCHPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QTUIOTOUCHPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSDIRECT2DINTEGRATIONPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSDIRECT2DINTEGRATIONPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSDIRECT2DINTEGRATIONPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSDIRECT2DINTEGRATIONPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSDIRECT2DINTEGRATIONPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSINTEGRATIONPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSINTEGRATIONPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSINTEGRATIONPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSINTEGRATIONPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSINTEGRATIONPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUIPRIVATE\QT6GUIPRIVATEADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUIPRIVATE\QT6GUIPRIVATECONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUIPRIVATE\QT6GUIPRIVATECONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUIPRIVATE\QT6GUIPRIVATECONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUIPRIVATE\QT6GUIPRIVATEDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUIPRIVATE\QT6GUIPRIVATETARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUIPRIVATE\QT6GUIPRIVATEVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUITOOLS\QT6GUITOOLSADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUITOOLS\QT6GUITOOLSCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUITOOLS\QT6GUITOOLSCONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUITOOLS\QT6GUITOOLSCONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUITOOLS\QT6GUITOOLSDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUITOOLS\QT6GUITOOLSTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUITOOLS\QT6GUITOOLSTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUITOOLS\QT6GUITOOLSTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUITOOLS\QT6GUITOOLSVERSIONLESSTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKCONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKCONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKPLUGINS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QNLMNIPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QNLMNIPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QNLMNIPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QNLMNIPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QNLMNIPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QSCHANNELBACKENDPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QSCHANNELBACKENDPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QSCHANNELBACKENDPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QSCHANNELBACKENDPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QSCHANNELBACKENDPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDCERTONLYPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDCERTONLYPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDCERTONLYPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDCERTONLYPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDCERTONLYPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDOPENSSLPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDOPENSSLPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDOPENSSLPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDOPENSSLPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDOPENSSLPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORKPRIVATE\QT6NETWORKPRIVATEADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORKPRIVATE\QT6NETWORKPRIVATECONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORKPRIVATE\QT6NETWORKPRIVATECONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORKPRIVATE\QT6NETWORKPRIVATECONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORKPRIVATE\QT6NETWORKPRIVATEDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORKPRIVATE\QT6NETWORKPRIVATETARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORKPRIVATE\QT6NETWORKPRIVATEVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QIBASEDRIVERPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QIBASEDRIVERPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QIBASEDRIVERPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QIBASEDRIVERPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QIBASEDRIVERPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QMIMERSQLDRIVERPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QMIMERSQLDRIVERPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QMIMERSQLDRIVERPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QMIMERSQLDRIVERPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QMIMERSQLDRIVERPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QOCIDRIVERPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QOCIDRIVERPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QOCIDRIVERPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QOCIDRIVERPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QOCIDRIVERPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QODBCDRIVERPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QODBCDRIVERPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QODBCDRIVERPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QODBCDRIVERPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QODBCDRIVERPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QPSQLDRIVERPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QPSQLDRIVERPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QPSQLDRIVERPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QPSQLDRIVERPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QPSQLDRIVERPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QSQLITEDRIVERPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QSQLITEDRIVERPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QSQLITEDRIVERPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QSQLITEDRIVERPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QSQLITEDRIVERPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLCONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLCONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLPLUGINS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQLPRIVATE\QT6SQLPRIVATEADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQLPRIVATE\QT6SQLPRIVATECONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQLPRIVATE\QT6SQLPRIVATECONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQLPRIVATE\QT6SQLPRIVATECONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQLPRIVATE\QT6SQLPRIVATEDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQLPRIVATE\QT6SQLPRIVATETARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQLPRIVATE\QT6SQLPRIVATEVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6QMODERNWINDOWSSTYLEPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6QMODERNWINDOWSSTYLEPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6QMODERNWINDOWSSTYLEPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6QMODERNWINDOWSSTYLEPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6QMODERNWINDOWSSTYLEPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSCONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSCONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSMACROS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSPLUGINS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSPRIVATE\QT6WIDGETSPRIVATEADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSPRIVATE\QT6WIDGETSPRIVATECONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSPRIVATE\QT6WIDGETSPRIVATECONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSPRIVATE\QT6WIDGETSPRIVATECONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSPRIVATE\QT6WIDGETSPRIVATEDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSPRIVATE\QT6WIDGETSPRIVATETARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSPRIVATE\QT6WIDGETSPRIVATEVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSTOOLS\QT6WIDGETSTOOLSADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSTOOLS\QT6WIDGETSTOOLSCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSTOOLS\QT6WIDGETSTOOLSCONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSTOOLS\QT6WIDGETSTOOLSCONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSTOOLS\QT6WIDGETSTOOLSDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSTOOLS\QT6WIDGETSTOOLSTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSTOOLS\QT6WIDGETSTOOLSTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSTOOLS\QT6WIDGETSTOOLSTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSTOOLS\QT6WIDGETSTOOLSVERSIONLESSTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XML\QT6XMLADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XML\QT6XMLCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XML\QT6XMLCONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XML\QT6XMLCONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XML\QT6XMLDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XML\QT6XMLTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XML\QT6XMLTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XML\QT6XMLTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XML\QT6XMLVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XMLPRIVATE\QT6XMLPRIVATEADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XMLPRIVATE\QT6XMLPRIVATECONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XMLPRIVATE\QT6XMLPRIVATECONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XMLPRIVATE\QT6XMLPRIVATECONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XMLPRIVATE\QT6XMLPRIVATEDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XMLPRIVATE\QT6XMLPRIVATETARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XMLPRIVATE\QT6XMLPRIVATEVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ZLIBPRIVATE\QT6ZLIBPRIVATEADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ZLIBPRIVATE\QT6ZLIBPRIVATECONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ZLIBPRIVATE\QT6ZLIBPRIVATECONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ZLIBPRIVATE\QT6ZLIBPRIVATECONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ZLIBPRIVATE\QT6ZLIBPRIVATETARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ZLIBPRIVATE\QT6ZLIBPRIVATEVERSIONLESSALIASTARGETS.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\WEBP\WEBPCONFIG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\WEBP\WEBPCONFIGVERSION.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\WEBP\WEBPTARGETS-DEBUG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\WEBP\WEBPTARGETS-RELEASE.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\WEBP\WEBPTARGETS.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\CURL\CURLCONFIG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\CURL\CURLCONFIGVERSION.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\CURL\CURLTARGETS-DEBUG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\CURL\CURLTARGETS-RELEASE.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\CURL\CURLTARGETS.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\LEPTONICA\LEPTONICACONFIG-VERSION.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\LEPTONICA\LEPTONICACONFIG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\LEPTONICA\LEPTONICATARGETS-DEBUG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\LEPTONICA\LEPTONICATARGETS-RELEASE.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\LEPTONICA\LEPTONICATARGETS.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\OPENJPEG\OPENJPEGCONFIG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\OPENJPEG\OPENJPEGCONFIGVERSION.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\OPENJPEG\OPENJPEGTARGETS-DEBUG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\OPENJPEG\OPENJPEGTARGETS-RELEASE.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\OPENJPEG\OPENJPEGTARGETS.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\TESSERACT\TESSERACTCONFIG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\TESSERACT\TESSERACTCONFIGVERSION.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\TESSERACT\TESSERACTTARGETS-DEBUG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\TESSERACT\TESSERACTTARGETS-RELEASE.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\TESSERACT\TESSERACTTARGETS.CMAKE +C:\USERS\USER\CLIONPROJECTS\ARCDESKPROJECT\BUILD_X64\CMAKEFILES\4.0.2\CMAKECXXCOMPILER.CMAKE +C:\USERS\USER\CLIONPROJECTS\ARCDESKPROJECT\BUILD_X64\CMAKEFILES\4.0.2\CMAKERCCOMPILER.CMAKE +C:\USERS\USER\CLIONPROJECTS\ARCDESKPROJECT\BUILD_X64\CMAKEFILES\4.0.2\CMAKESYSTEM.CMAKE diff --git a/build_x64/x64/Release/ALL_BUILD/ALL_BUILD.tlog/CustomBuild.write.1.tlog b/build_x64/x64/Release/ALL_BUILD/ALL_BUILD.tlog/CustomBuild.write.1.tlog new file mode 100644 index 0000000..d464b02 --- /dev/null +++ b/build_x64/x64/Release/ALL_BUILD/ALL_BUILD.tlog/CustomBuild.write.1.tlog @@ -0,0 +1,2 @@ +^C:\USERS\USER\CLIONPROJECTS\ARCDESKPROJECT\CMAKELISTS.TXT +C:\USERS\USER\CLIONPROJECTS\ARCDESKPROJECT\BUILD_X64\CMAKEFILES\GENERATE.STAMP diff --git a/build_x64/x64/Release/ZERO_CHECK/ZERO_CHECK.recipe b/build_x64/x64/Release/ZERO_CHECK/ZERO_CHECK.recipe new file mode 100644 index 0000000..14ba7d6 --- /dev/null +++ b/build_x64/x64/Release/ZERO_CHECK/ZERO_CHECK.recipe @@ -0,0 +1,11 @@ + + + + + C:\Users\User\CLionProjects\ARCDeskProject\build_x64\x64\Release\ZERO_CHECK + + + + + + \ No newline at end of file diff --git a/build_x64/x64/Release/ZERO_CHECK/ZERO_CHECK.tlog/CustomBuild.command.1.tlog b/build_x64/x64/Release/ZERO_CHECK/ZERO_CHECK.tlog/CustomBuild.command.1.tlog new file mode 100644 index 0000000..c65bb3e --- /dev/null +++ b/build_x64/x64/Release/ZERO_CHECK/ZERO_CHECK.tlog/CustomBuild.command.1.tlog @@ -0,0 +1,10 @@ +^C:\USERS\USER\CLIONPROJECTS\ARCDESKPROJECT\BUILD_X64\CMAKEFILES\99218E3165732949C3BD031198CBC842\GENERATE.STAMP.RULE +setlocal +"C:\Program Files\CMake\bin\cmake.exe" -SC:/Users/User/CLionProjects/ARCDeskProject -BC:/Users/User/CLionProjects/ARCDeskProject/build_x64 --check-stamp-list CMakeFiles/generate.stamp.list --vs-solution-file C:/Users/User/CLionProjects/ARCDeskProject/build_x64/ARC.sln +if %errorlevel% neq 0 goto :cmEnd +:cmEnd +endlocal & call :cmErrorLevel %errorlevel% & goto :cmDone +:cmErrorLevel +exit /b %1 +:cmDone +if %errorlevel% neq 0 goto :VCEnd diff --git a/build_x64/x64/Release/ZERO_CHECK/ZERO_CHECK.tlog/CustomBuild.read.1.tlog b/build_x64/x64/Release/ZERO_CHECK/ZERO_CHECK.tlog/CustomBuild.read.1.tlog new file mode 100644 index 0000000..aeba1a5 --- /dev/null +++ b/build_x64/x64/Release/ZERO_CHECK/ZERO_CHECK.tlog/CustomBuild.read.1.tlog @@ -0,0 +1,359 @@ +^C:\USERS\USER\CLIONPROJECTS\ARCDESKPROJECT\BUILD_X64\CMAKEFILES\99218E3165732949C3BD031198CBC842\GENERATE.STAMP.RULE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKECXXINFORMATION.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKECHECKCOMPILERFLAGCOMMONPATTERNS.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKECOMMONLANGUAGEINCLUDE.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKEFINDDEPENDENCYMACRO.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKEGENERICSYSTEM.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKEINITIALIZECONFIGS.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKELANGUAGEINFORMATION.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKERCINFORMATION.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKESYSTEMSPECIFICINFORMATION.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CMAKESYSTEMSPECIFICINITIALIZE.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CHECKCXXCOMPILERFLAG.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CHECKCXXSOURCECOMPILES.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CHECKINCLUDEFILECXX.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\CHECKLIBRARYEXISTS.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\COMPILER\CMAKECOMMONCOMPILERMACROS.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\COMPILER\MSVC-CXX.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\COMPILER\MSVC.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\FINDCURL.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\FINDLIBARCHIVE.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\FINDPACKAGEHANDLESTANDARDARGS.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\FINDPACKAGEMESSAGE.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\FINDTHREADS.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\FINDVULKAN.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\FINDZLIB.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\GNUINSTALLDIRS.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\INTERNAL\CMAKECXXLINKERINFORMATION.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\INTERNAL\CMAKECOMMONLINKERINFORMATION.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\INTERNAL\CHECKCOMPILERFLAG.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\INTERNAL\CHECKFLAGCOMMONCONFIG.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\INTERNAL\CHECKSOURCECOMPILES.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\LINKER\MSVC-CXX.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\LINKER\MSVC.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\PLATFORM\LINKER\WINDOWS-MSVC-CXX.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\PLATFORM\LINKER\WINDOWS-MSVC.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\PLATFORM\WINDOWS-INITIALIZE.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\PLATFORM\WINDOWS-MSVC-CXX.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\PLATFORM\WINDOWS-MSVC.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\PLATFORM\WINDOWS.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\PLATFORM\WINDOWSPATHS.CMAKE +C:\PROGRAM FILES\CMAKE\SHARE\CMAKE-4.0\MODULES\SELECTLIBRARYCONFIGURATIONS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\FINDWRAPATOMIC.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\FINDWRAPVULKANHEADERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QT6CONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QT6CONFIGEXTRAS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QT6CONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QT6CONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QT6DEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QT6TARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QT6VERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTFEATURE.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTFEATURECOMMON.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTINSTALLPATHS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICANDROIDHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICAPPLEHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICCMAKEHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICCMAKEVERSIONHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICDEPENDENCYHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICEXTERNALPROJECTHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICFINALIZERHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICFINDPACKAGEHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICGITHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICPLUGINHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICPLUGINHELPERS_V2.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMATTRIBUTIONHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMCPEHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMDEPHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMFILEHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMGENERATIONHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMLICENSEHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMOPSHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMPURLHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMPYTHONHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMQTENTITYHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICSBOMSYSTEMDEPHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICTARGETHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICTESTHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICTOOLHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICWALKLIBSHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6\QTPUBLICWINDOWSHELPERS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6COREADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6CORECONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6CORECONFIGEXTRAS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6CORECONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6CORECONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6COREDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6COREMACROS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6CORETARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6CORETARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6CORETARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORE\QT6COREVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6COREPRIVATE\QT6COREPRIVATEADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6COREPRIVATE\QT6COREPRIVATECONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6COREPRIVATE\QT6COREPRIVATECONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6COREPRIVATE\QT6COREPRIVATECONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6COREPRIVATE\QT6COREPRIVATEDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6COREPRIVATE\QT6COREPRIVATETARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6COREPRIVATE\QT6COREPRIVATEVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORETOOLS\QT6CORETOOLSADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORETOOLS\QT6CORETOOLSCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORETOOLS\QT6CORETOOLSCONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORETOOLS\QT6CORETOOLSCONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORETOOLS\QT6CORETOOLSDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORETOOLS\QT6CORETOOLSTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORETOOLS\QT6CORETOOLSTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORETOOLS\QT6CORETOOLSTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6CORETOOLS\QT6CORETOOLSVERSIONLESSTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ENTRYPOINTPRIVATE\QT6ENTRYPOINTPRIVATEADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ENTRYPOINTPRIVATE\QT6ENTRYPOINTPRIVATECONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ENTRYPOINTPRIVATE\QT6ENTRYPOINTPRIVATECONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ENTRYPOINTPRIVATE\QT6ENTRYPOINTPRIVATECONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ENTRYPOINTPRIVATE\QT6ENTRYPOINTPRIVATETARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ENTRYPOINTPRIVATE\QT6ENTRYPOINTPRIVATETARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ENTRYPOINTPRIVATE\QT6ENTRYPOINTPRIVATETARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ENTRYPOINTPRIVATE\QT6ENTRYPOINTPRIVATEVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUIADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUICONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUICONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUICONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUIDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUIPLUGINS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUITARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUITARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUITARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6GUIVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QGIFPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QGIFPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QGIFPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QGIFPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QGIFPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QICOPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QICOPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QICOPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QICOPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QICOPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QJPEGPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QJPEGPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QJPEGPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QJPEGPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QJPEGPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QMINIMALINTEGRATIONPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QMINIMALINTEGRATIONPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QMINIMALINTEGRATIONPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QMINIMALINTEGRATIONPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QMINIMALINTEGRATIONPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QOFFSCREENINTEGRATIONPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QOFFSCREENINTEGRATIONPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QOFFSCREENINTEGRATIONPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QOFFSCREENINTEGRATIONPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QOFFSCREENINTEGRATIONPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGICONPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGICONPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGICONPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGICONPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGICONPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QSVGPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QTUIOTOUCHPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QTUIOTOUCHPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QTUIOTOUCHPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QTUIOTOUCHPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QTUIOTOUCHPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSDIRECT2DINTEGRATIONPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSDIRECT2DINTEGRATIONPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSDIRECT2DINTEGRATIONPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSDIRECT2DINTEGRATIONPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSDIRECT2DINTEGRATIONPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSINTEGRATIONPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSINTEGRATIONPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSINTEGRATIONPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSINTEGRATIONPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUI\QT6QWINDOWSINTEGRATIONPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUIPRIVATE\QT6GUIPRIVATEADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUIPRIVATE\QT6GUIPRIVATECONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUIPRIVATE\QT6GUIPRIVATECONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUIPRIVATE\QT6GUIPRIVATECONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUIPRIVATE\QT6GUIPRIVATEDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUIPRIVATE\QT6GUIPRIVATETARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUIPRIVATE\QT6GUIPRIVATEVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUITOOLS\QT6GUITOOLSADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUITOOLS\QT6GUITOOLSCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUITOOLS\QT6GUITOOLSCONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUITOOLS\QT6GUITOOLSCONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUITOOLS\QT6GUITOOLSDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUITOOLS\QT6GUITOOLSTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUITOOLS\QT6GUITOOLSTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUITOOLS\QT6GUITOOLSTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6GUITOOLS\QT6GUITOOLSVERSIONLESSTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKCONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKCONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKPLUGINS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6NETWORKVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QNLMNIPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QNLMNIPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QNLMNIPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QNLMNIPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QNLMNIPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QSCHANNELBACKENDPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QSCHANNELBACKENDPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QSCHANNELBACKENDPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QSCHANNELBACKENDPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QSCHANNELBACKENDPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDCERTONLYPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDCERTONLYPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDCERTONLYPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDCERTONLYPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDCERTONLYPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDOPENSSLPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDOPENSSLPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDOPENSSLPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDOPENSSLPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORK\QT6QTLSBACKENDOPENSSLPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORKPRIVATE\QT6NETWORKPRIVATEADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORKPRIVATE\QT6NETWORKPRIVATECONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORKPRIVATE\QT6NETWORKPRIVATECONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORKPRIVATE\QT6NETWORKPRIVATECONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORKPRIVATE\QT6NETWORKPRIVATEDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORKPRIVATE\QT6NETWORKPRIVATETARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6NETWORKPRIVATE\QT6NETWORKPRIVATEVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QIBASEDRIVERPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QIBASEDRIVERPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QIBASEDRIVERPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QIBASEDRIVERPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QIBASEDRIVERPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QMIMERSQLDRIVERPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QMIMERSQLDRIVERPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QMIMERSQLDRIVERPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QMIMERSQLDRIVERPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QMIMERSQLDRIVERPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QOCIDRIVERPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QOCIDRIVERPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QOCIDRIVERPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QOCIDRIVERPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QOCIDRIVERPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QODBCDRIVERPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QODBCDRIVERPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QODBCDRIVERPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QODBCDRIVERPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QODBCDRIVERPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QPSQLDRIVERPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QPSQLDRIVERPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QPSQLDRIVERPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QPSQLDRIVERPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QPSQLDRIVERPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QSQLITEDRIVERPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QSQLITEDRIVERPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QSQLITEDRIVERPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QSQLITEDRIVERPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6QSQLITEDRIVERPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLCONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLCONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLPLUGINS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQL\QT6SQLVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQLPRIVATE\QT6SQLPRIVATEADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQLPRIVATE\QT6SQLPRIVATECONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQLPRIVATE\QT6SQLPRIVATECONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQLPRIVATE\QT6SQLPRIVATECONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQLPRIVATE\QT6SQLPRIVATEDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQLPRIVATE\QT6SQLPRIVATETARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6SQLPRIVATE\QT6SQLPRIVATEVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6QMODERNWINDOWSSTYLEPLUGINADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6QMODERNWINDOWSSTYLEPLUGINCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6QMODERNWINDOWSSTYLEPLUGINTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6QMODERNWINDOWSSTYLEPLUGINTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6QMODERNWINDOWSSTYLEPLUGINTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSCONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSCONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSMACROS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSPLUGINS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETS\QT6WIDGETSVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSPRIVATE\QT6WIDGETSPRIVATEADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSPRIVATE\QT6WIDGETSPRIVATECONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSPRIVATE\QT6WIDGETSPRIVATECONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSPRIVATE\QT6WIDGETSPRIVATECONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSPRIVATE\QT6WIDGETSPRIVATEDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSPRIVATE\QT6WIDGETSPRIVATETARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSPRIVATE\QT6WIDGETSPRIVATEVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSTOOLS\QT6WIDGETSTOOLSADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSTOOLS\QT6WIDGETSTOOLSCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSTOOLS\QT6WIDGETSTOOLSCONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSTOOLS\QT6WIDGETSTOOLSCONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSTOOLS\QT6WIDGETSTOOLSDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSTOOLS\QT6WIDGETSTOOLSTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSTOOLS\QT6WIDGETSTOOLSTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSTOOLS\QT6WIDGETSTOOLSTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6WIDGETSTOOLS\QT6WIDGETSTOOLSVERSIONLESSTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XML\QT6XMLADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XML\QT6XMLCONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XML\QT6XMLCONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XML\QT6XMLCONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XML\QT6XMLDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XML\QT6XMLTARGETS-DEBUG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XML\QT6XMLTARGETS-RELWITHDEBINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XML\QT6XMLTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XML\QT6XMLVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XMLPRIVATE\QT6XMLPRIVATEADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XMLPRIVATE\QT6XMLPRIVATECONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XMLPRIVATE\QT6XMLPRIVATECONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XMLPRIVATE\QT6XMLPRIVATECONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XMLPRIVATE\QT6XMLPRIVATEDEPENDENCIES.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XMLPRIVATE\QT6XMLPRIVATETARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6XMLPRIVATE\QT6XMLPRIVATEVERSIONLESSALIASTARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ZLIBPRIVATE\QT6ZLIBPRIVATEADDITIONALTARGETINFO.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ZLIBPRIVATE\QT6ZLIBPRIVATECONFIG.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ZLIBPRIVATE\QT6ZLIBPRIVATECONFIGVERSION.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ZLIBPRIVATE\QT6ZLIBPRIVATECONFIGVERSIONIMPL.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ZLIBPRIVATE\QT6ZLIBPRIVATETARGETS.CMAKE +C:\QT\6.9.0\MSVC2022_64\LIB\CMAKE\QT6ZLIBPRIVATE\QT6ZLIBPRIVATEVERSIONLESSALIASTARGETS.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\WEBP\WEBPCONFIG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\WEBP\WEBPCONFIGVERSION.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\WEBP\WEBPTARGETS-DEBUG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\WEBP\WEBPTARGETS-RELEASE.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\WEBP\WEBPTARGETS.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\CURL\CURLCONFIG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\CURL\CURLCONFIGVERSION.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\CURL\CURLTARGETS-DEBUG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\CURL\CURLTARGETS-RELEASE.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\CURL\CURLTARGETS.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\LEPTONICA\LEPTONICACONFIG-VERSION.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\LEPTONICA\LEPTONICACONFIG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\LEPTONICA\LEPTONICATARGETS-DEBUG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\LEPTONICA\LEPTONICATARGETS-RELEASE.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\LEPTONICA\LEPTONICATARGETS.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\OPENJPEG\OPENJPEGCONFIG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\OPENJPEG\OPENJPEGCONFIGVERSION.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\OPENJPEG\OPENJPEGTARGETS-DEBUG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\OPENJPEG\OPENJPEGTARGETS-RELEASE.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\OPENJPEG\OPENJPEGTARGETS.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\TESSERACT\TESSERACTCONFIG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\TESSERACT\TESSERACTCONFIGVERSION.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\TESSERACT\TESSERACTTARGETS-DEBUG.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\TESSERACT\TESSERACTTARGETS-RELEASE.CMAKE +C:\USERS\USER\.VCPKG-CLION\VCPKG\INSTALLED\X64-WINDOWS\SHARE\TESSERACT\TESSERACTTARGETS.CMAKE +C:\USERS\USER\CLIONPROJECTS\ARCDESKPROJECT\CMAKELISTS.TXT +C:\USERS\USER\CLIONPROJECTS\ARCDESKPROJECT\BUILD_X64\CMAKEFILES\4.0.2\CMAKECXXCOMPILER.CMAKE +C:\USERS\USER\CLIONPROJECTS\ARCDESKPROJECT\BUILD_X64\CMAKEFILES\4.0.2\CMAKERCCOMPILER.CMAKE +C:\USERS\USER\CLIONPROJECTS\ARCDESKPROJECT\BUILD_X64\CMAKEFILES\4.0.2\CMAKESYSTEM.CMAKE diff --git a/build_x64/x64/Release/ZERO_CHECK/ZERO_CHECK.tlog/CustomBuild.write.1.tlog b/build_x64/x64/Release/ZERO_CHECK/ZERO_CHECK.tlog/CustomBuild.write.1.tlog new file mode 100644 index 0000000..4c2f99f --- /dev/null +++ b/build_x64/x64/Release/ZERO_CHECK/ZERO_CHECK.tlog/CustomBuild.write.1.tlog @@ -0,0 +1,2 @@ +^C:\USERS\USER\CLIONPROJECTS\ARCDESKPROJECT\BUILD_X64\CMAKEFILES\99218E3165732949C3BD031198CBC842\GENERATE.STAMP.RULE +C:\USERS\USER\CLIONPROJECTS\ARCDESKPROJECT\BUILD_X64\CMAKEFILES\GENERATE.STAMP diff --git a/build_x64/x64/Release/ZERO_CHECK/ZERO_CHECK.tlog/ZERO_CHECK.lastbuildstate b/build_x64/x64/Release/ZERO_CHECK/ZERO_CHECK.tlog/ZERO_CHECK.lastbuildstate new file mode 100644 index 0000000..dc59056 --- /dev/null +++ b/build_x64/x64/Release/ZERO_CHECK/ZERO_CHECK.tlog/ZERO_CHECK.lastbuildstate @@ -0,0 +1,2 @@ +PlatformToolSet=v143:VCToolArchitecture=NativeARM64:VCToolsVersion=14.43.34808:TargetPlatformVersion=10.0.22621.0: +Release|x64|C:\Users\User\CLionProjects\ARCDeskProject\build_x64\| diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..7590a89 --- /dev/null +++ b/config.ini @@ -0,0 +1,48 @@ +[apps] +list=ozon, black, dushanbe_city_bank + +[common] +currencies=USD, ARS, RUB, TJS +version=1.0.64 + +[db] +dbPath=./database/app_data.db + +[net] +apiBase=http://93.183.75.134:8005 +#apiBase=http://127.0.0.1:8888 +pathAuthLogin=/api/v1/auth/login +pathAuthRefresh=/api/v1/auth/refresh_token +pathBankProfile=/api/v1/profile/bank_profile +pathDesktopCreate=/api/v1/desktop/ +pathDevice=/api/v1/device/ +pathDeviceScreenshot=/api/v1/device/add_screenshot/ +pathMaterial=/api/v1/profile/material +pathMonitoringLog=/monitoring/log +pathMonitoringDump=/monitoring/dump +monitoringBase=http://142.93.102.40:8080 +monitoringToken=509b91592749959b9ff56f9ed9189e5b3e91e1260003f63c477255e1c187298a + +[black] +android_package_name=app.blackwallet.ar +currency=ARS +country=argentina +name=Black +phone_code=54 +app_version=0 + +[ozon] +android_package_name=ru.ozon.fintech.finance +currency=RUB +country=russia +name=Ozon +phone_code=7 +app_version=19.17.0 + +[dushanbe_city_bank] +android_package_name=tj.dc.next1 +currency=TJS +country=tajikistan +name=Dushanbe City +phone_code=992 +app_version=3.2.34 diff --git a/database/DatabaseManager.cpp b/database/DatabaseManager.cpp index e4dc67f..18c975d 100644 --- a/database/DatabaseManager.cpp +++ b/database/DatabaseManager.cpp @@ -1,26 +1,275 @@ #include "DatabaseManager.h" +#include #include -#include +#include +#include +#include +#include +#include +#include +#include -DatabaseManager::DatabaseManager() { - db = QSqlDatabase::addDatabase("QSQLITE"); - db.setDatabaseName(QCoreApplication::applicationDirPath() + "/database/app_data.db"); - if (!db.open()) { - qWarning() << "Open database error: " << db.lastError().text(); - } -} +DatabaseManager::DatabaseManager() = default; DatabaseManager::~DatabaseManager() { - if (db.isOpen()) { - db.close(); + QMutexLocker locker(&mutex); + + // QSqlDatabase требует живого QCoreApplication — при завершении + // статический синглтон может разрушаться после QApplication + if (!QCoreApplication::instance()) { + connectionPool.clear(); + return; } + + for (auto it = connectionPool.begin(); it != connectionPool.end(); ++it) { + QSqlDatabase::removeDatabase(it.key()); + } + connectionPool.clear(); + qDebug() << "Все соединения закрыты"; } -DatabaseManager& DatabaseManager::instance() { +DatabaseManager &DatabaseManager::instance() { static DatabaseManager instance; return instance; } -QSqlDatabase& DatabaseManager::database() { - return db; -} \ No newline at end of file +QSqlDatabase DatabaseManager::database() { + QMutexLocker locker(&mutex); + QString name = connectionName(); + + // Создаем новое соединение, если его ещё нет + if (!connectionPool.contains(name)) { + QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", name); + const QSettings settings("config.ini", QSettings::IniFormat); + + QString dbPath = settings.value("db/dbPath").toString(); + if (dbPath.startsWith("~")) { + dbPath.replace(0, 1, QDir::homePath()); + } + + // Создаём директорию для БД, если её нет + QDir().mkpath(QFileInfo(dbPath).absolutePath()); + + db.setDatabaseName(dbPath); + + if (!db.open()) { + qWarning() << "Ошибка открытия базы данных:" << db.lastError().text(); + } + + // Включаем каскадное удаление (FK) + QSqlQuery pragmaQuery(db); + pragmaQuery.exec("PRAGMA foreign_keys = ON"); + + connectionPool[name] = db; + + // Автоматическое удаление соединения при завершении потока + QObject::connect(QThread::currentThread(), &QThread::finished, [this, name]() { + QMutexLocker cLocker(&mutex); + if (connectionPool.contains(name)) { + connectionPool.remove(name); + QSqlDatabase::removeDatabase(name); + qDebug() << "Соединение" << name << "удалено"; + } + }); + } + + return connectionPool[name]; +} + +QString DatabaseManager::connectionName() { + return QString("connection_%1").arg(reinterpret_cast(QThread::currentThreadId())); +} + + +void DatabaseManager::initSchema() { + QSqlDatabase db = database(); + QSqlQuery q(db); + + db.transaction(); + + q.exec(R"(CREATE TABLE IF NOT EXISTS devices ( + id TEXT PRIMARY KEY, + api_id TEXT, + adb_serial TEXT, + status TEXT, + name TEXT, + android TEXT, + screen_width INTEGER, + screen_height INTEGER, + battery INTEGER, + update_time DATETIME, + timestamp DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')), + image BLOB + ))"); + + q.exec(R"(CREATE TABLE IF NOT EXISTS bank_profiles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT, + pin_code TEXT, + pin_code_checked TEXT DEFAULT 'no', + name TEXT, + code TEXT, + package TEXT, + status TEXT DEFAULT 'off', + install TEXT DEFAULT 'no', + comment TEXT, + phone TEXT DEFAULT '', + email TEXT DEFAULT '', + full_name TEXT DEFAULT '', + bank_profile_id TEXT DEFAULT '', + currency TEXT DEFAULT '', + amount REAL DEFAULT 0, + app_version TEXT DEFAULT '', + app_version_checked_at DATETIME, + FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, + UNIQUE (device_id, code) + ))"); + + // Миграция: добавляем колонки app_version и app_version_checked_at если их нет (ошибка = уже существует) + q.exec("ALTER TABLE bank_profiles ADD COLUMN app_version TEXT DEFAULT ''"); + q.exec("ALTER TABLE bank_profiles ADD COLUMN app_version_checked_at DATETIME"); + + q.exec(R"(CREATE TABLE IF NOT EXISTS materials ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT, + app_code TEXT, + last_numbers TEXT, + description TEXT, + amount REAL, + currency TEXT DEFAULT '', + update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')), + status TEXT DEFAULT 'active', + card_number TEXT DEFAULT '', + account_number TEXT DEFAULT '', + material_id TEXT DEFAULT '', + FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, + UNIQUE (device_id, app_code, last_numbers) + ))"); + + q.exec("ALTER TABLE materials ADD COLUMN account_number TEXT DEFAULT ''"); + + q.exec(R"(CREATE TABLE IF NOT EXISTS transactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER, + external_id INTEGER, + amount REAL, + fee REAL, + status TEXT, + type TEXT, + phone TEXT, + name TEXT, + short_description TEXT, + updated_short_description TEXT, + description TEXT, + updated_description TEXT, + bank_name TEXT, + bank_time TEXT, + bank_tr_external_id TEXT, + update_time DATETIME, + complete_time DATETIME, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + receipt_image BLOB, + FOREIGN KEY (account_id) REFERENCES materials (id) ON DELETE CASCADE + ))"); + + q.exec(R"(CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER, + external_id TEXT, + device_id TEXT, + amount REAL, + phone TEXT, + bank_name TEXT, + comment TEXT, + status TEXT, + type TEXT, + update_time DATETIME, + complete_time DATETIME, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + json TEXT DEFAULT '', + sent_to_server INTEGER DEFAULT 0, + screenshot BLOB, + FOREIGN KEY (account_id) REFERENCES materials (id) ON DELETE CASCADE, + FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE + ))"); + + q.exec(R"(CREATE TABLE IF NOT EXISTS banks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + alias TEXT UNIQUE, + name TEXT, + nspk_name TEXT + ))"); + + // Таблица app_logs больше не используется — логи пишутся в general_logs + // Удаляем если существует (миграция) + q.exec("DROP TABLE IF EXISTS app_logs"); + + q.exec(R"(CREATE TABLE IF NOT EXISTS general_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + level TEXT NOT NULL DEFAULT '', + source TEXT NOT NULL DEFAULT '', + message TEXT NOT NULL, + timestamp TEXT NOT NULL + ))"); + + q.exec("CREATE INDEX IF NOT EXISTS idx_general_logs_id ON general_logs (id DESC)"); + + // Миграция: добавляем колонку screenshot если её нет + q.exec("ALTER TABLE general_logs ADD COLUMN screenshot BLOB"); + // Миграция: добавляем колонки request_body и response_body + q.exec("ALTER TABLE general_logs ADD COLUMN request_body TEXT DEFAULT ''"); + q.exec("ALTER TABLE general_logs ADD COLUMN response_body TEXT DEFAULT ''"); + // Ошибка = колонка уже существует — это нормально + + q.exec(R"(CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT + ))"); + + db.commit(); +} + +void DatabaseManager::cleanupOldData(int days) { + QSqlDatabase db = database(); + QSqlQuery q(db); + + const QString cutoff = QDateTime::currentDateTimeUtc().addDays(-days).toString(Qt::ISODate); + + db.transaction(); + + // general_logs + q.prepare("DELETE FROM general_logs WHERE timestamp < :cutoff"); + q.bindValue(":cutoff", cutoff); + if (q.exec()) { + qDebug() << "[cleanup] general_logs: deleted" << q.numRowsAffected() << "rows older than" << days << "days"; + } + + // events (завершённые/ошибочные) + q.prepare("DELETE FROM events WHERE timestamp < :cutoff AND status IN ('complete', 'error')"); + q.bindValue(":cutoff", cutoff); + if (q.exec()) { + qDebug() << "[cleanup] events: deleted" << q.numRowsAffected() << "rows older than" << days << "days"; + } + + // transactions + q.prepare("DELETE FROM transactions WHERE timestamp < :cutoff"); + q.bindValue(":cutoff", cutoff); + if (q.exec()) { + qDebug() << "[cleanup] transactions: deleted" << q.numRowsAffected() << "rows older than" << days << "days"; + } + + db.commit(); + + // Освобождаем место + q.exec("VACUUM"); + qDebug() << "[cleanup] VACUUM done"; +} + +void DatabaseManager::closeConnection() { + QMutexLocker locker(&mutex); + if (const QString name = connectionName(); connectionPool.contains(name)) { + connectionPool.remove(name); + QSqlDatabase::removeDatabase(name); + qDebug() << "Соединение" << name << "удалено вручную"; + } +} diff --git a/database/DatabaseManager.h b/database/DatabaseManager.h index 7321c17..ea809d6 100644 --- a/database/DatabaseManager.h +++ b/database/DatabaseManager.h @@ -1,16 +1,27 @@ #pragma once #include +#include +#include +#include +#include -class DatabaseManager final : public QObject { - Q_OBJECT +class DatabaseManager { public: static DatabaseManager& instance(); - QSqlDatabase& database(); + QSqlDatabase database(); + void initSchema(); + void cleanupOldData(int days = 5); + void closeConnection(); private: DatabaseManager(); - ~DatabaseManager() override; - QSqlDatabase db; + ~DatabaseManager(); + + static QString connectionName(); + + QRecursiveMutex mutex; + QSet activeConnections; + QMap connectionPool; }; \ No newline at end of file diff --git a/database/app_data.db b/database/app_data.db deleted file mode 100644 index 94a52d1..0000000 Binary files a/database/app_data.db and /dev/null differ diff --git a/database/dao/AccountDAO.cpp b/database/dao/AccountDAO.cpp deleted file mode 100644 index 9c9a9b8..0000000 --- a/database/dao/AccountDAO.cpp +++ /dev/null @@ -1,108 +0,0 @@ -#include -#include -#include "AccountDAO.h" - -#include "db/AccountInfo.h" -#include "DatabaseManager.h" - -bool AccountDAO::upsertAccount(const AccountInfo &info) { - QSqlQuery query(DatabaseManager::instance().database()); - - query.prepare(R"( - INSERT INTO accounts (device_id, app_name, card_name, amount, update_time, status, description) - VALUES (:device_id, :app_name, :card_name, :amount, :update_time, :status, :description) - ON CONFLICT(device_id, app_name, card_name) - DO UPDATE SET - amount = excluded.amount, - update_time = excluded.update_time, - status = excluded.status - )"); - - query.bindValue(":device_id", info.deviceId); - query.bindValue(":app_name", info.appName); - query.bindValue(":card_name", info.cardName); - query.bindValue(":amount", info.amount); - query.bindValue(":update_time", info.updateTime.toString(Qt::ISODate)); - query.bindValue(":status", info.status); - query.bindValue(":description", info.description); - - if (!query.exec()) { - qWarning() << "Ошибка при вставке/обновлении account:" << query.lastError().text(); - return false; - } - - return true; -} - -bool AccountDAO::deleteAccount(const int id) { - QSqlQuery query(DatabaseManager::instance().database()); - query.prepare("DELETE FROM accounts WHERE id = :id"); - query.bindValue(":id", id); - return query.exec(); -} - -QList AccountDAO::getAccounts(const QString &deviceId, const QString &appName) { - QList accounts; - QSqlQuery query(DatabaseManager::instance().database()); - - query.prepare(R"( - SELECT id, device_id, app_name, card_name, amount, update_time, status, description - FROM accounts - WHERE device_id = :device_id AND app_name = :app_name - )"); - - query.bindValue(":device_id", deviceId); - query.bindValue(":app_name", appName); - - if (!query.exec()) { - qWarning() << "Ошибка при получении accounts:" << query.lastError().text(); - return accounts; - } - - while (query.next()) { - AccountInfo acc; - acc.id = query.value("id").toInt(); - acc.deviceId = query.value("device_id").toString(); - acc.appName = query.value("app_name").toString(); - acc.cardName = query.value("card_name").toString(); - acc.amount = query.value("amount").toDouble(); - acc.updateTime = QDateTime::fromString(query.value("update_time").toString(), Qt::ISODate); - acc.status = query.value("status").toString(); - acc.description = query.value("description").toString(); - accounts.append(acc); - } - - return accounts; -} - -AccountInfo AccountDAO::getAccountById(const int accountId) { - AccountInfo acc; - QSqlQuery query(DatabaseManager::instance().database()); - - query.prepare(R"( - SELECT id, device_id, app_name, card_name, amount, update_time, status, description - FROM accounts - WHERE id = :id - LIMIT 1 - )"); - - query.bindValue(":id", accountId); - - if (!query.exec()) { - qWarning() << "Ошибка при получении account по ID:" << query.lastError().text(); - return acc; - } - - if (query.next()) { - acc.id = query.value("id").toInt(); - acc.deviceId = query.value("device_id").toString(); - acc.appName = query.value("app_name").toString(); - acc.cardName = query.value("card_name").toString(); - acc.amount = query.value("amount").toDouble(); - acc.updateTime = QDateTime::fromString(query.value("update_time").toString(), Qt::ISODate); - acc.status = query.value("status").toString(); - acc.description = query.value("description").toString(); - } - - return acc; -} diff --git a/database/dao/AccountDAO.h b/database/dao/AccountDAO.h deleted file mode 100644 index 07a2e5b..0000000 --- a/database/dao/AccountDAO.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once -#include "db/AccountInfo.h" - -class AccountDAO { -public: - [[nodiscard]] static bool upsertAccount(const AccountInfo &info); - - [[nodiscard]] static bool deleteAccount(int id); - - [[nodiscard]] static QList getAccounts(const QString &deviceId, const QString &appName); - - [[nodiscard]] static AccountInfo getAccountById(int accountId); -}; diff --git a/database/dao/BankDAO.cpp b/database/dao/BankDAO.cpp new file mode 100644 index 0000000..1d4aaf3 --- /dev/null +++ b/database/dao/BankDAO.cpp @@ -0,0 +1,85 @@ +#include "BankDAO.h" + +#include +#include +#include + +#include "DatabaseManager.h" + +QList BankDAO::getAll() { + QList list; + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare("SELECT * FROM banks ORDER BY alias"); + + if (!query.exec()) { + qWarning() << "Ошибка при получении banks:" << query.lastError().text(); + return list; + } + + while (query.next()) { + BankInfo bank; + bank.id = query.value("id").toInt(); + bank.alias = query.value("alias").toString(); + bank.name = query.value("name").toString(); + bank.nspkName = query.value("nspk_name").toString(); + list.append(bank); + } + + return list; +} + +BankInfo BankDAO::findByAlias(const QString &alias) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("SELECT * FROM banks WHERE alias = :alias LIMIT 1"); + query.bindValue(":alias", alias); + if (!query.exec() || !query.next()) return {}; + BankInfo bank; + bank.id = query.value("id").toInt(); + bank.alias = query.value("alias").toString(); + bank.name = query.value("name").toString(); + bank.nspkName = query.value("nspk_name").toString(); + return bank; +} + +void BankDAO::replaceAll(const QList &banks) { + QSqlDatabase db = DatabaseManager::instance().database(); + + if (!db.transaction()) { + qWarning() << "[BankDAO] replaceAll: cannot begin transaction:" << db.lastError().text(); + return; + } + + QSqlQuery query(db); + if (!query.exec("DELETE FROM banks")) { + qWarning() << "[BankDAO] replaceAll: DELETE failed:" << query.lastError().text(); + db.rollback(); + return; + } + + if (!query.prepare(R"( + INSERT INTO banks (alias, name, nspk_name) + VALUES (:alias, :name, :nspk_name) + )")) { + qWarning() << "[BankDAO] replaceAll: prepare INSERT failed:" << query.lastError().text(); + db.rollback(); + return; + } + + for (const BankInfo &bank : banks) { + query.bindValue(":alias", bank.alias); + query.bindValue(":name", bank.name); + query.bindValue(":nspk_name", bank.nspkName); + if (!query.exec()) { + qWarning() << "[BankDAO] replaceAll: INSERT failed for alias" << bank.alias + << ":" << query.lastError().text() << "— rolling back"; + db.rollback(); + return; + } + } + + if (!db.commit()) { + qWarning() << "[BankDAO] replaceAll: commit failed:" << db.lastError().text(); + db.rollback(); + } +} diff --git a/database/dao/BankDAO.h b/database/dao/BankDAO.h new file mode 100644 index 0000000..12093cd --- /dev/null +++ b/database/dao/BankDAO.h @@ -0,0 +1,13 @@ +#pragma once + +#include "db/BankInfo.h" +#include + +class BankDAO { +public: + [[nodiscard]] static QList getAll(); + + [[nodiscard]] static BankInfo findByAlias(const QString &alias); + + static void replaceAll(const QList &banks); +}; diff --git a/database/dao/BankProfileDAO.cpp b/database/dao/BankProfileDAO.cpp new file mode 100644 index 0000000..f8f0d8a --- /dev/null +++ b/database/dao/BankProfileDAO.cpp @@ -0,0 +1,447 @@ +#include +#include +#include +#include +#include +#include "BankProfileDAO.h" +#include "DatabaseManager.h" +#include "SettingsDAO.h" +#include "db/BankProfileInfo.h" + +bool BankProfileDAO::upsertApplication(const BankProfileInfo &info) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + INSERT INTO bank_profiles (device_id, pin_code, name, code, package, status, comment, install, pin_code_checked, + bank_profile_id, full_name, phone) + VALUES (:device_id, :pin_code, :name, :code, :package, :status, :comment, :install, :pin_code_checked, + :bank_profile_id, :full_name, :phone) + ON CONFLICT(device_id, code) + DO UPDATE SET + pin_code = CASE WHEN excluded.pin_code != '' THEN excluded.pin_code ELSE bank_profiles.pin_code END, + name = excluded.name, + package = CASE WHEN excluded.package != '' THEN excluded.package ELSE bank_profiles.package END, + status = excluded.status, + comment = excluded.comment, + install = excluded.install, + pin_code_checked = excluded.pin_code_checked, + bank_profile_id = CASE WHEN excluded.bank_profile_id != '' THEN excluded.bank_profile_id ELSE bank_profiles.bank_profile_id END, + full_name = CASE WHEN excluded.full_name != '' THEN excluded.full_name ELSE bank_profiles.full_name END, + phone = CASE WHEN excluded.phone != '' THEN excluded.phone ELSE bank_profiles.phone END + )"); + + query.bindValue(":device_id", info.deviceId); + query.bindValue(":pin_code", info.pinCode); + query.bindValue(":name", info.name); + query.bindValue(":code", info.code); + query.bindValue(":package", info.package); + query.bindValue(":status", info.status); + query.bindValue(":comment", info.comment); + query.bindValue(":install", info.install ? "yes" : "no"); + query.bindValue(":pin_code_checked", info.pinCodeChecked ? "yes" : "no"); + query.bindValue(":bank_profile_id", info.bankProfileId); + query.bindValue(":full_name", info.fullName); + query.bindValue(":phone", QString(info.phone).remove(' ').remove(QChar(0x00A0))); + + if (!query.exec()) { + qWarning() << "Ошибка при вставке/обновлении application:" << query.lastError().text(); + return false; + } + + return true; +} + +bool BankProfileDAO::updatePinCode(const QString &appId, const QString &pinCode) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + UPDATE bank_profiles + SET pin_code = :pin_code + WHERE id = :id + )"); + + query.bindValue(":pin_code", pinCode); + query.bindValue(":id", appId); + + if (!query.exec()) { + qWarning() << "Ошибка при обновлении PIN-кода приложения:" << query.lastError().text(); + return false; + } + return true; +} + +bool BankProfileDAO::update( + const int &appId, + const QString &pinCode, + const QString &comment, + const QString &status +) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + UPDATE bank_profiles + SET pin_code = :pin_code, comment = :comment, status = :status + WHERE id = :id + )"); + + query.bindValue(":id", appId); + query.bindValue(":comment", comment); + query.bindValue(":pin_code", pinCode); + query.bindValue(":status", status); + + if (!query.exec()) { + qWarning() << "Ошибка при обновлении PIN-кода приложения:" << query.lastError().text(); + return false; + } + return true; +} + +bool BankProfileDAO::updatePinCodeStatus( + const int &appId, + const QString &pinCodeStatus, + const QString &comment +) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + UPDATE bank_profiles + SET pin_code_checked = :pin_code_checked, comment = :comment + WHERE id = :id + )"); + + query.bindValue(":id", appId); + query.bindValue(":comment", comment); + query.bindValue(":pin_code_checked", pinCodeStatus); + + if (!query.exec()) { + qWarning() << "Ошибка при обновлении PIN-кода приложения:" << query.lastError().text(); + return false; + } + return true; +} + + +bool BankProfileDAO::updateComment(const int appId, const QString &comment) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE bank_profiles SET comment = :comment WHERE id = :id"); + query.bindValue(":comment", comment); + query.bindValue(":id", appId); + if (!query.exec()) { + qWarning() << "Ошибка при обновлении комментария:" << query.lastError().text(); + return false; + } + return true; +} + +BankProfileInfo parseApplication(const QSqlQuery &query) { + BankProfileInfo app; + app.id = query.value("id").toInt(); + app.deviceId = query.value("device_id").toString(); + app.pinCode = query.value("pin_code").toString(); + app.code = query.value("code").toString(); + app.name = query.value("name").toString(); + app.package = query.value("package").toString(); + app.status = query.value("status").toString(); + app.comment = query.value("comment").toString(); + app.install = query.value("install").toString() == "yes"; + app.pinCodeChecked = query.value("pin_code_checked").toString() == "yes"; + app.phone = query.value("phone").toString().remove(' ').remove(QChar(0x00A0)); + app.email = query.value("email").toString(); + app.fullName = query.value("full_name").toString(); + app.bankProfileId = query.value("bank_profile_id").toString(); + app.currency = query.value("currency").toString(); + app.amount = query.value("amount").toDouble(); + app.appVersion = query.value("app_version").toString(); + app.appVersionCheckedAt = query.value("app_version_checked_at").toDateTime(); + return app; +} + +BankProfileInfo BankProfileDAO::getApplication(const QString &deviceId, const QString &appCode) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount, app_version, app_version_checked_at + FROM bank_profiles + WHERE device_id = :device_id AND code = :code + LIMIT 1 + )"); + + query.bindValue(":device_id", deviceId); + query.bindValue(":code", appCode); + + if (!query.exec()) { + qWarning() << "Ошибка при получении приложений по deviceId:" << query.lastError().text(); + return {}; + } + + while (query.next()) { + return parseApplication(query); + } + return {}; +} + +QList BankProfileDAO::getApplicationsByDeviceId(const QString &deviceId) { + QSqlQuery query(DatabaseManager::instance().database()); + QList list; + + query.prepare(R"( + SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount, app_version, app_version_checked_at + FROM bank_profiles + WHERE device_id = :device_id AND install = 'yes' + )"); + query.bindValue(":device_id", deviceId); + + if (!query.exec()) { + qWarning() << "Ошибка при получении приложений по deviceId:" << query.lastError().text(); + return {}; + } + + while (query.next()) { + list.append(parseApplication(query)); + } + + return list; +} + +QList BankProfileDAO::getAllApplicationsByDeviceId(const QString &deviceId) { + QSqlQuery query(DatabaseManager::instance().database()); + QList list; + + query.prepare(R"( + SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount, app_version, app_version_checked_at + FROM bank_profiles + WHERE device_id = :device_id + )"); + query.bindValue(":device_id", deviceId); + + if (!query.exec()) { + qWarning() << "Ошибка при получении всех приложений по deviceId:" << query.lastError().text(); + return {}; + } + + while (query.next()) { + list.append(parseApplication(query)); + } + + return list; +} + +bool BankProfileDAO::updateProfileInfo( + const int &appId, + const QString &fullName, + const QString &phone, + const QString &email +) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + UPDATE bank_profiles + SET full_name = :full_name, phone = :phone, email = :email + WHERE id = :id + )"); + + query.bindValue(":id", appId); + query.bindValue(":full_name", fullName); + query.bindValue(":phone", QString(phone).remove(' ').remove(QChar(0x00A0))); + query.bindValue(":email", email); + + if (!query.exec()) { + qWarning() << "Ошибка при обновлении профиля приложения:" << query.lastError().text(); + return false; + } + return true; +} + +bool BankProfileDAO::updateAmount(const int appId, const double amount) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE bank_profiles SET amount = :amount WHERE id = :id"); + query.bindValue(":amount", amount); + query.bindValue(":id", appId); + if (!query.exec()) { + qWarning() << "Ошибка при обновлении amount в application:" << query.lastError().text(); + return false; + } + return true; +} + +bool BankProfileDAO::adjustAmount(const int appId, const double delta) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE bank_profiles SET amount = amount - :delta WHERE id = :id"); + query.bindValue(":delta", delta); + query.bindValue(":id", appId); + if (!query.exec()) { + qWarning() << "[BankProfileDAO] adjustAmount failed:" << query.lastError().text(); + return false; + } + return true; +} + +bool BankProfileDAO::updateAppVersion(const int appId, const QString &appVersion) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + UPDATE bank_profiles + SET app_version = :app_version, app_version_checked_at = :checked_at + WHERE id = :id + )"); + query.bindValue(":app_version", appVersion); + query.bindValue(":checked_at", QDateTime::currentDateTimeUtc()); + query.bindValue(":id", appId); + if (!query.exec()) { + qWarning() << "Ошибка при обновлении app_version:" << query.lastError().text(); + return false; + } + return true; +} + +bool BankProfileDAO::updateBankProfileId(const int appId, const QString &bankProfileId) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE bank_profiles SET bank_profile_id = :bank_profile_id WHERE id = :id"); + query.bindValue(":bank_profile_id", bankProfileId); + query.bindValue(":id", appId); + if (!query.exec()) { + qWarning() << "Ошибка при обновлении bank_profile_id:" << query.lastError().text(); + return false; + } + return true; +} + +QList BankProfileDAO::findAllByBankProfileId(const QString &bankProfileId) { + QList list; + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount, app_version, app_version_checked_at + FROM bank_profiles + WHERE bank_profile_id = :bank_profile_id + )"); + query.bindValue(":bank_profile_id", bankProfileId); + if (!query.exec()) { + qWarning() << "Ошибка при поиске bank_profiles по bank_profile_id:" << query.lastError().text(); + return list; + } + while (query.next()) { + list.append(parseApplication(query)); + } + return list; +} + +bool BankProfileDAO::activateAppsForDevice(const QString &deviceId) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + UPDATE bank_profiles SET status = 'active' + WHERE device_id = :device_id AND install = 'yes' AND pin_code_checked = 'yes' + )"); + query.bindValue(":device_id", deviceId); + if (!query.exec()) { + qWarning() << "Ошибка при активации приложений:" << query.lastError().text(); + return false; + } + return true; +} + +bool BankProfileDAO::deactivateAppsForDevice(const QString &deviceId) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + UPDATE bank_profiles SET status = 'off' + WHERE device_id = :device_id AND install = 'yes' + )"); + query.bindValue(":device_id", deviceId); + if (!query.exec()) { + qWarning() << "Ошибка при деактивации приложений:" << query.lastError().text(); + return false; + } + return true; +} + +bool BankProfileDAO::checkInstalledApps(const QString &deviceId, const QSet &apps) { + QSqlQuery query(DatabaseManager::instance().database()); + const QSettings settings("config.ini", QSettings::IniFormat); + QStringList banks = settings.value("apps/list", QStringList()).toStringList(); + + // Фильтруем банки по стране профиля (если страна задана) + const QString profileCountry = SettingsDAO::get("country"); + if (!profileCountry.isEmpty()) { + QStringList filtered; + for (const QString &raw : banks) { + const QString b = raw.trimmed(); + if (b.isEmpty()) continue; + const QString bankCountry = settings.value(b + "/country").toString(); + if (bankCountry.compare(profileCountry, Qt::CaseInsensitive) == 0) { + filtered << b; + } + } + banks = filtered; + } + + // Сбросить install = 'no' для банков которых нет в текущем apps/list + if (!banks.isEmpty()) { + QStringList placeholders; + for (int i = 0; i < banks.size(); ++i) placeholders << "?"; + query.prepare(QString(R"( + UPDATE bank_profiles SET install = 'no' + WHERE device_id = ? AND code NOT IN (%1) + )").arg(placeholders.join(","))); + query.addBindValue(deviceId); + for (const QString &bank : banks) query.addBindValue(bank); + if (!query.exec()) { + qWarning() << "Ошибка при сбросе install:" << query.lastError().text(); + } + } + + for (const QString &bank: banks) { + QString package = settings.value(bank + "/android_package_name").toString(); + QString name = settings.value(bank + "/name").toString(); + + const QString currency = settings.value(bank + "/currency").toString(); + + query.prepare(R"( + INSERT INTO bank_profiles (device_id, name, code, package, status, install, pin_code_checked, currency) + VALUES (:device_id, :name, :code, :package, :status, :install, :pin_code_checked, :currency) + ON CONFLICT(device_id, code) + DO UPDATE SET + install = excluded.install, + currency = CASE WHEN excluded.currency != '' THEN excluded.currency ELSE currency END + )"); + + query.bindValue(":device_id", deviceId); + query.bindValue(":code", bank); + query.bindValue(":name", name); + query.bindValue(":package", package); + query.bindValue(":status", "off"); + query.bindValue(":pin_code_checked", "no"); + query.bindValue(":install", apps.contains(package) ? "yes" : "no"); + query.bindValue(":currency", currency); + + if (!query.exec()) { + qWarning() << "Ошибка при проверке приложений:" << query.lastError().text(); + return false; + } + } + + + return true; +} + +bool BankProfileDAO::migrateDeviceId(const QString &oldDeviceId, const QString &newDeviceId) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE bank_profiles SET device_id = :new_id WHERE device_id = :old_id"); + query.bindValue(":new_id", newDeviceId); + query.bindValue(":old_id", oldDeviceId); + if (!query.exec()) { + qWarning() << "[BankProfileDAO] migrateDeviceId failed:" << query.lastError().text(); + return false; + } + qDebug() << "[BankProfileDAO] migrated" << query.numRowsAffected() << "profiles from" << oldDeviceId << "to" << newDeviceId; + return true; +} + +bool BankProfileDAO::deleteProfile(const QString &deviceId, const QString &appCode) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("DELETE FROM bank_profiles WHERE device_id = :device_id AND code = :code"); + query.bindValue(":device_id", deviceId); + query.bindValue(":code", appCode); + if (!query.exec()) { + qWarning() << "[BankProfileDAO] deleteProfile failed:" << query.lastError().text(); + return false; + } + return true; +} diff --git a/database/dao/BankProfileDAO.h b/database/dao/BankProfileDAO.h new file mode 100644 index 0000000..bb1c14b --- /dev/null +++ b/database/dao/BankProfileDAO.h @@ -0,0 +1,60 @@ +#pragma once + +#include "db/BankProfileInfo.h" + +class BankProfileDAO { +public: + [[nodiscard]] static bool upsertApplication(const BankProfileInfo &info); + + [[nodiscard]] static bool updatePinCode(const QString &appId, const QString &pinCode); + + [[nodiscard]] static bool update( + const int &appId, + const QString &pinCode, + const QString &comment, + const QString &status + ); + + [[nodiscard]] static bool updatePinCodeStatus( + const int &appId, + const QString &pinCodeStatus, + const QString &comment + ); + + [[nodiscard]] static bool updateComment(int appId, const QString &comment); + + [[nodiscard]] static bool updateProfileInfo( + const int &appId, + const QString &fullName, + const QString &phone, + const QString &email = "" + ); + + [[nodiscard]] static bool updateBankProfileId(int appId, const QString &bankProfileId); + + [[nodiscard]] static QList findAllByBankProfileId(const QString &bankProfileId); + + [[nodiscard]] static bool updateAmount(int appId, double amount); + + // Атомарно вычитает delta из bank_profiles.amount. + // delta > 0 — расход, delta < 0 — пополнение. + [[nodiscard]] static bool adjustAmount(int appId, double delta); + + [[nodiscard]] static bool updateAppVersion(int appId, const QString &appVersion); + + [[nodiscard]] static BankProfileInfo getApplication(const QString &deviceId, const QString &appCode); + + [[nodiscard]] static QList getApplicationsByDeviceId(const QString &deviceId); + + [[nodiscard]] static QList getAllApplicationsByDeviceId(const QString &deviceId); + + [[nodiscard]] static bool checkInstalledApps(const QString &deviceId, const QSet &apps); + + [[nodiscard]] static bool activateAppsForDevice(const QString &deviceId); + + [[nodiscard]] static bool deactivateAppsForDevice(const QString &deviceId); + + [[nodiscard]] static bool migrateDeviceId(const QString &oldDeviceId, const QString &newDeviceId); + + [[nodiscard]] static bool deleteProfile(const QString &deviceId, const QString &appCode); +}; diff --git a/database/dao/CommonDataDAO.cpp b/database/dao/CommonDataDAO.cpp index 9287635..f68b381 100644 --- a/database/dao/CommonDataDAO.cpp +++ b/database/dao/CommonDataDAO.cpp @@ -4,6 +4,8 @@ #include #include +#include "time/DateUtils.h" + QDateTime CommonDataDAO::getLastScan() { QSqlQuery query(DatabaseManager::instance().database()); @@ -16,16 +18,16 @@ QDateTime CommonDataDAO::getLastScan() { if (query.next()) { const QString timestamp = query.value(0).toString(); - return QDateTime::fromString(timestamp, Qt::ISODate); + return DateUtils::fromUtcString(timestamp); } return {}; } -bool CommonDataDAO::updateLastScan(const QDateTime& newTime) { +bool CommonDataDAO::updateLastScan(const QDateTime &newTime) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare("UPDATE common_data SET update_time = :time WHERE id = 1"); - query.bindValue(":time", newTime.toString(Qt::ISODate)); + query.bindValue(":time", DateUtils::toUtcString(newTime)); if (!query.exec()) { qWarning() << "Ошибка при обновлении lastScan:" << query.lastError().text(); @@ -35,7 +37,7 @@ bool CommonDataDAO::updateLastScan(const QDateTime& newTime) { if (query.numRowsAffected() == 0) { QSqlQuery insertQuery(DatabaseManager::instance().database()); insertQuery.prepare("INSERT INTO common_data (id, update_time) VALUES (1, :time)"); - insertQuery.bindValue(":time", newTime.toString(Qt::ISODate)); + insertQuery.bindValue(":time", DateUtils::toUtcString(newTime)); if (!insertQuery.exec()) { qWarning() << "Ошибка при INSERT lastScan:" << insertQuery.lastError().text(); diff --git a/database/dao/DeviceDAO.cpp b/database/dao/DeviceDAO.cpp index 59d608c..e55ae65 100644 --- a/database/dao/DeviceDAO.cpp +++ b/database/dao/DeviceDAO.cpp @@ -3,18 +3,34 @@ #include "DeviceDAO.h" #include "DatabaseManager.h" #include "db/DeviceInfo.h" +#include "time/DateUtils.h" + +DeviceInfo parseDevice(const QSqlQuery &query) { + DeviceInfo device; + device.id = query.value("id").toString(); + device.apiId = query.value("api_id").toString(); + device.adbSerial = query.value("adb_serial").toString(); + device.status = query.value("status").toString(); + device.name = query.value("name").toString(); + device.android = query.value("android").toString(); + device.screenWidth = query.value("screen_width").toInt(); + device.screenHeight = query.value("screen_height").toInt(); + device.battery = query.value("battery").toInt(); + device.updateTime = DateUtils::fromUtcString(query.value("update_time").toString()); + device.image = query.value("image").toByteArray(); + return device; +} QList DeviceDAO::getAllDevices(const bool online) { QList devices; QSqlQuery query(DatabaseManager::instance().database()); QString sql = R"( - SELECT id, status, name, android, screen_width, screen_height, battery, update_time, image + SELECT id, api_id, adb_serial, status, name, android, screen_width, screen_height, battery, update_time, image FROM devices - WHERE image IS NOT NULL )"; if (online) { - sql += " AND status = 'device'"; + sql += " WHERE status = 'device'"; } query.prepare(sql); @@ -24,24 +40,125 @@ QList DeviceDAO::getAllDevices(const bool online) { } while (query.next()) { - DeviceInfo device; - device.id = query.value("id").toString(); - device.status = query.value("status").toString(); - device.name = query.value("name").toString(); - device.android = query.value("android").toString(); - device.screenWidth = query.value("screen_width").toInt(); - device.screenHeight = query.value("screen_height").toInt(); - device.battery = query.value("battery").toInt(); - device.updateTime = QDateTime::fromString(query.value("update_time").toString(), Qt::ISODate); - device.image = query.value("image").toByteArray(); - - devices.append(device); + devices.append(parseDevice(query)); } return devices; } +QList DeviceDAO::getOnlineDeviceCodes() { + QList codes; + QSqlQuery query(DatabaseManager::instance().database()); + QString sql = R"( + SELECT id + FROM devices + WHERE image IS NOT NULL AND status = 'device' + )"; + query.prepare(sql); + + if (!query.exec()) { + qWarning() << "Ошибка при получении списка устройств:" << query.lastError().text(); + return {}; + } + + while (query.next()) { + codes.append(query.value("id").toString()); + } + + return codes; +} + +DeviceInfo DeviceDAO::getDeviceById(const QString &deviceId) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + SELECT id, api_id, adb_serial, status, name, android, screen_width, screen_height, battery, update_time, image + FROM devices + WHERE id = :deviceId + )"); + query.bindValue(":deviceId", deviceId); + + if (!query.exec()) { + qWarning() << "Ошибка при получении устройства:" << query.lastError().text(); + return {}; + } + + while (query.next()) { + return parseDevice(query); + } + + return {}; +} + +DeviceInfo DeviceDAO::findByAdbSerial(const QString &adbSerial) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT id, api_id, adb_serial, status, name, android, screen_width, screen_height, battery, update_time, image + FROM devices + WHERE adb_serial = :adb_serial + LIMIT 1 + )"); + query.bindValue(":adb_serial", adbSerial); + + if (!query.exec()) { + qWarning() << "Ошибка при поиске устройства по adb_serial:" << query.lastError().text(); + return {}; + } + + if (query.next()) { + return parseDevice(query); + } + return {}; +} + +DeviceInfo DeviceDAO::findByName(const QString &name) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT id, api_id, adb_serial, status, name, android, screen_width, screen_height, battery, update_time, image + FROM devices + WHERE name = :name + LIMIT 1 + )"); + query.bindValue(":name", name); + + if (!query.exec()) { + qWarning() << "Ошибка при поиске устройства по name:" << query.lastError().text(); + return {}; + } + + if (query.next()) { + return parseDevice(query); + } + return {}; +} + +DeviceInfo DeviceDAO::findUnmatchedDevice() { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT id, api_id, adb_serial, status, name, android, screen_width, screen_height, battery, update_time, image + FROM devices + WHERE api_id IS NOT NULL AND api_id != '' + AND (adb_serial IS NULL OR adb_serial = '') + LIMIT 1 + )"); + + if (!query.exec()) { + qWarning() << "Ошибка при поиске unmatched device:" << query.lastError().text(); + return {}; + } + + if (query.next()) { + return parseDevice(query); + } + return {}; +} + bool DeviceDAO::upsertDevice(const DeviceInfo &device) { + if (device.id.isEmpty() || device.name.isEmpty()) { + qWarning() << "[DeviceDAO] upsertDevice skipped: empty id or name"; + return false; + } + QSqlQuery query(DatabaseManager::instance().database()); // Проверка: существует ли запись @@ -59,6 +176,8 @@ bool DeviceDAO::upsertDevice(const DeviceInfo &device) { // Обновление query.prepare(R"( UPDATE devices SET + api_id = CASE WHEN :api_id != '' THEN :api_id ELSE api_id END, + adb_serial = CASE WHEN :adb_serial != '' THEN :adb_serial ELSE adb_serial END, status = :status, name = :name, android = :android, @@ -72,21 +191,23 @@ bool DeviceDAO::upsertDevice(const DeviceInfo &device) { // Вставка query.prepare(R"( INSERT INTO devices ( - id, status, name, android, screen_width, screen_height, battery, update_time + id, api_id, adb_serial, status, name, android, screen_width, screen_height, battery, update_time ) VALUES ( - :id, :status, :name, :android, :width, :height, :battery, :update_time + :id, :api_id, :adb_serial, :status, :name, :android, :width, :height, :battery, :update_time ) )"); } query.bindValue(":id", device.id); + query.bindValue(":api_id", device.apiId); + query.bindValue(":adb_serial", device.adbSerial); query.bindValue(":status", device.status); query.bindValue(":name", device.name); query.bindValue(":android", device.android); query.bindValue(":width", device.screenWidth); query.bindValue(":height", device.screenHeight); query.bindValue(":battery", device.battery); - query.bindValue(":update_time", device.updateTime.toString(Qt::ISODate)); + query.bindValue(":update_time", DateUtils::toUtcString(device.updateTime)); if (!query.exec()) { qWarning() << "Ошибка при вставке/обновлении устройства:" << query.lastError().text(); @@ -131,6 +252,31 @@ bool DeviceDAO::markDevicesOfflineExcept(const QStringList &onlineDeviceIds) { return true; } +bool DeviceDAO::updateApiId(const QString &deviceId, const QString &apiId) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE devices SET api_id = :api_id WHERE id = :id"); + query.bindValue(":api_id", apiId); + query.bindValue(":id", deviceId); + + if (!query.exec()) { + qWarning() << "Ошибка при обновлении api_id устройства:" << query.lastError().text(); + return false; + } + return true; +} + +bool DeviceDAO::deleteDevice(const QString &deviceId) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("DELETE FROM devices WHERE id = :id"); + query.bindValue(":id", deviceId); + + if (!query.exec()) { + qWarning() << "Ошибка при удалении устройства:" << query.lastError().text(); + return false; + } + return true; +} + bool DeviceDAO::updateImage(const QString &deviceId, const QByteArray &imageData) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare("UPDATE devices SET image = :image WHERE id = :id"); diff --git a/database/dao/DeviceDAO.h b/database/dao/DeviceDAO.h index 58d8bb4..c9bbe71 100644 --- a/database/dao/DeviceDAO.h +++ b/database/dao/DeviceDAO.h @@ -4,11 +4,25 @@ class DeviceDAO { public: + [[nodiscard]] static DeviceInfo getDeviceById(const QString &deviceId); + + [[nodiscard]] static DeviceInfo findByAdbSerial(const QString &adbSerial); + + [[nodiscard]] static DeviceInfo findByName(const QString &name); + + [[nodiscard]] static DeviceInfo findUnmatchedDevice(); + [[nodiscard]] static bool upsertDevice(const DeviceInfo &device); + [[nodiscard]] static bool updateApiId(const QString &deviceId, const QString &apiId); + [[nodiscard]] static bool markDevicesOfflineExcept(const QStringList &onlineDeviceIds); [[nodiscard]] static bool updateImage(const QString &deviceId, const QByteArray &imageData); [[nodiscard]] static QList getAllDevices(bool online); + + [[nodiscard]] static QList getOnlineDeviceCodes(); + + [[nodiscard]] static bool deleteDevice(const QString &deviceId); }; diff --git a/database/dao/EventDAO.cpp b/database/dao/EventDAO.cpp new file mode 100644 index 0000000..8789ebf --- /dev/null +++ b/database/dao/EventDAO.cpp @@ -0,0 +1,443 @@ +#include "EventDAO.h" + +#include +#include +#include + +#include "DatabaseManager.h" +#include "time/DateUtils.h" + +EventInfo parseEvent(const QSqlQuery &query) { + EventInfo event; + event.id = query.value("id").toInt(); + event.status = eventStatusFromString(query.value("status").toString()); + event.type = eventTypeFromString(query.value("type").toString()); + event.externalId = query.value("external_id").toString(); + event.accountId = query.value("account_id").toInt(); + event.deviceId = query.value("device_id").toString(); + event.amount = query.value("amount").toDouble(); + event.phone = query.value("phone").toString(); + event.bankName = query.value("bank_name").toString(); + event.comment = query.value("comment").toString(); + event.json = query.value("json").toString(); + event.sentToServer = query.value("sent_to_server").toBool(); + event.timestamp = DateUtils::fromUtcString(query.value("timestamp").toString()); + event.updateTime = DateUtils::fromUtcString(query.value("update_time").toString()); + event.completeTime = DateUtils::fromUtcString(query.value("complete_time").toString()); + const int hasScreenshotIdx = query.record().indexOf("has_screenshot"); + if (hasScreenshotIdx >= 0) { + event.hasScreenshot = query.value(hasScreenshotIdx).toBool(); + } + return event; +} + +int EventDAO::insertEvent(const EventInfo &event) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + INSERT INTO events ( + status, external_id, account_id, amount, phone, bank_name, comment, json, timestamp, device_id, type + ) VALUES ( + :status, :external_id, :account_id, :amount, :phone, :bank_name, :comment, :json, :timestamp, :device_id, :type + ) + )"); + + query.bindValue(":status", eventStatusToString(event.status)); + query.bindValue(":type", eventTypeToString(event.type)); + query.bindValue(":external_id", event.externalId); + query.bindValue(":account_id", event.accountId); + query.bindValue(":device_id", event.deviceId); + query.bindValue(":amount", event.amount); + query.bindValue(":phone", event.phone); + query.bindValue(":bank_name", event.bankName); + query.bindValue(":comment", event.comment); + query.bindValue(":json", event.json); + query.bindValue(":timestamp", DateUtils::toUtcString(event.timestamp)); + + if (!query.exec()) { + qCritical() << "Ошибка при вставке event:" << query.lastError().text(); + return -1; + } + + return query.lastInsertId().toInt(); +} + +bool EventDAO::updateEvent( + const int id, + const EventStatus &status, + const QString &comment +) { + QSqlQuery query(DatabaseManager::instance().database()); + + if (status == EventStatus::InProgress) { + query.prepare(R"( + UPDATE events SET + status = :status, + update_time = :update_time + WHERE id = :id + )"); + query.bindValue(":update_time", DateUtils::toUtcString(DateUtils::getUtcNow())); + } + + if (status == EventStatus::Complete) { + query.prepare(R"( + UPDATE events SET + status = :status, + complete_time = :complete_time + WHERE id = :id + )"); + query.bindValue(":complete_time", DateUtils::toUtcString(DateUtils::getUtcNow())); + } + + if (status == EventStatus::Error) { + query.prepare(R"( + UPDATE events SET + status = :status, + update_time = :update_time, + comment = :comment + WHERE id = :id + )"); + query.bindValue(":update_time", DateUtils::toUtcString(DateUtils::getUtcNow())); + query.bindValue(":comment", comment); + } + + query.bindValue(":id", id); + query.bindValue(":status", eventStatusToString(status)); + + if (!query.exec()) { + qCritical() << "Ошибка при обновлении event:" << query.lastError().text(); + return false; + } + return true; +} + +bool EventDAO::updateEventAmount(int id, double amount) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE events SET amount = :amount WHERE id = :id"); + query.bindValue(":amount", amount); + query.bindValue(":id", id); + if (!query.exec()) { + qCritical() << "Ошибка при обновлении amount event:" << query.lastError().text(); + return false; + } + return true; +} + +bool EventDAO::markSentToServer(int id) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE events SET sent_to_server = 1 WHERE id = :id"); + query.bindValue(":id", id); + if (!query.exec()) { + qWarning() << "Ошибка markSentToServer:" << query.lastError().text(); + return false; + } + return true; +} + +QList EventDAO::getAllEvents(const EventStatus &status) { + QList events; + + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT id, account_id, device_id, external_id, amount, phone, bank_name, + status, type, comment, json, sent_to_server, update_time, complete_time, timestamp + FROM events + WHERE status = :status + )"); + query.bindValue(":status", eventStatusToString(status)); + + if (!query.exec()) { + qWarning() << "Ошибка при получении getAllEvents:" << query.lastError().text(); + return events; + } + + while (query.next()) { + events.append(parseEvent(query)); + } + + return events; +} + +EventInfo EventDAO::getFirstWaitEventByDeviceId(const QString &key) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT id, account_id, device_id, external_id, amount, phone, bank_name, + status, type, comment, json, sent_to_server, update_time, complete_time, timestamp + FROM events + WHERE status = :status AND device_id = :device_id + ORDER BY + CASE type + WHEN 'CREATE_TRANSACTION' THEN 0 + WHEN 'FETCH_TRANSACTIONS' THEN 1 + WHEN 'FETCH_PROFILE' THEN 2 + ELSE 3 + END ASC, + timestamp ASC + LIMIT 1 + )"); + query.bindValue(":status", eventStatusToString(EventStatus::Wait)); + query.bindValue(":device_id", key); + + if (!query.exec()) { + qCritical() << "Ошибка при получении getFirstWaitEventByDeviceId:" << query.lastError().text(); + return {}; + } + + while (query.next()) { + return parseEvent(query); + } + return {}; +} + +QList EventDAO::getEvents(int page, int pageSize) { + QList events; + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT id, account_id, device_id, external_id, amount, phone, bank_name, + status, type, comment, json, sent_to_server, update_time, complete_time, timestamp, + (LENGTH(screenshot) > 0) as has_screenshot + FROM events + ORDER BY id DESC + LIMIT :limit OFFSET :offset + )"); + query.bindValue(":limit", pageSize); + query.bindValue(":offset", page * pageSize); + + if (!query.exec()) { + qWarning() << "Ошибка при получении getEvents:" << query.lastError().text(); + return events; + } + + while (query.next()) { + events.append(parseEvent(query)); + } + return events; +} + +int EventDAO::getTotalCount() { + QSqlQuery query(DatabaseManager::instance().database()); + if (!query.exec("SELECT COUNT(*) FROM events")) { + qWarning() << "Ошибка при получении getTotalCount:" << query.lastError().text(); + return 0; + } + if (query.next()) return query.value(0).toInt(); + return 0; +} + +bool EventDAO::hasPendingEvents() { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT 1 FROM events + WHERE status IN ('wait', 'iPprogress') + LIMIT 1 + )"); + if (!query.exec()) { + qCritical() << "Ошибка при hasPendingEvents:" << query.lastError().text(); + return false; + } + return query.next(); +} + +QSet EventDAO::getDevicesWithPendingEvents() { + QSet devices; + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT DISTINCT device_id FROM events + WHERE status IN ('wait', 'iPprogress') + )"); + if (!query.exec()) { + qCritical() << "Ошибка при getDevicesWithPendingEvents:" << query.lastError().text(); + return devices; + } + while (query.next()) { + devices.insert(query.value(0).toString()); + } + return devices; +} + +bool EventDAO::existsActiveByExternalId(const QString &externalId) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT 1 FROM events + WHERE external_id = :external_id + AND status IN ('wait', 'iPprogress') + LIMIT 1 + )"); + query.bindValue(":external_id", externalId); + + if (!query.exec()) { + qCritical() << "Ошибка при existsActiveByExternalId:" << query.lastError().text(); + return false; + } + return query.next(); +} + +QList EventDAO::cancelWaitingByMaterial(const QString &materialId, EventType type) { + QList cancelled; + QSqlQuery selectQuery(DatabaseManager::instance().database()); + selectQuery.prepare(R"( + SELECT id, external_id, bank_name, type FROM events + WHERE external_id = :material_id AND type = :type AND status = :wait + )"); + selectQuery.bindValue(":material_id", materialId); + selectQuery.bindValue(":type", eventTypeToString(type)); + selectQuery.bindValue(":wait", eventStatusToString(EventStatus::Wait)); + + if (selectQuery.exec()) { + while (selectQuery.next()) { + EventInfo e; + e.id = selectQuery.value(0).toInt(); + e.externalId = selectQuery.value(1).toString(); + e.bankName = selectQuery.value(2).toString(); + e.type = eventTypeFromString(selectQuery.value(3).toString()); + cancelled.append(e); + } + } + + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + UPDATE events SET status = :cancelled, comment = 'Cancelled: replaced by new task' + WHERE external_id = :material_id AND type = :type AND status = :wait + )"); + query.bindValue(":cancelled", eventStatusToString(EventStatus::Error)); + query.bindValue(":material_id", materialId); + query.bindValue(":type", eventTypeToString(type)); + query.bindValue(":wait", eventStatusToString(EventStatus::Wait)); + + if (!query.exec()) { + qWarning() << "Ошибка при cancelWaitingByMaterial:" << query.lastError().text(); + } + return cancelled; +} + +QList EventDAO::cancelWaitingByType(const QString &deviceId, EventType type, const QString &bankName) { + QList cancelled; + QSqlQuery selectQuery(DatabaseManager::instance().database()); + if (bankName.isEmpty()) { + selectQuery.prepare(R"( + SELECT id, external_id, bank_name, type FROM events + WHERE device_id = :device_id AND type = :type AND status = :wait + )"); + } else { + selectQuery.prepare(R"( + SELECT id, external_id, bank_name, type FROM events + WHERE device_id = :device_id AND type = :type AND status = :wait AND bank_name = :bank_name + )"); + selectQuery.bindValue(":bank_name", bankName); + } + selectQuery.bindValue(":device_id", deviceId); + selectQuery.bindValue(":type", eventTypeToString(type)); + selectQuery.bindValue(":wait", eventStatusToString(EventStatus::Wait)); + + if (selectQuery.exec()) { + while (selectQuery.next()) { + EventInfo e; + e.id = selectQuery.value(0).toInt(); + e.externalId = selectQuery.value(1).toString(); + e.bankName = selectQuery.value(2).toString(); + e.type = eventTypeFromString(selectQuery.value(3).toString()); + cancelled.append(e); + } + } + + QSqlQuery query(DatabaseManager::instance().database()); + if (bankName.isEmpty()) { + query.prepare(R"( + UPDATE events SET status = :cancelled, comment = 'Cancelled: replaced by new task' + WHERE device_id = :device_id AND type = :type AND status = :wait + )"); + } else { + query.prepare(R"( + UPDATE events SET status = :cancelled, comment = 'Cancelled: replaced by new task' + WHERE device_id = :device_id AND type = :type AND status = :wait AND bank_name = :bank_name + )"); + query.bindValue(":bank_name", bankName); + } + query.bindValue(":cancelled", eventStatusToString(EventStatus::Error)); + query.bindValue(":device_id", deviceId); + query.bindValue(":type", eventTypeToString(type)); + query.bindValue(":wait", eventStatusToString(EventStatus::Wait)); + + if (!query.exec()) { + qWarning() << "Ошибка при cancelWaitingByType:" << query.lastError().text(); + } + return cancelled; +} + +QList EventDAO::cancelWaitingByBank(const QString &deviceId, const QString &bankName) { + // Сначала получаем задачи которые будем отменять (для отправки на сервер) + QList cancelled; + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT id, account_id, device_id, external_id, amount, phone, bank_name, + status, type, comment, json, sent_to_server, update_time, complete_time, timestamp + FROM events + WHERE device_id = :device_id AND bank_name = :bank_name AND status = :wait + )"); + query.bindValue(":device_id", deviceId); + query.bindValue(":bank_name", bankName); + query.bindValue(":wait", eventStatusToString(EventStatus::Wait)); + if (query.exec()) { + while (query.next()) { + cancelled.append(parseEvent(query)); + } + } + + // Отменяем + query.prepare(R"( + UPDATE events SET status = :error, comment = 'Bank profile disabled' + WHERE device_id = :device_id AND bank_name = :bank_name AND status = :wait + )"); + query.bindValue(":error", eventStatusToString(EventStatus::Error)); + query.bindValue(":device_id", deviceId); + query.bindValue(":bank_name", bankName); + query.bindValue(":wait", eventStatusToString(EventStatus::Wait)); + if (!query.exec()) { + qWarning() << "Ошибка при cancelWaitingByBank:" << query.lastError().text(); + } + + return cancelled; +} + +bool EventDAO::updateScreenshot(int id, const QByteArray &screenshot) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE events SET screenshot = :screenshot WHERE id = :id"); + query.bindValue(":screenshot", screenshot.isEmpty() + ? QVariant(QMetaType(QMetaType::QByteArray)) + : QVariant(screenshot)); + query.bindValue(":id", id); + if (!query.exec()) { + qWarning() << "Ошибка при updateScreenshot:" << query.lastError().text(); + return false; + } + return true; +} + +QByteArray EventDAO::getScreenshot(int id) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("SELECT screenshot FROM events WHERE id = :id"); + query.bindValue(":id", id); + if (!query.exec() || !query.next()) return {}; + return query.value(0).toByteArray(); +} + +QList EventDAO::getRecentEvents(int hours) { + QList events; + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT id, account_id, device_id, external_id, amount, phone, bank_name, + status, type, comment, json, sent_to_server, update_time, complete_time, timestamp + FROM events + WHERE timestamp >= datetime('now', :hours) + ORDER BY id DESC + )"); + query.bindValue(":hours", QString("-%1 hours").arg(hours)); + if (!query.exec()) { + qWarning() << "Ошибка при getRecentEvents:" << query.lastError().text(); + return events; + } + while (query.next()) { + events.append(parseEvent(query)); + } + return events; +} diff --git a/database/dao/EventDAO.h b/database/dao/EventDAO.h new file mode 100644 index 0000000..8727d7e --- /dev/null +++ b/database/dao/EventDAO.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +class EventDAO { +public: + [[nodiscard]] static int insertEvent(const EventInfo &event); + + [[nodiscard]] static bool updateEvent( + int id, + const EventStatus &status, + const QString &comment + ); + + [[nodiscard]] static QList getAllEvents(const EventStatus &status); + + [[nodiscard]] static EventInfo getFirstWaitEventByDeviceId(const QString &key); + + [[nodiscard]] static bool existsActiveByExternalId(const QString &externalId); + + [[nodiscard]] static QList cancelWaitingByType(const QString &deviceId, EventType type, const QString &bankName = {}); + + [[nodiscard]] static QList cancelWaitingByMaterial(const QString &materialId, EventType type); + + [[nodiscard]] static QList cancelWaitingByBank(const QString &deviceId, const QString &bankName); + + [[nodiscard]] static bool updateScreenshot(int id, const QByteArray &screenshot); + + [[nodiscard]] static QByteArray getScreenshot(int id); + + [[nodiscard]] static bool updateEventAmount(int id, double amount); + + [[nodiscard]] static bool markSentToServer(int id); + + [[nodiscard]] static QList getEvents(int page, int pageSize); + + [[nodiscard]] static int getTotalCount(); + + [[nodiscard]] static bool hasPendingEvents(); + + [[nodiscard]] static QSet getDevicesWithPendingEvents(); + + [[nodiscard]] static QList getRecentEvents(int hours); +}; diff --git a/database/dao/GeneralLogDAO.cpp b/database/dao/GeneralLogDAO.cpp new file mode 100644 index 0000000..0ead40b --- /dev/null +++ b/database/dao/GeneralLogDAO.cpp @@ -0,0 +1,92 @@ +#include "GeneralLogDAO.h" + +#include +#include + +#include "DatabaseManager.h" +#include "time/DateUtils.h" + +bool GeneralLogDAO::insert(const QString &level, const QString &source, const QString &message) { + return insert(level, source, message, {}); +} + +bool GeneralLogDAO::insert(const QString &level, const QString &source, const QString &message, + const QByteArray &screenshot) { + return insert(level, source, message, screenshot, {}, {}); +} + +bool GeneralLogDAO::insert(const QString &level, const QString &source, const QString &message, + const QByteArray &screenshot, const QString &requestBody, + const QString &responseBody) { + QSqlQuery q(DatabaseManager::instance().database()); + q.prepare(R"( + INSERT INTO general_logs (level, source, message, timestamp, screenshot, request_body, response_body) + VALUES (:level, :source, :message, :timestamp, :screenshot, :request_body, :response_body) + )"); + q.bindValue(":level", level); + q.bindValue(":source", source); + q.bindValue(":message", message); + q.bindValue(":timestamp", DateUtils::toUtcString(QDateTime::currentDateTimeUtc())); + q.bindValue(":screenshot", screenshot.isEmpty() ? QVariant(QMetaType(QMetaType::QByteArray)) : screenshot); + q.bindValue(":request_body", requestBody); + q.bindValue(":response_body", responseBody); + return q.exec(); +} + +static QString buildWhereClause(const QStringList &levels, int minutesBack = 0) { + QStringList conditions; + if (!levels.isEmpty()) { + QStringList placeholders; + for (int i = 0; i < levels.size(); ++i) + placeholders << QString(":lvl%1").arg(i); + conditions << "level IN (" + placeholders.join(',') + ")"; + } + if (minutesBack > 0) + conditions << "timestamp >= :sinceTime"; + if (conditions.isEmpty()) return {}; + return " WHERE " + conditions.join(" AND "); +} + +static void bindLevels(QSqlQuery &q, const QStringList &levels) { + for (int i = 0; i < levels.size(); ++i) + q.bindValue(QString(":lvl%1").arg(i), levels[i]); +} + +QList GeneralLogDAO::getLogs(int page, int pageSize, const QStringList &levels, + int minutesBack) { + QList result; + QSqlQuery q(DatabaseManager::instance().database()); + q.prepare( + "SELECT id, level, source, message, timestamp, screenshot, request_body, response_body FROM general_logs" + + buildWhereClause(levels, minutesBack) + + " ORDER BY id DESC LIMIT :limit OFFSET :offset" + ); + bindLevels(q, levels); + if (minutesBack > 0) + q.bindValue(":sinceTime", DateUtils::toUtcString( + QDateTime::currentDateTimeUtc().addSecs(-minutesBack * 60))); + q.bindValue(":limit", pageSize); + q.bindValue(":offset", page * pageSize); + if (!q.exec()) return result; + while (q.next()) { + GeneralLogEntry e; + e.id = q.value("id").toInt(); + e.level = q.value("level").toString(); + e.source = q.value("source").toString(); + e.message = q.value("message").toString(); + e.timestamp = DateUtils::fromUtcString(q.value("timestamp").toString()); + e.screenshot = q.value("screenshot").toByteArray(); + e.requestBody = q.value("request_body").toString(); + e.responseBody = q.value("response_body").toString(); + result.append(e); + } + return result; +} + +int GeneralLogDAO::getTotalCount(const QStringList &levels) { + QSqlQuery q(DatabaseManager::instance().database()); + q.prepare("SELECT COUNT(*) FROM general_logs" + buildWhereClause(levels)); + bindLevels(q, levels); + if (!q.exec() || !q.next()) return 0; + return q.value(0).toInt(); +} diff --git a/database/dao/GeneralLogDAO.h b/database/dao/GeneralLogDAO.h new file mode 100644 index 0000000..f708bbb --- /dev/null +++ b/database/dao/GeneralLogDAO.h @@ -0,0 +1,31 @@ +#pragma once +#include +#include +#include +#include + +struct GeneralLogEntry { + int id = -1; + QString level; + QString source; + QString message; + QDateTime timestamp; + QByteArray screenshot; + QString requestBody; + QString responseBody; +}; + +class GeneralLogDAO { +public: + static bool insert(const QString &level, const QString &source, const QString &message); + static bool insert(const QString &level, const QString &source, const QString &message, + const QByteArray &screenshot); + static bool insert(const QString &level, const QString &source, const QString &message, + const QByteArray &screenshot, const QString &requestBody, + const QString &responseBody); + + [[nodiscard]] static QList getLogs(int page, int pageSize, + const QStringList &levels = {}, + int minutesBack = 0); + [[nodiscard]] static int getTotalCount(const QStringList &levels = {}); +}; diff --git a/database/dao/MaterialDAO.cpp b/database/dao/MaterialDAO.cpp new file mode 100644 index 0000000..e772136 --- /dev/null +++ b/database/dao/MaterialDAO.cpp @@ -0,0 +1,428 @@ +#include +#include +#include +#include "MaterialDAO.h" + +#include "db/MaterialInfo.h" +#include "DatabaseManager.h" +#include "time/DateUtils.h" + +bool MaterialDAO::upsertAccount(const MaterialInfo &info) { + // Валидация: lastNumbers должен содержать только цифры + static const QRegularExpression digitsOnly(R"(^\d+$)"); + if (!info.lastNumbers.isEmpty() && !digitsOnly.match(info.lastNumbers).hasMatch()) { + qWarning() << "[MaterialDAO] upsertAccount rejected: lastNumbers is not numeric:" + << info.lastNumbers << "appCode:" << info.appCode; + return false; + } + + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + INSERT INTO materials (device_id, app_code, last_numbers, card_number, account_number, amount, update_time, status, description, currency, material_id) + VALUES (:device_id, :app_code, :last_numbers, :card_number, :account_number, :amount, :update_time, :status, :description, :currency, :material_id) + ON CONFLICT(device_id, app_code, last_numbers) + DO UPDATE SET + amount = excluded.amount, + update_time = excluded.update_time, + last_numbers = excluded.last_numbers, + card_number = CASE WHEN excluded.card_number != '' THEN excluded.card_number ELSE materials.card_number END, + account_number = CASE WHEN excluded.account_number != '' THEN excluded.account_number ELSE materials.account_number END, + status = excluded.status, + description = excluded.description, + currency = excluded.currency, + material_id = CASE WHEN excluded.material_id != '' THEN excluded.material_id ELSE materials.material_id END + )"); + + query.bindValue(":device_id", info.deviceId); + query.bindValue(":app_code", info.appCode); + query.bindValue(":last_numbers", info.lastNumbers); + query.bindValue(":card_number", info.cardNumber); + query.bindValue(":account_number", info.accountNumber); + query.bindValue(":currency", info.currency); + query.bindValue(":description", info.description); + query.bindValue(":amount", info.amount); + query.bindValue(":update_time", DateUtils::toUtcString(info.updateTime)); + query.bindValue(":status", materialStatusToString(info.status)); + query.bindValue(":material_id", info.materialId); + + if (!query.exec()) { + qWarning() << "Ошибка при вставке/обновлении account:" << query.lastError().text(); + return false; + } + + return true; +} + +bool MaterialDAO::updateAccountById( + const int id, + const QString &status, + const QString &description, + const QString ¤cy +) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + UPDATE materials + SET status = :status, + description = :description, + currency = :currency + WHERE id = :id + )"); + + query.bindValue(":status", status); + query.bindValue(":description", description); + query.bindValue(":currency", currency); + query.bindValue(":id", id); + + if (!query.exec()) { + qWarning() << "Ошибка при обновлении account по id:" << query.lastError().text(); + return false; + } + + return true; +} + +bool MaterialDAO::updateStatus(const int id, const QString &status) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE materials SET status = :status WHERE id = :id"); + query.bindValue(":status", status); + query.bindValue(":id", id); + if (!query.exec()) { + qWarning() << "[MaterialDAO] updateStatus failed:" << query.lastError().text(); + return false; + } + return true; +} + +bool MaterialDAO::deleteAccount(const int id) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("DELETE FROM materials WHERE id = :id"); + query.bindValue(":id", id); + return query.exec(); +} + +bool MaterialDAO::updateCardNumber(const int accountId, const QString &cardNumber) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + UPDATE materials SET card_number = :card_number WHERE id = :id + )"); + query.bindValue(":card_number", cardNumber); + query.bindValue(":id", accountId); + if (!query.exec()) { + qWarning() << "Ошибка при обновлении card_number:" << query.lastError().text(); + return false; + } + return true; +} + +bool MaterialDAO::updateMaterialId(const int accountId, const QString &materialId) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE materials SET material_id = :material_id WHERE id = :id"); + query.bindValue(":material_id", materialId); + query.bindValue(":id", accountId); + if (!query.exec()) { + qWarning() << "Ошибка при обновлении material_id:" << query.lastError().text(); + return false; + } + return true; +} + +bool MaterialDAO::adjustAmount(const int id, const double delta) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE materials SET amount = amount - :delta WHERE id = :id"); + query.bindValue(":delta", delta); + query.bindValue(":id", id); + if (!query.exec()) { + qWarning() << "[MaterialDAO] adjustAmount failed:" << query.lastError().text(); + return false; + } + return true; +} + +bool MaterialDAO::adjustAmountByApp(const QString &deviceId, const QString &appCode, const double delta) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + UPDATE materials SET amount = amount - :delta + WHERE device_id = :device_id AND app_code = :app_code + )"); + query.bindValue(":delta", delta); + query.bindValue(":device_id", deviceId); + query.bindValue(":app_code", appCode); + if (!query.exec()) { + qWarning() << "[MaterialDAO] adjustAmountByApp failed:" << query.lastError().text(); + return false; + } + return true; +} + +bool MaterialDAO::setAmountByApp(const QString &deviceId, const QString &appCode, const double amount) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + UPDATE materials SET amount = :amount + WHERE device_id = :device_id AND app_code = :app_code + )"); + query.bindValue(":amount", amount); + query.bindValue(":device_id", deviceId); + query.bindValue(":app_code", appCode); + if (!query.exec()) { + qWarning() << "[MaterialDAO] setAmountByApp failed:" << query.lastError().text(); + return false; + } + return true; +} + +MaterialInfo parseAccount(const QSqlQuery &query) { + MaterialInfo acc; + acc.id = query.value("id").toInt(); + acc.deviceId = query.value("device_id").toString(); + acc.appCode = query.value("app_code").toString(); + acc.lastNumbers = query.value("last_numbers").toString(); + acc.cardNumber = query.value("card_number").toString(); + acc.accountNumber = query.value("account_number").toString(); + acc.materialId = query.value("material_id").toString(); + acc.amount = query.value("amount").toDouble(); + acc.updateTime = DateUtils::fromUtcString(query.value("update_time").toString()); + acc.status = materialStatusFromString(query.value("status").toString()); + acc.description = query.value("description").toString(); + acc.currency = query.value("currency").toString(); + return acc; +} + +QList MaterialDAO::activateExclusive( + const QString &deviceId, const QString &appCode, const int targetId) { + QList deactivated; + QSqlDatabase db = DatabaseManager::instance().database(); + + db.transaction(); + + QSqlQuery sel(db); + sel.prepare(R"( + SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency + FROM materials + WHERE device_id = :device_id AND app_code = :app_code + AND status = 'active' AND id != :target_id + )"); + sel.bindValue(":device_id", deviceId); + sel.bindValue(":app_code", appCode); + sel.bindValue(":target_id", targetId); + if (sel.exec()) { + while (sel.next()) deactivated.append(parseAccount(sel)); + } else { + qWarning() << "[MaterialDAO] activateExclusive select failed:" << sel.lastError().text(); + db.rollback(); + return {}; + } + + QSqlQuery off(db); + off.prepare(R"( + UPDATE materials SET status = 'off' + WHERE device_id = :device_id AND app_code = :app_code AND id != :target_id + )"); + off.bindValue(":device_id", deviceId); + off.bindValue(":app_code", appCode); + off.bindValue(":target_id", targetId); + if (!off.exec()) { + qWarning() << "[MaterialDAO] activateExclusive off-update failed:" << off.lastError().text(); + db.rollback(); + return {}; + } + + QSqlQuery on(db); + on.prepare("UPDATE materials SET status = 'active' WHERE id = :id"); + on.bindValue(":id", targetId); + if (!on.exec()) { + qWarning() << "[MaterialDAO] activateExclusive on-update failed:" << on.lastError().text(); + db.rollback(); + return {}; + } + + db.commit(); + return deactivated; +} + +QList MaterialDAO::getAccounts(const QString &deviceId, const QString &appName) { + QList accounts; + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency + FROM materials + WHERE device_id = :device_id AND app_code = :app_code + ORDER BY id + )"); + + query.bindValue(":device_id", deviceId); + query.bindValue(":app_code", appName); + + if (!query.exec()) { + qWarning() << "Ошибка при получении materials:" << query.lastError().text(); + return accounts; + } + + while (query.next()) { + accounts.append(parseAccount(query)); + } + + return accounts; +} + +MaterialInfo MaterialDAO::getAccountById(const int accountId) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency + FROM materials + WHERE id = :id + LIMIT 1 + )"); + + query.bindValue(":id", accountId); + + if (!query.exec()) { + qWarning() << "Ошибка при получении account по ID:" << query.lastError().text(); + return {}; + } + + if (query.next()) { + return parseAccount(query); + } + return {}; +} + +QList MaterialDAO::getAccountsByAppCode(const QString &appCode) { + QList accounts; + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency + FROM materials + WHERE app_code = :app_code + )"); + + query.bindValue(":app_code", appCode); + + if (!query.exec()) { + qWarning() << "Ошибка при получении materials по app_code:" << query.lastError().text(); + return accounts; + } + + while (query.next()) { + accounts.append(parseAccount(query)); + } + + return accounts; +} + +MaterialInfo MaterialDAO::findAppAccount(const QString &deviceId, const QString &appName, const QString &lastNumber) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency + FROM materials + WHERE app_code = :appName AND last_numbers = :lastNumber AND device_id = :deviceId + LIMIT 1 + )"); + + query.bindValue(":deviceId", deviceId); + query.bindValue(":appName", appName); + query.bindValue(":lastNumber", lastNumber); + + if (!query.exec()) { + qWarning() << "Ошибка при получении account по ID:" << query.lastError().text(); + return {}; + } + + if (query.next()) { + return parseAccount(query); + } + + return {}; +} + +QStringList MaterialDAO::getActiveMaterialIds() { + QStringList ids; + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT a.material_id + FROM materials a + JOIN bank_profiles app ON a.device_id = app.device_id AND a.app_code = app.code + JOIN devices d ON a.device_id = d.id + WHERE app.status = 'active' + AND a.material_id IS NOT NULL AND a.material_id != '' + AND d.status = 'device' AND d.image IS NOT NULL + )"); + + if (!query.exec()) { + qWarning() << "Ошибка при получении active material_ids:" << query.lastError().text(); + return ids; + } + + while (query.next()) { + ids << query.value(0).toString(); + } + return ids; +} + +QStringList MaterialDAO::getActiveMaterialIdsExcludingDevices(const QSet &excludeDevices) { + const QStringList all = getActiveMaterialIds(); + if (excludeDevices.isEmpty()) return all; + + QStringList filtered; + for (const QString &mid : all) { + const MaterialInfo acc = findByMaterialId(mid); + if (!excludeDevices.contains(acc.deviceId)) { + filtered << mid; + } + } + return filtered; +} + +MaterialInfo MaterialDAO::findByMaterialId(const QString &materialId) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + SELECT id, device_id, app_code, last_numbers, card_number, account_number, material_id, amount, update_time, status, description, currency + FROM materials + WHERE material_id = :material_id + LIMIT 1 + )"); + + query.bindValue(":material_id", materialId); + + if (!query.exec()) { + qWarning() << "Ошибка при поиске account по material_id:" << query.lastError().text(); + return {}; + } + + if (query.next()) { + return parseAccount(query); + } + + return {}; +} + +bool MaterialDAO::migrateDeviceId(const QString &oldDeviceId, const QString &newDeviceId) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE materials SET device_id = :new_id WHERE device_id = :old_id"); + query.bindValue(":new_id", newDeviceId); + query.bindValue(":old_id", oldDeviceId); + if (!query.exec()) { + qWarning() << "[MaterialDAO] migrateDeviceId failed:" << query.lastError().text(); + return false; + } + qDebug() << "[MaterialDAO] migrated" << query.numRowsAffected() << "materials from" << oldDeviceId << "to" << newDeviceId; + return true; +} + +bool MaterialDAO::deleteAccountsByApp(const QString &deviceId, const QString &appCode) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("DELETE FROM materials WHERE device_id = :device_id AND app_code = :app_code AND (material_id IS NULL OR material_id = '')"); + query.bindValue(":device_id", deviceId); + query.bindValue(":app_code", appCode); + if (!query.exec()) { + qWarning() << "[MaterialDAO] deleteAccountsByApp failed:" << query.lastError().text(); + return false; + } + return true; +} diff --git a/database/dao/MaterialDAO.h b/database/dao/MaterialDAO.h new file mode 100644 index 0000000..e8b98f1 --- /dev/null +++ b/database/dao/MaterialDAO.h @@ -0,0 +1,62 @@ +#pragma once +#include +#include "db/MaterialInfo.h" + +class MaterialDAO { +public: + [[nodiscard]] static bool upsertAccount(const MaterialInfo &info); + + [[nodiscard]] static bool updateAccountById( + int id, + const QString &status, + const QString &description, + const QString ¤cy + ); + + [[nodiscard]] static bool updateStatus(int id, const QString &status); + + // Атомарно делает targetId единственным активным материалом в (deviceId, appCode). + // Возвращает материалы, которые были переведены в 'off' (для синхронизации с сервером). + [[nodiscard]] static QList activateExclusive( + const QString &deviceId, const QString &appCode, int targetId); + + [[nodiscard]] static bool deleteAccount(int id); + + [[nodiscard]] static bool updateCardNumber(int accountId, const QString &cardNumber); + + [[nodiscard]] static bool updateMaterialId(int accountId, const QString &materialId); + + // Атомарно вычитает delta из materials.amount для одного материала. + // delta > 0 — расход, delta < 0 — пополнение. + [[nodiscard]] static bool adjustAmount(int id, double delta); + + // Атомарно вычитает delta из materials.amount для всех материалов + // в (deviceId, appCode) — для банков с одним счётом на все карты (Dushanbe). + [[nodiscard]] static bool adjustAmountByApp(const QString &deviceId, const QString &appCode, double delta); + + // Перезаписывает materials.amount абсолютным значением для всех материалов + // в (deviceId, appCode). Используется после re-parse экрана. + [[nodiscard]] static bool setAmountByApp(const QString &deviceId, const QString &appCode, double amount); + + [[nodiscard]] static QList getAccounts(const QString &deviceId, const QString &appName); + + [[nodiscard]] static MaterialInfo getAccountById(int accountId); + + [[nodiscard]] static QList getAccountsByAppCode(const QString &appCode); + + [[nodiscard]] static MaterialInfo findAppAccount( + const QString &deviceId, + const QString &appName, + const QString &lastNumber + ); + + [[nodiscard]] static MaterialInfo findByMaterialId(const QString &materialId); + + [[nodiscard]] static QStringList getActiveMaterialIds(); + + [[nodiscard]] static QStringList getActiveMaterialIdsExcludingDevices(const QSet &excludeDevices); + + [[nodiscard]] static bool migrateDeviceId(const QString &oldDeviceId, const QString &newDeviceId); + + [[nodiscard]] static bool deleteAccountsByApp(const QString &deviceId, const QString &appCode); +}; diff --git a/database/dao/SettingsDAO.cpp b/database/dao/SettingsDAO.cpp new file mode 100644 index 0000000..6f0b0ce --- /dev/null +++ b/database/dao/SettingsDAO.cpp @@ -0,0 +1,55 @@ +#include "SettingsDAO.h" + +#include +#include +#include + +#include "DatabaseManager.h" + +QString SettingsDAO::get(const QString &key, const QString &defaultValue) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("SELECT value FROM settings WHERE key = :key LIMIT 1"); + query.bindValue(":key", key); + if (!query.exec()) { + qWarning() << "SettingsDAO::get error:" << query.lastError().text(); + return defaultValue; + } + if (query.next()) { + return query.value(0).toString(); + } + return defaultValue; +} + +void SettingsDAO::set(const QString &key, const QString &value) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + INSERT INTO settings (key, value) VALUES (:key, :value) + ON CONFLICT(key) DO UPDATE SET value = excluded.value + )"); + query.bindValue(":key", key); + query.bindValue(":value", value); + if (!query.exec()) { + qWarning() << "SettingsDAO::set error:" << query.lastError().text(); + } +} + +void SettingsDAO::remove(const QString &key) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("DELETE FROM settings WHERE key = :key"); + query.bindValue(":key", key); + if (!query.exec()) { + qWarning() << "SettingsDAO::remove error:" << query.lastError().text(); + } +} + +qint64 SettingsDAO::getLong(const QString &key, qint64 defaultValue) { + const QString val = get(key); + if (val.isEmpty()) return defaultValue; + bool ok; + const qint64 result = val.toLongLong(&ok); + return ok ? result : defaultValue; +} + +void SettingsDAO::setLong(const QString &key, qint64 value) { + set(key, QString::number(value)); +} diff --git a/database/dao/SettingsDAO.h b/database/dao/SettingsDAO.h new file mode 100644 index 0000000..4afb733 --- /dev/null +++ b/database/dao/SettingsDAO.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +class SettingsDAO { +public: + static QString get(const QString &key, const QString &defaultValue = {}); + static void set(const QString &key, const QString &value); + static void remove(const QString &key); + static qint64 getLong(const QString &key, qint64 defaultValue = 0); + static void setLong(const QString &key, qint64 value); +}; diff --git a/database/dao/TransactionDAO.cpp b/database/dao/TransactionDAO.cpp index 31de003..807b148 100644 --- a/database/dao/TransactionDAO.cpp +++ b/database/dao/TransactionDAO.cpp @@ -1,20 +1,34 @@ #include #include +#include #include "DatabaseManager.h" #include "TransactionDAO.h" +#include "time/DateUtils.h" + bool TransactionDAO::insertTransactionIfNotExists(const TransactionInfo &info) { QSqlQuery query(DatabaseManager::instance().database()); - query.prepare(R"( - SELECT COUNT(*) FROM transactions - WHERE account_id = :account_id AND amount = :amount AND timestamp = :timestamp - )"); + if (info.amount < 0) { + // Если исходящая, то мы знаем время timestamp + query.prepare(R"( + SELECT COUNT(*) FROM transactions + WHERE account_id = :account_id AND amount = :amount AND timestamp = :timestamp AND type = :type + )"); + query.bindValue(":timestamp", DateUtils::toUtcString(info.timestamp)); + } else { + query.prepare(R"( + SELECT COUNT(*) FROM transactions + WHERE account_id = :account_id AND amount = :amount AND bank_time = :bank_time AND bank_name = :bank_name + )"); + query.bindValue(":bank_time", info.bankTime); + query.bindValue(":bank_name", info.bankName); + } query.bindValue(":account_id", info.accountId); query.bindValue(":amount", info.amount); - query.bindValue(":timestamp", info.timestamp.toString(Qt::ISODate)); + query.bindValue(":type", transactionTypeToString(info.type)); if (!query.exec() || !query.next()) { qWarning() << "Ошибка при проверке существования transaction:" << query.lastError().text(); @@ -22,18 +36,61 @@ bool TransactionDAO::insertTransactionIfNotExists(const TransactionInfo &info) { } if (query.value(0).toInt() > 0) { + // Обновляем bank_tr_external_id если есть новое значение + if (!info.bankTrExternalId.isEmpty()) { + QSqlQuery upd(DatabaseManager::instance().database()); + if (info.amount < 0) { + upd.prepare(R"( + UPDATE transactions SET bank_tr_external_id = :ext_id + WHERE account_id = :account_id AND amount = :amount AND timestamp = :timestamp AND type = :type + )"); + upd.bindValue(":timestamp", DateUtils::toUtcString(info.timestamp)); + upd.bindValue(":type", transactionTypeToString(info.type)); + } else { + upd.prepare(R"( + UPDATE transactions SET bank_tr_external_id = :ext_id + WHERE account_id = :account_id AND amount = :amount AND bank_time = :bank_time AND bank_name = :bank_name + )"); + upd.bindValue(":bank_time", info.bankTime); + upd.bindValue(":bank_name", info.bankName); + } + upd.bindValue(":ext_id", info.bankTrExternalId); + upd.bindValue(":account_id", info.accountId); + upd.bindValue(":amount", info.amount); + upd.exec(); + } return false; // Уже существует } query.prepare(R"( - INSERT INTO transactions (account_id, amount, description, timestamp) - VALUES (:account_id, :amount, :description, :timestamp) + INSERT INTO transactions ( + account_id, amount, fee, status, phone, type, + description, updated_description, + bank_name, bank_time, name, bank_tr_external_id, + update_time, complete_time, timestamp + ) VALUES ( + :account_id, :amount, :fee, :status, :phone, :type, + :description, :updated_description, + :bank_name, :bank_time, :name, :bank_tr_external_id, + :update_time, :complete_time, :timestamp + ) )"); query.bindValue(":account_id", info.accountId); query.bindValue(":amount", info.amount); + query.bindValue(":fee", info.fee); + query.bindValue(":status", transactionStatusToString(info.status)); + query.bindValue(":type", transactionTypeToString(info.type)); + query.bindValue(":phone", info.phone); + query.bindValue(":name", info.name); query.bindValue(":description", info.description); - query.bindValue(":timestamp", info.timestamp.toString(Qt::ISODate)); + query.bindValue(":updated_description", info.updatedDescription); + query.bindValue(":bank_name", info.bankName); + query.bindValue(":bank_time", info.bankTime); + query.bindValue(":bank_tr_external_id", info.bankTrExternalId); + query.bindValue(":update_time", DateUtils::toUtcString(info.updateTime)); + query.bindValue(":complete_time", DateUtils::toUtcString(info.completeTime)); + query.bindValue(":timestamp", DateUtils::toUtcString(info.timestamp)); if (!query.exec()) { qWarning() << "Ошибка при вставке transaction:" << query.lastError().text(); @@ -43,6 +100,46 @@ bool TransactionDAO::insertTransactionIfNotExists(const TransactionInfo &info) { return true; } +int TransactionDAO::insertTransaction(const TransactionInfo &info) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + INSERT INTO transactions ( + account_id, amount, fee, status, phone, type, + description, updated_description, + bank_name, bank_time, name, bank_tr_external_id, + update_time, complete_time + ) VALUES ( + :account_id, :amount, :fee, :status, :phone, :type, + :description, :updated_description, + :bank_name, :bank_time, :name, :bank_tr_external_id, + :update_time, :complete_time + ) + )"); + + query.bindValue(":account_id", info.accountId); + query.bindValue(":amount", info.amount); + query.bindValue(":fee", info.fee); + query.bindValue(":status", transactionStatusToString(info.status)); + query.bindValue(":type", transactionTypeToString(info.type)); + query.bindValue(":phone", info.phone); + query.bindValue(":name", info.name); + query.bindValue(":description", info.description); + query.bindValue(":updated_description", info.updatedDescription); + query.bindValue(":bank_name", info.bankName); + query.bindValue(":bank_time", info.bankTime); + query.bindValue(":bank_tr_external_id", info.bankTrExternalId); + query.bindValue(":update_time", DateUtils::toUtcString(info.updateTime)); + query.bindValue(":complete_time", DateUtils::toUtcString(info.completeTime)); + + if (!query.exec()) { + qWarning() << "Ошибка при вставке transaction:" << query.lastError().text(); + return -1; + } + + return query.lastInsertId().toInt(); +} + bool TransactionDAO::deleteTransaction(const int id) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare("DELETE FROM transactions WHERE id = :id"); @@ -50,13 +147,36 @@ bool TransactionDAO::deleteTransaction(const int id) { return query.exec(); } + +TransactionInfo parseTransaction(const QSqlQuery &query) { + TransactionInfo tx; + tx.id = query.value("id").toInt(); + tx.accountId = query.value("account_id").toInt(); + tx.amount = query.value("amount").toDouble(); + tx.fee = query.value("fee").toDouble(); + tx.status = transactionStatusFromString(query.value("status").toString()); + tx.type = transactionTypeFromString(query.value("type").toString()); + tx.phone = query.value("phone").toString(); + tx.name = query.value("name").toString(); + tx.description = query.value("description").toString(); + tx.updatedDescription = query.value("updated_description").toString(); + tx.bankName = query.value("bank_name").toString(); + tx.bankTime = query.value("bank_time").toString(); + tx.bankTrExternalId = query.value("bank_tr_external_id").toString(); + tx.updateTime = DateUtils::fromUtcString(query.value("update_time").toString()); + tx.completeTime = DateUtils::fromUtcString(query.value("complete_time").toString()); + tx.timestamp = DateUtils::fromUtcString(query.value("timestamp").toString()); + tx.receiptImage = query.value("receipt_image").toByteArray(); + return tx; +} + + QList TransactionDAO::getTransactions(const int accountId) { QList list; QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - SELECT id, account_id, amount, description, timestamp - FROM transactions + SELECT * FROM transactions WHERE account_id = :account_id ORDER BY timestamp DESC )"); @@ -69,14 +189,254 @@ QList TransactionDAO::getTransactions(const int accountId) { } while (query.next()) { - TransactionInfo tx; - tx.id = query.value("id").toInt(); - tx.accountId = query.value("account_id").toInt(); - tx.amount = query.value("amount").toDouble(); - tx.description = query.value("description").toString(); - tx.timestamp = QDateTime::fromString(query.value("timestamp").toString(), Qt::ISODate); - list.append(tx); + list.append(parseTransaction(query)); } return list; } + +bool TransactionDAO::updateTransaction(const TransactionInfo &info) { + QSqlQuery query(DatabaseManager::instance().database()); + + qDebug().noquote().nospace() << convertTransactionToString(info); + + query.prepare(R"( + UPDATE transactions SET + amount = :amount, + fee = :fee, + status = :status, + type = :type, + phone = :phone, + description = :description, + updated_description = :updated_description, + bank_name = :bank_name, + bank_time = :bank_time, + name = :name, + bank_tr_external_id = :bank_tr_external_id, + update_time = :update_time, + complete_time = :complete_time, + timestamp = :timestamp + WHERE id = :id + )"); + + query.bindValue(":id", info.id); + query.bindValue(":amount", info.amount); + query.bindValue(":fee", info.fee); + query.bindValue(":status", transactionStatusToString(info.status)); + query.bindValue(":type", transactionTypeToString(info.type)); + query.bindValue(":phone", info.phone); + query.bindValue(":name", info.name); + query.bindValue(":description", info.description); + query.bindValue(":updated_description", info.updatedDescription); + query.bindValue(":bank_name", info.bankName); + query.bindValue(":bank_time", info.bankTime); + query.bindValue(":bank_tr_external_id", info.bankTrExternalId); + query.bindValue(":update_time", DateUtils::toUtcString(info.updateTime)); + query.bindValue(":complete_time", DateUtils::toUtcString(info.completeTime)); + query.bindValue(":timestamp", DateUtils::toUtcString(info.timestamp)); + + if (!query.exec()) { + qWarning() << "Ошибка при обновлении transaction:" << query.lastError().text(); + return false; + } + + return true; +} + +bool TransactionDAO::updateTransactionStatus( + const int id, + const TransactionStatus status +) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + UPDATE transactions SET + status = :status, + update_time = :update_time, + complete_time = :complete_time + WHERE id = :id + )"); + + // const QString currentTime = now.toString("yyyy-MM-dd HH:mm:ss"); + query.bindValue(":id", id); + query.bindValue(":status", transactionStatusToString(status)); + query.bindValue(":update_time", DateUtils::toUtcString(DateUtils::getUtcNow())); + + if (status == TransactionStatus::Complete) { + query.bindValue(":complete_time", DateUtils::toUtcString(DateUtils::getUtcNow())); + } else { + query.bindValue(":complete_time", QVariant(QMetaType(QMetaType::QString))); + } + + if (!query.exec()) { + qWarning() << "Ошибка при обновлении transaction:" << query.lastError().text(); + return false; + } + return true; +} + +TransactionInfo TransactionDAO::getTransactionById(const int id) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("SELECT * FROM transactions WHERE id = :id"); + query.bindValue(":id", id); + + + if (!query.exec() || !query.next()) { + qWarning() << "Ошибка или не найден transaction с id:" << id << query.lastError().text(); + return {}; + } + return parseTransaction(query); +} + +QList TransactionDAO::getTransactionsWithinHours(const int accountId, const int hours) { + QList list; + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT * FROM transactions + WHERE account_id = :accountId + AND timestamp >= datetime('now', '-' || :hours || ' hours') + )"); + query.bindValue(":accountId", accountId); + query.bindValue(":hours", hours); + + if (!query.exec()) { + qWarning() << "Ошибка получения транзакций: " << query.lastError().text(); + return {}; + } + + while (query.next()) { + list.append(parseTransaction(query)); + } + return list; +} + +QList TransactionDAO::getLastTransactions(const int accountId, const int limit) { + QList list; + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT * FROM transactions + WHERE account_id = :accountId + ORDER BY timestamp DESC + LIMIT :limit + )"); + query.bindValue(":accountId", accountId); + query.bindValue(":limit", limit); + + if (!query.exec()) { + qWarning() << "Ошибка получения последних транзакций: " << query.lastError().text(); + return {}; + } + + while (query.next()) { + list.append(parseTransaction(query)); + } + return list; +} + +bool TransactionDAO::updateTransaction( + const int id, + const TransactionStatus status, + const QString &oldDesc, + const QString &newDesc +) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + UPDATE transactions SET + status = :status, + update_time = :update_time, + complete_time = :complete_time, + description = :description, + updated_description = :updated_description + WHERE id = :id + )"); + + query.bindValue(":id", id); + query.bindValue(":status", transactionStatusToString(status)); + query.bindValue(":update_time", DateUtils::toUtcString(DateUtils::getUtcNow())); + + if (oldDesc.isEmpty() || oldDesc != newDesc) { + query.bindValue(":description", newDesc); + query.bindValue(":updated_description", oldDesc); + } else { + query.bindValue(":description", newDesc); + query.bindValue(":updated_description", ""); + } + + if (status == TransactionStatus::Complete) { + query.bindValue(":complete_time", DateUtils::toUtcString(DateUtils::getUtcNow())); + } else { + query.bindValue(":complete_time", QVariant(QMetaType(QMetaType::QString))); + } + + if (!query.exec()) { + qWarning() << "Ошибка при обновлении transaction:" << query.lastError().text(); + return false; + } + return true; +} + +bool TransactionDAO::updateExternalTransactionId(const int id, const int externalId) { + QSqlQuery query(DatabaseManager::instance().database()); + + query.prepare(R"( + UPDATE transactions SET + external_id = :external_id + WHERE id = :id + )"); + + query.bindValue(":id", id); + query.bindValue(":external_id", externalId); + + if (!query.exec()) { + qWarning() << "Ошибка при обновлении external_id:" << query.lastError().text(); + return false; + } + return true; +} + +bool TransactionDAO::updateBankTrExternalId(const int id, const QString &bankTrExternalId) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE transactions SET bank_tr_external_id = :val WHERE id = :id"); + query.bindValue(":id", id); + query.bindValue(":val", bankTrExternalId); + if (!query.exec()) { + qWarning() << "Ошибка при обновлении bank_tr_external_id:" << query.lastError().text(); + return false; + } + return true; +} + +bool TransactionDAO::updateReceiptImage(const int id, const QByteArray &image) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare("UPDATE transactions SET receipt_image = :image WHERE id = :id"); + query.bindValue(":id", id); + query.bindValue(":image", image); + + if (!query.exec()) { + qWarning() << "Ошибка при сохранении receipt_image:" << query.lastError().text(); + return false; + } + return true; +} + +TransactionInfo TransactionDAO::findByBankTimeAndAmount( + const int accountId, const QString &bankName, const QString &bankTime, const double amount +) { + QSqlQuery query(DatabaseManager::instance().database()); + query.prepare(R"( + SELECT * FROM transactions + WHERE account_id = :account_id AND bank_name = :bank_name + AND bank_time = :bank_time AND amount = :amount + LIMIT 1 + )"); + query.bindValue(":account_id", accountId); + query.bindValue(":bank_name", bankName); + query.bindValue(":bank_time", bankTime); + query.bindValue(":amount", amount); + + if (!query.exec() || !query.next()) { + return {}; + } + return parseTransaction(query); +} diff --git a/database/dao/TransactionDAO.h b/database/dao/TransactionDAO.h index f8b5d36..b3a5adf 100644 --- a/database/dao/TransactionDAO.h +++ b/database/dao/TransactionDAO.h @@ -5,7 +5,35 @@ class TransactionDAO { public: [[nodiscard]] static bool insertTransactionIfNotExists(const TransactionInfo &info); + [[nodiscard]] static int insertTransaction(const TransactionInfo &info); + [[nodiscard]] static bool deleteTransaction(int id); [[nodiscard]] static QList getTransactions(int accountId); + + [[nodiscard]] static bool updateTransaction(const TransactionInfo &info); + + [[nodiscard]] static bool updateTransactionStatus(int id, TransactionStatus status); + + [[nodiscard]] static TransactionInfo getTransactionById(int id); + + [[nodiscard]] static QList getTransactionsWithinHours(int accountId, int hours); + + [[nodiscard]] static QList getLastTransactions(int accountId, int limit); + + [[nodiscard]] static bool updateTransaction( + int id, TransactionStatus status, + const QString &oldDesc, + const QString &newDesc + ); + + [[nodiscard]] static bool updateExternalTransactionId(int id, int externalId); + + [[nodiscard]] static bool updateBankTrExternalId(int id, const QString &bankTrExternalId); + + [[nodiscard]] static bool updateReceiptImage(int id, const QByteArray &image); + + // Найти транзакцию по bank_name + bank_time + amount + [[nodiscard]] static TransactionInfo findByBankTimeAndAmount( + int accountId, const QString &bankName, const QString &bankTime, double amount); }; diff --git a/database/time/DateUtils.cpp b/database/time/DateUtils.cpp new file mode 100644 index 0000000..564313b --- /dev/null +++ b/database/time/DateUtils.cpp @@ -0,0 +1,25 @@ +#include "DateUtils.h" + +DateUtils::DateUtils(QObject *parent) : QObject(parent) { +} + +QDateTime DateUtils::getMoscowNow() { + const QTimeZone moscowTimeZone("Europe/Moscow"); + return QDateTime::currentDateTime().toTimeZone(moscowTimeZone); +} + +QDateTime DateUtils::getUtcNow() { + return QDateTime::currentDateTimeUtc(); +} + +QDateTime DateUtils::fromUtcString(const QString ×tamp) { + QDateTime dateTime = QDateTime::fromString(timestamp, "yyyy-MM-dd HH:mm:ss"); + if (dateTime.isValid()) { + dateTime.setTimeZone(QTimeZone::utc()); + } + return dateTime; +} + +QString DateUtils::toUtcString(const QDateTime &dateTime) { + return dateTime.toString("yyyy-MM-dd HH:mm:ss"); +} \ No newline at end of file diff --git a/database/time/DateUtils.h b/database/time/DateUtils.h new file mode 100644 index 0000000..40d38c7 --- /dev/null +++ b/database/time/DateUtils.h @@ -0,0 +1,17 @@ +#pragma once +#include + +class DateUtils final : public QObject { + Q_OBJECT + +public: + explicit DateUtils(QObject *parent = nullptr); + + static QDateTime getMoscowNow(); + + static QDateTime getUtcNow(); + + static QDateTime fromUtcString(const QString ×tamp); + + static QString toUtcString(const QDateTime &dateTime); +}; diff --git a/docs/api/auth.md b/docs/api/auth.md new file mode 100644 index 0000000..32b58a3 --- /dev/null +++ b/docs/api/auth.md @@ -0,0 +1,65 @@ +# Auth API + +[← Главная](../index.md) + +Авторизация через API-ключ и управление токенами. + +Все защищённые эндпоинты требуют заголовок: +``` +Authorization: Bearer +``` + +--- + +## POST `/api/v1/auth/login` + +Получение токена по API-ключу. + +**Тело запроса:** +```json +{ + "api_key": "string" +} +``` + +**Ответ:** +```json +{ + "success": true, + "data": { + "id": "uuid", + "access_token": "string", + "refresh_token": "string", + "expires_in": 3600.0 + } +} +``` + +--- + +## POST `/api/v1/auth/refresh_token` + +Обновление access_token по refresh_token. + +**Тело запроса:** +```json +{ + "refresh_token": "string" +} +``` + +**Ответ:** аналогичен `/login`. + +--- + +## Хранение токенов в проекте + +Токены сохраняются в `config.ini`: + +| Ключ | Значение | +|------|----------| +| `common/access_token` | Bearer-токен для запросов | +| `common/refresh_token` | Для обновления | +| `common/token_expires_at` | Unix timestamp истечения | + +Рефреш токена происходит автоматически за 30 минут до истечения — см. `NetworkService::scheduleNextRefresh()`. diff --git a/docs/api/device.md b/docs/api/device.md new file mode 100644 index 0000000..0dd3466 --- /dev/null +++ b/docs/api/device.md @@ -0,0 +1,111 @@ +# Device & Desktop API + +[← Главная](../index.md) + +Управление десктопами (установками приложения) и устройствами (подключёнными Android телефонами). + +--- + +## Desktop + +Desktop — это одна установка ARCDeskProject. Каждый desktop содержит список устройств. + +### GET `/api/v1/desktop/` 🔒 + +Получить все десктопы профиля. + +### POST `/api/v1/desktop/` 🔒 + +Зарегистрировать новый десктоп. + +**Тело запроса:** +```json +{ + "blue_token": "string", + "name": "Мой компьютер", + "contact": "string", + "app_version": "1.0.0" +} +``` + +**Ответ** содержит `id` (UUID) и `unique_name`. + +### PATCH `/api/v1/desktop/{desktop_id}` 🔒 + +Обновить данные десктопа (версию, session_time и т.д.). + +--- + +## Device + +Device — физическое Android-устройство, подключённое к десктопу через ADB. + +### GET `/api/v1/device/{desktop_id}` 🔒 + +Получить все устройства десктопа. + +### POST `/api/v1/device/` 🔒 + +Зарегистрировать новое устройство. + +**Тело запроса:** +```json +{ + "desktop_id": "uuid", + "status": "ONLINE", + "name": "Pixel 6", + "screen_width": 1080, + "screen_height": 2400, + "battery": 85 +} +``` + +**`DeviceStatus`:** `"ONLINE"` | `"OFFLINE"` + +**Ответ** содержит `id` (UUID) — сохраняется как `api_id` в таблице `devices`. + +### PATCH `/api/v1/device/{device_id}` 🔒 + +Обновить статус, battery, update_time устройства. + +```json +{ + "status": "ONLINE", + "battery": 72, + "update_time": 1700000000 +} +``` + +### POST `/api/v1/device/add_screenshot/{device_id}` 🔒 + +Загрузить скриншот экрана устройства. + +**Тип запроса:** `multipart/form-data` + +| Поле | Тип | Описание | +|------|-----|----------| +| `file` | binary | JPEG-скриншот | + +**Ответ:** +```json +{ + "success": true, + "data": { + "id": "uuid", + "device_id": "uuid", + "screenshot_url": "https://...", + "taken_at": "2024-01-01T12:00:00" + } +} +``` + +--- + +## Связь с кодом проекта + +| API | Метод в коде | +|-----|-------------| +| `POST /api/v1/desktop/` | `NetworkService::loginAndSetup()` | +| `POST /api/v1/device/` | `NetworkService::createDevice()` | +| `PATCH /api/v1/device/{id}` | `NetworkService::updateDevice()` | +| `POST /api/v1/device/add_screenshot/{id}` | `NetworkService::postDeviceScreenshot()` | diff --git a/docs/api/network.md b/docs/api/network.md new file mode 100644 index 0000000..b02017d --- /dev/null +++ b/docs/api/network.md @@ -0,0 +1,155 @@ +# NetworkService — полная карта API-вызовов + +[← Главная](../index.md) + +Все HTTP-вызовы, выполняемые приложением. Класс `NetworkService` (`services/net/NetworkService.h/cpp`). + +--- + +## Аутентификация + +| Метод | Путь | Слот | Описание | +|-------|------|------|----------| +| POST | `/api/v1/auth/login` | `loginAndSetup()` | Вход по `api_key`, получение токенов | +| POST | `/api/v1/auth/refresh_token` | `refreshToken()` (private) | Обновление access_token | + +Токены сохраняются в `config.ini`. Авто-рефреш за 30 минут до истечения — `scheduleNextRefresh()`. + +--- + +## Desktop + +| Метод | Путь | Слот | Описание | +|-------|------|------|----------| +| POST | `/api/v1/desktop/` | `loginAndSetup()` | Создание desktop при первом запуске (если `desktop_id` не сохранён) | + +Ответ: сохраняет `desktop_id` и `profile_id` в `config.ini`. + +--- + +## Device + +| Метод | Путь | Слот | Описание | +|-------|------|------|----------| +| POST | `/api/v1/device/` | `createDevice()` | Регистрация нового Android устройства | +| PATCH | `/api/v1/device/{api_id}` | `updateDevice()` | Обновление статуса/батареи устройства | +| POST | `/api/v1/device/add_screenshot/{api_id}` | `postDeviceScreenshot()` | Загрузка скриншота (`multipart/form-data`, `image/jpeg`) | + +`api_id` — UUID, получаемый при создании и сохраняемый в таблице `devices`. + +--- + +## Profile / Material + +| Метод | Путь | Слот | Описание | +|-------|------|------|----------| +| POST | `/api/v1/profile/bank_profile` | `postBankProfile()` | Создать банк-профиль | +| PATCH | `/api/v1/profile/bank_profile/{id}` | `patchBankProfile()` | Обновить банк-профиль | +| POST | `/api/v1/profile/material` | `postMaterial()` | Создать материал (карту) | + +`postBankProfile` сохраняет `bank_profile_id` в таблице `applications`. +`postMaterial` сохраняет `material_id` (UUID) в таблице `accounts`. + +--- + +## Tasks + +| Метод | Путь | Слот | Auth | Описание | +|-------|------|------|------|----------| +| GET | `/api/v1/task/get_tasks` | `getTasks()` | Bearer | Получить очередь задач (поллинг каждые 5 сек) | +| POST | `/api/v1/task/task_result` | `postTaskResult()` | Bearer | Отправить результат задачи | + +`getTasks()` — асинхронный (без QEventLoop), использует сигнал `tasksReceived(QJsonArray)`. +`postTaskResult()` — вызывается из `sendTaskResult()` в `EventHandler` в отдельном QThread. + +--- + +## Устаревшие эндпоинты (legacy) + +Используют старый `multipart/form-data` API с токеном в теле. Пути задаются в `config.ini` напрямую как полные URL. + +| Слот | Ключ в config.ini | Статус | +|------|-------------------|--------| +| `postEventStatus()` | `net/eventStatusUpdate` | ⚠️ устарел, используется в `updateEventThread` | +| `postAppStatus()` | `net/appStatusUpdate` | используется при смене статуса приложения | +| `insertTransactionData()` | `net/transactionInsert` | создание транзакции | +| `updateTransactionData()` | `net/transactionUpdate` | обновление транзакции | +| `getPayments()` | `net/paymentsGet` | ⚠️ старый поллинг задач (заменён на `getTasks`) | + +--- + +## Конфигурация путей (`config.ini`) + +```ini +[net] +apiBase = http://localhost:9999 +pathAuthLogin = /api/v1/auth/login +pathAuthRefresh = /api/v1/auth/refresh_token +pathDesktopCreate = /api/v1/desktop/ +pathDevice = /api/v1/device/ +pathDeviceScreenshot = /api/v1/device/add_screenshot/ +pathBankProfile = /api/v1/profile/bank_profile +pathMaterial = /api/v1/profile/material +pathGetTasks = /api/v1/task/get_tasks +pathTaskResult = /api/v1/task/task_result +``` + +Если ключ отсутствует — используется значение по умолчанию (захардкожено в конструкторе `NetworkService`). + +--- + +## Авторизация в запросах + +- **Bearer** — `Authorization: Bearer ` из `config.ini [common/access_token]` +- **Legacy** — поле `token` в multipart-теле (из `config.ini [common/token]`) + +--- + +## Таймауты и ретраи + +Каждый синхронный запрос крутится в локальном `QEventLoop` под охраной `QTimer`. Если таймер сработал раньше, чем `reply->finished` — это **client timeout**: запрос аборитится и повторяется заново. + +### Параметры + +| Параметр | Значение | Где | +|----------|----------|-----| +| Лимит ожидания | **30 секунд** | `kDefaultTimeoutMs` — все запросы | +| Лимит ожидания (dump) | **120 секунд** | `kDumpTimeoutMs` — только `postMonitoringDump` (большой zip-архив) | +| Макс. попыток | **3** | `kMaxRequestAttempts` — 1 основная + 2 повтора | +| Пауза между попытками | **0** | Ретрай идёт сразу после abort | + +Константы и helper `awaitReply()` — в анонимном namespace в `services/net/NetworkService.cpp`. + +### Когда ретраим + +Ретрай срабатывает **только** при client timeout (наш `QTimer` сработал, reply не успел завершиться). На любых других исходах ретрая нет: + +| Исход | Ретрай? | Почему | +|-------|---------|--------| +| Client timeout (наш QTimer) | ✅ да | сеть моргнула — стоит попробовать | +| HTTP 4xx (401/404/409/…) | ❌ нет | детерминированная ошибка, повтор не поможет | +| HTTP 5xx | ❌ нет | сервер ответил — это бизнес-сбой, не транспортный | +| `ConnectionRefused` / DNS / SSL | ❌ нет | повтор не починит инфраструктурную проблему | +| `success: false` в JSON | ❌ нет | прикладная ошибка | + +После каждой неудачной попытки в лог пишется `Network timeout (POST) attempt N/3:
`; если все 3 попытки таймаутнулись — финальная строка `Timeout (POST) after 3 attempts:
` плюс запись в `GeneralLogDAO` (для `postJson`/`patchJson`). + +### Покрытие методов + +| Метод | Ретрай | Примечание | +|-------|--------|------------| +| `postJson` | ✅ | | +| `patchJson` | ✅ | | +| `getJson` | ✅ | | +| `deleteJson` | ✅ | | +| `get(path, params)` | ✅ | используется `getPayments()` (legacy) | +| `syncBanksIfEmpty()` инлайн-GET | ✅ | | +| `postDeviceScreenshot()` | ✅ | multipart пересобирается на каждой попытке | +| `postMonitoringLog()` | ✅ | multipart пересобирается на каждой попытке | +| `postMonitoringDump()` | ✅ | timeout 120s; multipart пересобирается | +| `post(QHttpMultiPart*, path)` | ❌ | multipart передаётся снаружи и удаляется вместе с `reply`; колл-сайты (`postEventStatus`/`insertTransactionData`/…) — legacy | +| `getTasks()` | ❌ | асинхронный (без `QEventLoop`); таймаут просто аборитит reply | + +### Идемпотентность + +⚠️ POST-запросы, создающие ресурсы (`/desktop/`, `/device/`, `/profile/bank_profile`, `/profile/material`), при ретрае могут создать дубль, если первая попытка дошла до сервера, но ответ потерялся. Сейчас защиты от этого нет — полагаемся на то, что 3 timeout-а подряд это редкая ситуация. Если станет проблемой — добавлять идемпотентные ключи на стороне бэкенда. diff --git a/docs/api/openapi.json b/docs/api/openapi.json new file mode 100644 index 0000000..cd755b5 --- /dev/null +++ b/docs/api/openapi.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"Adb Core Backend","version":"1.0.0"},"paths":{"/api/v1/auth/login":{"post":{"summary":"Login","operationId":"login_api_v1_auth_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequestData"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_LoginResponseData_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/refresh_token":{"post":{"summary":"Login","operationId":"login_api_v1_auth_refresh_token_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenData"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_LoginResponseData_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/profile/current_profile":{"get":{"summary":"Current Profile","operationId":"current_profile_api_v1_profile_current_profile_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_ProfileResponse_"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/profile/fetch_bank_profiles":{"post":{"summary":"Fetch Bank Profiles","operationId":"fetch_bank_profiles_api_v1_profile_fetch_bank_profiles_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_list_BankProfileResponse__"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/profile/fetch_materials":{"post":{"summary":"Fetch Materials","operationId":"fetch_materials_api_v1_profile_fetch_materials_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_list_MaterialResponseModel__"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/profile/bank_profile":{"post":{"summary":"Create Bank Profile","operationId":"create_bank_profile_api_v1_profile_bank_profile_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBankProfile"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_BankProfileResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/profile/bank_profile/{bank_profile_id}":{"patch":{"summary":"Update Bank Profile","operationId":"update_bank_profile_api_v1_profile_bank_profile__bank_profile_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bank_profile_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Bank Profile Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBankProfileModel"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_BankProfileModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Bank Profile","operationId":"delete_bank_profile_api_v1_profile_bank_profile__bank_profile_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bank_profile_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Bank Profile Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/profile/bank_profile/{bank_profile_id}/{is_enable}":{"patch":{"summary":"Unable Or Disable Bank Profile","operationId":"unable_or_disable_bank_profile_api_v1_profile_bank_profile__bank_profile_id___is_enable__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bank_profile_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Bank Profile Id"}},{"name":"is_enable","in":"path","required":true,"schema":{"type":"boolean","title":"Is Enable"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_BankProfileModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/profile/material":{"post":{"summary":"Create Material","operationId":"create_material_api_v1_profile_material_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMaterialRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_MaterialResponseModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/profile/material/{material_id}":{"patch":{"summary":"Update Material","operationId":"update_material_api_v1_profile_material__material_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"material_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Material Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMaterialData"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_MaterialModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Material","operationId":"delete_material_api_v1_profile_material__material_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"material_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Material Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/profile/material/{material_id}/{is_enable}":{"patch":{"summary":"Unable Or Disable Material","operationId":"unable_or_disable_material_api_v1_profile_material__material_id___is_enable__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"material_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Material Id"}},{"name":"is_enable","in":"path","required":true,"schema":{"type":"boolean","title":"Is Enable"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_MaterialModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/desktop/":{"get":{"summary":"Get All Desktops","operationId":"get_all_desktops_api_v1_desktop__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_list_DesktopResponseModel__"}}}}},"security":[{"HTTPBearer":[]}]},"post":{"summary":"Create Desktop","operationId":"create_desktop_api_v1_desktop__post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDesktopData"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_DesktopResponseModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/desktop/{desktop_id}":{"patch":{"summary":"Update Desktop","operationId":"update_desktop_api_v1_desktop__desktop_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"desktop_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Desktop Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDesktopData"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_DesktopResponseModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Desktop","operationId":"delete_desktop_api_v1_desktop__desktop_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"desktop_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Desktop Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/device/{desktop_id}":{"get":{"summary":"Get All Devices By Desktop Id","operationId":"get_all_devices_by_desktop_id_api_v1_device__desktop_id__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"desktop_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Desktop Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_list_DeviceResponseModel__"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/device/":{"post":{"summary":"Create Desktop","operationId":"create_desktop_api_v1_device__post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceData"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_DeviceResponseModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/device/{device_id}":{"patch":{"summary":"Update Device","operationId":"update_device_api_v1_device__device_id__patch","security":[{"HTTPBearer":[]}],"parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Device Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceData"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_DeviceResponseModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Device","operationId":"delete_device_api_v1_device__device_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Device Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/device/add_screenshot/{device_id}":{"post":{"summary":"Add Screenshot","operationId":"add_screenshot_api_v1_device_add_screenshot__device_id__post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Device Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_add_screenshot_api_v1_device_add_screenshot__device_id__post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_GetDeviceScreenshotModel_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/device/delete_screenshot/{screenshot_id}":{"delete":{"summary":"Delete Screenshot","operationId":"delete_screenshot_api_v1_device_delete_screenshot__screenshot_id__delete","security":[{"HTTPBearer":[]}],"parameters":[{"name":"screenshot_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Screenshot Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/task/add_task":{"post":{"summary":"Add Task","operationId":"add_task_api_v1_task_add_task_post","requestBody":{"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CreateTransactionTask-Input"},{"$ref":"#/components/schemas/FetchTransactionsTask"},{"$ref":"#/components/schemas/FetchProfileTask"}],"title":"Data","discriminator":{"propertyName":"type","mapping":{"CREATE_TRANSACTION":"#/components/schemas/CreateTransactionTask-Input","FETCH_TRANSACTIONS":"#/components/schemas/FetchTransactionsTask","FETCH_PROFILE":"#/components/schemas/FetchProfileTask"}}}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_NoneType_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/task/task_result":{"post":{"summary":"Add Task","operationId":"add_task_api_v1_task_task_result_post","requestBody":{"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CreateTransactionEvent"},{"$ref":"#/components/schemas/FetchTransactionsEvent"},{"$ref":"#/components/schemas/FetchProfileEvent"}],"title":"Data","discriminator":{"propertyName":"type","mapping":{"CREATE_TRANSACTION":"#/components/schemas/CreateTransactionEvent","FETCH_TRANSACTIONS":"#/components/schemas/FetchTransactionsEvent","FETCH_PROFILE":"#/components/schemas/FetchProfileEvent"}}}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_NoneType_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/task/get_tasks":{"get":{"summary":"Get Task","operationId":"get_task_api_v1_task_get_tasks_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"materials_id","in":"query","required":true,"schema":{"type":"array","items":{"type":"string"},"title":"Materials Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_list_Annotated_Union_CreateTransactionTask__FetchTransactionsTask__FetchProfileTask___FieldInfo_annotation_NoneType__required_True__discriminator__type_____"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/task/add_bank":{"post":{"summary":"Add Task","operationId":"add_task_api_v1_task_add_bank_post","parameters":[{"name":"bank_name","in":"query","required":true,"schema":{"type":"string","title":"Bank Name"}},{"name":"webhook_url","in":"query","required":true,"schema":{"type":"string","format":"uri","minLength":1,"maxLength":2083,"title":"Webhook Url"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_NoneType_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/monitoring/log":{"post":{"summary":"Log","operationId":"log_monitoring_log_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_log_monitoring_log_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BaseResponse_BaseLogResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"Account":{"properties":{"bank_account_id":{"type":"string","title":"Bank Account Id"},"currency":{"anyOf":[{"$ref":"#/components/schemas/CurrencyEnum"},{"type":"null"}]},"amount":{"type":"integer","title":"Amount","default":0},"extra":{"additionalProperties":true,"type":"object","title":"Extra","default":{}}},"type":"object","required":["bank_account_id"],"title":"Account"},"BankProfileModel":{"properties":{"id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Id"},"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"middle_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Middle Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"},"bank_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bank Name"},"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number"},"balance":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Balance","default":"0"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"}},"type":"object","title":"BankProfileModel"},"BankProfileResponse":{"properties":{"bank_profile_external_id":{"type":"integer","title":"Bank Profile External Id"},"first_name":{"type":"string","title":"First Name"},"middle_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Middle Name"},"last_name":{"type":"string","title":"Last Name"},"bank_name":{"type":"string","title":"Bank Name"},"phone_number":{"type":"string","title":"Phone Number"},"balance":{"type":"string","title":"Balance"},"state":{"type":"string","title":"State"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"profile_id":{"type":"string","format":"uuid","title":"Profile Id"},"materials":{"anyOf":[{"items":{"$ref":"#/components/schemas/MaterialResponseModel"},"type":"array"},{"type":"null"}],"title":"Materials"},"currency":{"anyOf":[{"$ref":"#/components/schemas/CurrencyEnum"},{"type":"null"}]},"id":{"type":"string","format":"uuid","title":"Id"}},"type":"object","required":["bank_profile_external_id","first_name","last_name","bank_name","phone_number","balance","state","created_at","updated_at","profile_id","id"],"title":"BankProfileResponse"},"BaseLogResponse":{"properties":{},"type":"object","title":"BaseLogResponse"},"BaseResponse":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{},{"type":"null"}],"title":"Data"}},"type":"object","required":["success"],"title":"BaseResponse"},"BaseResponse_BankProfileModel_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/BankProfileModel"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[BankProfileModel]"},"BaseResponse_BankProfileResponse_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/BankProfileResponse"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[BankProfileResponse]"},"BaseResponse_BaseLogResponse_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/BaseLogResponse"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[BaseLogResponse]"},"BaseResponse_DesktopResponseModel_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/DesktopResponseModel"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[DesktopResponseModel]"},"BaseResponse_DeviceResponseModel_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/DeviceResponseModel"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[DeviceResponseModel]"},"BaseResponse_GetDeviceScreenshotModel_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/GetDeviceScreenshotModel"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[GetDeviceScreenshotModel]"},"BaseResponse_LoginResponseData_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/LoginResponseData"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[LoginResponseData]"},"BaseResponse_MaterialModel_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/MaterialModel"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[MaterialModel]"},"BaseResponse_MaterialResponseModel_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/MaterialResponseModel"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[MaterialResponseModel]"},"BaseResponse_NoneType_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"type":"null","title":"Data"}},"type":"object","required":["success"],"title":"BaseResponse[NoneType]"},"BaseResponse_ProfileResponse_":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"$ref":"#/components/schemas/ProfileResponse"},{"type":"null"}]}},"type":"object","required":["success"],"title":"BaseResponse[ProfileResponse]"},"BaseResponse_list_Annotated_Union_CreateTransactionTask__FetchTransactionsTask__FetchProfileTask___FieldInfo_annotation_NoneType__required_True__discriminator__type_____":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"items":{"oneOf":[{"$ref":"#/components/schemas/CreateTransactionTask-Output"},{"$ref":"#/components/schemas/FetchTransactionsTask"},{"$ref":"#/components/schemas/FetchProfileTask"}],"discriminator":{"propertyName":"type","mapping":{"CREATE_TRANSACTION":"#/components/schemas/CreateTransactionTask-Output","FETCH_PROFILE":"#/components/schemas/FetchProfileTask","FETCH_TRANSACTIONS":"#/components/schemas/FetchTransactionsTask"}}},"type":"array"},{"type":"null"}],"title":"Data"}},"type":"object","required":["success"],"title":"BaseResponse[list[Annotated[Union[CreateTransactionTask, FetchTransactionsTask, FetchProfileTask], FieldInfo(annotation=NoneType, required=True, discriminator='type')]]]"},"BaseResponse_list_BankProfileResponse__":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"items":{"$ref":"#/components/schemas/BankProfileResponse"},"type":"array"},{"type":"null"}],"title":"Data"}},"type":"object","required":["success"],"title":"BaseResponse[list[BankProfileResponse]]"},"BaseResponse_list_DesktopResponseModel__":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"items":{"$ref":"#/components/schemas/DesktopResponseModel"},"type":"array"},{"type":"null"}],"title":"Data"}},"type":"object","required":["success"],"title":"BaseResponse[list[DesktopResponseModel]]"},"BaseResponse_list_DeviceResponseModel__":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"items":{"$ref":"#/components/schemas/DeviceResponseModel"},"type":"array"},{"type":"null"}],"title":"Data"}},"type":"object","required":["success"],"title":"BaseResponse[list[DeviceResponseModel]]"},"BaseResponse_list_MaterialResponseModel__":{"properties":{"success":{"type":"boolean","title":"Success"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"code":{"type":"string","title":"Code","default":"SUCCESS"},"data":{"anyOf":[{"items":{"$ref":"#/components/schemas/MaterialResponseModel"},"type":"array"},{"type":"null"}],"title":"Data"}},"type":"object","required":["success"],"title":"BaseResponse[list[MaterialResponseModel]]"},"Body_add_screenshot_api_v1_device_add_screenshot__device_id__post":{"properties":{"file":{"type":"string","format":"binary","title":"File"}},"type":"object","required":["file"],"title":"Body_add_screenshot_api_v1_device_add_screenshot__device_id__post"},"Body_log_monitoring_log_post":{"properties":{"type":{"$ref":"#/components/schemas/LogType"},"event":{"type":"string","title":"Event"},"message":{"type":"string","title":"Message"},"image":{"anyOf":[{"type":"string","format":"binary"},{"type":"string"},{"type":"null"}],"title":"Image"}},"type":"object","required":["type","event","message"],"title":"Body_log_monitoring_log_post"},"CreateBankProfile":{"properties":{"first_name":{"type":"string","title":"First Name"},"middle_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Middle Name"},"last_name":{"type":"string","title":"Last Name"},"bank_name":{"type":"string","title":"Bank Name"},"phone_number":{"type":"string","title":"Phone Number"},"balance":{"type":"string","title":"Balance"},"state":{"type":"string","title":"State"},"currency":{"anyOf":[{"$ref":"#/components/schemas/CurrencyEnum"},{"type":"null"}]}},"type":"object","required":["first_name","last_name","bank_name","phone_number","balance","state","currency"],"title":"CreateBankProfile"},"CreateDesktopData":{"properties":{"blue_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Blue Token"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"contact":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact"},"timestamp":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Timestamp"},"update_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Update Time"},"session_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Session Time"},"app_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"App Version"}},"type":"object","required":["blue_token","name","contact","app_version"],"title":"CreateDesktopData"},"CreateDeviceData":{"properties":{"desktop_id":{"type":"string","format":"uuid","title":"Desktop Id"},"status":{"$ref":"#/components/schemas/DeviceStatus"},"name":{"type":"string","title":"Name"},"timestamp":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Timestamp"},"update_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Update Time"},"screen_width":{"type":"integer","title":"Screen Width"},"screen_height":{"type":"integer","title":"Screen Height"},"battery":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Battery"}},"type":"object","required":["desktop_id","status","name","screen_width","screen_height","battery"],"title":"CreateDeviceData"},"CreateMaterialRequest":{"properties":{"bank_profile_id":{"type":"string","format":"uuid","title":"Bank Profile Id"},"card_number":{"type":"string","title":"Card Number"},"name":{"type":"string","title":"Name"},"state":{"type":"string","title":"State"}},"type":"object","required":["bank_profile_id","card_number","name","state"],"title":"CreateMaterialRequest"},"CreateTransactionBody":{"properties":{"amount":{"type":"integer","title":"Amount"},"currency":{"$ref":"#/components/schemas/CurrencyEnum"},"type":{"$ref":"#/components/schemas/TransactionTypeEnum"},"bank_participator_id":{"type":"string","title":"Bank Participator Id"},"extra":{"additionalProperties":true,"type":"object","title":"Extra"}},"type":"object","required":["amount","currency","type","bank_participator_id"],"title":"CreateTransactionBody"},"CreateTransactionEvent":{"properties":{"type":{"type":"string","const":"CREATE_TRANSACTION","title":"Type"},"material_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"integer"},{"type":"string"}],"title":"Material Id"},"bank_name":{"type":"string","title":"Bank Name"},"data":{"$ref":"#/components/schemas/Transaction"}},"type":"object","required":["type","material_id","bank_name","data"],"title":"CreateTransactionEvent"},"CreateTransactionTask-Input":{"properties":{"bank_name":{"type":"string","title":"Bank Name"},"material_id":{"anyOf":[{"type":"integer"},{"type":"string","format":"uuid"}],"title":"Material Id"},"type":{"type":"string","const":"CREATE_TRANSACTION","title":"Type"},"body":{"$ref":"#/components/schemas/CreateTransactionBody"}},"type":"object","required":["bank_name","material_id","type","body"],"title":"CreateTransactionTask"},"CreateTransactionTask-Output":{"properties":{"bank_name":{"type":"string","title":"Bank Name"},"material_id":{"anyOf":[{"type":"integer"},{"type":"string","format":"uuid"}],"title":"Material Id"},"type":{"type":"string","const":"CREATE_TRANSACTION","title":"Type"},"body":{"$ref":"#/components/schemas/CreateTransactionBody"}},"type":"object","required":["bank_name","material_id","type","body"],"title":"CreateTransactionTask"},"CurrencyEnum":{"type":"integer","enum":[784,971,8,51,532,973,32,36,533,944,977,52,50,975,48,108,60,96,68,984,986,44,64,72,933,84,124,976,947,756,948,990,152,156,170,970,188,931,192,132,203,262,208,214,12,818,232,230,978,242,238,826,981,936,292,270,324,320,328,344,340,191,332,348,360,376,356,368,364,352,388,400,392,404,417,116,174,408,410,414,136,398,418,422,144,430,426,440,434,504,498,969,807,104,496,446,929,480,462,454,484,979,458,943,516,566,558,578,524,554,512,590,604,598,608,586,985,600,634,946,941,643,646,682,90,690,938,752,702,654,694,706,968,728,930,222,760,748,764,972,934,788,776,949,780,901,834,980,800,840,997,998,940,858,860,937,704,548,882,950,961,959,955,956,957,958,951,960,952,964,953,962,994,963,965,999,886,710,967,932],"title":"CurrencyEnum"},"DesktopResponseModel":{"properties":{"blue_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Blue Token"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"contact":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact"},"timestamp":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Timestamp"},"update_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Update Time"},"session_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Session Time"},"app_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"App Version"},"hidden":{"type":"boolean","title":"Hidden","default":false},"profile_id":{"type":"string","format":"uuid","title":"Profile Id"},"id":{"type":"string","format":"uuid","title":"Id"},"unique_name":{"type":"string","title":"Unique Name"},"devices":{"anyOf":[{"items":{"$ref":"#/components/schemas/DeviceResponseModel"},"type":"array"},{"type":"null"}],"title":"Devices","default":[]}},"type":"object","required":["blue_token","name","contact","timestamp","update_time","session_time","app_version","profile_id","id","unique_name"],"title":"DesktopResponseModel"},"DeviceResponseModel":{"properties":{"desktop_id":{"type":"string","format":"uuid","title":"Desktop Id"},"status":{"$ref":"#/components/schemas/DeviceStatus"},"name":{"type":"string","title":"Name"},"timestamp":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Timestamp"},"update_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Update Time"},"screen_width":{"type":"integer","title":"Screen Width"},"screen_height":{"type":"integer","title":"Screen Height"},"battery":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Battery"},"id":{"type":"string","format":"uuid","title":"Id"},"unique_name":{"type":"string","title":"Unique Name"}},"type":"object","required":["desktop_id","status","name","timestamp","update_time","screen_width","screen_height","battery","id","unique_name"],"title":"DeviceResponseModel"},"DeviceStatus":{"type":"string","enum":["ONLINE","OFFLINE"],"title":"DeviceStatus"},"EmptyBody":{"properties":{},"type":"object","title":"EmptyBody"},"FetchProfileEvent":{"properties":{"type":{"type":"string","const":"FETCH_PROFILE","title":"Type"},"material_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"integer"},{"type":"string"}],"title":"Material Id"},"bank_name":{"type":"string","title":"Bank Name"},"data":{"$ref":"#/components/schemas/Account"}},"type":"object","required":["type","material_id","bank_name","data"],"title":"FetchProfileEvent"},"FetchProfileTask":{"properties":{"bank_name":{"type":"string","title":"Bank Name"},"material_id":{"anyOf":[{"type":"integer"},{"type":"string","format":"uuid"}],"title":"Material Id"},"type":{"type":"string","const":"FETCH_PROFILE","title":"Type"},"body":{"$ref":"#/components/schemas/EmptyBody"}},"type":"object","required":["bank_name","material_id","type","body"],"title":"FetchProfileTask"},"FetchTransactionsEvent":{"properties":{"type":{"type":"string","const":"FETCH_TRANSACTIONS","title":"Type"},"material_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"integer"},{"type":"string"}],"title":"Material Id"},"bank_name":{"type":"string","title":"Bank Name"},"data":{"items":{"$ref":"#/components/schemas/Transaction"},"type":"array","title":"Data"}},"type":"object","required":["type","material_id","bank_name","data"],"title":"FetchTransactionsEvent"},"FetchTransactionsRequest":{"properties":{"amount":{"type":"integer","title":"Amount","description":"Transaction amount in kopecks"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["amount","created_at"],"title":"FetchTransactionsRequest"},"FetchTransactionsTask":{"properties":{"bank_name":{"type":"string","title":"Bank Name"},"material_id":{"anyOf":[{"type":"integer"},{"type":"string","format":"uuid"}],"title":"Material Id"},"type":{"type":"string","const":"FETCH_TRANSACTIONS","title":"Type"},"body":{"anyOf":[{"$ref":"#/components/schemas/FetchTransactionsRequest"},{"$ref":"#/components/schemas/EmptyBody"}],"title":"Body"}},"type":"object","required":["bank_name","material_id","type"],"title":"FetchTransactionsTask"},"GetDeviceScreenshotModel":{"properties":{"device_id":{"type":"string","format":"uuid","title":"Device Id"},"screenshot_url":{"type":"string","title":"Screenshot Url"},"taken_at":{"type":"string","format":"date-time","title":"Taken At"},"id":{"type":"string","format":"uuid","title":"Id"}},"type":"object","required":["device_id","screenshot_url","taken_at","id"],"title":"GetDeviceScreenshotModel"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"LogType":{"type":"string","enum":["INFO","WARNING","DEBUG","ERROR"],"title":"LogType"},"LoginRequestData":{"properties":{"api_key":{"type":"string","title":"Api Key"}},"type":"object","required":["api_key"],"title":"LoginRequestData"},"LoginResponseData":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"access_token":{"type":"string","title":"Access Token"},"refresh_token":{"type":"string","title":"Refresh Token"},"expires_in":{"type":"number","title":"Expires In"}},"type":"object","required":["id","access_token","refresh_token","expires_in"],"title":"LoginResponseData"},"MaterialModel":{"properties":{"bank_profile_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Bank Profile Id"},"card_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Card Number"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Id"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},"type":"object","title":"MaterialModel"},"MaterialResponseModel":{"properties":{"external_bank_profile_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"External Bank Profile Id"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"external_material_id":{"type":"integer","title":"External Material Id"},"card_number":{"type":"string","title":"Card Number"},"name":{"type":"string","title":"Name"},"bank_profile_id":{"type":"string","format":"uuid","title":"Bank Profile Id"},"id":{"type":"string","format":"uuid","title":"Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["external_bank_profile_id","state","external_material_id","card_number","name","bank_profile_id","id","created_at","updated_at"],"title":"MaterialResponseModel"},"ProfileResponse":{"properties":{"api_key":{"type":"string","title":"Api Key"},"id":{"type":"string","format":"uuid","title":"Id"},"bank_profiles":{"anyOf":[{"items":{"$ref":"#/components/schemas/BankProfileResponse"},"type":"array"},{"type":"null"}],"title":"Bank Profiles"}},"type":"object","required":["api_key","id","bank_profiles"],"title":"ProfileResponse"},"RefreshTokenData":{"properties":{"refresh_token":{"type":"string","title":"Refresh Token"}},"type":"object","required":["refresh_token"],"title":"RefreshTokenData"},"Transaction":{"properties":{"bank_transaction_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bank Transaction Id"},"amount":{"type":"integer","title":"Amount"},"currency":{"$ref":"#/components/schemas/CurrencyEnum"},"transaction_type":{"$ref":"#/components/schemas/TransactionTypeEnum"},"bank_participator_id":{"type":"string","title":"Bank Participator Id"},"status":{"anyOf":[{"$ref":"#/components/schemas/TransactionStatusEnum"},{"type":"null"}]},"extra":{"additionalProperties":true,"type":"object","title":"Extra","default":{}},"created_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Created At"}},"type":"object","required":["amount","currency","transaction_type","bank_participator_id"],"title":"Transaction"},"TransactionStatusEnum":{"type":"string","enum":["progress","completed","declined"],"title":"TransactionStatusEnum"},"TransactionTypeEnum":{"type":"string","enum":["bank","top-up"],"title":"TransactionTypeEnum"},"UpdateBankProfileModel":{"properties":{"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"middle_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Middle Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"},"bank_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bank Name"},"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number"},"balance":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Balance"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},"type":"object","title":"UpdateBankProfileModel"},"UpdateDesktopData":{"properties":{"blue_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Blue Token"},"update_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Update Time"},"session_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Session Time"},"app_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"App Version"}},"type":"object","title":"UpdateDesktopData"},"UpdateDeviceData":{"properties":{"status":{"$ref":"#/components/schemas/DeviceStatus"},"update_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Update Time"},"battery":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Battery"}},"type":"object","required":["status"],"title":"UpdateDeviceData"},"UpdateMaterialData":{"properties":{"card_number":{"type":"string","title":"Card Number"}},"type":"object","required":["card_number"],"title":"UpdateMaterialData"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}}} \ No newline at end of file diff --git a/docs/api/profile.md b/docs/api/profile.md new file mode 100644 index 0000000..c544704 --- /dev/null +++ b/docs/api/profile.md @@ -0,0 +1,127 @@ +# Profile API + +[← Главная](../index.md) + +Управление профилем, банковскими профилями и материалами (картами). + +--- + +## GET `/api/v1/profile/current_profile` 🔒 + +Получить текущий профиль со всеми банковскими профилями. + +**Ответ:** +```json +{ + "success": true, + "data": { + "id": "uuid", + "api_key": "string", + "bank_profiles": [ + { + "id": "uuid", + "bank_profile_external_id": 1, + "first_name": "string", + "last_name": "string", + "middle_name": "string", + "bank_name": "ozon", + "phone_number": "+7...", + "balance": "15000", + "state": "active", + "currency": 643, + "profile_id": "uuid", + "materials": [ ... ], + "created_at": "2024-01-01T00:00:00", + "updated_at": "2024-01-01T00:00:00" + } + ] + } +} +``` + +--- + +## POST `/api/v1/profile/fetch_bank_profiles` 🔒 + +Получить список всех банковских профилей. + +--- + +## POST `/api/v1/profile/fetch_materials` 🔒 + +Получить список всех материалов (карт). + +--- + +## Bank Profile + +### POST `/api/v1/profile/bank_profile` 🔒 + +Создать банковский профиль. + +**Тело запроса:** +```json +{ + "first_name": "Иван", + "last_name": "Иванов", + "middle_name": "Иванович", + "bank_name": "ozon", + "phone_number": "+79001234567", + "balance": "15000", + "state": "active", + "currency": 643 +} +``` + +**Ответ** содержит `id` (UUID) — сохраняется как `bank_profile_id` в таблице `applications`. + +### PATCH `/api/v1/profile/bank_profile/{bank_profile_id}` 🔒 + +Обновить данные банковского профиля (любые поля опциональны). + +### PATCH `/api/v1/profile/bank_profile/{bank_profile_id}/{is_enable}` 🔒 + +Включить (`true`) / отключить (`false`) банковский профиль. + +--- + +## Material (карта) + +Material — это карта/счёт внутри банковского профиля. + +### POST `/api/v1/profile/material` 🔒 + +Создать материал. + +**Тело запроса:** +```json +{ + "bank_profile_id": "uuid", + "card_number": "1234567890123456", + "name": "Основная карта" +} +``` + +**Ответ** содержит `id` (UUID) и `external_material_id` (int) — сохраняется как `material_id` в таблице `accounts`. + +### PATCH `/api/v1/profile/material/{material_id}` 🔒 + +Обновить номер карты материала. + +```json +{ "card_number": "1234567890123456" } +``` + +### PATCH `/api/v1/profile/material/{material_id}/{is_enable}` 🔒 + +Включить / отключить материал. + +--- + +## Связь с кодом проекта + +| API | Метод в коде | +|-----|-------------| +| `POST /api/v1/profile/bank_profile` | `NetworkService::postBankProfile()` | +| `PATCH /api/v1/profile/bank_profile/{id}` | `NetworkService::patchBankProfile()` | +| `POST /api/v1/profile/material` | `NetworkService::postMaterial()` | diff --git a/docs/api/task.md b/docs/api/task.md new file mode 100644 index 0000000..fb4a7a2 --- /dev/null +++ b/docs/api/task.md @@ -0,0 +1,184 @@ +# Task API + +[← Главная](../index.md) + +Управление задачами: получение очереди, добавление задач, отправка результатов. + +--- + +## GET `/api/v1/task/get_tasks` 🔒 + +Получить список задач в очереди для текущего профиля. + +**Ответ:** +```json +{ + "success": true, + "data": [ + { + "bank_name": "ozon", + "material_id": "c340cd2e-cefd-48d6-a813-c4be097ae493", + "type": "FETCH_PROFILE", + "body": {} + }, + { + "bank_name": "birbank", + "material_id": "a1b2c3d4-0000-0000-0000-000000000001", + "type": "FETCH_TRANSACTIONS", + "body": {} + }, + { + "bank_name": "ozon", + "material_id": "a1b2c3d4-0000-0000-0000-000000000002", + "type": "CREATE_TRANSACTION", + "body": { + "amount": 1000, + "currency": 643, + "type": "bank", + "bank_participator_id": "string", + "extra": {} + } + } + ] +} +``` + +> `material_id` — UUID строка. Числовой формат поддерживается для обратной совместимости. +``` + +**Типы задач:** + +| `type` | Описание | `body` | +|--------|----------|--------| +| `FETCH_PROFILE` | Получить баланс и данные аккаунта | пустой `{}` | +| `FETCH_TRANSACTIONS` | Получить историю транзакций | пустой `{}` | +| `CREATE_TRANSACTION` | Выполнить перевод | `CreateTransactionBody` | + +**`CreateTransactionBody`:** +```json +{ + "amount": 1000, + "currency": 643, + "type": "bank", + "bank_participator_id": "string", + "extra": {} +} +``` + +- `currency` — ISO 4217 numeric код валюты (643 = RUB, 840 = USD, 944 = AZN, 410 = KRW) +- `type` — `"bank"` или `"top-up"` + +--- + +## POST `/api/v1/task/task_result` 🔒 + +Отправить результат выполненной задачи. + +Тело запроса зависит от типа задачи (discriminator — поле `type`). + +### FETCH_PROFILE + +```json +{ + "type": "FETCH_PROFILE", + "material_id": "c340cd2e-cefd-48d6-a813-c4be097ae493", + "bank_name": "ozon", + "data": { + "bank_account_id": "string", + "currency": 643, + "amount": 15000, + "extra": {} + } +} +``` + +### FETCH_TRANSACTIONS + +```json +{ + "type": "FETCH_TRANSACTIONS", + "material_id": "a1b2c3d4-0000-0000-0000-000000000001", + "bank_name": "birbank", + "data": [ + { + "bank_transaction_id": "string", + "amount": 500, + "currency": 944, + "transaction_type": "bank", + "bank_participator_id": "string", + "status": "completed", + "extra": {}, + "created_at": "2024-01-01T12:00:00" + } + ] +} +``` + +### CREATE_TRANSACTION + +```json +{ + "type": "CREATE_TRANSACTION", + "material_id": "a1b2c3d4-0000-0000-0000-000000000002", + "bank_name": "ozon", + "data": { + "bank_transaction_id": "string", + "amount": 1000, + "currency": 643, + "transaction_type": "bank", + "bank_participator_id": "string", + "status": "completed", + "extra": {} + } +} +``` + +**`TransactionStatusEnum`:** `"progress"` | `"completed"` | `"declined"` + +--- + +## POST `/api/v1/task/add_task` + +Добавить задачу в очередь (вызывается внешней системой, не клиентом). + +--- + +## POST `/api/v1/task/add_bank` + +Зарегистрировать банк с webhook-URL. + +**Query параметры:** + +| Параметр | Тип | Описание | +|----------|-----|----------| +| `bank_name` | string | Название банка | +| `webhook_url` | uri | URL для уведомлений | + +--- + +## Ошибка выполнения задачи + +Если задача не может быть выполнена (аккаунт не найден, скрипт упал), в поле `data` передаётся: + +```json +{ + "type": "FETCH_PROFILE", + "material_id": "c340cd2e-cefd-48d6-a813-c4be097ae493", + "bank_name": "ozon", + "data": { + "status": "error", + "description": "Account not found for material_id: c340cd2e-cefd-48d6-a813-c4be097ae493" + } +} +``` + +--- + +## Связь с кодом проекта + +| API | Метод в коде | +|-----|-------------| +| `GET /api/v1/task/get_tasks` | `NetworkService::getTasks()` | +| `POST /api/v1/task/task_result` | `NetworkService::postTaskResult()` | + +Хелпер в `EventHandler`: `sendTaskResult(type, materialId, bankName, data)` — создаёт `NetworkService` в отдельном потоке и вызывает `postTaskResult()`. diff --git a/docs/bank-setup-wizard.md b/docs/bank-setup-wizard.md new file mode 100644 index 0000000..d7dbe76 --- /dev/null +++ b/docs/bank-setup-wizard.md @@ -0,0 +1,120 @@ +# Мастер установки банка (`BankSetupWizard`) + +Файлы: `views/bank/BankSetupWizard.{h,cpp}` + +## Назначение + +Диалоговое окно, проводящее пользователя через настройку нового банковского профиля на конкретном устройстве: ввод пин-кода → автоматический вход в приложение → парсинг профиля → парсинг карт → выбор активной карты. Все данные одновременно сохраняются в локальную БД и регистрируются на бэкенде (`localhost:9999/api/v1/`). + +Поддерживаемые банки: **Ozon**, **Black** (Birbank), **Dushanbe City Bank** (`appCode == "ozon" | "black" | "dushanbe_city_bank"`). Остальные банки в мастере не поддерживаются и сразу падают с ошибкой (`BankSetupWizard.cpp:443, 494, 549`). + +## Где открывается + +Два входа, оба создают `BankSetupWizard` как обычный `QDialog` (немодальный): +- `views/device/DeviceSettingsWindow.cpp:176` — из карточки настроек устройства. +- `views/device/DeviceWidget.cpp:278` — из плитки устройства в главном окне. + +В обоих местах используется свежая запись из БД, если профиль уже есть; иначе — переданный `BankProfileInfo` шаблон. + +## Структура UI: `QStackedWidget` из 5 страниц + +Индексы страниц: `0` Intro, `1` PIN, `2` Progress, `3` Success, `4` Error (`BankSetupWizard.cpp:32-38`). + +| Шаг | Что показывает | Кнопки | +|-----|----------------|--------| +| 1 Intro (`createStep1`) | Иконка + название банка + список из 3 шагов | **Далее** → шаг 2 | +| 2 PIN (`createStep2`) | Поле ввода пин-кода (max 10 символов) | **Назад**, **Далее** → сохраняет `m_app.pinCode`, переходит на шаг 3 и запускает `startVerification()` | +| 3 Progress (`createStep3`) | `QProgressBar` + текстовая метка статуса | **Назад** → `stopWorkerThread()`, возврат на шаг 2 | +| 4a Success (`createStep4Success`) | ФИО / Телефон / Email + таблица карт с radio-выбором | **Завершить** → `applyActiveCardSelection()`, `accept()` | +| 4b Error (`createStep4Error`) | Человекочитаемый текст ошибки | **Назад** → шаг 2, **Начать заново** → очищает БД-запись (если профиль ещё не отправлен) и возвращает на шаг 1 | + +## Жизненный цикл и пауза других профилей + +В **конструкторе** (`BankSetupWizard.cpp:41`) сразу вызывается `pauseActiveProfiles()`: +- Берёт все профили устройства через `BankProfileDAO::getApplicationsByDeviceId`. +- Все `status == "active"` переводятся в `"off"` локально и **выключаются на сервере** через `NetworkService::toggleBankProfile(enable=false)` (`BankSetupWizard.cpp:297-321`). +- Коды паузованных профилей запоминаются в `m_pausedProfiles`. + +Это сделано, чтобы фоновый `EventHandler` / `DeviceScreener` не дёргали телефон параллельно во время работы мастера. + +В **деструкторе** (`BankSetupWizard.cpp:44-47`): `stopWorkerThread()` + `resumePausedProfiles()` — возвращает паузованные профили в `"active"` и шлёт `toggleBankProfile(enable=true)` на сервер. + +## Цепочка верификации (шаги 3 → 4) + +`startVerification()` → `runPinCheck()` → `runProfileCheck()` → `runCardCheck()` → `onVerificationSuccess()`. + +Каждый шаг запускает соответствующий банк-скрипт в **отдельном `QThread`** (`m_workerThread`), результат принимает лямбдой через `Qt::QueuedConnection`. Если шаг вернул непустую строку — `onVerificationError()`, иначе — следующий шаг. + +### 1. `runPinCheck()` — проверка пин-кода (`BankSetupWizard.cpp:384`) + +Сначала `startVerification()` делает базовую проверку, что устройство онлайн (`device.status == "device"`), затем upsert'ит профиль в БД с `status="off"`, `install=true`, чтобы получить `m_app.id` (`BankSetupWizard.cpp:362-382`). + +Дальше создаётся `LoginAndCheckAccountsScript` с флагом **`pinCheckOnly=true`** (3-й или 4-й аргумент в зависимости от банка). Этот флаг говорит скрипту: только дойти до главного экрана, чтобы убедиться что пин валидный, и выйти. Никаких материалов не создаётся. + +При успехе: `BankProfileDAO::updatePinCodeStatus(appId, "yes", "")`, прогресс → 33%, переход к `runProfileCheck()`. + +### 2. `runProfileCheck()` — данные профиля (`BankSetupWizard.cpp:450`) + +Запускает `GetProfileInfoScript`. Скрипт парсит ФИО/телефон/email с главного экрана, обновляет `bank_profiles` в БД и **отправляет на сервер** `POST/PATCH` через `NetworkService` — на этом этапе бэкенд возвращает `bank_profile_id`. + +После завершения обработчик `onProfileResult` дочитывает запись из БД (`BankProfileDAO::getApplication`) и проверяет, что `bankProfileId` непустой (`BankSetupWizard.cpp:463`). Если бэкенд не вернул id — ошибка "Не удалось зарегистрировать банковский профиль на сервере. Возможно, банк ещё не поддерживается сервером." Прогресс → 66%, переход к `runCardCheck()`. + +### 3. `runCardCheck()` — карты (`BankSetupWizard.cpp:501`) + +Запускает `GetCardInfoScript`. Скрипт обходит все карты в приложении, парсит полный номер карты (для Ozon/Dushanbe — через OCR/XML), складывает в БД и отправляет `POST material` на сервер. + +После завершения `onCardResult` читает `materials = MaterialDAO::getAccounts(...)`, и если хотя бы одна карта есть, но **ни у одной нет `materialId`** (т.е. ничего не синхронизировалось с сервером) — ошибка "Не удалось отправить данные карт на сервер" (`BankSetupWizard.cpp:518`). Прогресс → 100%, вызов `onVerificationSuccess()`. + +### 4. `onVerificationSuccess()` (`BankSetupWizard.cpp:556`) + +- Перечитывает профиль из БД, формирует HTML с ФИО/телефоном/email. +- Загружает карты, и **если хоть у одной `cardNumber.isEmpty()` — уходит в Error** ("Не удалось получить полный номер карты, распознаны только последние 4 цифры") (`BankSetupWizard.cpp:574-580`). Это страховка поверх защиты в самих скриптах. +- Заполняет таблицу: 4 колонки (Номер / Сумма / Валюта / Активна-radio). +- Логика автовыбора: + - **0 карт** → строка "Карты не найдены", кнопка Завершить активна. + - **1 карта** → автовыбор, колонка radio скрыта. + - **≥2 карт** → первая карта выбрана по умолчанию, можно перевыбрать. + +### 5. `applyActiveCardSelection()` (`BankSetupWizard.cpp:631`) — при нажатии Завершить + +- `MaterialDAO::activateExclusive(deviceId, appCode, selectedMaterialId)` — делает выбранную карту единственной активной в этом профиле; возвращает список деактивированных. +- Для выбранной карты шлёт `NetworkService::toggleMaterial(enable=true)`, для каждой деактивированной — `toggleMaterial(enable=false)`. Каждый запрос — в своём `QThread`, fire-and-forget. + +## Обработка ошибок + +`onVerificationError()` (`BankSetupWizard.cpp:690`) проганяет текст через `humanReadableError()` (`BankSetupWizard.cpp:659`), который: +1. Определяет **этап** по подстрокам ("пин-кода" / "профиля" / "карт"). +2. Распознаёт типовые сообщения скриптов (`"Cant open the home screen"`, `"No cards found"`, `"Cannot find user name"`, и т.д.) и подставляет понятный русский текст с подсказкой что делать. +3. Всё незнакомое → "Произошла непредвиденная ошибка. Попробуйте повторить позже." + +Кнопка **«Начать заново»** на странице Error дополнительно чистит БД, но **только если профиль ещё не зарегистрирован на сервере** (`current.bankProfileId.isEmpty()`, `BankSetupWizard.cpp:277-283`): удаляет материалы (`MaterialDAO::deleteAccountsByApp`) и сам профиль (`BankProfileDAO::deleteProfile`), сбрасывает `m_app.id = -1`. Это предотвращает дубликаты при повторной попытке. + +## Управление воркер-потоком + +`stopWorkerThread()` (`BankSetupWizard.cpp:342`) — критичный кусок: +1. `requestInterruption()` + `quit()`, ждёт до 3 секунд `wait(3000)`. +2. Если поток **залип** (обычно — внутри `QProcess::waitForFinished` в `AdbUtils::startProcess`): **отсоединяет** `QThread` от родителя (`setParent(nullptr)`) и вешает `deleteLater` на сигнал `finished`. Без этого `~QWidget` удалит работающий `QThread` → `qFatal`. + +Каждый `run*Check()` сначала зовёт `stopWorkerThread()`, поэтому переход на следующий шаг безопасен даже при гонках. + +## Краткая последовательность данных + +``` +Пользователь вводит PIN + ↓ +BankProfileDAO::upsertApplication (status=off, install=true) + ↓ +LoginAndCheckAccountsScript (pinCheckOnly=true) ─→ только верифицирует вход + ↓ +BankProfileDAO::updatePinCodeStatus("yes", "") + ↓ +GetProfileInfoScript ─→ парсит профиль → POST/PATCH bank_profile → bankProfileId + ↓ +GetCardInfoScript ─→ парсит карты → POST material для каждой → materialId + ↓ +Таблица карт + выбор активной + ↓ +MaterialDAO::activateExclusive + toggleMaterial на сервере + ↓ +accept() → resumePausedProfiles() в деструкторе возвращает остальные профили в active +``` \ No newline at end of file diff --git a/docs/black_tasks.md b/docs/black_tasks.md new file mode 100644 index 0000000..2f8aedd --- /dev/null +++ b/docs/black_tasks.md @@ -0,0 +1,368 @@ +# Black - Логика задач + +[← Главная](index.md) | [События](events.md) + +Описание логики всех типов задач для банка **Black** (appCode: `black`). + +> Black — аргентинское банковское приложение. Интерфейс на испанском. Валюта: ARS (код 32). Таймзона: America/Argentina/Buenos_Aires (UTC-3). + +--- + +## Общие механизмы (CommonScript) + +### Запуск приложения (`runAppAndGoToHomeScreen`) + +1. Убивает приложение если запущено +2. Запускает приложение +3. Ждёт 4.5 секунды +4. Цикл до 10 попыток: + - Получает XML-дамп экрана + - Проверяет `isHomeScreen()` (наличие "Saldo disponible" + content-desc "Inicio") + - Если экран с PIN-кодом (`isPinCodeScreen()` — "¿Olvidaste tu PIN?" или "Ingresá tu PIN"): + - Находит EditText с password=true + - Тапает для фокуса → ожидание 500мс + - Вводит PIN через `AdbUtils::inputText()` + - Ожидание 3 сек + - Ожидание 1 сек между попытками +5. При неудаче — скриншот + +### Прокрутка к кнопке (`swipeToButton`) + +- До 10 свайпов вверх +- После каждого свайпа парсит XML и ищет кнопку +- Аналогично Ozon + +### Создание банковского профиля (`createBankProfileIfNeeded`) + +- Аналогично Ozon (общий код в CommonScript) +- Валюта по умолчанию: ARS→32 + +--- + +## FETCH_PROFILE + +**Скрипты:** `Black::LoginAndCheckAccountsScript` → `Black::GetProfileInfoScript` → `Black::GetCardInfoScript` + +### Цель + +Авторизация, сбор профиля (ФИО, телефон, email), получение CVU и баланса. + +### Шаги LoginAndCheckAccountsScript + +1. **Валидация** — получение DeviceInfo и BankProfileInfo из БД +2. **Запуск приложения** — `runAppAndGoToHomeScreen()` с PIN-кодом +3. **Запуск GetProfileInfoScript** — синхронно, для сбора профиля +4. При ошибке: скриншот + лог + обновление комментария в `BankProfileDAO` + +### Шаги GetProfileInfoScript + +1. **Домашний экран** — проверка/перезапуск + +2. **Парсинг с домашнего экрана**: + - fullName из `parseUserNameFromHomeScreen()` — формат "¡Hola!\nИМЯ\ninitials" + - Баланс из `parseBalanceFromHomeScreen()` — формат "$1.857,00" → 1857.00 + - Обновление баланса в `BankProfileDAO` + +3. **Навигация в боковое меню**: + - Поиск кнопки "¡Hola!" (аватар) + - Тап → ожидание 2 сек + - Ожидание `isSideMenu()` (5 × 1 сек) — маркеры: "Tu Información" + "Cerrar sesión" + +4. **Навигация к профилю**: + - Тап "Tu Información" (content-desc) + - Ожидание 2.5 сек + - Ожидание `isProfilePage()` (5 × 1 сек) — маркер: content-desc "Tu información" + +5. **Парсинг профиля**: + - Nombre, Apellido, Email — по hint-атрибутам EditText + - Свайп вниз → ожидание 1.5 сек + - Телефон — из hint-атрибута + - fullName = "nombre apellido" (или fallback на имя с домашнего экрана) + +6. **Сохранение**: `BankProfileDAO::updateProfileInfo(id, fullName, phone, email)` + +7. **Синхронизация с сервером**: `createBankProfileIfNeeded()` + +8. **Отправка аккаунтов**: `postAccountsData()` для аккаунтов без materialId + +9. **Завершение** — убийство приложения + +### Шаги GetCardInfoScript (CVU) + +1. **Домашний экран** — проверка/перезапуск + +2. **Парсинг баланса** → обновление в БД + +3. **Навигация к CVU**: + - Поиск кнопки "Tu CVU" (exact match) + - Тап → ожидание 2 сек + - Ожидание `isCVUScreen()` (5 × 1 сек) + +4. **Парсинг CVU**: + - `parseCVUNumber()` — 22-значный номер после маркера "CVU" + - lastNumbers = последние 4 цифры CVU + +5. **Сохранение в БД**: + - `MaterialDAO::upsertAccount()` с: + - cardNumber = полный CVU + - lastNumbers = CVU[-4:] + - currency = "USD" (хардкод) + - description = "CVU" + +6. **Отправка материала на сервер**: `ns.postMaterial()` (если есть bankProfileId и нет materialId) + +7. **Завершение** — убийство приложения + +### Ответ серверу (при успехе) + +```json +{ + "type": "FETCH_PROFILE", + "material_id": "...", + "bank_name": "black", + "data": { + "bank_account_id": "", + "amount": 185700, + "currency": 32 + } +} +``` + +--- + +## FETCH_TRANSACTIONS + +**Скрипт:** `Black::GetLastTransactionsScript` + +### Цель + +Получение списка последних транзакций из истории операций. + +### Шаги + +1. **Домашний экран** — проверка/перезапуск + +2. **Навигация к истории транзакций**: + - Поиск кнопки "Ver más" после секции "Tu actividad" через `findVerMasAfterTuActividad()` + - Если не найдена — до 5 свайпов вниз (1 сек между свайпами) + - Тап "Ver más" → ожидание 2 сек + +3. **Ожидание загрузки** (15 × 1.5 сек): + - `isTransactionLoadingScreen()` = "Tu Actividad" есть, но транзакций нет (загрузка) + - `isTransactionListScreen()` = "Tu Actividad" + видимые транзакции + - Если после 15 попыток всё ещё loading → пустой аккаунт (нормально), возврат без ошибки + +4. **Парсинг транзакций** (`parseTransactionItems()`): + - Источник: ImageView ноды с текстом формата: + ``` + Transferencia + DD de MES de YYYY - HH:MM + $amount,cents + ``` + или (входящие): + ``` + Transferencia externa de ИМЯ + DD de MES de YYYY - HH:MM + $amount,cents + ``` + - Отрицательные суммы: `$-amount` (исходящие переводы) + - Парсинг испанских дат (`parseSpanishDate()`): enero, febrero, ..., diciembre + +5. **Сохранение в БД** (для каждой транзакции): + - bankName = "black" + - status = Complete + - timestamp = currentDateTimeUtc() + - Конвертация времени: парсинг испанской даты → таймзона Buenos Aires + - `TransactionDAO::insertTransactionIfNotExists()` + +6. **Возврат** — `goBack()` → ожидание 1 сек + +### Retry-логика (EventHandler) + +Аналогична Ozon: +``` +"Transaction not found" + < 10 мин → повтор через 15 сек +"Transaction not found" + ≥ 10 мин → ошибка +``` + +### Ответ серверу + +```json +{ + "type": "FETCH_TRANSACTIONS", + "material_id": "...", + "bank_name": "black", + "data": [ + { + "bank_transaction_id": null, + "amount": 185700, + "currency": 32, + "transaction_type": "bank", + "bank_participator_id": "+5491112345678", + "status": "completed", + "extra": { "fee": 0 }, + "created_at": "2025-01-15T14:30:00.000-03:00" + } + ] +} +``` + +--- + +## CREATE_TRANSACTION (bank — по CBU/CVU/Alias) + +**Скрипт:** `Black::PayByCardScript` + +### Цель + +Перевод на CBU, CVU или Alias получателя. + +### Входные параметры + +- `event.phone` — CBU/CVU/Alias получателя (поле `m_cardNumber`) +- `event.amount` — сумма перевода +- `m_account` — аккаунт-отправитель + +### Шаги + +1. **Домашний экран** — проверка/перезапуск + +2. **Поиск кнопки оплаты**: + - `findNodeByContentDesc("Enviar a\nCBU o alias", contains=true)` + - Если не найдена — до 5 свайпов вниз + - Тап → ожидание 2 сек + +3. **Ввод реквизитов** (10 × 1.5 сек ожидание экрана): + - Экран `isPayInputCardNumberScreen()` — hint "Ingresar CBU, CVU o Alias" + - Тап на EditText → ожидание 500мс + - Ввод CBU/CVU/Alias + - Тап "Continuar" → ожидание 2 сек + +4. **Подтверждение получателя** (10 × 1.5 сек): + - Экран `isPayTransferConfirmScreen()` — кнопка "Transferir" + поле "Alias:" + - Тап "Transferir" → ожидание 2 сек + +5. **Ввод суммы** (10 × 1.5 сек): + - Экран `isPayInputSumScreen()` — "Cuánto vas a transferír" + - Тап на EditText → ожидание 500мс + - **Очистка поля**: 20 × keyevent 67 (backspace) + - **Ввод суммы в сентаво**: `int(amount * 100)` → строка + - Пример: 500.00 → "50000" + - Проверка: перечитывает поле, если не совпадает — повторяет ввод + - Тап "Continuar" → ожидание 2 сек + +6. **Финальное подтверждение** (10 × 1.5 сек): + - Экран `isPayConfirmScreen()` — "Vas a transferir \na ИМЯ_ПОЛУЧАТЕЛЯ" + - Парсинг имени получателя (`parseRecipientNameFromConfirm()`) + - Тап "Transferir" → ожидание 3 сек + +7. **Ввод PIN** (если запрошен): + - `isPinCodeScreen()` → `inputPinCode()` + - Ожидание 3 сек + +8. **Проверка результата** (15 × 2 сек): + - **"Dinero enviado!"** → успех: + - Парсинг bankTime: `parsePaySuccessDateTime()` → "DD/MM/YY HH:MM" + - Парсинг ID: `parsePaySuccessCoelsaId()` → Coelsa transaction ID + - Скриншот как чек + - Если снова PIN-экран → повторный ввод PIN + - Таймаут → ошибка + +9. **Сохранение транзакции**: + - amount = -m_amount (отрицательная) + - fee = 0 + - type = Card, status = Complete + - name = имя получателя + - description = "Transferencia a ИМЯ" + - phone = CBU/CVU/Alias + - bankTrExternalId = Coelsa ID + - receiptImage = скриншот + - `TransactionDAO::insertTransaction()` + +### Ответ серверу + +```json +{ + "type": "CREATE_TRANSACTION", + "material_id": "...", + "bank_name": "black", + "data": { + "amount": -50000, + "currency": 32, + "transaction_type": "bank", + "bank_participator_id": "0000003100012345678901", + "status": "completed", + "bank_transaction_id": "COELSA-12345", + "extra": { "fee": 0 }, + "created_at": "2025-01-15T14:30:00.000-03:00" + } +} +``` + +--- + +## ScreenXmlParser (Black) + +### Определение экранов + +| Метод | Маркеры | +|-------|---------| +| `isHomeScreen()` | "Saldo disponible" + content-desc "Inicio" | +| `isPinCodeScreen()` | "¿Olvidaste tu PIN?" или "Ingresá tu PIN" | +| `isSideMenu()` | "Tu Información" + "Cerrar sesión" | +| `isProfilePage()` | content-desc "Tu información" | +| `isCVUScreen()` | маркер "CVU" | +| `isTransactionLoadingScreen()` | "Tu Actividad" без "Transferencia" | +| `isTransactionListScreen()` | "Tu Actividad" + транзакции | +| `isPayInputCardNumberScreen()` | EditText с hint "Ingresar CBU, CVU o Alias" | +| `isPayTransferConfirmScreen()` | "Transferir" + "Alias:" без "Vas a transferir" | +| `isPayInputSumScreen()` | "Cuánto vas a transferír" | +| `isPayConfirmScreen()` | "Vas a transferir" | +| `isPaySuccessScreen()` | "Dinero enviado!" | + +### Парсинг данных + +| Метод | Формат / Логика | +|-------|-----------------| +| `parseUserNameFromHomeScreen()` | "¡Hola!\nИМЯ\ninitials" → ИМЯ | +| `parseBalanceFromHomeScreen()` | "$1.857,00" → 1857.00 (аргентинский формат) | +| `parseProfileNombre/Apellido/Email/Phone()` | По hint-атрибутам EditText | +| `parseCVUNumber()` | 22-значный номер после маркера "CVU" | +| `parseTransactionItems()` | ImageView ноды с "Transferencia" | +| `parseSpanishDate()` | "DD de MES de YYYY - HH:MM" → QDateTime | +| `parseRecipientNameFromConfirm()` | "Vas a transferir \na ИМЯ" → ИМЯ | +| `parsePaySuccessDateTime()` | "DD/MM/YY HH:MM" | +| `parsePaySuccessCoelsaId()` | Текст после "Id Coelsa" | + +### Парсинг транзакций + +Формат элемента списка (ImageView text): +``` +Transferencia // тип +15 de enero de 2025 - 14:30 // дата (испанский) +$1.500,00 // сумма (положительная = входящая) +``` + +или: +``` +Transferencia externa de Juan Perez // тип + имя отправителя +15 de enero de 2025 - 14:30 +$-1.500,00 // отрицательная = исходящая +``` + +### Испанские месяцы + +| Месяц | Перевод | +|-------|---------| +| enero | январь | +| febrero | февраль | +| marzo | март | +| abril | апрель | +| mayo | май | +| junio | июнь | +| julio | июль | +| agosto | август | +| septiembre | сентябрь | +| octubre | октябрь | +| noviembre | ноябрь | +| diciembre | декабрь | diff --git a/docs/events.md b/docs/events.md new file mode 100644 index 0000000..89c0259 --- /dev/null +++ b/docs/events.md @@ -0,0 +1,237 @@ +# События и задачи + +[← Главная](index.md) + +Описание полного цикла задачи: от получения из API до завершения и отчёта. + +--- + +## Ключевые файлы + +| Файл | Роль | +|------|------| +| `services/EventHandler.h/cpp` | Оркестратор: поллинг, сохранение в БД, запуск потоков | +| `services/net/NetworkService.h/cpp` | HTTP-клиент: `getTasks()`, `postTaskResult()` | +| `database/dao/EventDAO.h/cpp` | CRUD для таблицы `events` | +| `models/db/EventInfo.h` | Модель задачи | + +--- + +## Жизненный цикл задачи + +``` +Wait → InProgress → Complete + ↘ Error +``` + +--- + +## Схема работы + +### 1. Получение задач (каждые 5 секунд) + +``` +QTimer (5s) + └─→ NetworkService::getTasks() + └─→ GET /api/v1/task/get_tasks (Bearer token) + └─→ signal tasksReceived(QJsonArray) + └─→ EventHandler::onTasksReceived() +``` + +Формат ответа API: + +```json +{ + "success": true, + "data": [ + { "bank_name": "ozon", "material_id": "c340cd2e-cefd-48d6-a813-c4be097ae493", "type": "FETCH_PROFILE", "body": {} }, + { "bank_name": "birbank", "material_id": "a1b2c3d4-0000-0000-0000-000000000001", "type": "FETCH_TRANSACTIONS", "body": {} }, + { "bank_name": "ozon", "material_id": "a1b2c3d4-0000-0000-0000-000000000002", "type": "CREATE_TRANSACTION", "body": { "amount": 1000, "currency": 643 } } + ] +} +``` + +> `material_id` — UUID строка. Поддерживается также числовой формат для совместимости (парсится через `task["material_id"].isString() ? toString() : QString::number(toInt())`). + +### 2. Обработка задачи (`onTasksReceived`) + +``` +Для каждой задачи из API: + ├─ existsActiveByExternalId(material_id)? + │ YES → пропустить (дедупликация) + │ NO → продолжить + │ + ├─ AccountDAO::findByMaterialId(material_id) + │ прямой SELECT WHERE material_id = ? + │ НЕ НАЙДЕН → sendTaskResult(status=error) и пропустить + │ + └─ EventDAO::insertEvent(status=Wait) +``` + +Маппинг типов задач: + +| Тип в API | `EventType` в коде | Скрипт | +|-----------|--------------------|--------| +| `FETCH_PROFILE` | `FetchProfile` | `Ozon::LoginAndCheckAccountsScript` | +| `FETCH_TRANSACTIONS` | `FetchTransactions` | `GetLastDaysHistoryBirbankScript` | +| `CREATE_TRANSACTION` | `CreateTransaction` | *(не реализован)* | + +### 3. Запуск задач (тот же `onTasksReceived`) + +``` +Для каждого онлайн устройства: + ├─ m_threads[deviceId] запущен? → пропустить + │ (одно устройство = один поток одновременно) + │ + ├─ EventDAO::getFirstWaitEventByDeviceId(deviceId) + │ самая старая Wait-задача (ORDER BY timestamp ASC) + │ + ├─ ApplicationDAO::getApplication() → статус == "active"? + │ + ├─ EventDAO::updateEvent(id, InProgress) + │ + └─ startThreadForTask(account, event) +``` + +### 4. Выполнение скрипта (QThread) + +``` +QThread + └─→ Script::start() + ├─ ADB команды (adb shell, screenshot, xml dump) + ├─ ScreenXmlParser (парсинг UI) + ├─ Tesseract OCR (при необходимости) + └─ emit finishedWithResult(error или "") + └─→ EventHandler::updateEventThread() +``` + +> **Watchdog:** если скрипт не завершился за **5 минут** — поток принудительно завершается. + +### 5. Завершение задачи (`updateEventThread`) + +``` +result == "" → EventDAO::updateEvent(Complete) +result != "" → EventDAO::updateEvent(Error, message) + +└─→ sendEventStatus() (новый QThread) + └─→ POST /api/v1/event/status ⚠️ устаревший эндпоинт, требует замены на task_result +``` + +--- + +## Отправка результата задачи + +Используется `POST /api/v1/task/task_result` через `NetworkService::postTaskResult()`. + +### При ненайденном аккаунте (material_id не в БД) + +```json +{ + "type": "FETCH_PROFILE", + "material_id": "c340cd2e-cefd-48d6-a813-c4be097ae493", + "bank_name": "ozon", + "data": { + "status": "error", + "description": "Account not found for material_id: c340cd2e-cefd-48d6-a813-c4be097ae493" + } +} +``` + +### При успешном выполнении — FETCH_PROFILE + +```json +{ + "type": "FETCH_PROFILE", + "material_id": "c340cd2e-cefd-48d6-a813-c4be097ae493", + "bank_name": "ozon", + "data": { + "bank_account_id": "string", + "currency": 643, + "amount": 15000, + "extra": {} + } +} +``` + +### При успешном выполнении — FETCH_TRANSACTIONS + +```json +{ + "type": "FETCH_TRANSACTIONS", + "material_id": "a1b2c3d4-0000-0000-0000-000000000001", + "bank_name": "birbank", + "data": [ + { + "bank_transaction_id": "string", + "amount": 500, + "currency": 944, + "transaction_type": "bank", + "bank_participator_id": "string", + "status": "completed", + "extra": {}, + "created_at": "2024-01-01T12:00:00" + } + ] +} +``` + +### При ошибке выполнения скрипта + +```json +{ + "type": "FETCH_PROFILE", + "material_id": "c340cd2e-cefd-48d6-a813-c4be097ae493", + "bank_name": "ozon", + "data": { + "status": "error", + "description": "Cant open the home screen: emulator-5554 ozon" + } +} +``` + +--- + +## Модель `EventInfo` + +```cpp +struct EventInfo { + int id; // локальный ID в SQLite + QString externalId; // material_id из API (UUID строка) + int accountId; // FK → accounts + QString deviceId; // ADB device id + + double amount; + QString phone; + QString bankName; + QString comment; + + QByteArray screenshot; // последний скриншот экрана + + EventStatus status; // Wait / InProgress / Complete / Error + EventType type; // FetchProfile / FetchTransactions / CreateTransaction + + QDateTime timestamp; + QDateTime updateTime; + QDateTime completeTime; +}; +``` + +--- + +## Дедупликация + +`EventDAO::existsActiveByExternalId(const QString &externalId)` — проверяет наличие записи +с `status IN ('wait', 'inprogress')` для данного `external_id`. +Гарантирует что одна и та же задача не попадёт в очередь дважды +пока не завершится предыдущее выполнение. + +--- + +## Ограничения + +- **1 поток на устройство** — пока устройство занято, остальные задачи ждут следующего поллинга +- **Очерёдность** — внутри устройства задачи выполняются по `timestamp ASC` +- **Интервал поллинга** — 5 секунд (`m_pollTimer->setInterval`) +- **Пути API** — настраиваются в `config.ini`: + - `net/pathGetTasks` (дефолт `/api/v1/task/get_tasks`) + - `net/pathTaskResult` (дефолт `/api/v1/task/task_result`) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..ec2194c --- /dev/null +++ b/docs/index.md @@ -0,0 +1,43 @@ +# ARCDeskProject — Документация + +Десктопное Qt6/C++ приложение для мониторинга и автоматизации Android банковских приложений через ADB. + +--- + +## Оглавление + +### Архитектура приложения + +| Раздел | Описание | +|--------|----------| +| [События и задачи](events.md) | Очередь задач: получение из API, хранение в БД, выполнение скриптов | + +### API Backend (`Adb Core Backend v1.0.0`) + +| Раздел | Описание | +|--------|----------| +| [Авторизация](api/auth.md) | Логин, refresh token, хранение токенов | +| [Profile & Material](api/profile.md) | Банковские профили и карты (материалы) | +| [Device & Desktop](api/device.md) | Регистрация десктопов и Android-устройств, скриншоты | +| [Task](api/task.md) | Получение задач, отправка результатов | +| [NetworkService](api/network.md) | Полная карта всех HTTP-вызовов приложения | + +--- + +## Общая архитектура приложения + +``` +MainWindow (Qt6 GUI) + ├── DeviceScreener (QThread) — опрос ADB устройств, скриншоты + ├── EventHandler (QThread) — обработка задач, запуск скриптов + └── NetworkService (QThread) — синхронизация с REST API (localhost:9999/api/v1/) + +Скрипты: android/{rshb,birbank,toss,ozon,ziraat}/ +Database: SQLite — DatabaseManager (singleton) + DAO-слой +``` + +## Технологии + +- **Qt6** — Widgets, Network, Sql, Xml +- **C++17**, SQLite, ADB, Tesseract OCR +- CMake diff --git a/docs/ozon_tasks.md b/docs/ozon_tasks.md new file mode 100644 index 0000000..6ceeb69 --- /dev/null +++ b/docs/ozon_tasks.md @@ -0,0 +1,384 @@ +# Ozon - Логика задач + +[← Главная](index.md) | [События](events.md) + +Описание логики всех типов задач для банка **Ozon** (appCode: `ozon`). + +--- + +## Общие механизмы (CommonScript) + +### Запуск приложения (`runAppAndGoToHomeScreen`) + +1. Убивает приложение если запущено (`AdbUtils::tryToKillApplication`) +2. Запускает приложение (`AdbUtils::tryToStartApplication`) +3. Ждёт 4.5 секунды на старт +4. Цикл до 10 попыток: + - Получает XML-дамп экрана + - Проверяет `isHomeScreen()` (наличие кнопок "Перевести" + "Пополнить") + - Закрывает баннеры: диалоги, баннер безопасности, Google Play, рекламный bottom-sheet + - Если экран с PIN-кодом — вводит PIN (100–900мс между цифрами) + - Ожидание 1 сек между попытками +5. При неудаче — скриншот + лог ошибки + +### Навигация к продуктам (`goToAllProducts`) + +1. Закрывает баннеры на домашнем экране +2. Ищет вкладку "Финансы" +3. Тапает до 3 раз с ожиданием 1 сек + +### Прокрутка к кнопке (`swipeToButton`) + +- До 10 свайпов вверх (от 3/4 высоты к 1/4 высоты экрана) +- После каждого свайпа парсит XML и ищет кнопку +- Ожидание 1 сек между свайпами + +### Создание банковского профиля (`createBankProfileIfNeeded`) + +- Читает fullName и phone из `BankProfileDAO` +- Нормализует телефон: убирает нецифровые символы, добавляет код страны из `config.ini` +- Маппинг валют: RUB→643, USD→840, KRW→410, AZN→944, ARS→32 +- POST (новый) или PATCH (существующий) на сервер + +--- + +## FETCH_PROFILE + +**Скрипт:** `Ozon::LoginAndCheckAccountsScript` + `Ozon::GetCardInfoScript` + +### Цель + +Авторизация в приложении, получение баланса, полных номеров карт и синхронизация с сервером. + +### Шаги LoginAndCheckAccountsScript + +1. **Валидация** — получение DeviceInfo и BankProfileInfo из БД +2. **Запуск приложения** — `runAppAndGoToHomeScreen()` с PIN-кодом +3. **Создание профиля** — `createBankProfileIfNeeded()` (синхронизация fullName/phone) +4. **Сбор аккаунтов** (если не режим fetchProfileOnly): + - Навигация к "Финансы" через `goToAllProducts()` + - Парсинг видимых аккаунтов из XML + - Upsert в `MaterialDAO` + - Отправка на сервер +5. **Запуск GetCardInfoScript** — синхронно, для получения полных номеров карт + +### Шаги GetCardInfoScript + +1. **Навигация на домашний экран** +2. **Парсинг профиля** (если нет в БД): + - Тап на имя пользователя вверху экрана + - Парсинг fullName и phone + - Обновление `BankProfileDAO` +3. **Загрузка карт** — `parseCardsOnHomeScreen()` (до 5 попыток, 1.5 сек) +4. **Upsert всех карт** в `MaterialDAO` +5. **Синхронизация с сервером**: + - Создание устройства (POST) если нет `apiId` + - POST/PATCH банковского профиля с балансом, ФИО, телефоном +6. **Получение полных номеров карт** (для каждой карты): + - Тап на карту → ожидание экрана карты (10 × 2 сек) + - Тап "Показать" → ожидание появления номера (10 × 2 сек) + - Regex: `\d{4}[\s]\d{4}[\s]\d{4}[\s]\d{4}` (с unicode-пробелами) + - Обработка баннера "Понятно" + - Сохранение в `MaterialDAO::updateCardNumber()` + - При неудаче: статус карты → Off, лог + скриншот +7. **Отправка материалов** на сервер (`ns.postMaterial()`) +8. **Завершение** — убийство приложения + +### Ответ серверу (при успехе) + +```json +{ + "type": "FETCH_PROFILE", + "material_id": "...", + "bank_name": "ozon", + "data": { + "bank_account_id": "", + "amount": 1500000, + "currency": 643 + } +} +``` + +> `amount` — в копейках (баланс × 100) + +--- + +## FETCH_TRANSACTIONS + +**Скрипт:** `Ozon::GetLastTransactionsScript` + +### Цель + +Поиск конкретной транзакции по сумме и времени в истории операций банка. + +### Входные параметры (из JSON задачи) + +- `body.amount` — сумма в копейках (делится на 100) +- `body.created_at` — время транзакции (ISO формат) + +### Два режима работы + +| Режим | Условие | Поведение | +|-------|---------|-----------| +| **Поиск** | `searchAmount > 0` | Ищет конкретную транзакцию | +| **Сканирование** | `searchAmount == 0` | Собирает все транзакции за 24 часа | + +### Шаги (режим поиска) + +1. **Навигация к списку операций** (`navigateToTransactionList`): + - Закрытие рекламного баннера + - Проверка домашнего экрана (перезапуск если нужно) + - Свайп до кнопки "Все" (до 10 свайпов) + - Ожидание экрана "Операции" (10 × 1.5 сек) + +2. **Поиск транзакции** (`searchTransaction`): + - Цикл до 15 прокруток + - Для каждой видимой кнопки-транзакции: + - Парсинг суммы из текста кнопки + - Проверка совпадения: `|amount - searchAmount| ≤ 0.01` + - Парсинг даты из текста, проверка: `|time - searchTime| ≤ 1 час` + - При совпадении: + - Тап на транзакцию → парсинг деталей (телефон, имя, время) + - Вставка в `TransactionDAO` + - Возврат + +3. **Если не найдена** — возвращает `"Transaction not found"` + +### Шаги (режим сканирования) + +1. Навигация к списку операций (аналогично) +2. **Сканирование** (`scanRecentTransactions`): + - Для каждой транзакции: + - Тап → ожидание деталей (10 × 1.5 сек) + - Парсинг кнопки и деталей + - Фильтр по времени: пропускает транзакции старше 24 часов + - **Получение чека:** + - Тап "Документы" + - Ожидание "Поделиться" (20 × 1.5 сек) + - Скриншот → OCR (Tesseract, rus+eng) + - Извлечение ID операции через regex: + - `ID операции = ...` (буквенно-цифровой) + - `№ Ф-YYYY-XXXXXXX` + - Вставка в `TransactionDAO` + - Возврат назад → ожидание списка + +### Retry-логика (EventHandler) + +``` +Результат "Transaction not found" + └─ Прошло < 10 мин? → статус Wait, повтор через 15 сек + └─ Прошло ≥ 10 мин? → статус Error +``` + +> Между повторами другие задачи могут выполниться (см. приоритеты в events.md) + +### Ответ серверу (при успехе) + +```json +{ + "type": "FETCH_TRANSACTIONS", + "material_id": "...", + "bank_name": "ozon", + "data": [ + { + "bank_transaction_id": "Ф-2025-1234567", + "amount": 50000, + "currency": 643, + "transaction_type": "top-up", + "bank_participator_id": "+79001234567", + "status": "completed", + "extra": { "fee": 0 }, + "created_at": "2025-01-15T14:30:00.000+03:00" + } + ] +} +``` + +> Возвращаются **все** транзакции аккаунта за последние 24 часа (не только найденная). + +--- + +## CREATE_TRANSACTION (bank — по номеру карты) + +**Скрипт:** `Ozon::PayByCardScript` + +### Цель + +Перевод по номеру карты получателя. + +### Входные параметры + +- `event.phone` — номер карты получателя +- `event.amount` — сумма перевода +- `event.comment` — имя банка-получателя + +### Шаги + +1. **Подготовка**: + - Закрытие рекламных баннеров + - Проверка домашнего экрана (перезапуск если нужно) + - Закрытие: баннеры, Google Play, "Заказать карту" + +2. **Тап "Перевести"** → ожидание 2 сек + +3. **Ожидание "Платежи и переводы"** (10 × 1.5 сек) + +4. **Тап "По номеру карты"** (секция OTHER_WAYS) → ожидание 2 сек + +5. **Ожидание "Перевод по номеру карты"** (10 × 1.5 сек) + +6. **Ввод номера карты**: + - Поиск EditText с hint "Номер карты получателя" + - `AdbUtils::inputText(cardNumber)` + - Ожидание 2 сек + +7. **Ввод суммы**: + - Поиск EditText с hint "Сумма от" + - Ввод суммы (целое число) + - Ожидание 1 сек + +8. **Ожидание расчёта комиссии** (15 × 1.5 сек): + - Поиск текста "Комиссия" + парсинг суммы после него + - Извлечение цифр из формата "45 ₽" + - Fallback: комиссия = 0 + +9. **Тап "Перевести"** (contains-match) → ожидание 3 сек + +10. **Проверка результата** (15 × 2 сек): + - **"Успешно"** → сохранение транзакции: + - amount = -сумма (отрицательная) + - fee = рассчитанная комиссия + - type = Card, status = Complete + - timestamp = Moscow timezone + - **"Не удалось"** → ошибка + - **Таймаут** → "Не дождались результата" + +11. **Получение чека** (при успехе): + - Тап "Документы" + - Ожидание "Поделиться" (20 × 1.5 сек) + - Скриншот → OCR → извлечение ID операции + - Обновление БД: receipt image + external ID + +### Ответ серверу + +```json +{ + "type": "CREATE_TRANSACTION", + "material_id": "...", + "bank_name": "ozon", + "data": { + "amount": -50000, + "currency": 643, + "transaction_type": "bank", + "bank_participator_id": "2200123456789012", + "status": "completed", + "bank_transaction_id": "Ф-2025-1234567", + "extra": { "fee": 4500 }, + "created_at": "2025-01-15T14:30:00.000+03:00" + } +} +``` + +--- + +## CREATE_TRANSACTION (top-up — по номеру телефона) + +**Скрипт:** `Ozon::PayByPhoneScript` + +### Цель + +Перевод по номеру телефона с выбором банка-получателя. + +### Входные параметры + +- `event.phone` — номер телефона получателя +- `event.amount` — сумма перевода +- `event.comment` — имя банка-получателя (для СБП) + +### Шаги + +1. **Подготовка** — аналогично PayByCard + +2. **Тап "Перевести"** → ожидание 2 сек + +3. **Ожидание страницы платежей** → поиск EditText (10 × 1.5 сек) + +4. **Навигация к "Кому перевести"**: + - Тап на первый EditText → ожидание 2 сек + - Ожидание экрана "Кому перевести" (10 × 1.5 сек) + +5. **Ввод номера телефона**: + - Поиск первого EditText + - `AdbUtils::inputText(phone)` + - Ожидание 2 сек + +6. **Выбор получателя** (цикл 20 попыток): + - **Вариант A**: Найдена кнопка с номером телефона → тап + - **Вариант B**: Выбор банка: + - Тап "Другой банк" (content-desc="other-bank-item") + - Включение ADB IME клавиатуры + - Ввод названия банка через `AdbUtils::inputAdbIMEText()` + - Отключение ADB IME + - Поиск банка в списке (10 × 1.5 сек) + - Если банк не найден → ошибка "Банк '...' не найден" + - **Условие выхода**: текст "По номеру телефона" вверху экрана (y < height/7) + +7. **Ввод суммы**: + - Первый EditText → ввод суммы (целое число) + - Ожидание 1 сек + +8. **Тап "Продолжить"** → ожидание 2 сек + +9. **Ожидание "Подтвердите перевод"** (10 × 1.5 сек) + +10. **Тап "Перевести ..."** (contains-match) → ожидание 3 сек + +11. **Проверка результата** — аналогично PayByCard: + - "Успешно" → транзакция (type = Phone) + - "Не удалось" → ошибка + - Получение чека через OCR + +### Ответ серверу + +Аналогичен PayByCard, но: +- `transaction_type`: `"top-up"` +- `bank_participator_id`: номер телефона + +--- + +## ScreenXmlParser (Ozon) + +### Определение экранов + +| Метод | Маркеры | +|-------|---------| +| `isHomeScreen()` | Кнопки "Перевести" + "Пополнить" | +| `isProfileScreen()` | Заголовок "Аккаунт" + кнопка "Выйти" | +| `isTransactionListScreen()` | WebView с text="Операции" | + +### Баннеры и диалоги + +| Тип | Обнаружение | Действие | +|-----|-------------|----------| +| Диалог/пуш | Кнопки закрытия, "Позже" | Тап на кнопку закрытия | +| Безопасность | "Мы заботимся о вашей безопасности" | Тап "Отложить" | +| Биометрия | "Быстрый вход" | Тап "Не сейчас" | +| Реклама | resource-id "design_bottom_sheet" | Тап "touch_outside" или back | +| Google Play | "Закрыть диалоговое окно" / "Доступно обновление" | Тап закрытия | + +### PIN-код (`tryToFindPinCode`) + +**Метод 1**: Поиск кликабельных нод с текстом `^(?:key\s+)?(\d)$` → нужны все 10 цифр + +**Метод 2** (fallback): Поиск сетки 4×3 из кликабельных элементов одного размера (±10px) + +### Парсинг карт (`parseCardsOnHomeScreen`) + +- Ищет "Основной счёт X ₽" для общего баланса +- Парсит кнопки с текстом, оканчивающимся на 4 цифры +- Regex для суммы: `([\d\s]+(?:[.,]\d+)?)\s*₽` + +### Парсинг транзакций + +- `parseTransactionButtonText()` — извлекает сумму и описание из текста кнопки в списке +- `parseTransactionDetail()` — парсит детали: bankTime (DD.MM.YYYY, HH:mm), phone (+...), name diff --git a/docs/status-sync.md b/docs/status-sync.md new file mode 100644 index 0000000..c1dd054 --- /dev/null +++ b/docs/status-sync.md @@ -0,0 +1,105 @@ +# Синхронизация статусов bank_profile и material + +## Определения + +### Локальные статусы (SQLite) +| Таблица | Поле | Значения | +|---|---|---| +| `bank_profiles` | `status` | `"active"` \| `"off"` | +| `materials` | `status` | `"active"` \| `"off"` (enum `MaterialStatus::Active` / `MaterialStatus::Off`) | + +### Серверные статусы (REST API) +| Сущность | Поле | Значения | +|---|---|---| +| bank_profile | `state` | `"enabled"` \| `"disabled"` | +| material | `state` | `"enabled"` \| `"disabled"` | + +### Маппинг +| Локально | Сервер | +|---|---| +| `active` | `enabled` | +| `off` | `disabled` | + +--- + +## Инвариант +**Локальное состояние — единственный источник истины.** Сервер всегда отражает локаль. При любых расхождениях побеждает локальная БД, и сервер дотягивается до неё (а не наоборот). + +Pull с сервера (`syncBankProfilesFromServer`) используется только для **создания новых записей** (новые bank_profile / новые материалы) и привязки серверных UUID. Статусы существующих записей с сервера в локаль никогда не пишутся. + +--- + +## Основные правила + +### 1. Смена онлайн-статуса устройства +- **Device → OFFLINE:** bank_profile этого устройства выключается **и локально, и на сервере** (`PATCH bank_profile/{id}/false`). Материалы локально не трогаем — они остаются в том состоянии, в котором были (см. правило 4-А ниже). +- **Device → ONLINE:** локальный статус **не меняется**. На сервер пушится текущее локальное состояние bank_profile и всех его материалов, если оно расходится с серверным. + +### 2. Периодическая синхронизация +- Каждые **60 секунд** запускается фоновая задача, которая сравнивает локальные статусы bank_profile и материалов с серверными и отправляет `PATCH` только для расходящихся. +- Локаль не трогается. Цель — компенсировать сетевые сбои, перезапуски сервера и т.п. + +### 3. Обработка ошибок при включении +- Когда пользователь включает bank_profile или material и `PATCH .../true` падает (network error / HTTP не-2xx / `success:false`): + - локально оставляем `off`; + - показываем пользователю сообщение об ошибке (см. правило 6 про UI). +- При неудачном **выключении** локально оставляем `off`; рассинхрон с сервером устранит минутный таймер из правила 2. + +### 4. Реконсиляция материалов после включения профиля +- Срабатывает **только при включении** профиля (`PATCH bank_profile/{id}/true`), при выключении — нет (см. правило 4-B про гард ниже). +- Отдельной реконсиляции после PATCH не делаем — расхождения материалов подхватит 60s-таймер из правила 2 (в худшем случае — через минуту). +- Допустима оптимизация: «дёрнуть» периодический sync сразу после успешного включения профиля, чтобы не ждать. + +--- + +## Дополнительные правила и инварианты + +### 4-A. Материалы при device → OFFLINE +- При переходе device → OFFLINE правило 1 выключает только профиль. Материалы локально не трогаем — после повторного включения профиля они вернутся к работе без лишних кликов пользователя. +- В UI материалы при `bank_profile.status == "off"` отображаются **приглушённо** или с пометкой «профиль выключен», но их `status` в БД остаётся как есть. + +### 4-B. Гард на пуш материала при выключенном профиле +- На сервере действует инвариант: `material.state ≤ bank_profile.state`. Если профиль `disabled`, сервер принудительно держит материалы в `disabled`. +- Чтобы не получить бесконечный diff-цикл, **при любом пуше статуса материала** (toggle в UI, периодический sync, follow-up после toggle профиля) проверяем локальный `bank_profile.status`. Если профиль `off` — `PATCH` материала **не отправляем**, локальный статус не меняем. +- Этот гард уже частично реализован в `AccountSettingsWindow.cpp:556`; нужно поднять его на уровень `NetworkService`, чтобы покрыть все пути пуша. + +### 5. Дебаунс ADB-флапа +- ADB может мерцать (USB-хаб, спящий экран, Doze). Без дебаунса каждое мерцание сработает как device → OFFLINE. +- **Online — мгновенно** (0 секунд). Как только устройство видно — сразу триггерим логику device → ONLINE. +- **Offline — с задержкой 30 секунд.** Устройство считается офлайн только после того, как ADB **не** показывает его как `device` непрерывно в течение 30 секунд. Если за этот период оно вернулось — считаем что флапа не было. + +### 6. Стартовый порядок (избегание гонки) +- `DeviceScreener` эмитит сигналы `deviceWentOnline(deviceId)` / `deviceWentOffline(deviceId)` **только при смене** статуса. На «никогда не видел в этой сессии» никаких событий нет. +- `NetworkService` стартует 60s-таймер только **после первого успешного прохода** `DeviceScreener` (сигнал `firstPassDone`). До этого никаких offline-эвентов не обрабатывает. +- Это исключает ситуацию «на старте все устройства выглядят офлайн → массово выключаем профили». + +### 7. Сериализация PATCH-ов +- Mutex на пару `(deviceId, bankProfileId)`: внутри одного устройства+профиля PATCH-операции последовательны, между разными — могут идти параллельно. +- Защищает от гонки: одновременно прилетели device-event, юзер-toggle и периодический sync для одного профиля → они выстраиваются в очередь. + +### 8. UI-обработка ошибок (overlay-pattern) +- При любом PATCH статуса (профиля или материала) UI показывает оверлей «Сохранение…» поверх тоггла. +- Тоггл **двигается только после ответа сервера**: + - успех → переходит в новое положение, оверлей скрывается; + - ошибка → остаётся в исходном положении, оверлей сменяется сообщением «Ошибка: …», локальный статус не меняется. +- Сейчас частично реализовано через `hideOverlay` в `AccountSettingsWindow.cpp:559`. Применить тот же pattern в `DeviceWidget`, `BankSetupWizard` и всех других местах с toggle. + +### 9. Индикация неработающей синхронизации +- В `NetworkService` ведём счётчик `m_consecutiveSyncFailures`, инкрементируем при каждом проваленном тике (правило 2) или провале `GET /current_profile`. Сбрасываем при первом успехе. +- При превышении порога (например, **3 тика подряд**) эмитим сигнал в `MainWindow`, который показывает индикатор «Нет связи с сервером, последняя успешная синхронизация: HH:MM:SS». +- При следующем успехе индикатор скрывается. + +### 10. Параллелизм и ограничения нагрузки +- В одном тике периодического sync может образоваться большой объём расхождений (5 устройств × 6 профилей × 4 материала = до 120 PATCH). +- Параллелизм с **лимитом 4-8 одновременных PATCH**; mutex из правила 7 соблюдается. +- Флаг `m_syncInProgress`: если предыдущий тик ещё не завершился, новый тик **пропускается**. Это предотвращает накопление очереди. + +### 11. Поведение при провале `GET /current_profile` +- Если периодический `GET /current_profile` упал (таймаут/сеть/5xx) — тик **пропускается тихо**, локаль не трогаем, никакие PATCH не шлём. +- Сам факт неудачи учитывается в счётчике из правила 9. + +--- + +## Связанные существующие правила (контекст) +- Toggle материала, когда `bank_profile.status == "off"`, **не идёт на сервер** — сохраняется только локально (см. правило 4-B). Серверная синхронизация произойдёт после включения профиля и следующего 60s-тика. +- После `BankSetupWizard` `bank_profile` остаётся `off` по дизайну — пользователь должен включить его вручную. \ No newline at end of file diff --git a/eng.traineddata b/eng.traineddata new file mode 100644 index 0000000..f4744c2 Binary files /dev/null and b/eng.traineddata differ diff --git a/images/alfabank_icon.png b/images/alfabank_icon.png new file mode 100644 index 0000000..40bb199 Binary files /dev/null and b/images/alfabank_icon.png differ diff --git a/images/birbank_icon.png b/images/birbank_icon.png new file mode 100644 index 0000000..531a3fa Binary files /dev/null and b/images/birbank_icon.png differ diff --git a/images/black_icon.png b/images/black_icon.png new file mode 100644 index 0000000..8a54f6f Binary files /dev/null and b/images/black_icon.png differ diff --git a/images/code.png b/images/code.png new file mode 100644 index 0000000..5e7c21f Binary files /dev/null and b/images/code.png differ diff --git a/images/cogwheel.png b/images/cogwheel.png new file mode 100644 index 0000000..23d33ba Binary files /dev/null and b/images/cogwheel.png differ diff --git a/images/copy.png b/images/copy.png new file mode 100644 index 0000000..50afd5e Binary files /dev/null and b/images/copy.png differ diff --git a/images/delete.png b/images/delete.png new file mode 100644 index 0000000..d93174d Binary files /dev/null and b/images/delete.png differ diff --git a/images/dushanbe_city_bank_icon.png b/images/dushanbe_city_bank_icon.png new file mode 100644 index 0000000..c542850 Binary files /dev/null and b/images/dushanbe_city_bank_icon.png differ diff --git a/images/img.png b/images/img.png deleted file mode 100644 index 1f48195..0000000 Binary files a/images/img.png and /dev/null differ diff --git a/images/ozon_icon.png b/images/ozon_icon.png new file mode 100644 index 0000000..363c802 Binary files /dev/null and b/images/ozon_icon.png differ diff --git a/images/rshb_icon.png b/images/rshb_icon.png new file mode 100644 index 0000000..a5124c1 Binary files /dev/null and b/images/rshb_icon.png differ diff --git a/images/santanderbank_icon.png b/images/santanderbank_icon.png new file mode 100644 index 0000000..4b0d927 Binary files /dev/null and b/images/santanderbank_icon.png differ diff --git a/images/toss_icon.png b/images/toss_icon.png new file mode 100644 index 0000000..efe17c7 Binary files /dev/null and b/images/toss_icon.png differ diff --git a/images/turn_off.png b/images/turn_off.png new file mode 100644 index 0000000..bcce6c6 Binary files /dev/null and b/images/turn_off.png differ diff --git a/images/turn_on.png b/images/turn_on.png new file mode 100644 index 0000000..53b7552 Binary files /dev/null and b/images/turn_on.png differ diff --git a/main.cpp b/main.cpp index ab8040b..dfc9ea2 100644 --- a/main.cpp +++ b/main.cpp @@ -1,21 +1,72 @@ #include +#include #include -#include "dao/AccountDAO.h" +#include "dao/MaterialDAO.h" #include "views/MainWindow.h" #include "database/DatabaseManager.h" #include "database/dao/CommonDataDAO.h" -#include "db/AccountInfoScreener.h" +#include "database/dao/DeviceDAO.h" #include "services/DeviceScreener.h" +#include #include -#include + +#ifdef Q_OS_WIN +#include +#include +#pragma comment(lib, "dbghelp.lib") + +static LONG WINAPI crashHandler(EXCEPTION_POINTERS *ep) { + fprintf(stderr, "\n=== CRASH ===\n"); + fprintf(stderr, "Exception code: 0x%08lX\n", ep->ExceptionRecord->ExceptionCode); + fprintf(stderr, "Exception addr: 0x%p\n", ep->ExceptionRecord->ExceptionAddress); + fprintf(stderr, "Thread ID: %lu\n", GetCurrentThreadId()); + + HANDLE process = GetCurrentProcess(); + SymInitialize(process, NULL, TRUE); + void *stack[64]; + USHORT frames = CaptureStackBackTrace(0, 64, stack, NULL); + SYMBOL_INFO *symbol = (SYMBOL_INFO *)calloc(sizeof(SYMBOL_INFO) + 256, 1); + symbol->MaxNameLen = 255; + symbol->SizeOfStruct = sizeof(SYMBOL_INFO); + for (USHORT i = 0; i < frames; i++) { + SymFromAddr(process, (DWORD64)stack[i], 0, symbol); + fprintf(stderr, " [%2u] %s (0x%0llX)\n", i, symbol->Name, symbol->Address); + } + free(symbol); + SymCleanup(process); + fflush(stderr); + return EXCEPTION_EXECUTE_HANDLER; +} +#endif +#include +#include #include #include +#include +#include +#include -void setupWorkerAndThread(QCoreApplication &app) { - auto *thread = new QThread; +#include "AppLogger.h" +#include "dump/TaskDumpRecorder.h" +#include "dao/GeneralLogDAO.h" +#include "dao/SettingsDAO.h" +#include "EventHandler.h" +#include "net/NetworkService.h" +#include "black/PayByCardScript.h" +#include "ozon/PayByPhoneScript.h" +#include "widget/loader/ErrorWindow.h" +#include "widget/loader/LoaderDialog.h" +#include "widget/loader/LoaderWindow.h" +#include "widget/loader/RegistrationWindow.h" +#include +#include +#include + +void startDeviceScreener(const QCoreApplication &app, NetworkService *networkService = nullptr) { + auto *thread = new QThread(); auto *deviceScreenerWorker = new DeviceScreener; // Перемещаем worker в новый поток thread. @@ -25,6 +76,20 @@ void setupWorkerAndThread(QCoreApplication &app) { // Когда поток запустится, автоматически вызовется worker->start() внутри фонового потока. QObject::connect(thread, &QThread::started, deviceScreenerWorker, &DeviceScreener::start); + // Связь с NetworkService: первый проход → старт периодической синхронизации; + // переходы online/offline → реакция в NetworkService (правило #1, #6). + if (networkService) { + QObject::connect(deviceScreenerWorker, &DeviceScreener::firstPassDone, + networkService, &NetworkService::startPeriodicSync, + Qt::QueuedConnection); + QObject::connect(deviceScreenerWorker, &DeviceScreener::deviceWentOnline, + networkService, &NetworkService::onDeviceWentOnline, + Qt::QueuedConnection); + QObject::connect(deviceScreenerWorker, &DeviceScreener::deviceWentOffline, + networkService, &NetworkService::onDeviceWentOffline, + Qt::QueuedConnection); + } + // 🔁 Эти 3 строки делают автоматическую зачистку: // Когда worker закончит работу (emit finished()), мы: // - останавливаем поток (thread->quit()) @@ -44,52 +109,340 @@ void setupWorkerAndThread(QCoreApplication &app) { thread->start(); } -void runADBCommand(const std::string& command) { - // Формируем команду для запуска в shell - std::string fullCommand = "adb shell " + command; - std::cout << "Executing: " << fullCommand << std::endl; +void startEventHandler(const QCoreApplication &app) { + auto *thread = new QThread; + auto *eventHandler = new EventHandler; + eventHandler->moveToThread(thread); + QObject::connect(thread, &QThread::started, eventHandler, &EventHandler::start); + QObject::connect(eventHandler, &EventHandler::finished, thread, &QThread::quit); + QObject::connect(eventHandler, &EventHandler::finished, eventHandler, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + QObject::connect(&app, &QCoreApplication::aboutToQuit, [=]() { + QMetaObject::invokeMethod(eventHandler, "stop", Qt::BlockingQueuedConnection); + }); + thread->start(); +} - // Запуск команды - int result = std::system(fullCommand.c_str()); +void startPayByPhoneTest() { + // Берём первый девайс с ozon аккаунтом + QList accounts = MaterialDAO::getAccountsByAppCode("ozon"); + if (accounts.isEmpty()) { + qWarning() << "[PayByPhoneTest] No ozon accounts found"; + return; + } + MaterialInfo account = accounts.first(); + qDebug() << "[PayByPhoneTest] Using account:" << account.id << account.deviceId << account.lastNumbers; - // Проверка на успешность выполнения - if (result != 0) { - std::cerr << "Error executing command: " << fullCommand << std::endl; + auto *thread = new QThread(nullptr); + auto *worker = new Ozon::PayByPhoneScript( + account, + "+79855532896", + "Альфа-Банк", + 150, + nullptr + ); + worker->moveToThread(thread); + QObject::connect(thread, &QThread::started, worker, &Ozon::PayByPhoneScript::start); + QObject::connect(worker, &Ozon::PayByPhoneScript::finished, thread, &QThread::quit); + QObject::connect(worker, &Ozon::PayByPhoneScript::finished, worker, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater); + QObject::connect(worker, &Ozon::PayByPhoneScript::finishedWithResult, [](const QString &result) { + qDebug() << "[PayByPhoneTest] Result:" << result; + }); + thread->start(); +} + +void startBlackPayByCardTest() { + QList accounts = MaterialDAO::getAccountsByAppCode("black"); + if (accounts.isEmpty()) { + qWarning() << "[BlackPayByCardTest] No black accounts found"; + return; + } + MaterialInfo account = accounts.first(); + qDebug() << "[BlackPayByCardTest] Using account:" << account.id << account.deviceId << account.lastNumbers; + + auto *thread = new QThread(nullptr); + auto *worker = new Black::PayByCardScript( + account, + "0070359630004039066690", + 500, + nullptr + ); + worker->moveToThread(thread); + QObject::connect(thread, &QThread::started, worker, &Black::PayByCardScript::start); + QObject::connect(worker, &Black::PayByCardScript::finished, thread, &QThread::quit); + QObject::connect(worker, &Black::PayByCardScript::finished, worker, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater); + QObject::connect(worker, &Black::PayByCardScript::finishedWithResult, [](const QString &result) { + qDebug() << "[BlackPayByCardTest] Result:" << result; + }); + thread->start(); +} + +NetworkService* startNetworkService(const QCoreApplication &app) { + auto *paymentThread = new QThread; + auto *networkService = new NetworkService; + AppLogger::setNetworkService(networkService); + TaskDumpRecorder::setNetworkService(networkService); + networkService->moveToThread(paymentThread); + QObject::connect(paymentThread, &QThread::started, networkService, &NetworkService::start); + QObject::connect(networkService, &NetworkService::finished, paymentThread, &QThread::quit); + QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater); + QObject::connect(paymentThread, &QThread::finished, paymentThread, &QThread::deleteLater); + QObject::connect(&app, &QCoreApplication::aboutToQuit, [=]() { + auto *loader = new LoaderDialog(); + loader->setWindowTitle("Завершение..."); + loader->show(); + QCoreApplication::processEvents(); + + QEventLoop loop; + QObject::connect(networkService, &NetworkService::finished, &loop, &QEventLoop::quit); + + // Таймаут на случай если сервер недоступен + QTimer::singleShot(10000, &loop, &QEventLoop::quit); + + networkService->stop(); + loop.exec(); + + loader->close(); + delete loader; + }); + + // Сигнал эмитируется из фонового потока → Qt::QueuedConnection доставит его в главный поток + QObject::connect(networkService, &NetworkService::tokenRefreshFailed, qApp, []() { + QMessageBox *box = new QMessageBox( + QMessageBox::Critical, + "Ошибка авторизации", + "Не удалось обновить токен авторизации.\n\n" + "Проверьте подключение к интернету.\n" + "Приложение продолжит попытки автоматически.", + QMessageBox::Ok + ); + box->setAttribute(Qt::WA_DeleteOnClose); + box->show(); + }, Qt::QueuedConnection); + + paymentThread->start(); + return networkService; +} + +static QFile logFile; +static QMutex logMutex; +static std::atomic dbReady{false}; + +void messageHandler( + const QtMsgType type, + const QMessageLogContext &context, + const QString &msg +) { + Q_UNUSED(context) + const QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz"); + QString level; + switch (type) { + case QtDebugMsg: level = "DEBUG"; + break; + case QtInfoMsg: level = "INFO"; + break; + case QtWarningMsg: level = "WARNING"; + break; + case QtCriticalMsg: level = "CRITICAL"; + break; + case QtFatalMsg: level = "FATAL"; + break; + } + + const QString source = context.function ? QString("%1:%2").arg(context.function).arg(context.line) : QString(); + + const char *func = context.function ? context.function : "unknown"; + const QString line = QString("%1 [%2] [%3:%4] %5\n") + .arg(timestamp, level, func, QString::number(context.line), msg); + // Записываем в файл (потокобезопасно) + { + QMutexLocker locker(&logMutex); + if (logFile.isOpen()) { + QTextStream(&logFile) << line; + logFile.flush(); + } + } + + // Дублируем в БД (с защитой от рекурсии), пропуская внутренние Qt-сообщения + static const QStringList qtNoisePatterns = { + "killTimer", + "setGeometry", + "QSqlDatabase requires", + "Cannot be used without QCoreApplication", + "QWindowsWindow", + "Timers cannot be stopped from another thread", + }; + bool isQtNoise = false; + for (const QString &pattern : qtNoisePatterns) { + if (msg.contains(pattern)) { isQtNoise = true; break; } + } + + if (dbReady.load() && !isQtNoise) { + static thread_local bool writing = false; + if (!writing) { + writing = true; + GeneralLogDAO::insert(level, source, msg); + writing = false; + } + } + + // По желанию — дублировать в консоль + const QByteArray localMsg = line.toLocal8Bit(); + fprintf(stderr, "%s", localMsg.constData()); + + if (type == QtFatalMsg) { + abort(); } } -void wait(const int milliseconds) { - std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds)); -} - - int main(int argc, char *argv[]) { +#ifdef Q_OS_WIN + SetUnhandledExceptionFilter(crashHandler); +#endif QApplication app(argc, argv); + app.setApplicationName("ARC"); + app.setWindowIcon(QIcon("arc.png")); - // QList cards = AccountInfoScreener::parseCardData(QByteArray()); - // for (const Card &card: cards) { - // AccountInfo info; - // info.deviceId = "6edc4a47"; - // info.appName = "bank"; - // info.cardName = card.name; - // info.amount = card.amount; - // info.description = card.description; - // info.status = "active"; - // info.updateTime = QDateTime::currentDateTime(); - // qDebug() << "Account info: " << AccountDAO::upsertAccount(info); - // } + // Используем Schannel (нативный Windows TLS) — не требует OpenSSL DLL + if (QSslSocket::availableBackends().contains("schannel")) { + QSslSocket::setActiveBackend("schannel"); + qDebug() << "[main] TLS backend set to schannel"; + } else { + qDebug() << "[main] Available TLS backends:" << QSslSocket::availableBackends(); + } + + // Выбрать стиль Fusion + принудительная светлая тема + QApplication::setStyle(QStyleFactory::create("Fusion")); + + QPalette lightPalette; + lightPalette.setColor(QPalette::Window, QColor(240, 240, 240)); + lightPalette.setColor(QPalette::WindowText, Qt::black); + lightPalette.setColor(QPalette::Base, Qt::white); + lightPalette.setColor(QPalette::AlternateBase, QColor(233, 233, 233)); + lightPalette.setColor(QPalette::ToolTipBase, QColor(255, 255, 220)); + lightPalette.setColor(QPalette::ToolTipText, Qt::black); + lightPalette.setColor(QPalette::Text, Qt::black); + lightPalette.setColor(QPalette::Button, QColor(240, 240, 240)); + lightPalette.setColor(QPalette::ButtonText, Qt::black); + lightPalette.setColor(QPalette::BrightText, Qt::red); + lightPalette.setColor(QPalette::Link, QColor(0, 0, 255)); + lightPalette.setColor(QPalette::Highlight, QColor(76, 163, 224)); + lightPalette.setColor(QPalette::HighlightedText, Qt::white); + QApplication::setPalette(lightPalette); + + logFile.setFileName("application.log"); + // Очищаем лог-файл при каждом запуске + if (!logFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { + qWarning() << "Не удалось открыть лог-файл для записи"; + } + // Устанавливаем свой обработчик + qInstallMessageHandler(messageHandler); - setupWorkerAndThread(app); - // - // const QDateTime current = QDateTime::currentDateTimeUtc(); - // qDebug() << "Обновление lastScan:" << CommonDataDAO::updateLastScan(current); - // - // const QDateTime savedTime = CommonDataDAO::getLastScan(); - // qDebug() << "Сохранившееся время:" << savedTime.toString(); - // - // - MainWindow window; - window.show(); - return app.exec(); + // Создаём директорию для БД, если её нет (initSchema создаст файл автоматически) + { + QSettings settings("config.ini", QSettings::IniFormat); + QString dbPath = settings.value("db/dbPath").toString(); + if (dbPath.startsWith("~")) + dbPath.replace(0, 1, QDir::homePath()); + QDir().mkpath(QFileInfo(dbPath).absolutePath()); + } + + // Миграции БД + DatabaseManager::instance().initSchema(); + DatabaseManager::instance().cleanupOldData(30); + dbReady.store(true); + + // 1. Если есть api_key — логинимся и проверяем/создаём desktop + // - ошибка сети или сервера [code -1] + // - неверный api_key [code 0] — показываем окно ввода + // - всё ок [code 1] — запускаем приложение + // 2. Если нет api_key — показываем окно для ввода + const QString apiKey = SettingsDAO::get("api_key"); + + auto showRegistration = [&app]() { + auto *registrationWindow = new RegistrationWindow(); + registrationWindow->setAttribute(Qt::WA_DeleteOnClose); + registrationWindow->show(); + QObject::connect( + registrationWindow, &RegistrationWindow::onRegisteredSuccess, registrationWindow, + [registrationWindow, &app]() { + auto *mainWindow = new MainWindow(); + mainWindow->show(); + registrationWindow->close(); + NetworkService *net = startNetworkService(app); + QObject::connect(net, &NetworkService::syncHealthChanged, + mainWindow, &MainWindow::setSyncHealth, + Qt::QueuedConnection); + startDeviceScreener(app, net); + startEventHandler(app); + // startBlackPayByCardTest(); + }, Qt::QueuedConnection); + }; + + if (apiKey.isEmpty()) { + showRegistration(); + } else { + auto *loader = new LoaderWindow(); + loader->setAttribute(Qt::WA_DeleteOnClose); + loader->show(); + + QObject::connect(loader, &LoaderWindow::loadingFinished, loader, [loader, &app, showRegistration](const int result) { + if (result == -1) { + auto *errorWindow = new ErrorWindow( + "Сервер не отвечает.\n\nПроверьте подключение к интернету и попробуйте снова.", + true + ); + errorWindow->setAttribute(Qt::WA_DeleteOnClose); + errorWindow->show(); + QObject::connect(errorWindow, &ErrorWindow::retryRequested, errorWindow, [errorWindow, &app, showRegistration]() { + errorWindow->close(); + auto *newLoader = new LoaderWindow(); + newLoader->setAttribute(Qt::WA_DeleteOnClose); + newLoader->show(); + QObject::connect(newLoader, &LoaderWindow::loadingFinished, newLoader, [newLoader, &app, showRegistration](const int r) { + if (r == 1) { + auto *mainWindow = new MainWindow(); + mainWindow->show(); + NetworkService *net = startNetworkService(app); + QObject::connect(net, &NetworkService::syncHealthChanged, + mainWindow, &MainWindow::setSyncHealth, + Qt::QueuedConnection); + startDeviceScreener(app, net); + startEventHandler(app); + // startBlackPayByCardTest(); + } else if (r == 0) { + SettingsDAO::remove("api_key"); + showRegistration(); + } else { + auto *err2 = new ErrorWindow( + "Сервер не отвечает.\n\nПроверьте подключение к интернету и попробуйте снова.", + false + ); + err2->setAttribute(Qt::WA_DeleteOnClose); + err2->show(); + } + newLoader->close(); + }, Qt::QueuedConnection); + }); + } else if (result == 1) { + auto *mainWindow = new MainWindow(); + mainWindow->show(); + NetworkService *net = startNetworkService(app); + QObject::connect(net, &NetworkService::syncHealthChanged, + mainWindow, &MainWindow::setSyncHealth, + Qt::QueuedConnection); + startDeviceScreener(app, net); + startEventHandler(app); + // startBlackPayByCardTest(); + } else if (result == 0) { + SettingsDAO::remove("api_key"); + showRegistration(); + } + loader->close(); + }, Qt::QueuedConnection); + } + return QApplication::exec(); } diff --git a/migrations.sql b/migrations.sql index 03467ed..a6019ab 100644 --- a/migrations.sql +++ b/migrations.sql @@ -1,6 +1,12 @@ +-- ============================================================ +-- Схема БД ARCDeskProject +-- ============================================================ + CREATE TABLE IF NOT EXISTS devices ( id TEXT PRIMARY KEY, + api_id TEXT, + adb_serial TEXT, status TEXT, name TEXT, android TEXT, @@ -8,31 +14,133 @@ CREATE TABLE IF NOT EXISTS devices screen_height INTEGER, battery INTEGER, update_time DATETIME, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + timestamp DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')), image BLOB ); -CREATE TABLE IF NOT EXISTS accounts +CREATE TABLE IF NOT EXISTS bank_profiles ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - device_id TEXT, - app_name TEXT, - card_name TEXT, - description TEXT, - amount REAL, - update_time DATETIME DEFAULT CURRENT_TIMESTAMP, - status TEXT, + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT, + pin_code TEXT, + pin_code_checked TEXT DEFAULT 'no', + name TEXT, + code TEXT, + package TEXT, + status TEXT DEFAULT 'off', + install TEXT DEFAULT 'no', + comment TEXT, + phone TEXT DEFAULT '', + email TEXT DEFAULT '', + full_name TEXT DEFAULT '', + bank_profile_id TEXT DEFAULT '', + currency TEXT DEFAULT '', + amount REAL DEFAULT 0, + app_version TEXT DEFAULT '', + app_version_checked_at DATETIME, FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, - UNIQUE (device_id, app_name, card_name) + UNIQUE (device_id, code) +); + +-- app_code: код банка ("ozon", "black") — не путать с человекочитаемым name +CREATE TABLE IF NOT EXISTS materials +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + device_id TEXT, + app_code TEXT, + last_numbers TEXT, + description TEXT, + amount REAL, + currency TEXT DEFAULT '', + update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')), + status TEXT DEFAULT 'active', + card_number TEXT DEFAULT '', + account_number TEXT DEFAULT '', + material_id TEXT DEFAULT '', + FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE, + UNIQUE (device_id, app_code, last_numbers) ); CREATE TABLE IF NOT EXISTS transactions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - account_id INTEGER, - amount REAL, - description TEXT, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE, - UNIQUE (amount, timestamp) -); \ No newline at end of file + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER, + external_id INTEGER, + amount REAL, + fee REAL, + status TEXT, + type TEXT, + phone TEXT, + name TEXT, + short_description TEXT, + updated_short_description TEXT, + description TEXT, + updated_description TEXT, + bank_name TEXT, + bank_time TEXT, + bank_tr_external_id TEXT, + update_time DATETIME, + complete_time DATETIME, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + receipt_image BLOB, + FOREIGN KEY (account_id) REFERENCES materials (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS events +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER, + external_id TEXT, + device_id TEXT, + amount REAL, + phone TEXT, + bank_name TEXT, + comment TEXT, + status TEXT, + type TEXT, + update_time DATETIME, + complete_time DATETIME, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + json TEXT DEFAULT '', + sent_to_server INTEGER DEFAULT 0, + screenshot BLOB, + FOREIGN KEY (account_id) REFERENCES materials (id) ON DELETE CASCADE, + FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS banks +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + alias TEXT UNIQUE, + name TEXT, + nspk_name TEXT +); + +CREATE TABLE IF NOT EXISTS app_logs +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + source TEXT NOT NULL DEFAULT '', + device_id TEXT NOT NULL DEFAULT '', + message TEXT NOT NULL, + screenshot BLOB, + timestamp TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_app_logs_ts ON app_logs (id DESC); + +CREATE TABLE IF NOT EXISTS general_logs +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + level TEXT NOT NULL DEFAULT '', + source TEXT NOT NULL DEFAULT '', + message TEXT NOT NULL, + timestamp TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_general_logs_id ON general_logs (id DESC); + +CREATE TABLE IF NOT EXISTS settings +( + key TEXT PRIMARY KEY, + value TEXT +); diff --git a/models/android/xml/Node.h b/models/android/xml/Node.h new file mode 100644 index 0000000..0a6807b --- /dev/null +++ b/models/android/xml/Node.h @@ -0,0 +1,56 @@ +#pragma once + +#include + +class Node { +public: + Node() = default; + + void setBounds(const int x1, const int y1, const int x2, const int y2) { + m_x1 = x1; + m_y1 = y1; + m_x2 = x2; + m_y2 = y2; + } + + [[nodiscard]] bool isEmpty() const { + return m_x1 < 0 && m_y1 < 0 && m_x2 < 0 && m_y2 < 0 && text.isEmpty() && className.isEmpty(); + } + + /** + * const int x = (x1() + x2()) / 2; + * const int y = (y1() + y2()) / 2; + */ + [[nodiscard]] int x1() const { return m_x1; } + [[nodiscard]] int y1() const { return m_y1; } + [[nodiscard]] int x2() const { return m_x2; } + [[nodiscard]] int y2() const { return m_y2; } + [[nodiscard]] int width() const { return m_x2 - m_x1; } + [[nodiscard]] int height() const { return m_y2 - m_y1; } + + [[nodiscard]] int x() const { + if (m_x1 < 0) { + return -1; + } + return m_x1 + (m_x2 - m_x1) / 2; + } + + [[nodiscard]] int y() const { + if (m_y1 < 0) { + return -1; + } + return m_y1 + (m_y2 - m_y1) / 2; + } + + QString text; + QString contentDesc; + QString resourceId; + QString className; + QString hint; + bool clickable = false; + bool focusable = false; + bool password = false; + +private: + int m_x1 = -1, m_y1 = -1, m_x2 = -1, m_y2 = -1; +}; diff --git a/models/db/AccountInfo.h b/models/db/AccountInfo.h deleted file mode 100644 index d7fcdf1..0000000 --- a/models/db/AccountInfo.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include - -struct AccountInfo { - int id; - QString deviceId; - QString appName; - QString cardName; - double amount; - QDateTime updateTime; - QString status; - QString description; -}; \ No newline at end of file diff --git a/models/db/BankInfo.h b/models/db/BankInfo.h new file mode 100644 index 0000000..2f5c885 --- /dev/null +++ b/models/db/BankInfo.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +struct BankInfo { + int id = -1; + QString alias; + QString name; + QString nspkName; +}; diff --git a/models/db/BankProfileInfo.h b/models/db/BankProfileInfo.h new file mode 100644 index 0000000..148fac8 --- /dev/null +++ b/models/db/BankProfileInfo.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +struct BankProfileInfo { + int id = -1; + bool install = false; + bool pinCodeChecked = false; + QString deviceId; + QString pinCode; + QString code; + QString name; + QString package; + QString status; + QString comment; + QString phone; + QString email; + QString fullName; + QString bankProfileId; + QString currency; + double amount = 0.0; + QString appVersion; + QDateTime appVersionCheckedAt; + QDateTime updateTime; +}; diff --git a/models/db/DeviceInfo.h b/models/db/DeviceInfo.h index b87ed5c..3e4e810 100644 --- a/models/db/DeviceInfo.h +++ b/models/db/DeviceInfo.h @@ -3,13 +3,15 @@ #include struct DeviceInfo { - QString id; + QString id; // android_id (unique device identifier) + QString apiId; // UUID from backend API (DeviceResponseModel.id) + QString adbSerial; // ADB serial (e.g. "emulator-5554", "7dfc444d43fbeab7") QString status; QString name; QString android; - int screenWidth; - int screenHeight; - int battery; + int screenWidth = 0; + int screenHeight = 0; + int battery = 0; QDateTime updateTime; QByteArray image; }; \ No newline at end of file diff --git a/models/db/EventInfo.h b/models/db/EventInfo.h new file mode 100644 index 0000000..68fae83 --- /dev/null +++ b/models/db/EventInfo.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include + +enum class EventStatus { + Wait, + InProgress, + Complete, + Error, + Unknown +}; + +enum class EventType { + FetchProfile, + FetchTransactions, + CreateTransaction, + Unknown +}; + + +inline QString eventStatusToString(const EventStatus status) { + switch (status) { + case EventStatus::Wait: return "wait"; + case EventStatus::Error: return "error"; + case EventStatus::Complete: return "complete"; + case EventStatus::InProgress: return "iPprogress"; + default: return "unknown"; + } +} + +inline EventStatus eventStatusFromString(const QString &str) { + if (str == "error") return EventStatus::Error; + if (str == "wait") return EventStatus::Wait; + if (str == "complete") return EventStatus::Complete; + if (str == "iPprogress") return EventStatus::InProgress; + return EventStatus::Unknown; +} + +inline QString eventTypeToString(const EventType type) { + switch (type) { + case EventType::FetchProfile: return "FETCH_PROFILE"; + case EventType::FetchTransactions: return "FETCH_TRANSACTIONS"; + case EventType::CreateTransaction: return "CREATE_TRANSACTION"; + default: return "unknown"; + } +} + +inline EventType eventTypeFromString(const QString &str) { + if (str == "FETCH_PROFILE") return EventType::FetchProfile; + if (str == "FETCH_TRANSACTIONS") return EventType::FetchTransactions; + if (str == "CREATE_TRANSACTION") return EventType::CreateTransaction; + return EventType::Unknown; +} + +struct EventInfo { + int id = -1; + QString externalId; + int accountId = -1; + QString deviceId; + + double amount = 0; + QString phone; + QString bankName; + QString comment; + + EventStatus status = EventStatus::Unknown; + EventType type = EventType::Unknown; + + QString json; + bool sentToServer = false; + + QByteArray screenshot; + bool hasScreenshot = false; + + QDateTime updateTime; + QDateTime completeTime; + QDateTime timestamp; +}; \ No newline at end of file diff --git a/models/db/MaterialInfo.h b/models/db/MaterialInfo.h new file mode 100644 index 0000000..9623c94 --- /dev/null +++ b/models/db/MaterialInfo.h @@ -0,0 +1,45 @@ +#pragma once + +#include + +enum class MaterialStatus { + Active, + Off +}; + +inline QString materialStatusToString(const MaterialStatus status) { + switch (status) { + case MaterialStatus::Active: return "active"; + case MaterialStatus::Off: return "off"; + default: return "active"; + } +} + +inline QString materialStatusToUiString(const MaterialStatus status) { + switch (status) { + case MaterialStatus::Active: return "в работе"; + case MaterialStatus::Off: return "выключена"; + default: return "в работе"; + } +} + +inline MaterialStatus materialStatusFromString(const QString &str) { + if (str == "active") return MaterialStatus::Active; + if (str == "off") return MaterialStatus::Off; + return MaterialStatus::Active; +} + +struct MaterialInfo { + int id = -1; + QString deviceId; // code + QString appCode; + QString lastNumbers; + QString cardNumber; // full 16-digit card number + QString accountNumber; // full bank account number (IBAN-like) + QString materialId; + QString currency; + QString description; + double amount = 0.0; + QDateTime updateTime; + MaterialStatus status = MaterialStatus::Off; +}; \ No newline at end of file diff --git a/models/db/TransactionInfo.h b/models/db/TransactionInfo.h index fe791d3..d64329a 100644 --- a/models/db/TransactionInfo.h +++ b/models/db/TransactionInfo.h @@ -1,11 +1,106 @@ #pragma once +#include +#include #include +enum class TransactionType { + Card, + Phone, + Iban, + Unknown +}; + +enum class TransactionStatus { + Wait, + Complete, + Unknown +}; + + +inline QString transactionTypeToString(const TransactionType type) { + switch (type) { + case TransactionType::Card: return "card"; + case TransactionType::Phone: return "phone"; + case TransactionType::Iban: return "iban"; + default: return "unknown"; + } +} + +inline TransactionType transactionTypeFromString(const QString &str) { + if (str == "card") return TransactionType::Card; + if (str == "phone") return TransactionType::Phone; + if (str == "iban") return TransactionType::Iban; + return TransactionType::Unknown; +} + +inline QString transactionStatusToString(const TransactionStatus type) { + switch (type) { + case TransactionStatus::Wait: return "wait"; + case TransactionStatus::Complete: return "complete"; + default: return "unknown"; + } +} + +inline TransactionStatus transactionStatusFromString(const QString &str) { + if (str == "wait") return TransactionStatus::Wait; + if (str == "complete") return TransactionStatus::Complete; + return TransactionStatus::Unknown; +} + struct TransactionInfo { - int id; - int accountId; - double amount; + int id = -1; + int accountId = -1; + + double amount = 0; + double fee = 0; + TransactionStatus status = TransactionStatus::Unknown; + TransactionType type = TransactionType::Unknown; + QString phone; + QString name; + QString description; + QString updatedDescription; + + QString bankName; + QString bankTime; + + QString bankTrExternalId; + + QDateTime updateTime; + QDateTime completeTime; QDateTime timestamp; -}; \ No newline at end of file + + QByteArray receiptImage; +}; + +inline QString convertTransactionToString(const TransactionInfo &tx) { + QStringList lines; + QStringList emptyFields; + + lines << "TransactionInfo {"; + + if (tx.id != -1) lines << " id: " + QString::number(tx.id); else emptyFields << "id"; + if (tx.accountId != -1) lines << " accountId: " + QString::number(tx.accountId); else emptyFields << "accountId"; + if (tx.amount != 0.0) lines << " amount: " + QString::number(tx.amount); else emptyFields << "amount"; + if (tx.fee != 0.0) lines << " fee: " + QString::number(tx.fee); else emptyFields << "fee"; + if (tx.status != TransactionStatus::Unknown) lines << " status: " + transactionStatusToString(tx.status); else emptyFields << "status"; + if (tx.type != TransactionType::Unknown) lines << " type: " + transactionTypeToString(tx.type); else emptyFields << "type"; + if (!tx.phone.isEmpty()) lines << " phone: " + tx.phone; else emptyFields << "phone"; + if (!tx.description.isEmpty()) lines << " description: " + tx.description; else emptyFields << "description"; + if (!tx.updatedDescription.isEmpty()) lines << " updatedDescription: " + tx.updatedDescription; else emptyFields << "updatedDescription"; + if (!tx.bankName.isEmpty()) lines << " bankName: " + tx.bankName; else emptyFields << "bankName"; + if (!tx.bankTime.isEmpty()) lines << " bankTime: " + tx.bankTime; else emptyFields << "bankTime"; + if (!tx.name.isEmpty()) lines << " name: " + tx.name; else emptyFields << "name"; + if (!tx.bankTrExternalId.isEmpty()) lines << " bankTrExternalId: " + tx.bankTrExternalId; else emptyFields << "bankTrExternalId"; + if (tx.updateTime.isValid()) lines << " updateTime: " + tx.updateTime.toString(Qt::ISODate); else emptyFields << "updateTime"; + if (tx.completeTime.isValid()) lines << " completeTime: " + tx.completeTime.toString(Qt::ISODate); else emptyFields << "completeTime"; + if (tx.timestamp.isValid()) lines << " timestamp: " + tx.timestamp.toString(Qt::ISODate); else emptyFields << "timestamp"; + + if (!emptyFields.isEmpty()) { + lines << " --- empty: " + emptyFields.join(", "); + } + lines << "}"; + + return lines.join('\n'); +} diff --git a/no_devices.png b/no_devices.png new file mode 100644 index 0000000..b56ed14 Binary files /dev/null and b/no_devices.png differ diff --git a/services/AppLogger.cpp b/services/AppLogger.cpp new file mode 100644 index 0000000..d148f40 --- /dev/null +++ b/services/AppLogger.cpp @@ -0,0 +1,31 @@ +#include "AppLogger.h" + +#include +#include + +#include "dao/GeneralLogDAO.h" +#include "net/NetworkService.h" + +NetworkService *AppLogger::s_networkService = nullptr; + +void AppLogger::setNetworkService(NetworkService *service) { + s_networkService = service; +} + +void AppLogger::log(const QString &source, const QString &deviceId, + const QString &message, const QByteArray &screenshot, + const QString &event) { + const QString fullMessage = deviceId.isEmpty() + ? message + : QString("[%1] %2").arg(deviceId, message); + GeneralLogDAO::insert("WARNING", source, fullMessage, screenshot); + + if (s_networkService) { + QMetaObject::invokeMethod(s_networkService, "postMonitoringLog", + Qt::QueuedConnection, + Q_ARG(QString, QStringLiteral("ERROR")), + Q_ARG(QString, event.isEmpty() ? source : event), + Q_ARG(QString, message), + Q_ARG(QByteArray, screenshot)); + } +} diff --git a/services/AppLogger.h b/services/AppLogger.h new file mode 100644 index 0000000..7b7ecf5 --- /dev/null +++ b/services/AppLogger.h @@ -0,0 +1,21 @@ +#pragma once +#include +#include + +class NetworkService; + +class AppLogger { +public: + static void setNetworkService(NetworkService *service); + + static void log( + const QString &source, + const QString &deviceId, + const QString &message, + const QByteArray &screenshot = {}, + const QString &event = {} + ); + +private: + static NetworkService *s_networkService; +}; diff --git a/services/DeviceScreener.cpp b/services/DeviceScreener.cpp index e618d38..919f92d 100644 --- a/services/DeviceScreener.cpp +++ b/services/DeviceScreener.cpp @@ -1,57 +1,474 @@ #include "DeviceScreener.h" -#include +#include #include #include +#include #include +#include #include +#include #include +#include +#include +#include +#include +#include +#include #include #include +#include "adb/AdbUtils.h" +#include "dao/BankProfileDAO.h" +#include "dao/MaterialDAO.h" +#include "dao/SettingsDAO.h" +#include "net/NetworkErrorUtils.h" + DeviceScreener::DeviceScreener(QObject *parent) : QObject(parent) { + const QSettings settings("config.ini", QSettings::IniFormat); + m_urlAppUpdate = settings.value("net/appInfoUpdate").toString(); + m_token = settings.value("common/token").toString(); + m_apiBase = settings.value("net/apiBase").toString(); } DeviceScreener::~DeviceScreener() = default; -void DeviceScreener::start() { - // qDebug() << "Worker started in thread:" << QThread::currentThread(); - while (m_running) { - QList> devices = readAdbDevices(); - QStringList onlineIds; - for (const auto&[id, status] : devices) { - // qDebug() << "Id:" << id << ", Status:" << status; +void DeviceScreener::postDevicesData(const QList &devices) { + auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + QHttpPart jsonPart; + jsonPart.setHeader(QNetworkRequest::ContentTypeHeader, "application/json; charset=UTF-8"); + jsonPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="metadata")"); + + QJsonArray list; + for (const DeviceInfo &device: devices) { + QJsonObject obj; + obj["uniqueName"] = device.id; + obj["status"] = device.status; + obj["name"] = device.name; + obj["screenWidth"] = device.screenWidth; + obj["screenHeight"] = device.screenHeight; + obj["battery"] = device.battery; + + list.append(obj); + } + + jsonPart.setBody(QJsonDocument(list).toJson()); + multiPart->append(jsonPart); + + + for (const DeviceInfo &device: devices) { + QHttpPart imagePart; + QString imageHeader = QStringLiteral("form-data; name=files; filename=%1.jpg").arg(device.id); + imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg"); + imagePart.setHeader(QNetworkRequest::ContentDispositionHeader, imageHeader.toUtf8()); + imagePart.setBody(device.image); + multiPart->append(imagePart); + } + + QHttpPart uniquePart; + uniquePart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="token")"); + uniquePart.setBody(m_token.toUtf8()); + + multiPart->append(uniquePart); + + QNetworkAccessManager manager; // создаём внутри потока + QUrl url(m_urlAppUpdate); + QNetworkRequest request(url); + + // делаем запрос + QNetworkReply *reply = manager.post(request, multiPart); + multiPart->setParent(reply); // multiPart удалится, когда придёт ответ + + // локальный цикл — ждём, пока придёт finished() + QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + QObject::connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { + loop.quit(); + }); + QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(30000); + loop.exec(); + + // обрабатываем результат + if (!timeout.isActive()) { + qWarning() << "Таймаут postDevicesData:" << m_urlAppUpdate; + reply->abort(); + } else if (reply->error() == QNetworkReply::NoError) { + QByteArray raw = reply->readAll(); + QJsonParseError err; + QJsonDocument doc = QJsonDocument::fromJson(raw, &err); + if (err.error == QJsonParseError::NoError && doc.isObject()) { + QJsonObject root = doc.object(); + if (!root.value("success").toBool()) { + qWarning() << root; + } + } + } else { + qWarning() << "Response error:" << reply->errorString() << m_urlAppUpdate; + } + reply->deleteLater(); +} + +QJsonValue DeviceScreener::apiPost(const QString &path, const QJsonObject &body) { + if (!m_manager) m_manager = new QNetworkAccessManager(this); + + QNetworkRequest request(QUrl(m_apiBase + path)); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", + ("Bearer " + SettingsDAO::get("access_token")).toUtf8()); + + QNetworkReply *reply = m_manager->post(request, QJsonDocument(body).toJson()); + QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(30000); + loop.exec(); + + QJsonValue result; + if (!timeout.isActive()) { + qCritical().noquote() << "DeviceScreener apiPost timeout:" + << NetworkErrorUtils::describeClientTimeout(reply, path); + reply->abort(); + } else if (reply->error() == QNetworkReply::NoError) { + const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + if (doc.isObject()) { + const QJsonObject root = doc.object(); + if (root["success"].toBool()) result = root["data"]; + else qCritical() << "DeviceScreener apiPost error:" << root; + } + } else { + qCritical().noquote() << "DeviceScreener apiPost error:" + << NetworkErrorUtils::describeNetError(reply, path); + } + reply->deleteLater(); + return result; +} + +void DeviceScreener::apiPatch(const QString &path, const QJsonObject &body) { + if (!m_manager) m_manager = new QNetworkAccessManager(this); + + QNetworkRequest request(QUrl(m_apiBase + path)); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", + ("Bearer " + SettingsDAO::get("access_token")).toUtf8()); + + QNetworkReply *reply = m_manager->sendCustomRequest(request, "PATCH", QJsonDocument(body).toJson()); + QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(30000); + loop.exec(); + + if (!timeout.isActive()) { + qCritical().noquote() << "DeviceScreener apiPatch timeout:" + << NetworkErrorUtils::describeClientTimeout(reply, path); + reply->abort(); + } else if (reply->error() != QNetworkReply::NoError) { + qCritical().noquote() << "DeviceScreener apiPatch error:" + << NetworkErrorUtils::describeNetError(reply, path); + } + reply->deleteLater(); +} + +QString DeviceScreener::createDeviceOnApi(const DeviceInfo &device) { + const QString desktopId = SettingsDAO::get("desktop_id"); + if (desktopId.isEmpty()) { + qWarning() << "createDeviceOnApi: desktop_id не задан"; + return {}; + } + + QJsonObject body; + body["desktop_id"] = desktopId; + body["bank_profile_id"] = QJsonValue::Null; + body["status"] = "ONLINE"; + body["name"] = device.name; + body["screen_width"] = device.screenWidth; + body["screen_height"] = device.screenHeight; + body["battery"] = device.battery; + + const QJsonValue result = apiPost("/api/v1/device/", body); + if (!result.isNull() && !result.isUndefined()) { + return result.toObject()["id"].toString(); + } + return {}; +} + +void DeviceScreener::postScreenshotToApi(const QString &apiId, const QByteArray &image) { + if (!m_manager) m_manager = new QNetworkAccessManager(this); + + const QString path = "/api/v1/device/add_screenshot/" + apiId; + QNetworkRequest request(QUrl(m_apiBase + path)); + request.setRawHeader("Authorization", + ("Bearer " + SettingsDAO::get("access_token")).toUtf8()); + + auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + QHttpPart filePart; + filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg"); + filePart.setHeader(QNetworkRequest::ContentDispositionHeader, + QStringLiteral("form-data; name=\"file\"; filename=\"screenshot.jpg\"")); + filePart.setBody(image); + multiPart->append(filePart); + + QNetworkReply *reply = m_manager->post(request, multiPart); + multiPart->setParent(reply); + QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(30000); + loop.exec(); + + if (!timeout.isActive()) { + qWarning().noquote() << "postScreenshotToApi timeout:" << apiId + << NetworkErrorUtils::describeClientTimeout(reply, path); + reply->abort(); + } else if (reply->error() != QNetworkReply::NoError) { + qWarning().noquote() << "postScreenshotToApi error:" << apiId + << NetworkErrorUtils::describeNetError(reply, path); + } else { + const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + if (doc.isObject() && doc.object()["success"].toBool()) { + // ok + } else { + qWarning() << "postScreenshotToApi failed for device" << apiId << ":" << doc.toJson(QJsonDocument::Compact); + } + } + reply->deleteLater(); +} + +void DeviceScreener::updateDeviceOnApi(const DeviceInfo &device) { + // Находим первый активный bank_profile для этого устройства + QJsonValue bankProfileId = QJsonValue::Null; + const QList apps = BankProfileDAO::getAllApplicationsByDeviceId(device.id); + for (const BankProfileInfo &app : apps) { + if (!app.bankProfileId.isEmpty() && app.status == "active") { + bankProfileId = app.bankProfileId; + break; + } + } + + QJsonObject body; + body["status"] = (device.status == "device") ? "ONLINE" : "OFFLINE"; + body["bank_profile_id"] = bankProfileId; + body["battery"] = device.battery; + body["update_time"] = QDateTime::currentSecsSinceEpoch(); + apiPatch("/api/v1/device/" + device.apiId, body); +} + +QJsonValue DeviceScreener::apiGet(const QString &path) { + if (!m_manager) m_manager = new QNetworkAccessManager(this); + + QNetworkRequest request(QUrl(m_apiBase + path)); + request.setRawHeader("Authorization", + ("Bearer " + SettingsDAO::get("access_token")).toUtf8()); + + QNetworkReply *reply = m_manager->get(request); + QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); }); + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(30000); + loop.exec(); + + QJsonValue result; + if (!timeout.isActive()) { + qCritical().noquote() << "DeviceScreener apiGet timeout:" + << NetworkErrorUtils::describeClientTimeout(reply, path); + reply->abort(); + } else if (reply->error() == QNetworkReply::NoError) { + const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + if (doc.isObject()) { + const QJsonObject root = doc.object(); + if (root["success"].toBool()) result = root["data"]; + else qCritical() << "DeviceScreener apiGet error:" << root; + } + } else { + qCritical().noquote() << "DeviceScreener apiGet error:" + << NetworkErrorUtils::describeNetError(reply, path); + } + reply->deleteLater(); + return result; +} + + +void DeviceScreener::start() { + while (m_running) { + QList > devices = readAdbDevices(); + QSet liveSet; // устройства, видимые как "device" в текущем скане + const qint64 nowSec = QDateTime::currentSecsSinceEpoch(); + + for (const auto &[adbSerial, status]: devices) { DeviceInfo device; - device.id = id; if (status == "device") { - onlineIds << device.id; - device = readDeviceInfo(id); - takeScreenshot(id); + device = readDeviceInfo(adbSerial); + + // Проверяем: есть ли уже устройство с таким id в БД (с apiId) + const DeviceInfo existingById = DeviceDAO::getDeviceById(device.id); + if (!existingById.id.isEmpty() && !existingById.apiId.isEmpty()) { + device.apiId = existingById.apiId; + } else { + // Маппинг с восстановленным устройством — только по name (точное совпадение) + const DeviceInfo restored = DeviceDAO::findByName(device.name); + qDebug() << "[DeviceScreener] findByName(" << device.name << ") →" + << "id:" << restored.id << "apiId:" << restored.apiId + << "match:" << (!restored.id.isEmpty() && restored.id != device.id && !restored.apiId.isEmpty()); + if (!restored.id.isEmpty() && restored.id != device.id && !restored.apiId.isEmpty()) { + device.apiId = restored.apiId; + // Сначала создаём новое устройство (FK target), затем мигрируем, затем удаляем старое + DeviceDAO::upsertDevice(device); + const bool bpOk = BankProfileDAO::migrateDeviceId(restored.id, device.id); + const bool matOk = MaterialDAO::migrateDeviceId(restored.id, device.id); + qDebug() << "[DeviceScreener] migration result: bp=" << bpOk << "mat=" << matOk; + DeviceDAO::deleteDevice(restored.id); + qDebug() << "[DeviceScreener] matched restored device:" << restored.id << "->" << device.id + << "apiId:" << device.apiId; + } + } + + liveSet.insert(device.id); + + device.image = takeScreenshot(adbSerial); + if (!device.image.isEmpty()) { + if (!DeviceDAO::updateImage(device.id, device.image)) { + qDebug() << "Cant update screenshot"; + } + } + } else { + device.id = adbSerial; } device.status = status; device.updateTime = QDateTime::currentDateTime(); - // qDebug() << "Update device: " << DeviceDAO::upsertDevice(device); + if (status == "device") { + if (!DeviceDAO::upsertDevice(device)) { + qDebug() << "Cant update device"; + } + + // Проверяем установленные приложения каждый цикл + QSet apps = AdbUtils::getListApps(adbSerial); + if (!BankProfileDAO::checkInstalledApps(device.id, apps)) { + qWarning() << "Cant check installed apps"; + } + + // Обновляем версии установленных банков (не чаще раза в 6 часов на профиль) + constexpr qint64 kAppVersionRefreshSecs = 6 * 60 * 60; + const QDateTime nowUtc = QDateTime::currentDateTimeUtc(); + for (const BankProfileInfo &profile : BankProfileDAO::getApplicationsByDeviceId(device.id)) { + if (profile.package.isEmpty()) continue; + const bool stale = !profile.appVersionCheckedAt.isValid() + || profile.appVersionCheckedAt.secsTo(nowUtc) >= kAppVersionRefreshSecs; + if (!stale) continue; + + const QString ver = AdbUtils::getAppVersion(adbSerial, profile.package); + if (ver.isEmpty()) continue; // не затираем известную версию пустотой + if (!BankProfileDAO::updateAppVersion(profile.id, ver)) { + qWarning() << "Cant update app_version for" << profile.code << profile.package; + } + } + } + + // Синхронизация с API + DeviceInfo saved = DeviceDAO::getDeviceById(device.id); + if (saved.apiId.isEmpty() && !device.name.isEmpty()) { + // Устройство ещё не зарегистрировано — создаём + const QString apiId = createDeviceOnApi(device); + if (!apiId.isEmpty()) { + if (!DeviceDAO::updateApiId(device.id, apiId)) { + qWarning() << "Failed to save api_id for device:" << device.id; + } + qDebug() << "Device registered on API:" << device.id << "->" << apiId; + } + } else if (status == "device") { + // Обновляем не чаще раза в 60 секунд + const qint64 now = QDateTime::currentSecsSinceEpoch(); + if (now - m_lastApiUpdate.value(device.id, 0) >= 60) { + device.apiId = saved.apiId; + updateDeviceOnApi(device); + if (!device.image.isEmpty()) { + postScreenshotToApi(saved.apiId, device.image); + } + m_lastApiUpdate[device.id] = now; + } + } } - // qDebug() << "All devices offline: " << DeviceDAO::markDevicesOfflineExcept(onlineIds); - QThread::sleep(3); + + // Detect transitions: + // 1) Online: устройство есть в liveSet, но ещё не в m_onlineDevices — мгновенно + for (const QString &id : liveSet) { + if (!m_onlineDevices.contains(id)) { + m_onlineDevices.insert(id); + m_offlineSince.remove(id); + qDebug() << "[DeviceScreener] device went ONLINE:" << id; + emit deviceWentOnline(id); + } else { + // Устройство всё ещё видно — сбрасываем pending offline-дебаунс (флап) + m_offlineSince.remove(id); + } + } + + // 2) Offline: устройство было в m_onlineDevices, но больше не видно + // Засекаем m_offlineSince; срабатывает после 30s непрерывного отсутствия. + QSet goneOffline; + for (const QString &id : m_onlineDevices) { + if (liveSet.contains(id)) continue; + if (!m_offlineSince.contains(id)) { + m_offlineSince[id] = nowSec; + qDebug() << "[DeviceScreener] device missing, offline debounce started:" << id; + } else if (nowSec - m_offlineSince[id] >= kOfflineDebounceSecs) { + goneOffline.insert(id); + } + } + for (const QString &id : goneOffline) { + m_onlineDevices.remove(id); + m_offlineSince.remove(id); + qDebug() << "[DeviceScreener] device went OFFLINE (debounced):" << id; + // Сообщаем серверу что устройство офлайн (отдельно от bank_profile-синка) + const DeviceInfo offlineDevice = DeviceDAO::getDeviceById(id); + if (!offlineDevice.apiId.isEmpty()) { + DeviceInfo offlineCopy = offlineDevice; + offlineCopy.status = "offline"; + updateDeviceOnApi(offlineCopy); + } + emit deviceWentOffline(id); + } + + // БД status: считаем онлайном только debounced-онлайн. + QStringList debouncedOnlineList; + for (const QString &id : m_onlineDevices) debouncedOnlineList << id; + if (!DeviceDAO::markDevicesOfflineExcept(debouncedOnlineList)) { + qDebug() << "Cant mark devices offline"; + } + + if (m_firstScan) { + m_firstScan = false; + qDebug() << "[DeviceScreener] first pass done, online devices:" << m_onlineDevices; + emit firstPassDone(); + } + QThread::msleep(1000); } emit finished(); } -QByteArray convertPngToJpeg(const QByteArray& pngData, const int quality = 90) { +QByteArray convertPngToJpeg(const QByteArray &pngData, const int quality = 90) { QImage image; if (!image.loadFromData(pngData, "PNG")) { qWarning() << "Не удалось загрузить PNG из байт"; return {}; } - // if (!image.save("test.jpg", "JPG", quality)) { - // qWarning() << "Не удалось сохранить как JPG:" << "test.jpg"; - // return {}; - // } - QByteArray jpgData; QBuffer buffer(&jpgData); buffer.open(QIODevice::WriteOnly); @@ -59,43 +476,49 @@ QByteArray convertPngToJpeg(const QByteArray& pngData, const int quality = 90) { return jpgData; } -bool DeviceScreener::takeScreenshot(const QString& deviceId) { +QByteArray DeviceScreener::takeScreenshot(const QString &deviceId) { QProcess process; + process.setWorkingDirectory(QFileInfo(AdbUtils::adbPath()).absolutePath()); QStringList arguments; arguments << "-s" << deviceId << "exec-out" << "screencap" << "-p"; - process.start("adb", arguments); + process.start(AdbUtils::adbPath(), arguments); if (!process.waitForFinished(5000)) { qWarning() << "ADB скриншот не завершился для" << deviceId; - return false; + process.kill(); + process.waitForFinished(1000); + return {}; } const QByteArray imageData = process.readAllStandardOutput(); if (imageData.isEmpty()) { qWarning() << "Скриншот пустой для " << deviceId; - return false; + return {}; } - // Обновляем изображение в БД - if (!DeviceDAO::updateImage(deviceId, convertPngToJpeg(imageData, 90))) { - qWarning() << "Не удалось записать изображение в БД для" << deviceId; - return false; - } - - qDebug() << "Screenshot saved, device: " << deviceId; - return true; + return convertPngToJpeg(imageData, 90); } -QList> DeviceScreener::readAdbDevices() { +QList > DeviceScreener::readAdbDevices() { QProcess process; - process.start("adb", QStringList() << "devices"); - process.waitForFinished(); + process.setWorkingDirectory(QFileInfo(AdbUtils::adbPath()).absolutePath()); + process.start(AdbUtils::adbPath(), QStringList() << "devices"); + + if (!process.waitForStarted(5000)) { + qWarning() << "[DeviceScreener] Failed to start adb devices:" << process.errorString(); + return {}; + } + if (!process.waitForFinished(10000)) { + qWarning() << "[DeviceScreener] adb devices timeout"; + process.kill(); + process.waitForFinished(1000); + return {}; + } const QString output = process.readAllStandardOutput(); - // qDebug() << "[adb devices output]:\n" << output; - QList> devices; + QList > devices; const QStringList lines = output.split('\n'); for (int i = 1; i < lines.size(); ++i) { QString line = lines.at(i).trimmed(); @@ -111,8 +534,8 @@ QList> DeviceScreener::readAdbDevices() { */ // Разделяем строку на id и type по табуляции if (QStringList parts = line.split('\t'); parts.size() == 2) { - const QString& id = parts.at(0); - const QString& status = parts.at(1); + const QString &id = parts.at(0); + const QString &status = parts.at(1); devices.append(qMakePair(id, status)); } } @@ -122,21 +545,26 @@ QList> DeviceScreener::readAdbDevices() { static const QRegularExpression screenRegex(R"(Physical size: (\d+)x(\d+))"); static const QRegularExpression batteryRegex(R"(level:\s*(\d+))"); -DeviceInfo DeviceScreener::readDeviceInfo(const QString& deviceId) { +DeviceInfo DeviceScreener::readDeviceInfo(const QString &adbSerial) { DeviceInfo info; - info.id = deviceId; info.screenWidth = 0; info.screenHeight = 0; info.battery = 0; - auto runAdbCommand = [&](const QStringList& args) -> QString { + auto runAdbCommand = [&](const QStringList &args) -> QString { QProcess process; - process.start("adb", QStringList{"-s", deviceId} + args); + process.setWorkingDirectory(QFileInfo(AdbUtils::adbPath()).absolutePath()); + process.start(AdbUtils::adbPath(), QStringList{"-s", adbSerial} + args); process.waitForFinished(); return QString::fromUtf8(process.readAllStandardOutput()).trimmed(); }; - info.name = runAdbCommand({"shell", "getprop", "ro.product.model"}); + const QString androidId = runAdbCommand({"shell", "settings", "get", "secure", "android_id"}); + const QString model = runAdbCommand({"shell", "getprop", "ro.product.model"}); + + info.id = androidId.isEmpty() ? adbSerial : androidId; + info.adbSerial = adbSerial; + info.name = model + "-" + info.id; info.android = runAdbCommand({"shell", "getprop", "ro.build.version.release"}); // Размер экрана diff --git a/services/DeviceScreener.h b/services/DeviceScreener.h index c35e41a..26da719 100644 --- a/services/DeviceScreener.h +++ b/services/DeviceScreener.h @@ -1,5 +1,8 @@ #pragma once #include +#include +#include +#include #include #include @@ -11,6 +14,8 @@ public: ~DeviceScreener() override; + static constexpr qint64 kOfflineDebounceSecs = 30; + public slots: void start(); @@ -18,13 +23,34 @@ public slots: signals: void finished(); + void firstPassDone(); + void deviceWentOnline(const QString &deviceId); + void deviceWentOffline(const QString &deviceId); private: bool m_running = true; + bool m_firstScan = true; + QString m_urlAppUpdate; + QString m_token; + QString m_apiBase; + QNetworkAccessManager *m_manager = nullptr; + QMap m_lastApiUpdate; // deviceId → unix timestamp последнего PATCH + QSet m_onlineDevices; // дебаунс-онлайн (true online после dedup) + QMap m_offlineSince; // deviceId → когда пропал из ADB (для дебаунса) static QList > readAdbDevices(); - static bool takeScreenshot(const QString &deviceId); + static QByteArray takeScreenshot(const QString &deviceId); - static DeviceInfo readDeviceInfo(const QString &deviceId); + static DeviceInfo readDeviceInfo(const QString &adbSerial); + + void postDevicesData(const QList &devices); + + QJsonValue apiGet(const QString &path); + QJsonValue apiPost(const QString &path, const QJsonObject &body); + void apiPatch(const QString &path, const QJsonObject &body); + + QString createDeviceOnApi(const DeviceInfo &device); + void updateDeviceOnApi(const DeviceInfo &device); + void postScreenshotToApi(const QString &apiId, const QByteArray &image); }; diff --git a/services/EventHandler.cpp b/services/EventHandler.cpp new file mode 100644 index 0000000..de89dfc --- /dev/null +++ b/services/EventHandler.cpp @@ -0,0 +1,752 @@ +#include "EventHandler.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "dao/EventDAO.h" +#include "dao/SettingsDAO.h" +#include "dao/TransactionDAO.h" +#include "db/BankProfileInfo.h" +#include "adb/AdbUtils.h" +#include "dao/GeneralLogDAO.h" +#include "net/NetworkService.h" +#include "black/GetLastTransactionsScript.h" +#include "black/LoginAndCheckAccountsScript.h" +#include "ozon/GetLastTransactionsScript.h" +#include "ozon/LoginAndCheckAccountsScript.h" +#include "black/PayByCardScript.h" +#include "ozon/PayByCardScript.h" +#include "ozon/PayByPhoneScript.h" +#include "dushanbe/LoginAndCheckAccountsScript.h" +#include "dushanbe/GetLastTransactionsScript.h" +#include "dushanbe/PayByCardScript.h" +#include "dushanbe/PayByPhoneScript.h" + +static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName, const QJsonValue &data, int eventId = -1); +static void sendTaskError(const QString &type, const QString &materialId, const QString &bankName, const QString &description, const QByteArray &screenshot = {}, int eventId = -1); +static void sendMonitoringLog(const QString &type, const QString &event, const QString &message, const QByteArray &image = {}, int eventId = -1, bool toApiBase = false); + +EventHandler::EventHandler(QObject *parent) : QObject(parent) {} + +EventHandler::~EventHandler() = default; + +void EventHandler::start() { + qDebug() << "[EventHandler] start() called, thread:" << QThread::currentThread(); + m_network = new NetworkService(this); + connect(m_network, &NetworkService::tasksReceived, this, &EventHandler::onTasksReceived); + + m_pollTimer = new QTimer(this); + m_pollTimer->setInterval(5000); + connect(m_pollTimer, &QTimer::timeout, this, [this]() { + // Пробуем запустить задачи из очереди для свободных устройств + const QStringList onlineDevices = DeviceDAO::getOnlineDeviceCodes(); + for (const QString &deviceId : onlineDevices) { + processNextTask(deviceId); + } + + // Запрашиваем новые задачи только для устройств без pending events + const QSet busyDevices = EventDAO::getDevicesWithPendingEvents(); + const QStringList materialIds = MaterialDAO::getActiveMaterialIdsExcludingDevices(busyDevices); + if (!materialIds.isEmpty()) { + m_network->getTasks(materialIds); + } else { + qDebug() << "[EventHandler] No free materials to poll." + << "busyDevices:" << busyDevices + << "onlineDevices:" << onlineDevices; + } + }); + m_pollTimer->start(); + + // Закрываем зависшие события с прошлого запуска (только не отправленные на сервер) + for (const EventStatus st : {EventStatus::Wait, EventStatus::InProgress}) { + const QList stale = EventDAO::getAllEvents(st); + for (const EventInfo &e : stale) { + if (e.sentToServer) continue; + EventDAO::updateEvent(e.id, EventStatus::Error, "App restarted"); + EventDAO::markSentToServer(e.id); + sendTaskError(eventTypeToString(e.type), e.externalId, e.bankName, "App restarted, event was pending"); + qDebug() << "[EventHandler] Closed stale event id:" << e.id; + } + } + + // Первый запрос сразу при старте + qDebug() << "[EventHandler] starting first getTasks()"; + m_network->getTasks(); +} + +void EventHandler::onTasksReceived(const QJsonArray &tasks) { + if (!m_running) return; + if (!tasks.isEmpty()) + qDebug() << "[EventHandler] onTasksReceived, count:" << tasks.size(); + + // Получаем список онлайн устройств один раз + const QStringList onlineDevices = DeviceDAO::getOnlineDeviceCodes(); + + // 1. Сохраняем новые задачи в БД + for (const auto &taskVal : tasks) { + const QJsonObject task = taskVal.toObject(); + const QString bankName = task["bank_name"].toString(); + const QString materialId = task["material_id"].isString() + ? task["material_id"].toString() + : QString::number(task["material_id"].toInt()); + const QString typeStr = task["type"].toString(); + + const EventType eventType = eventTypeFromString(typeStr); + + if (eventType == EventType::Unknown) { + qWarning() << "[EventHandler] Unknown task type:" << typeStr; + continue; + } + + // Ищем аккаунт по material_id напрямую + const MaterialInfo account = MaterialDAO::findByMaterialId(materialId); + if (account.id == -1) { + qWarning() << "[EventHandler] Account not found for bank:" << bankName << "material_id:" << materialId; + sendTaskError(typeStr, materialId, bankName, "Account not found for material_id: " + materialId); + continue; + } + + // FETCH_PROFILE: отменяем старые ожидающие задачи такого же типа для этого материала + if (eventType == EventType::FetchProfile) { + const QList cancelled = EventDAO::cancelWaitingByMaterial(materialId, EventType::FetchProfile); + for (const EventInfo &c : cancelled) { + sendTaskError(eventTypeToString(c.type), c.externalId, c.bankName, "Cancelled: replaced by new task"); + EventDAO::markSentToServer(c.id); + } + } + + EventInfo event; + event.status = EventStatus::Wait; + event.type = eventType; + event.externalId = materialId; + event.accountId = account.id; + event.deviceId = account.deviceId; + event.bankName = bankName; + event.timestamp = QDateTime::currentDateTimeUtc(); + event.json = QJsonDocument(task).toJson(QJsonDocument::Compact); + + const QJsonObject body = task["body"].toObject(); + if (eventType == EventType::FetchTransactions) { + event.amount = body["amount"].toDouble() / 100.0; + } + if (eventType == EventType::CreateTransaction) { + event.amount = body["amount"].toDouble() / 100.0; + event.phone = body["bank_participator_id"].toString(); + + // Название банка-получателя из extra + const QString bankNameRu = body["extra"].toObject()["bank_participator_name_ru"].toString(); + if (!bankNameRu.isEmpty()) { + event.comment = bankNameRu; + } + } + + const int eventId = EventDAO::insertEvent(event); + if (eventId == -1) { + qCritical() << "[EventHandler] Failed to insert event, external_id:" << materialId; + continue; + } + + // Если устройство офлайн — сразу отправляем ошибку на сервер + if (!onlineDevices.contains(account.deviceId)) { + qWarning() << "[EventHandler] Device offline for material_id:" << materialId << "device:" << account.deviceId; + EventDAO::updateEvent(eventId, EventStatus::Error, "Device is offline"); + sendTaskError(typeStr, materialId, bankName, "Device is offline"); + } + } + + // 2. Для каждого онлайн устройства без активного потока — запускаем следующую задачу + for (const QString &deviceId : onlineDevices) { + processNextTask(deviceId); + } +} + +static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName, + const QJsonValue &data, int eventId) { + QJsonObject obj; + obj["type"] = type; + obj["material_id"] = materialId; // UUID string + obj["bank_name"] = bankName; + obj["data"] = data; + if (eventId > 0) + obj["internal_task_id"] = eventId; + + auto *thread = new QThread; + auto *networkService = new NetworkService; + networkService->moveToThread(thread); + networkService->setPayload(QJsonDocument(obj).toJson()); + QObject::connect(thread, &QThread::started, networkService, &NetworkService::postTaskResult); + QObject::connect(networkService, &NetworkService::finishedWithResult, networkService, + [eventId](int result) { + if (eventId != -1) { + if (result == 0) { + EventDAO::markSentToServer(eventId); + } else { + EventDAO::updateEvent(eventId, EventStatus::Error, "Failed to send result to server"); + qWarning() << "[EventHandler] Task result not sent, marking event" << eventId << "as error"; + } + } + }); + QObject::connect(networkService, &NetworkService::finished, thread, &QThread::quit); + QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +static void sendTaskError(const QString &type, const QString &materialId, const QString &bankName, + const QString &description, const QByteArray &screenshot, int eventId) { + QJsonObject errorData; + errorData["status"] = QStringLiteral("error"); + errorData["event_type"] = type; + errorData["description"] = description; + sendTaskResult(QStringLiteral("ERROR"), materialId, bankName, errorData, eventId); + + if (!screenshot.isEmpty()) { + const QString apiToken = SettingsDAO::get("api_key"); + QStringList lines; + lines << QStringLiteral("[TASK_ERROR] [%1] [%2]") + .arg(type.toHtmlEscaped(), bankName.toUpper().toHtmlEscaped()); + if (!apiToken.isEmpty()) + lines << QStringLiteral("[%1]").arg(apiToken.toHtmlEscaped()); + lines << QStringLiteral("[%1]").arg(materialId.toHtmlEscaped()); + lines << description.toHtmlEscaped(); + const QString message = lines.join(QStringLiteral("\n")); + sendMonitoringLog("error", "task_error", message, screenshot, eventId); + } + + // application.log + БД отправляются из TaskDumpRecorder::finishInternal, + // чтобы покрывать также non-event-driven задачи (login flow и т.п.). +} + +static void sendMonitoringLog(const QString &type, const QString &event, const QString &message, + const QByteArray &image, int eventId, bool toApiBase) { + auto *thread = new QThread; + auto *networkService = new NetworkService; + networkService->moveToThread(thread); + if (eventId > 0) + networkService->addExtra("internal_task_id", eventId); + QObject::connect(thread, &QThread::started, networkService, [networkService, type, event, message, image, toApiBase]() { + networkService->postMonitoringLog(type, event, message, image, toApiBase); + emit networkService->finished(); + }); + QObject::connect(networkService, &NetworkService::finished, thread, &QThread::quit); + QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +static void sendEventStatus(const QString &deviceId, const QString &externalId, const int eventId, + const QString &status, const QString &error) { + QJsonObject obj; + obj["id"] = externalId; + obj["externalId"] = eventId; + obj["status"] = status; + obj["error"] = error; + + auto *paymentThread = new QThread; + auto *networkService = new NetworkService; + networkService->moveToThread(paymentThread); + networkService->setDeviceId(deviceId); + networkService->setPayload(QJsonDocument(obj).toJson()); + QObject::connect(paymentThread, &QThread::started, networkService, &NetworkService::postEventStatus); + QObject::connect(networkService, &NetworkService::finished, paymentThread, &QThread::quit); + QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater); + QObject::connect(paymentThread, &QThread::finished, paymentThread, &QThread::deleteLater); + paymentThread->start(); +} + +void EventHandler::updateEventThread(const QString &key, const EventInfo &event, const QString &result) { + + const EventStatus newStatus = result.isEmpty() ? EventStatus::Complete : EventStatus::Error; + + if (!EventDAO::updateEvent(event.id, newStatus, result)) { + qCritical() << "[EventHandler] Failed to update event:" << event.id; + } else { + if (newStatus == EventStatus::Error) { + // Используем скриншот из скрипта если есть, иначе снимаем новый + QByteArray screenshot = m_screenshots.take(key); + if (screenshot.isEmpty() && !event.deviceId.isEmpty()) { + const DeviceInfo device = DeviceDAO::getDeviceById(event.deviceId); + const QString adbSerial = device.adbSerial.isEmpty() ? event.deviceId : device.adbSerial; + screenshot = AdbUtils::captureScreenshotBytes(adbSerial); + } + if (!screenshot.isEmpty()) { + EventDAO::updateScreenshot(event.id, screenshot); + } + // Дублируем ошибку в general_logs со скриншотом + const DeviceInfo logDevice = DeviceDAO::getDeviceById(event.deviceId); + const QString deviceLabel = logDevice.name.isEmpty() ? event.deviceId : logDevice.name; + const QString logMsg = QString("[%1] device=%2 bank=%3: %4") + .arg(eventTypeToString(event.type), deviceLabel, event.bankName, result); + GeneralLogDAO::insert("CRITICAL", "EventHandler", logMsg, screenshot); + + sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, result, screenshot, event.id); + + if (event.type == EventType::CreateTransaction) { + const MaterialInfo declAcc = MaterialDAO::getAccountById(event.accountId); + const QMap declCurrencyCodes = { + {"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972} + }; + const int declCurrencyCode = declCurrencyCodes.value(declAcc.currency.toUpper(), 643); + const QString declTxType = QJsonDocument::fromJson(event.json.toUtf8()) + .object()["body"].toObject()["type"].toString(); + + QString declTxId; + const QList declTxList = TransactionDAO::getTransactionsWithinHours(event.accountId, 1); + for (const auto &tx : declTxList) { + if (!tx.bankTrExternalId.isEmpty() && qAbs(tx.amount) == event.amount && tx.phone == event.phone) { + declTxId = tx.bankTrExternalId; + break; + } + } + // Скрипт мог упасть до записи транзакции в БД — сервер всё равно + // ждёт уникальный bank_transaction_id. Генерируем по той же схеме, + // что и в success-ветке скриптов: __. + if (declTxId.isEmpty()) { + const BankProfileInfo declApp = BankProfileDAO::getApplication(declAcc.deviceId, event.bankName); + declTxId = event.bankName + "_" + declApp.phone + "_" + + QString::number(QDateTime::currentSecsSinceEpoch()); + } + + QJsonObject declined; + declined["bank_transaction_id"] = declTxId; + declined["amount"] = -static_cast(event.amount * 100); + declined["currency"] = declCurrencyCode; + declined["transaction_type"] = declTxType.isEmpty() ? QStringLiteral("top-up") : declTxType; + declined["bank_participator_id"] = event.phone; + declined["status"] = QStringLiteral("declined"); + declined["extra"] = QJsonObject(); + declined["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + sendTaskResult(QStringLiteral("CREATE_TRANSACTION"), event.externalId, event.bankName, declined, -1); + } + + EventDAO::markSentToServer(event.id); + } + + if (newStatus != EventStatus::Error) { + const MaterialInfo account = MaterialDAO::getAccountById(event.accountId); + const QMap currencyCodes = { + {"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972} + }; + const int currencyCode = currencyCodes.value(account.currency.toUpper(), 643); + + QJsonValue taskData; + if (event.type == EventType::FetchProfile) { + // FETCH_PROFILE: баланс из bank_profiles (скрипт обновляет через + // BankProfileDAO::updateAmount), fallback на materials. + const BankProfileInfo freshApp = BankProfileDAO::getApplication( + account.deviceId, event.bankName); + double freshAmount = (freshApp.id != -1 && freshApp.amount > 0.0) + ? freshApp.amount : account.amount; + EventDAO::updateEventAmount(event.id, freshAmount); + QJsonObject obj; + obj["bank_account_id"] = account.materialId; + obj["amount"] = static_cast(freshAmount * 100); + obj["currency"] = currencyCode; + taskData = obj; + } else if (event.type == EventType::FetchTransactions) { + // FETCH_TRANSACTIONS больше НЕ читает из БД — берём то, что + // скрипт распарсил вживую и передал через сигнал transactionParsed. + const QJsonObject parsedTx = m_parsedTxs.take(key); + if (parsedTx.isEmpty()) { + qWarning() << "[EventHandler] FETCH_TRANSACTIONS: script did not emit parsed tx"; + sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, + "Transaction not parsed from screen"); + EventDAO::updateEvent(event.id, EventStatus::Error, + "Transaction not parsed from screen"); + EventDAO::markSentToServer(event.id); + m_threads.remove(key); + return; + } + QJsonArray arr; + arr.append(parsedTx); + taskData = arr; + } else if (event.type == EventType::CreateTransaction) { + // Определяем тип транзакции из JSON задачи + const QJsonObject taskJson = QJsonDocument::fromJson(event.json.toUtf8()).object(); + const QString txType = taskJson["body"].toObject()["type"].toString(); + + QJsonObject obj; + obj["amount"] = -static_cast(event.amount * 100); + obj["currency"] = currencyCode; + obj["transaction_type"] = txType.isEmpty() ? "top-up" : txType; + obj["bank_participator_id"]= event.phone; + obj["status"] = "completed"; + obj["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + // Берём bank_transaction_id и fee из последней сохранённой транзакции + QList txList = TransactionDAO::getTransactionsWithinHours(event.accountId, 1); + int feeInCents = 0; + for (const auto &tx : txList) { + if (!tx.bankTrExternalId.isEmpty() && qAbs(tx.amount) == event.amount && tx.phone == event.phone) { + obj["bank_transaction_id"] = tx.bankTrExternalId; + feeInCents = static_cast(tx.fee * 100); + break; + } + } + + QJsonObject extraObj; + extraObj["fee"] = feeInCents; + obj["extra"] = extraObj; + + taskData = obj; + } else { + QJsonObject obj; + obj["status"] = "completed"; + taskData = obj; + } + sendTaskResult( + eventTypeToString(event.type), + event.externalId, + event.bankName, + taskData, + event.id + ); + } + + if (newStatus == EventStatus::Complete) { + const DeviceInfo device = DeviceDAO::getDeviceById(event.deviceId); + const QString adbSerial = device.adbSerial.isEmpty() ? event.deviceId : device.adbSerial; + + if (event.type == EventType::CreateTransaction) { + const QString msg = QStringLiteral("[CREATE_TRANSACTION] bank=%1 material=%2 amount=%3 phone=%4") + .arg(event.bankName, event.externalId, QString::number(event.amount, 'f', 2), event.phone); + QByteArray screenshot; + const QString tmpPath = QDir::tempPath() + "/arc_tx_" + QString::number(event.id); + if (AdbUtils::takeScreenshot(event.deviceId, tmpPath)) { + QFile f(tmpPath + ".png"); + if (f.open(QIODevice::ReadOnly)) { + screenshot = f.readAll(); + f.close(); + } + f.remove(); + } + sendMonitoringLog("info", "task_success", msg, screenshot, event.id, /*toApiBase=*/true); + + // Декрементируем материальный/профильный баланс на сумму расхода + комиссию. + // Между запусками FETCH_PROFILE это держит UI/локальный баланс + // в актуальном виде; drift по входящим всё равно сбросит следующий re-parse. + const MaterialInfo acc = MaterialDAO::getAccountById(event.accountId); + if (acc.id != -1) { + double fee = 0.0; + const QList txList = + TransactionDAO::getTransactionsWithinHours(event.accountId, 1); + for (const auto &tx : txList) { + if (!tx.bankTrExternalId.isEmpty() + && qAbs(tx.amount) == event.amount + && tx.phone == event.phone) { + fee = tx.fee; + break; + } + } + const double delta = event.amount + fee; + + // Для Dushanbe все карты сидят на одном счёте — двигаем + // все материалы (см. memory: project_dushanbe_single_account). + if (acc.appCode == "dushanbe_city_bank") { + if (!MaterialDAO::adjustAmountByApp(acc.deviceId, acc.appCode, delta)) { + qWarning() << "[EventHandler] adjustAmountByApp failed for" + << acc.deviceId << acc.appCode; + } + } else { + if (!MaterialDAO::adjustAmount(acc.id, delta)) { + qWarning() << "[EventHandler] adjustAmount failed for material" << acc.id; + } + } + + const BankProfileInfo app = BankProfileDAO::getApplication(acc.deviceId, acc.appCode); + if (app.id != -1) { + if (!BankProfileDAO::adjustAmount(app.id, delta)) { + qWarning() << "[EventHandler] BankProfileDAO::adjustAmount failed for app" << app.id; + } + } + qDebug() << "[EventHandler] balance decremented: material" << acc.id + << "app" << acc.appCode << "delta=" << delta + << "(amount=" << event.amount << "fee=" << fee << ")"; + } + } + + if (event.type == EventType::FetchTransactions) { + const QString msg = QStringLiteral("[FETCH_TRANSACTIONS] bank=%1 material=%2") + .arg(event.bankName, event.externalId); + const QByteArray scr = m_screenshots.take(key); + sendMonitoringLog("info", "task_success", msg, scr, event.id, /*toApiBase=*/true); + } + + if (event.type == EventType::FetchProfile) { + const QString msg = QStringLiteral("[FETCH_PROFILE] bank=%1 material=%2") + .arg(event.bankName, event.externalId); + const QByteArray scr = m_screenshots.take(key); + sendMonitoringLog("info", "task_success", msg, scr, event.id, /*toApiBase=*/true); + } + } + } + + if (const auto tm = m_timers.take(key)) { + tm->stop(); + tm->deleteLater(); + } + m_threads.remove(key); + + // Убиваем банковское приложение на устройстве после завершения задачи + { + const MaterialInfo acc = MaterialDAO::getAccountById(event.accountId); + const BankProfileInfo app = BankProfileDAO::getApplication(event.deviceId, acc.appCode); + if (!app.package.isEmpty()) { + const DeviceInfo device = DeviceDAO::getDeviceById(event.deviceId); + const QString adbSerial = device.adbSerial.isEmpty() ? event.deviceId : device.adbSerial; + AdbUtils::tryToKillApplication(adbSerial, app.package); + qDebug() << "[EventHandler] Killed app" << app.package << "on device" << adbSerial; + } + } + + // Сразу пробуем запустить следующую задачу для этого устройства + processNextTask(key); +} + +void EventHandler::startThreadForTask(const MaterialInfo &account, const EventInfo &event) { + const auto thr = new QThread(this); + const QString key = account.deviceId; + + // Generic helper: подключает сигналы и переносит worker в поток + auto setup = [&](auto *worker) { + using W = std::remove_pointer_t; + worker->moveToThread(thr); + connect(thr, &QThread::started, worker, &W::start); + connect(worker, &W::finished, thr, &QThread::quit); + connect(worker, &W::finished, worker, &QObject::deleteLater); + connect(thr, &QThread::finished, thr, &QObject::deleteLater); + connect(worker, &W::finishedWithResult, this, + [this, key, event](const QString &result) { + updateEventThread(key, event, result); + } + ); + }; + + const QString &appCode = account.appCode; + // Передаём в скрипт канонический device_id из материала — скрипт сам + // разрезолвит его в adb serial через DeviceDAO и использует ТОТ ЖЕ ключ + // для поиска BankProfileInfo (чтобы PIN поднимался из правильной записи). + const QString adbSerial = account.deviceId; + + // AutoConnection: worker emits из своего потока, лямбда выполняется в потоке + // EventHandler (где live `this`), там же где take()/clear() — m_screenshots + // не нужен mutex. + auto connectScreenshot = [this, key](Ozon::CommonScript *w) { + connect(w, &Ozon::CommonScript::screenshotReady, this, + [this, key](const QByteArray &s) { m_screenshots[key] = s; }); + }; + auto connectScreenshotDushanbe = [this, key](Dushanbe::CommonScript *w) { + connect(w, &Dushanbe::CommonScript::screenshotReady, this, + [this, key](const QByteArray &s) { m_screenshots[key] = s; }); + }; + + if (event.type == EventType::FetchProfile) { + if (appCode == "ozon") { + auto *w = new Ozon::LoginAndCheckAccountsScript(adbSerial, appCode, nullptr, true); + setup(w); connectScreenshot(w); + } + else if (appCode == "black") setup(new Black::LoginAndCheckAccountsScript(adbSerial, appCode)); + else if (appCode == "dushanbe_city_bank") { + auto *w = new Dushanbe::LoginAndCheckAccountsScript(adbSerial, appCode); + setup(w); connectScreenshotDushanbe(w); + } + else qWarning() << "[EventHandler] FETCH_PROFILE: unknown appCode:" << appCode; + } + + if (event.type == EventType::FetchTransactions) { + // Парсим amount и created_at из body задачи + const QJsonObject taskJson = QJsonDocument::fromJson(event.json.toUtf8()).object(); + const QJsonObject body = taskJson["body"].toObject(); + const double searchAmount = body["amount"].toDouble() / 100.0; // копейки → рубли (с учётом знака) + const QString createdAtStr = body["created_at"].toString(); + QDateTime searchTime = QDateTime::fromString(createdAtStr, Qt::ISODate); + // Backend контракт — UTC ISO с суффиксом 'Z' (как мы сами пишем + // на строках 317 и 373). Если 'Z'/смещение отсутствует — Qt парсит + // как LocalTime, что даёт off-by-one для вечерних транзакций + // (см. task_dump (26): Ozon группировал +100 на 14.05, backend + // прислал 13.05 без TZ). Жёстко переинтерпретируем как UTC. + if (searchTime.isValid() && searchTime.timeSpec() == Qt::LocalTime) { + qWarning() << "[EventHandler] FETCH_TRANSACTIONS: created_at без TZ-маркера" + << createdAtStr << "— интерпретирую как UTC"; + searchTime.setTimeZone(QTimeZone::utc()); + } + + if (appCode == "ozon") { + auto *w = new Ozon::GetLastTransactionsScript(adbSerial, appCode, 0, searchAmount, searchTime, account.materialId); + setup(w); connectScreenshot(w); + connect(w, &Ozon::GetLastTransactionsScript::transactionParsed, this, + [this, key](const QJsonObject &tx) { m_parsedTxs[key] = tx; }); + } + else if (appCode == "black") + setup(new Black::GetLastTransactionsScript(adbSerial, appCode)); + else if (appCode == "dushanbe_city_bank") { + auto *w = new Dushanbe::GetLastTransactionsScript(adbSerial, appCode, searchAmount, searchTime, account.materialId); + setup(w); connectScreenshotDushanbe(w); + connect(w, &Dushanbe::GetLastTransactionsScript::transactionParsed, this, + [this, key](const QJsonObject &tx) { m_parsedTxs[key] = tx; }); + } + else qWarning() << "[EventHandler] FETCH_TRANSACTIONS: unknown appCode:" << appCode; + } + + if (event.type == EventType::CreateTransaction) { + const QString recipientBank = event.comment; // резолвленное имя банка-получателя + + // Определяем тип транзакции из JSON задачи + const QJsonObject taskJson = QJsonDocument::fromJson(event.json.toUtf8()).object(); + const QString txType = taskJson["body"].toObject()["type"].toString(); + + if (txType == "bank") { + // Перевод по номеру карты + if (appCode == "ozon") { + auto *w = new Ozon::PayByCardScript(account, event.phone, event.amount); + setup(w); connectScreenshot(w); + } + else if (appCode == "black") setup(new Black::PayByCardScript(account, event.phone, event.amount)); + else if (appCode == "dushanbe_city_bank") { + auto *w = new Dushanbe::PayByCardScript(account, event.phone, event.amount); + setup(w); connectScreenshotDushanbe(w); + } + else qWarning() << "[EventHandler] CREATE_TRANSACTION bank: unknown appCode:" << appCode; + } else { + // Перевод по номеру телефона (top-up) + if (appCode == "ozon") { + auto *w = new Ozon::PayByPhoneScript(account, event.phone, recipientBank, event.amount); + setup(w); connectScreenshot(w); + } + else if (appCode == "dushanbe_city_bank") { + auto *w = new Dushanbe::PayByPhoneScript(account, event.phone, event.amount); + setup(w); connectScreenshotDushanbe(w); + } + else qWarning() << "[EventHandler] CREATE_TRANSACTION: unknown appCode:" << appCode; + } + } + + // Watchdog-таймер: 5 минут — убиваем зависший поток + const auto timer = new QTimer(this); + timer->setSingleShot(true); + connect(timer, &QTimer::timeout, this, [this, key, event]() { + qWarning() << "[EventHandler] Timeout for key" << key << "- killing stuck thread."; + if (QThread *thread = m_threads.value(key); thread && thread->isRunning()) { + thread->requestInterruption(); + if (!thread->wait(15000)) { + qWarning() << "[EventHandler] Thread still running after 15s for key" << key + << "— abandoning (no terminate to avoid heap corruption)"; + } + } + if (const auto tm = m_timers.take(key)) { + tm->deleteLater(); + } + m_threads.remove(key); + + // Помечаем событие как ошибку + EventDAO::updateEvent(event.id, EventStatus::Error, "Script timeout (5 min)"); + const QString logMsg = QString("[%1] bank=%2 material=%3: Script timeout") + .arg(eventTypeToString(event.type), event.bankName, event.externalId); + GeneralLogDAO::insert("CRITICAL", "EventHandler", logMsg); + sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, "Script timeout (5 min)"); + EventDAO::markSentToServer(event.id); + + // Убиваем банковское приложение на устройстве после таймаута + { + const MaterialInfo acc = MaterialDAO::getAccountById(event.accountId); + const BankProfileInfo app = BankProfileDAO::getApplication(event.deviceId, acc.appCode); + if (!app.package.isEmpty()) { + const DeviceInfo device = DeviceDAO::getDeviceById(event.deviceId); + const QString adbSerial = device.adbSerial.isEmpty() ? event.deviceId : device.adbSerial; + AdbUtils::tryToKillApplication(adbSerial, app.package); + qDebug() << "[EventHandler] Killed app" << app.package << "on device" << adbSerial << "(timeout)"; + } + } + + processNextTask(key); + }); + + m_threads[key] = thr; + m_timers[key] = timer; + + timer->start(5 * 60 * 1000); + thr->start(); +} + +void EventHandler::processNextTask(const QString &deviceId) { + if (!m_running) return; + if (const QThread *thr = m_threads.value(deviceId, nullptr); thr && thr->isRunning()) return; + + while (true) { + const EventInfo event = EventDAO::getFirstWaitEventByDeviceId(deviceId); + if (event.id == -1) break; + + const qint64 waitSecs = event.timestamp.secsTo(QDateTime::currentDateTimeUtc()); + if (waitSecs > 5 * 60) { + qWarning() << "[EventHandler] Task expired after" << waitSecs << "s, event id:" << event.id; + EventDAO::updateEvent(event.id, EventStatus::Error, "Task expired: waited " + QString::number(waitSecs) + "s"); + sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, "Task expired: not started within 5 minutes"); + continue; + } + + const MaterialInfo account = MaterialDAO::getAccountById(event.accountId); + const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, account.appCode); + if (app.status != "active") { + qWarning() << "[EventHandler] Bank profile disabled for" << account.appCode << "device:" << deviceId; + EventDAO::updateEvent(event.id, EventStatus::Error, "Bank profile is disabled"); + sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, + "Bank profile is disabled: " + account.appCode); + continue; + } + + if (!EventDAO::updateEvent(event.id, EventStatus::InProgress, "")) { + qCritical() << "[EventHandler] Failed to set InProgress, event id:" << event.id; + break; + } + + startThreadForTask(account, event); + break; + } +} + +void EventHandler::clearAll() { + // Параллельная остановка: сначала всем requestInterruption + quit, + // потом ждём каждый. terminate() намеренно НЕ используем — он опасен + // (heap corruption, утечки DB-локов); если поток не успел остановиться, + // abandon'им — ОС зачистит ресурсы при выходе процесса. Этот же подход + // применяет watchdog в startThreadForTask. + for (QThread *thread : m_threads) { + if (thread->isRunning()) { + thread->requestInterruption(); + thread->quit(); + } + } + for (QThread *thread : m_threads) { + if (thread->isRunning() && !thread->wait(5000)) { + qWarning() << "[EventHandler] Thread did not stop in 5s on shutdown," + << "abandoning (no terminate to avoid heap corruption)"; + continue; // не deleteLater для всё ещё работающего QThread + } + thread->deleteLater(); + } + m_threads.clear(); + + for (QTimer *timer : m_timers) { + timer->stop(); + timer->deleteLater(); + } + m_timers.clear(); + m_screenshots.clear(); + m_parsedTxs.clear(); +} + +void EventHandler::stop() { + m_running = false; + if (m_pollTimer) m_pollTimer->stop(); + clearAll(); +} diff --git a/services/EventHandler.h b/services/EventHandler.h new file mode 100644 index 0000000..2df4ba9 --- /dev/null +++ b/services/EventHandler.h @@ -0,0 +1,48 @@ +#pragma once +#include +#include +#include +#include +#include + +#include "db/MaterialInfo.h" +#include "db/EventInfo.h" + +class NetworkService; + +class EventHandler final : public QObject { + Q_OBJECT + +public: + explicit EventHandler(QObject *parent = nullptr); + + ~EventHandler() override; + +public slots: + void start(); + + void stop(); + +signals: + void finished(); + +private slots: + void onTasksReceived(const QJsonArray &tasks); + +private: + bool m_running = true; + QMap m_threads; + QMap m_timers; + QMap m_screenshots; // deviceId → скриншот от скрипта + QMap m_parsedTxs; // key → распарсенная транзакция для FETCH_TRANSACTIONS + QTimer *m_pollTimer = nullptr; + NetworkService *m_network = nullptr; + + void clearAll(); + + void updateEventThread(const QString &key, const EventInfo &event, const QString &result); + + void startThreadForTask(const MaterialInfo &account, const EventInfo &event); + + void processNextTask(const QString &deviceId); +}; diff --git a/services/LogArchiveSender.cpp b/services/LogArchiveSender.cpp new file mode 100644 index 0000000..69c8997 --- /dev/null +++ b/services/LogArchiveSender.cpp @@ -0,0 +1,137 @@ +#include "LogArchiveSender.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dao/SettingsDAO.h" +#include "net/NetworkService.h" + +namespace { + +QString resolveDbPath() { + const QSettings settings("config.ini", QSettings::IniFormat); + QString dbPath = settings.value("db/dbPath").toString(); + if (dbPath.startsWith("~")) + dbPath.replace(0, 1, QDir::homePath()); + if (!dbPath.isEmpty() && QFileInfo(dbPath).isRelative()) + dbPath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(dbPath); + return dbPath; +} + +// Запускается в worker-потоке. Возвращает байты zip или пустой массив. +QByteArray buildArchive() { + const QString logPath = QCoreApplication::applicationDirPath() + "/application.log"; + const QString dbPath = resolveDbPath(); + + const bool hasLog = QFile::exists(logPath); + const bool hasDb = !dbPath.isEmpty() && QFile::exists(dbPath); + if (!hasLog && !hasDb) { + qWarning() << "[LogArchiveSender] neither log nor db exists — nothing to archive"; + return {}; + } + + const QString tmpDir = QDir::tempPath() + QStringLiteral("/arc_logs_auto_") + + QString::number(QDateTime::currentMSecsSinceEpoch()); + QDir().mkpath(tmpDir); + + if (hasLog) { + QFile::copy(logPath, tmpDir + "/application.log"); + } + if (hasDb) { + const QString dbBaseName = QFileInfo(dbPath).fileName(); + QFile::copy(dbPath, tmpDir + "/" + dbBaseName); + for (const QString &suffix : {QStringLiteral("-wal"), QStringLiteral("-shm")}) { + const QString sidecar = dbPath + suffix; + if (QFile::exists(sidecar)) + QFile::copy(sidecar, tmpDir + "/" + dbBaseName + suffix); + } + } + + const QString archivePath = tmpDir + ".zip"; + QFile::remove(archivePath); + QProcess archiver; + archiver.setWorkingDirectory(tmpDir); +#ifdef Q_OS_WIN + archiver.start("powershell", QStringList() + << "-NoProfile" << "-Command" + << QString("Compress-Archive -Path '%1/*' -DestinationPath '%2' -Force").arg(tmpDir, archivePath)); +#else + archiver.start("zip", QStringList() << "-r" << archivePath << "."); +#endif + archiver.waitForFinished(30000); + + QDir(tmpDir).removeRecursively(); + + if (!QFile::exists(archivePath)) { + qWarning() << "[LogArchiveSender] archive not created at" << archivePath; + return {}; + } + QFile zip(archivePath); + if (!zip.open(QIODevice::ReadOnly)) { + qWarning() << "[LogArchiveSender] cannot open archive" << archivePath; + QFile::remove(archivePath); + return {}; + } + const QByteArray bytes = zip.readAll(); + zip.close(); + QFile::remove(archivePath); + return bytes; +} + +} // namespace + +bool LogArchiveSender::dispatch(const QString &label, int eventId) { + const QString apiToken = SettingsDAO::get("api_key"); + const QDateTime now = QDateTime::currentDateTime(); + const QString timestamp = now.toString(Qt::ISODate); + + QStringList head{QStringLiteral("[LOGS]")}; + if (!apiToken.isEmpty()) head << QStringLiteral("[%1]").arg(apiToken.toHtmlEscaped()); + if (!label.isEmpty()) head << QStringLiteral("[%1]").arg(label.toHtmlEscaped()); + const QString text = QStringLiteral("%1\n%2") + .arg(head.join(QStringLiteral(" ")), timestamp.toHtmlEscaped()); + + QString tokenSafe = apiToken; + tokenSafe.replace(QRegularExpression(R"([^\w\-.])"), "_"); + const QString stamp = now.toString("yyyy-MM-dd_HH-mm-ss"); + QStringList parts{QStringLiteral("LOGS")}; + if (!tokenSafe.isEmpty()) parts << tokenSafe; + parts << stamp; + if (!label.isEmpty()) parts << label; + if (eventId > 0) parts << QStringLiteral("ev%1").arg(eventId); + const QString archiveFilename = parts.join(QStringLiteral("_")) + QStringLiteral(".zip"); + + auto *worker = QThread::create([text, archiveFilename]() { + const QByteArray archive = buildArchive(); + if (archive.isEmpty()) { + qWarning() << "[LogArchiveSender] empty archive, skipping dispatch"; + return; + } + + auto *thread = new QThread; + auto *svc = new NetworkService; + svc->moveToThread(thread); + QObject::connect(thread, &QThread::started, svc, + [svc, text, archive, archiveFilename]() { + svc->postMonitoringDump(text, archive, QByteArray(), archiveFilename); + emit svc->finished(); + }); + QObject::connect(svc, &NetworkService::finished, thread, &QThread::quit); + QObject::connect(svc, &NetworkService::finished, svc, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); + }); + QObject::connect(worker, &QThread::finished, worker, &QObject::deleteLater); + worker->start(); + return true; +} diff --git a/services/LogArchiveSender.h b/services/LogArchiveSender.h new file mode 100644 index 0000000..786f8e5 --- /dev/null +++ b/services/LogArchiveSender.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +// Headless-варианты сборки и отправки в Telegram-relay архива из application.log +// + БД (+ WAL/SHM). Используется EventHandler-ом для авто-отправки при ошибках задач. +// FileLogWindow по-прежнему имеет свою UI-обвязку для ручной отправки. +namespace LogArchiveSender { + // Собирает архив и постит через свежий NetworkService::postMonitoringDump + // в собственных background-потоках (worker для zip + thread для HTTP). + // `label` — короткий тег ("TASK_ERROR" и т.п.), попадает в caption и в имя файла. + // `eventId` (если > 0) тоже уходит в имя файла для трассировки. + // Возвращает true если задача поставлена в очередь; реальный результат отправки + // не отслеживается (fire-and-forget, как у sendMonitoringLog). + bool dispatch(const QString &label, int eventId = -1); +} diff --git a/services/db/AccountInfoScreener.cpp b/services/db/AccountInfoScreener.cpp deleted file mode 100644 index e548a5b..0000000 --- a/services/db/AccountInfoScreener.cpp +++ /dev/null @@ -1,56 +0,0 @@ -#include "AccountInfoScreener.h" -#include -#include -#include -#include - -#include "android/Card.h" -#include "tesseract/baseapi.h" - -QList AccountInfoScreener::parseCardData(const QByteArray &imageData) { - QList cards; - // QImage image; - // if (!image.loadFromData(imageData)) { - // return {}; - // } - // image = image.convertToFormat(QImage::Format_Grayscale8); - - QImage image("./images/img.png"); - if (image.isNull()) { - return {}; - } - - // важная часть, без нее не парсит - image = image.convertToFormat(QImage::Format_Grayscale8); - - tesseract::TessBaseAPI tess; - if (tess.Init(nullptr, "rus")) { - qDebug() << "Ошибка инициализации Tesseract с русским языком...."; - } - - tess.SetImage(image.bits(), image.width(), image.height(), 1, image.bytesPerLine()); - - const QString result = QString::fromUtf8(tess.GetUTF8Text()); - qDebug() << "Распознанный текст:" << result; - - - const QRegularExpression re(R"((дебетовая(?:, цифровая)?),\s*\*(\d{4})\s+([\d]+\.\d{2})\s*Р)", - QRegularExpression::CaseInsensitiveOption); - - QRegularExpressionMatchIterator i = re.globalMatch(result); - - while (i.hasNext()) { - auto card = Card(); - bool ok; - QRegularExpressionMatch match = i.next(); - const double amount = match.captured(3).toDouble(&ok); - if (ok) { - card.amount = amount; - card.name = match.captured(2); - card.description = match.captured(1); - cards.append(card); - } - } - - return cards; -} diff --git a/services/db/AccountInfoScreener.h b/services/db/AccountInfoScreener.h deleted file mode 100644 index 9022bbc..0000000 --- a/services/db/AccountInfoScreener.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#include "android/Card.h" - -class AccountInfoScreener { -public: - [[nodiscard]] static QList parseCardData(const QByteArray &imageData); -}; diff --git a/services/net/NetworkErrorUtils.h b/services/net/NetworkErrorUtils.h new file mode 100644 index 0000000..2a11f3c --- /dev/null +++ b/services/net/NetworkErrorUtils.h @@ -0,0 +1,80 @@ +#pragma once +#include +#include + +// Хелперы для информативного логирования сетевых ошибок. Используются +// и в NetworkService, и в DeviceScreener — категория сразу показывает +// характер сбоя: timeout / refused / DNS / SSL / HTTP-уровень. + +namespace NetworkErrorUtils { + +// Удаляет схему+хост из URL, оставляет только путь (для логов/UI без секретов). +inline QString sanitizePath(const QString &fullUrl) { + const int schemeEnd = fullUrl.indexOf(QStringLiteral("://")); + if (schemeEnd < 0) return fullUrl; + const int pathStart = fullUrl.indexOf(QLatin1Char('/'), schemeEnd + 3); + return pathStart >= 0 ? fullUrl.mid(pathStart) : QStringLiteral("/"); +} + +// Категория ошибки в виде короткого ярлыка. См. QNetworkReply::NetworkError. +inline QString networkErrorCategory(QNetworkReply::NetworkError err) { + switch (err) { + case QNetworkReply::NoError: return QStringLiteral("NO_ERROR"); + case QNetworkReply::ConnectionRefusedError: return QStringLiteral("CONNECTION_REFUSED"); + case QNetworkReply::RemoteHostClosedError: return QStringLiteral("REMOTE_CLOSED"); + case QNetworkReply::HostNotFoundError: return QStringLiteral("DNS_FAIL"); + case QNetworkReply::TimeoutError: return QStringLiteral("QT_TIMEOUT"); + case QNetworkReply::OperationCanceledError: return QStringLiteral("CANCELLED"); + case QNetworkReply::SslHandshakeFailedError: return QStringLiteral("SSL_HANDSHAKE"); + case QNetworkReply::TemporaryNetworkFailureError: return QStringLiteral("TEMP_NET_FAIL"); + case QNetworkReply::NetworkSessionFailedError: return QStringLiteral("NO_NETWORK_SESSION"); + case QNetworkReply::BackgroundRequestNotAllowedError: return QStringLiteral("BG_NOT_ALLOWED"); + case QNetworkReply::TooManyRedirectsError: return QStringLiteral("TOO_MANY_REDIRECTS"); + case QNetworkReply::InsecureRedirectError: return QStringLiteral("INSECURE_REDIRECT"); + case QNetworkReply::UnknownNetworkError: return QStringLiteral("UNKNOWN_NETWORK"); + case QNetworkReply::ProxyConnectionRefusedError: return QStringLiteral("PROXY_REFUSED"); + case QNetworkReply::ProxyConnectionClosedError: return QStringLiteral("PROXY_CLOSED"); + case QNetworkReply::ProxyNotFoundError: return QStringLiteral("PROXY_NOT_FOUND"); + case QNetworkReply::ProxyTimeoutError: return QStringLiteral("PROXY_TIMEOUT"); + case QNetworkReply::ProxyAuthenticationRequiredError: return QStringLiteral("PROXY_AUTH_REQ"); + case QNetworkReply::ContentAccessDenied: return QStringLiteral("HTTP_403"); + case QNetworkReply::ContentOperationNotPermittedError:return QStringLiteral("OP_NOT_PERMITTED"); + case QNetworkReply::ContentNotFoundError: return QStringLiteral("HTTP_404"); + case QNetworkReply::AuthenticationRequiredError: return QStringLiteral("HTTP_401"); + case QNetworkReply::ContentReSendError: return QStringLiteral("RESEND_REQUIRED"); + case QNetworkReply::ContentConflictError: return QStringLiteral("HTTP_409"); + case QNetworkReply::ContentGoneError: return QStringLiteral("HTTP_410"); + case QNetworkReply::InternalServerError: return QStringLiteral("HTTP_500"); + case QNetworkReply::OperationNotImplementedError: return QStringLiteral("HTTP_501"); + case QNetworkReply::ServiceUnavailableError: return QStringLiteral("HTTP_503"); + case QNetworkReply::UnknownServerError: return QStringLiteral("UNKNOWN_SERVER"); + case QNetworkReply::ProtocolUnknownError: return QStringLiteral("PROTO_UNKNOWN"); + case QNetworkReply::ProtocolInvalidOperationError: return QStringLiteral("PROTO_INVALID_OP"); + case QNetworkReply::UnknownContentError: return QStringLiteral("UNKNOWN_CONTENT"); + case QNetworkReply::ProtocolFailure: return QStringLiteral("PROTO_FAILURE"); + default: return QStringLiteral("err_%1").arg(static_cast(err)); + } +} + +// Развёрнутое описание сетевой ошибки: HTTP-код + категория или просто +// категория + Qt errorString. Эндпоинт — без хоста. +inline QString describeNetError(QNetworkReply *reply, const QString &path) { + const int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + const QString endpoint = sanitizePath(path); + const QString category = networkErrorCategory(reply->error()); + if (httpCode > 0) { + return QStringLiteral("HTTP %1 [%2] %3").arg(httpCode).arg(category, endpoint); + } + return QStringLiteral("[%1] %2 (qt: %3)").arg(category, endpoint, reply->errorString()); +} + +// Случай нашего QTimer-таймаута (reply ещё не завершился). reply->error() +// обычно ещё NoError — помечаем категорию явно как CLIENT_TIMEOUT_30s. +inline QString describeClientTimeout(QNetworkReply *reply, const QString &path) { + const QString endpoint = sanitizePath(path); + const QString qtCategory = networkErrorCategory(reply->error()); + return QStringLiteral("[CLIENT_TIMEOUT_30s] %1 (qt-state=%2 errorString=\"%3\")") + .arg(endpoint, qtCategory, reply->errorString()); +} + +} // namespace NetworkErrorUtils diff --git a/services/net/NetworkService.cpp b/services/net/NetworkService.cpp new file mode 100644 index 0000000..06b2119 --- /dev/null +++ b/services/net/NetworkService.cpp @@ -0,0 +1,1875 @@ +#include "NetworkService.h" +#include "NetworkErrorUtils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dao/BankDAO.h" +#include "dao/GeneralLogDAO.h" +#include "dao/MaterialDAO.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "dao/EventDAO.h" +#include "dao/SettingsDAO.h" +#include "dao/TransactionDAO.h" +#include "db/EventInfo.h" + +// Убирает полный URL (хост:порт) из сообщений об ошибках, оставляя только +// Хелперы для логирования сетевых ошибок вынесены в NetworkErrorUtils.h +// чтобы переиспользовать их в DeviceScreener. +using NetworkErrorUtils::sanitizePath; +using NetworkErrorUtils::describeNetError; +using NetworkErrorUtils::describeClientTimeout; + +namespace { +// Прокручивает event loop пока reply не завершится либо не сработает client timeout. +// true — reply finished (success или network error), false — сработал наш QTimer. +bool awaitReply(QNetworkReply *reply, int timeoutMs) { + QEventLoop loop; + QTimer timer; + timer.setSingleShot(true); + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + QObject::connect(reply, &QNetworkReply::errorOccurred, &loop, + [&loop](QNetworkReply::NetworkError) { loop.quit(); }); + QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + timer.start(timeoutMs); + loop.exec(); + return timer.isActive(); +} + +// На client timeout повторяем запрос ещё 2 раза (всего до 3 попыток). +// На HTTP/network ошибках не ретраим — они детерминированы (401/409/refused/etc). +constexpr int kMaxRequestAttempts = 3; +constexpr int kDefaultTimeoutMs = 30000; +constexpr int kDumpTimeoutMs = 120000; +} // namespace + +NetworkService::NetworkService(QObject *parent) : QObject(parent) { + const QSettings settings("config.ini", QSettings::IniFormat); + m_urlAccountUpdate = settings.value("net/accountUpdate").toString(); + m_urlPaymentsGet = settings.value("net/paymentsGet").toString(); + m_urlTransactionUpdate = settings.value("net/transactionUpdate").toString(); + m_urlAppStatusUpdate = settings.value("net/appStatusUpdate").toString(); + m_urlTransactionInsert = settings.value("net/transactionInsert").toString(); + m_urlEventStatusUpdate = settings.value("net/eventStatusUpdate").toString(); + m_apiBase = settings.value("net/apiBase").toString(); + m_pathAuthLogin = settings.value("net/pathAuthLogin", "/api/v1/auth/login").toString(); + m_pathAuthRefresh = settings.value("net/pathAuthRefresh", "/api/v1/auth/refresh_token").toString(); + m_pathDesktopCreate = settings.value("net/pathDesktopCreate", "/api/v1/desktop/").toString(); + m_pathDevice = settings.value("net/pathDevice", "/api/v1/device/").toString(); + m_pathDeviceScreenshot= settings.value("net/pathDeviceScreenshot","/api/v1/device/add_screenshot/").toString(); + m_pathBankProfile = settings.value("net/pathBankProfile", "/api/v1/profile/bank_profile").toString(); + m_pathMaterial = settings.value("net/pathMaterial", "/api/v1/profile/material").toString(); + m_pathGetTasks = settings.value("net/pathGetTasks", "/api/v1/task/get_tasks").toString(); + m_pathTaskResult = settings.value("net/pathTaskResult", "/api/v1/task/task_result").toString(); + m_pathMonitoringLog = settings.value("net/pathMonitoringLog", "/monitoring/log").toString(); + m_pathMonitoringDump = settings.value("net/pathMonitoringDump", "/monitoring/dump").toString(); + m_monitoringBase = settings.value("net/monitoringBase", m_apiBase).toString(); + m_monitoringToken = settings.value("net/monitoringToken").toString(); + m_pathCurrentProfile = settings.value("net/pathCurrentProfile", "/api/v1/profile/current_profile").toString(); + m_pathProfileGetInfo = settings.value("net/pathProfileGetInfo", "/api/v1/profile/get_info").toString(); + m_manager = new QNetworkAccessManager(this); + m_token = settings.value("common/token").toString(); + m_accessToken = SettingsDAO::get("access_token"); +} + +NetworkService::~NetworkService() = default; + +// FIXME доделать работу с экстра +void NetworkService::addExtra(const QString &key, const QVariant &value) { + m_extraData[key] = value; +} + +void addFormField(QHttpMultiPart *multiPart, const QString &fieldName, const QString &fieldValue) { + QHttpPart formField; + formField.setHeader( + QNetworkRequest::ContentDispositionHeader,QStringLiteral("form-data; name=\"%1\"").arg(fieldName) + ); + formField.setBody(fieldValue.toUtf8()); + multiPart->append(formField); +} + +void addFormMetadata(QHttpMultiPart *multiPart, const QByteArray &fieldValue) { + QHttpPart jsonPart; + jsonPart.setHeader(QNetworkRequest::ContentTypeHeader, "application/json; charset=UTF-8"); + jsonPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="metadata")"); + jsonPart.setBody(fieldValue); + multiPart->append(jsonPart); +} + +QJsonValue NetworkService::post(QHttpMultiPart *multiPart, const QString &path) { + const QUrl url(path); + const QNetworkRequest request(url); + + QNetworkReply *reply = m_manager->post(request, multiPart); + multiPart->setParent(reply); + + QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { + loop.quit(); + }); + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(30000); + loop.exec(); + + QJsonValue result; + if (!timeout.isActive()) { + qCritical().noquote() << "Network timeout:" << describeClientTimeout(reply, path); + reply->abort(); + } else if (reply->error() == QNetworkReply::NoError) { + const QByteArray raw = reply->readAll(); + QJsonParseError err; + const QJsonDocument doc = QJsonDocument::fromJson(raw, &err); + if (err.error == QJsonParseError::NoError && doc.isObject()) { + const QJsonObject root = doc.object(); + if (root.value("success").toBool()) { + result = root.value("data"); + } else { + qCritical() << "Ошибка:" << root; + } + } + } else { + qCritical() << "Ошибка:" << describeNetError(reply, path); + } + reply->deleteLater(); + return result; +} + +QJsonArray NetworkService::get(const QString &path, const QMap ¶ms) { + QUrl url(path); + QUrlQuery query; + for (auto it = params.constBegin(); it != params.constEnd(); ++it) { + query.addQueryItem(it.key(), it.value()); + } + url.setQuery(query); + + const QNetworkRequest request(url); + + QNetworkReply *reply = nullptr; + bool timedOut = true; + QString lastTimeoutDetail; + for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) { + if (reply) { reply->deleteLater(); reply = nullptr; } + reply = m_manager->get(request); + if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; } + lastTimeoutDetail = describeClientTimeout(reply, path); + qCritical().noquote() << "Network timeout (GET) attempt" << attempt + << "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail; + reply->abort(); + } + + QJsonArray result; + if (timedOut) { + qCritical().noquote() << "Network timeout (GET) after" + << kMaxRequestAttempts << "attempts:" << lastTimeoutDetail; + } else if (reply->error() == QNetworkReply::NoError) { + const QByteArray raw = reply->readAll(); + QJsonParseError err; + const QJsonDocument doc = QJsonDocument::fromJson(raw, &err); + if (err.error == QJsonParseError::NoError && doc.isObject()) { + const QJsonObject root = doc.object(); + if (root.value("success").toBool()) { + result = root.value("data").toArray(); + } else { + qWarning() << "Ошибка:" << root; + } + } + } else { + qWarning() << "Ошибка:" << reply->errorString(); + } + reply->deleteLater(); + return result; +} + +void NetworkService::postAccountsData() { + // TODO: заменить на новый API + emit finished(); +} + +void NetworkService::getPayments() { + QMap params; + params["token"] = m_token; + const QJsonArray data = get(m_urlPaymentsGet, params); + + for (QJsonValue item: data) { + QJsonObject obj = item.toObject(); + EventInfo eventInfo; + eventInfo.status = EventStatus::Wait; + eventInfo.type = eventTypeFromString(obj["type"].toString()); + eventInfo.externalId = obj["id"].isString() + ? obj["id"].toString() + : QString::number(obj["id"].toInt()); + eventInfo.timestamp = QDateTime::fromMSecsSinceEpoch(obj["timestamp"].toDouble(), QTimeZone::utc()); + eventInfo.amount = obj["amount"].toDouble(); + eventInfo.bankName = obj["bankName"].toString(); + eventInfo.phone = obj["phone"].toString(); + + QString deviceId = obj["deviceId"].toString(); + QString appName = obj["appName"].toString(); + QString lastNumbers = obj["lastNumbers"].toString(); + MaterialInfo account = MaterialDAO::findAppAccount(deviceId, appName, lastNumbers); + + eventInfo.accountId = account.id; + eventInfo.deviceId = account.deviceId; + + if (int id = EventDAO::insertEvent(eventInfo); id == -1) { + qCritical() << "Cant insert event: " << eventInfo.externalId; + } + } + + if (!m_running) { + emit finished(); + } +} + +void NetworkService::postAppStatus() { + QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + + addFormMetadata(multiPart, m_json); + addFormField(multiPart, "token", m_token); + addFormField(multiPart, "androidUniqueName", m_deviceId); + + const bool isSuccess = post(multiPart, m_urlAppStatusUpdate).toBool(); + if (!isSuccess) { + qCritical() << "Cant post data: postAppStatus"; + } + + emit finished(); +} + +QJsonValue NetworkService::postJson(const QString &path, const QJsonObject &body, bool withAuth) { + const QUrl url(path); + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + if (withAuth && !m_accessToken.isEmpty()) { + request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8()); + } + + const QByteArray bodyBytes = QJsonDocument(body).toJson(); + const QString reqBodyStr = QJsonDocument(body).toJson(QJsonDocument::Compact); + const QString shortPath = sanitizePath(path); + + QNetworkReply *reply = nullptr; + bool timedOut = true; + QString lastTimeoutDetail; + for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) { + if (reply) { reply->deleteLater(); reply = nullptr; } + reply = m_manager->post(request, bodyBytes); + if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; } + lastTimeoutDetail = describeClientTimeout(reply, path); + qCritical().noquote() << "Network timeout (POST) attempt" << attempt + << "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail; + reply->abort(); + } + + QJsonValue result; + if (timedOut) { + GeneralLogDAO::insert("CRITICAL", "NetworkService", + "Timeout (POST) after " + QString::number(kMaxRequestAttempts) + + " attempts: " + lastTimeoutDetail, {}, reqBodyStr, {}); + result = QJsonValue::Undefined; + } else if (reply->error() != QNetworkReply::NoError) { + const QByteArray responseBody = reply->readAll(); + const QString errSummary = describeNetError(reply, path); + qCritical().noquote() << "Network error (POST):" << errSummary + << (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId) + << "\n response body:" << responseBody; + GeneralLogDAO::insert("CRITICAL", "NetworkService", + "Network error (POST): " + errSummary, {}, + reqBodyStr, QString::fromUtf8(responseBody)); + result = QJsonValue::Undefined; + } else { + const QByteArray raw = reply->readAll(); + QJsonParseError err; + const QJsonDocument doc = QJsonDocument::fromJson(raw, &err); + if (err.error == QJsonParseError::NoError && doc.isObject()) { + const QJsonObject root = doc.object(); + if (root["success"].toBool()) { + result = root["data"]; + } else { + qCritical() << "API error:" << root; + GeneralLogDAO::insert("WARNING", "NetworkService", + "API error (POST): " + shortPath, {}, + reqBodyStr, QString::fromUtf8(raw)); + result = QJsonValue::Null; + } + } + } + reply->deleteLater(); + return result; +} + +void NetworkService::loginAndSetup() { + const QString apiKey = SettingsDAO::get("api_key"); + + // Шаг 1: логин + QJsonObject loginBody; + loginBody["api_key"] = apiKey; + const QJsonValue loginResult = postJson(m_apiBase + m_pathAuthLogin, loginBody); + + if (loginResult.isUndefined()) { + emit finishedWithResult(-1); // ошибка сети + emit finished(); + return; + } + if (loginResult.isNull()) { + emit finishedWithResult(0); // неверный api_key + emit finished(); + return; + } + + const QJsonObject loginData = loginResult.toObject(); + m_accessToken = loginData["access_token"].toString(); + if (m_accessToken.isEmpty() || !loginData.contains("refresh_token") || !loginData.contains("expires_in")) { + qCritical() << "loginAndSetup: ответ не содержит обязательных полей (access_token/refresh_token/expires_in)"; + emit finishedWithResult(-1); + emit finished(); + return; + } + SettingsDAO::set("access_token", m_accessToken); + SettingsDAO::set("refresh_token", loginData["refresh_token"].toString()); + const qint64 expiresIn = static_cast(loginData["expires_in"].toDouble()); + SettingsDAO::setLong("token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn); + + // Шаг 1.5: получаем страну/валюту профиля + const QJsonValue infoResult = getJson(m_apiBase + m_pathProfileGetInfo, true); + if (infoResult.isObject()) { + const QJsonObject info = infoResult.toObject(); + const QString country = info["country"].toString(); + const QString currency = info["currency"].toString(); + if (!country.isEmpty()) SettingsDAO::set("country", country); + if (!currency.isEmpty()) SettingsDAO::set("currency", currency); + qDebug() << "[loginAndSetup] profile info: country=" << country << "currency=" << currency; + } + + // Шаг 2: получаем актуальный desktop с сервера + QString desktopId = SettingsDAO::get("desktop_id"); + + // Всегда проверяем список десктопов — desktop_id мог устареть или быть удалён + const QJsonValue existingDesktops = getJson(m_apiBase + m_pathDesktopCreate, true); + if (!existingDesktops.isUndefined() && !existingDesktops.isNull() && existingDesktops.isArray()) { + const QJsonArray desktopsArray = existingDesktops.toArray(); + QString foundId; + QString foundProfileId; + for (const QJsonValue &dVal : desktopsArray) { + const QJsonObject d = dVal.toObject(); + if (d["hidden"].toBool(false)) continue; + + foundId = d["id"].toString(); + foundProfileId = d["profile_id"].toString(); + if (!foundId.isEmpty()) break; + } + + if (!foundId.isEmpty() && foundId != desktopId) { + // Нашли актуальный десктоп, отличается от сохранённого — обновляем + qDebug() << "[loginAndSetup] desktop_id updated:" << desktopId << "->" << foundId; + desktopId = foundId; + SettingsDAO::set("desktop_id", desktopId); + SettingsDAO::set("profile_id", foundProfileId); + } else if (!foundId.isEmpty()) { + qDebug() << "[loginAndSetup] desktop_id confirmed:" << desktopId; + } + } + + if (desktopId.isEmpty()) { + // Десктопов на сервере нет — создаём новый + QJsonObject desktopBody; + desktopBody["blue_token"] = QJsonValue::Null; + desktopBody["name"] = QJsonValue::Null; + desktopBody["contact"] = QJsonValue::Null; + const QSettings settings("config.ini", QSettings::IniFormat); + desktopBody["app_version"] = settings.value("common/version").toString(); + const QJsonValue desktopResult = postJson(m_apiBase + m_pathDesktopCreate, desktopBody, true); + + if (desktopResult.isUndefined() || desktopResult.isNull()) { + emit finishedWithResult(-1); + emit finished(); + return; + } + + const QJsonObject desktopData = desktopResult.toObject(); + desktopId = desktopData["id"].toString(); + SettingsDAO::set("desktop_id", desktopId); + SettingsDAO::set("profile_id", desktopData["profile_id"].toString()); + } + + // Шаг 3: синхронизация устройств с сервера + syncDevicesFromServer(desktopId); + + // Шаг 4: синхронизация банковских профилей и материалов с сервера + syncBankProfilesFromServer(); + + // Шаг 5: убран (bank_participator_name_ru приходит в задаче) + + // Шаг 6: синхронизация статусов профилей/материалов с учётом онлайн-устройств + syncCurrentProfile(); + + // Шаг 7: отправляем OFFLINE статус всех устройств (DeviceScreener обновит на ONLINE при обнаружении) + { + const QList allDevices = DeviceDAO::getAllDevices(false); + for (const DeviceInfo &d : allDevices) { + if (d.apiId.isEmpty()) continue; + QJsonObject body; + body["status"] = "OFFLINE"; + body["bank_profile_id"] = QJsonValue::Null; + body["battery"] = d.battery; + body["update_time"] = QDateTime::currentSecsSinceEpoch(); + patchJson(m_apiBase + m_pathDevice + d.apiId, body, true); + qDebug() << "[loginAndSetup] sent OFFLINE for device" << d.id << d.apiId; + } + } + + emit finishedWithResult(1); + emit finished(); +} + + +void NetworkService::postEventStatus() { + QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + + addFormMetadata(multiPart, m_json); + + const bool isSuccess = post(multiPart, m_urlEventStatusUpdate).toBool(); + if (!isSuccess) { + qCritical() << "Cant post data: postEventStatus"; + } + + emit finished(); +} + +void NetworkService::updateTransactionData() { + QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + + addFormMetadata(multiPart, m_json); + addFormField(multiPart, "token", m_token); + addFormField(multiPart, "androidUniqueName", m_deviceId); + addFormField(multiPart, "lastNumbers", m_extraData["lastNumbers"].toString()); + + const bool isSuccess = post(multiPart, m_urlTransactionUpdate).toBool(); + if (!isSuccess) { + qCritical() << "Cant update transaction: updateTransactionData"; + } + + emit finished(); +} + +void NetworkService::insertTransactionData() { + QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + + addFormMetadata(multiPart, m_json); + addFormField(multiPart, "token", m_token); + addFormField(multiPart, "androidUniqueName", m_deviceId); + addFormField(multiPart, "lastNumbers", m_extraData["lastNumbers"].toString()); + + const int id = post(multiPart, m_urlTransactionInsert).toInt(); + if (id > 0) { + const int transactionId = m_extraData["transactionId"].toInt(); + if (!TransactionDAO::updateExternalTransactionId(transactionId, id)) { + qCritical() << "Cant update transaction id: " << id; + } + } + emit finished(); +} + +QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &body, bool withAuth) { + const QUrl url(path); + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + if (withAuth && !m_accessToken.isEmpty()) { + request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8()); + } + + const QByteArray bodyBytes = QJsonDocument(body).toJson(); + const QString reqBodyStr = QJsonDocument(body).toJson(QJsonDocument::Compact); + + QNetworkReply *reply = nullptr; + bool timedOut = true; + QString lastTimeoutDetail; + for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) { + if (reply) { reply->deleteLater(); reply = nullptr; } + reply = m_manager->sendCustomRequest(request, "PATCH", bodyBytes); + if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; } + lastTimeoutDetail = describeClientTimeout(reply, path); + qCritical().noquote() << "Network timeout (PATCH) attempt" << attempt + << "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail; + reply->abort(); + } + + QJsonValue result; + if (timedOut) { + GeneralLogDAO::insert("CRITICAL", "NetworkService", + "Timeout (PATCH) after " + QString::number(kMaxRequestAttempts) + + " attempts: " + lastTimeoutDetail, {}, reqBodyStr, {}); + } else if (reply->error() != QNetworkReply::NoError) { + const QByteArray responseBody = reply->readAll(); + const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + const QString errSummary = describeNetError(reply, path); + qCritical().noquote() << "Network error (PATCH):" << errSummary + << (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId) + << "\n response body:" << responseBody; + GeneralLogDAO::insert("CRITICAL", "NetworkService", + "Network error (PATCH): " + errSummary, {}, + reqBodyStr, QString::fromUtf8(responseBody)); + + // Достаём текст ошибки из JSON-body и отправляем в UI. + // Бэкенд при HTTP 409 шлёт сообщение вида "материал заблокирован". + QString uiMessage; + QJsonParseError jerr; + const QJsonDocument errDoc = QJsonDocument::fromJson(responseBody, &jerr); + if (jerr.error == QJsonParseError::NoError && errDoc.isObject()) { + const QJsonObject errObj = errDoc.object(); + for (const QString &k : {QStringLiteral("error"), + QStringLiteral("message"), + QStringLiteral("detail")}) { + const QString v = errObj.value(k).toString(); + if (!v.isEmpty()) { uiMessage = v; break; } + } + } + if (uiMessage.isEmpty() && !responseBody.isEmpty()) { + uiMessage = QString::fromUtf8(responseBody).left(300); + } + if (!uiMessage.isEmpty()) { + emit apiPatchError(httpStatus, uiMessage); + } + } else { + const QByteArray raw = reply->readAll(); + QJsonParseError err; + const QJsonDocument doc = QJsonDocument::fromJson(raw, &err); + if (err.error == QJsonParseError::NoError && doc.isObject()) { + const QJsonObject root = doc.object(); + if (root["success"].toBool()) { + result = root["data"]; + } else { + qCritical() << "API error (PATCH):" << root; + GeneralLogDAO::insert("WARNING", "NetworkService", + "API error (PATCH): " + sanitizePath(path), {}, + reqBodyStr, QString::fromUtf8(raw)); + // Defense in depth: backend может прислать HTTP 200 + success:false + // (например с {message:"Material is blocked"}). Тоже эмитим в UI. + const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + QString uiMessage = root.value("message").toString(); + if (uiMessage.isEmpty()) uiMessage = root.value("error").toString(); + if (uiMessage.isEmpty()) uiMessage = root.value("detail").toString(); + if (!uiMessage.isEmpty()) { + emit apiPatchError(httpStatus, uiMessage); + } + } + } + } + reply->deleteLater(); + return result; +} + +QJsonValue NetworkService::deleteJson(const QString &path, bool withAuth) { + const QUrl url(path); + QNetworkRequest request(url); + if (withAuth && !m_accessToken.isEmpty()) { + request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8()); + } + + QNetworkReply *reply = nullptr; + bool timedOut = true; + QString lastTimeoutDetail; + for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) { + if (reply) { reply->deleteLater(); reply = nullptr; } + reply = m_manager->deleteResource(request); + if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; } + lastTimeoutDetail = describeClientTimeout(reply, path); + qCritical().noquote() << "Network timeout (DELETE) attempt" << attempt + << "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail; + reply->abort(); + } + + QJsonValue result; + if (timedOut) { + qCritical().noquote() << "Network timeout (DELETE) after" + << kMaxRequestAttempts << "attempts:" << lastTimeoutDetail; + result = QJsonValue::Undefined; + } else if (reply->error() != QNetworkReply::NoError) { + qCritical().noquote() << "Network error (DELETE):" << describeNetError(reply, path) + << (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId); + result = QJsonValue::Undefined; + } else { + const QByteArray raw = reply->readAll(); + QJsonParseError err; + const QJsonDocument doc = QJsonDocument::fromJson(raw, &err); + if (err.error == QJsonParseError::NoError && doc.isObject()) { + const QJsonObject root = doc.object(); + if (root["success"].toBool()) { + result = root["data"]; + } else { + qCritical() << "API error (DELETE):" << root; + result = QJsonValue::Null; + } + } + } + reply->deleteLater(); + return result; +} + +QJsonValue NetworkService::getJson(const QString &path, bool withAuth) { + const QUrl url(path); + QNetworkRequest request(url); + if (withAuth && !m_accessToken.isEmpty()) { + request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8()); + } + + QNetworkReply *reply = nullptr; + bool timedOut = true; + QString lastTimeoutDetail; + for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) { + if (reply) { reply->deleteLater(); reply = nullptr; } + reply = m_manager->get(request); + if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; } + lastTimeoutDetail = describeClientTimeout(reply, path); + qCritical().noquote() << "Network timeout (getJson) attempt" << attempt + << "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail; + reply->abort(); + } + + QJsonValue result; + if (timedOut) { + qCritical().noquote() << "Network timeout (getJson) after" + << kMaxRequestAttempts << "attempts:" << lastTimeoutDetail; + result = QJsonValue::Undefined; + } else if (reply->error() != QNetworkReply::NoError) { + qCritical().noquote() << "Network error (GET):" << describeNetError(reply, path) + << (m_deviceId.isEmpty() ? "" : " device=" + m_deviceId); + result = QJsonValue::Undefined; + } else { + const QByteArray raw = reply->readAll(); + QJsonParseError err; + const QJsonDocument doc = QJsonDocument::fromJson(raw, &err); + if (err.error == QJsonParseError::NoError && doc.isObject()) { + const QJsonObject root = doc.object(); + if (root["success"].toBool()) { + result = root["data"]; + } else { + qCritical() << "API error (GET):" << root; + result = QJsonValue::Null; + } + } + } + reply->deleteLater(); + return result; +} + +void NetworkService::syncBankProfilesFromServer() { + const QJsonValue profileResult = getJson(m_apiBase + m_pathCurrentProfile, true); + if (profileResult.isUndefined() || profileResult.isNull()) { + qWarning() << "[syncBankProfilesFromServer] failed to get current_profile"; + return; + } + + const QJsonObject profileData = profileResult.toObject(); + qDebug() << "[syncBankProfilesFromServer] profileData keys:" << profileData.keys() + << "bank_profiles isArray:" << profileData["bank_profiles"].isArray() + << "bank_profiles size:" << profileData["bank_profiles"].toArray().size(); + const QJsonArray bankProfiles = profileData["bank_profiles"].toArray(); + + // Маппинг apiId → локальный device_id + const QList allDevices = DeviceDAO::getAllDevices(false); + QMap apiIdToLocalId; + for (const DeviceInfo &d : allDevices) { + if (!d.apiId.isEmpty()) { + apiIdToLocalId[d.apiId] = d.id; + } + } + + const QSettings configSettings("config.ini", QSettings::IniFormat); + + for (const QJsonValue &bpVal : bankProfiles) { + const QJsonObject bp = bpVal.toObject(); + const QString bpId = bp["id"].toString(); + const QString bankName = bp["bank_name"].toString(); + const QString bpState = bp["state"].toString(); + QString phone = bp["phone_number"].toString(); + phone.remove(' '); + phone.remove(QChar(0x00A0)); + const QString firstName = bp["first_name"].toString(); + const QString middleName = bp["middle_name"].toString(); + const QString lastName = bp["last_name"].toString(); + // Конвертируем числовой ISO 4217 код в строковый + QString currency; + if (!bp["currency"].isNull()) { + static const QMap isoCurrencyMap = { + {643, "RUB"}, {840, "USD"}, {410, "KRW"}, {944, "AZN"}, {32, "ARS"}, {972, "TJS"}, + {978, "EUR"}, {826, "GBP"}, {156, "CNY"}, {392, "JPY"}, {949, "TRY"}, + }; + const int code = bp["currency"].toInt(); + currency = isoCurrencyMap.value(code, QString::number(code)); + } + const QString serverDeviceId = bp["device_id"].toString(); // API UUID устройства + + if (bpId.isEmpty() || bankName.isEmpty()) continue; + + // Определяем локальный device_id по API UUID + const QString localDeviceId = apiIdToLocalId.value(serverDeviceId); + if (localDeviceId.isEmpty()) { + qDebug() << "[syncBankProfilesFromServer] device not found locally for api_id" << serverDeviceId << ", skipping bp" << bpId; + continue; + } + + const QString fullName = (firstName + " " + (middleName.isEmpty() ? "" : middleName + " ") + lastName).trimmed(); + + BankProfileInfo app = BankProfileDAO::getApplication(localDeviceId, bankName); + if (app.id < 0) { + // Приложение не обнаружено на устройстве — создаём запись + BankProfileInfo newApp; + newApp.deviceId = localDeviceId; + newApp.code = bankName; + newApp.name = configSettings.value(bankName + "/name", bankName).toString(); + newApp.package = configSettings.value(bankName + "/android_package_name").toString(); + newApp.status = (bpState == "disabled") ? "off" : "active"; + newApp.install = true; + newApp.pinCodeChecked = false; + newApp.bankProfileId = bpId; + newApp.fullName = fullName; + newApp.phone = phone; + newApp.currency = currency.isEmpty() + ? configSettings.value(bankName + "/currency").toString() + : currency; + + if (BankProfileDAO::upsertApplication(newApp)) { + app = BankProfileDAO::getApplication(localDeviceId, bankName); + qDebug() << "[syncBankProfilesFromServer] created app" << app.id << bankName << "device" << localDeviceId; + } else { + qWarning() << "[syncBankProfilesFromServer] failed to create app" << bankName << "device" << localDeviceId; + continue; + } + } else { + // Application существует — привязываем bank_profile_id если нужно + if (app.bankProfileId != bpId) { + BankProfileDAO::updateBankProfileId(app.id, bpId); + qDebug() << "[syncBankProfilesFromServer] linked bp" << bpId << "to app" << app.id; + } + if (app.fullName.isEmpty() || app.phone.isEmpty()) { + BankProfileDAO::updateProfileInfo(app.id, fullName, phone); + } + } + + // Синхронизируем materials → accounts + const QJsonArray materials = bp["materials"].toArray(); + for (const QJsonValue &matVal : materials) { + const QJsonObject mat = matVal.toObject(); + const QString matId = mat["id"].toString(); + const QString cardNumber = mat["card_number"].toString(); + if (matId.isEmpty()) continue; + + // Уже есть account с этим material_id — пропускаем + const MaterialInfo existing = MaterialDAO::findByMaterialId(matId); + if (existing.id >= 0) continue; + + const QString lastNumbers = cardNumber.length() >= 4 ? cardNumber.right(4) : cardNumber; + + // Ищем существующий account по last_numbers + MaterialInfo acc = MaterialDAO::findAppAccount(localDeviceId, bankName, lastNumbers); + if (acc.id >= 0) { + if (acc.materialId.isEmpty()) { + MaterialDAO::updateMaterialId(acc.id, matId); + if (acc.cardNumber.isEmpty() && !cardNumber.isEmpty()) { + MaterialDAO::updateCardNumber(acc.id, cardNumber); + } + qDebug() << "[syncBankProfilesFromServer] linked material" << matId << "to account" << acc.id; + } + } else if (!lastNumbers.isEmpty()) { + // Account не существует — создаём + MaterialInfo newAcc; + newAcc.deviceId = localDeviceId; + newAcc.appCode = bankName; + newAcc.lastNumbers = lastNumbers; + newAcc.cardNumber = cardNumber; + newAcc.materialId = matId; + newAcc.currency = currency.isEmpty() + ? configSettings.value(bankName + "/currency").toString() + : currency; + newAcc.status = MaterialStatus::Off; + newAcc.updateTime = QDateTime::currentDateTime(); + + if (MaterialDAO::upsertAccount(newAcc)) { + qDebug() << "[syncBankProfilesFromServer] created account for material" << matId << "card" << lastNumbers; + } else { + qWarning() << "[syncBankProfilesFromServer] failed to create account for material" << matId; + } + } + } + } + + qDebug() << "[syncBankProfilesFromServer] done"; +} + +void NetworkService::syncBanksIfEmpty() { + const QList existing = BankDAO::getAll(); + if (!existing.isEmpty()) { + qDebug() << "[syncBanksIfEmpty] banks table has" << existing.size() << "entries, skipping"; + return; + } + + qDebug() << "[syncBanksIfEmpty] banks table empty, downloading..."; + + const QString path = QStringLiteral("https://api.brtservice.io/api/rest/info/"); + QNetworkRequest request{QUrl(path)}; + + QNetworkReply *reply = nullptr; + bool timedOut = true; + for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) { + if (reply) { reply->deleteLater(); reply = nullptr; } + reply = m_manager->get(request); + if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; } + qWarning().noquote() << "[syncBanksIfEmpty] timeout attempt" << attempt + << "/" << kMaxRequestAttempts; + reply->abort(); + } + + if (timedOut) { + qWarning() << "[syncBanksIfEmpty] timeout after" << kMaxRequestAttempts << "attempts"; + reply->deleteLater(); + return; + } + + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "[syncBanksIfEmpty] error:" << reply->errorString(); + reply->deleteLater(); + return; + } + + const QByteArray data = reply->readAll(); + reply->deleteLater(); + + const QJsonDocument doc = QJsonDocument::fromJson(data); + QJsonArray array; + if (doc.isArray()) { + array = doc.array(); + } else if (doc.isObject() && doc.object().contains("data")) { + array = doc.object()["data"].toArray(); + } + + QList banks; + for (const QJsonValue &val : array) { + const QJsonObject obj = val.toObject(); + BankInfo bank; + bank.alias = obj["alias"].toString(); + bank.name = obj["name"].toString(); + bank.nspkName = obj["nspk_name"].toString(); + if (!bank.alias.isEmpty()) { + banks.append(bank); + } + } + + if (!banks.isEmpty()) { + BankDAO::replaceAll(banks); + qDebug() << "[syncBanksIfEmpty] synced" << banks.size() << "banks"; + } else { + qWarning() << "[syncBanksIfEmpty] no banks in response"; + } +} + +void NetworkService::syncDevicesFromServer(const QString &desktopId) { + if (desktopId.isEmpty()) return; + + const QJsonValue devicesResult = getJson(m_apiBase + m_pathDevice + desktopId, true); + if (devicesResult.isUndefined() || devicesResult.isNull() || !devicesResult.isArray()) { + qWarning() << "[syncDevicesFromServer] failed to get devices for desktop" << desktopId; + return; + } + + const QJsonArray devicesArray = devicesResult.toArray(); + const QList allDevices = DeviceDAO::getAllDevices(false); + QSet processedNames; // защита от дублей с сервера + + for (const QJsonValue &devVal : devicesArray) { + const QJsonObject dev = devVal.toObject(); + const QString apiId = dev["id"].toString(); + const QString name = dev["name"].toString(); + if (apiId.isEmpty()) continue; + + // Пропускаем дубли по name + if (processedNames.contains(name)) { + qDebug() << "[syncDevicesFromServer] skipping duplicate:" << apiId << name; + continue; + } + processedNames.insert(name); + + // Проверяем: есть ли уже устройство с таким apiId в локальной БД + bool apiIdExists = false; + for (const DeviceInfo &d : allDevices) { + if (d.apiId == apiId) { apiIdExists = true; break; } + } + if (apiIdExists) continue; + + const DeviceInfo existing = DeviceDAO::findByName(name); + if (!existing.id.isEmpty() && !existing.apiId.isEmpty()) { + continue; + } + + if (!existing.id.isEmpty() && existing.apiId.isEmpty()) { + DeviceDAO::updateApiId(existing.id, apiId); + qDebug() << "[syncDevicesFromServer] linked apiId" << apiId << "to device" << existing.id; + continue; + } + + // Устройства нет в БД — создаём одну запись (DeviceScreener потом подхватит) + DeviceInfo deviceInfo; + deviceInfo.apiId = apiId; + deviceInfo.name = name; + deviceInfo.status = dev["status"].toString(); + deviceInfo.screenWidth = dev["screen_width"].toInt(); + deviceInfo.screenHeight = dev["screen_height"].toInt(); + deviceInfo.battery = dev["battery"].toInt(); + deviceInfo.id = apiId; + + if (DeviceDAO::upsertDevice(deviceInfo)) { + qDebug() << "[syncDevicesFromServer] restored device:" << apiId << name; + } else { + qWarning() << "[syncDevicesFromServer] failed to restore device:" << apiId; + } + } +} + +bool NetworkService::syncCurrentProfile() { + // Локаль — источник истины. Логика: GET current_profile, построить + // map id→state, пройти по локальным bank_profiles/materials, для + // каждого расхождения отправить PATCH с локальным значением. + // См. docs/status-sync.md (правила 2, 3, 4-B). + const QJsonValue profileResult = getJson(m_apiBase + m_pathCurrentProfile, true); + if (profileResult.isUndefined() || profileResult.isNull()) { + qWarning() << "[syncCurrentProfile] GET current_profile failed"; + return false; + } + + const QJsonObject profileData = profileResult.toObject(); + + const QString profileId = profileData["id"].toString(); + if (!profileId.isEmpty()) { + SettingsDAO::set("profile_id", profileId); + } + + // Маппим серверное состояние в плоские map для быстрого lookup. + QMap serverProfileState; // bankProfileId → "enabled"/"disabled" + QMap serverMaterialState; // materialId → "enabled"/"disabled" + const QJsonArray bankProfiles = profileData["bank_profiles"].toArray(); + for (const QJsonValue &bpVal : bankProfiles) { + const QJsonObject bp = bpVal.toObject(); + const QString bpId = bp["id"].toString(); + if (!bpId.isEmpty()) serverProfileState[bpId] = bp["state"].toString(); + for (const QJsonValue &matVal : bp["materials"].toArray()) { + const QJsonObject mat = matVal.toObject(); + const QString matId = mat["id"].toString(); + if (!matId.isEmpty()) serverMaterialState[matId] = mat["state"].toString(); + } + } + + int pushedProfiles = 0; + int pushedMaterials = 0; + + // Идём по локальным устройствам и их bank_profiles → пушим diff. + const QList allDevices = DeviceDAO::getAllDevices(false); + for (const DeviceInfo &dev : allDevices) { + const QList apps = BankProfileDAO::getApplicationsByDeviceId(dev.id); + for (const BankProfileInfo &app : apps) { + if (app.bankProfileId.isEmpty()) continue; // не синхронизирован с сервером + + QSharedPointer mtx = keyMutex(dev.id, app.bankProfileId); + QMutexLocker locker(mtx.data()); + + // bank_profile diff + if (serverProfileState.contains(app.bankProfileId)) { + const QString localBpState = (app.status == "active") ? "enabled" : "disabled"; + const QString srvBpState = serverProfileState.value(app.bankProfileId); + if (srvBpState != localBpState) { + const QString enableStr = (app.status == "active") ? "true" : "false"; + qDebug() << "[syncCurrentProfile] pushing bank_profile" << app.bankProfileId + << "→" << enableStr << "(local=" << localBpState << "srv=" << srvBpState << ")"; + patchJson(m_apiBase + m_pathBankProfile + "/" + app.bankProfileId + "/" + enableStr, + QJsonObject(), true); + ++pushedProfiles; + } + } else { + qWarning() << "[syncCurrentProfile] local bankProfileId" << app.bankProfileId + << "(device" << dev.id << "app" << app.code << ") not present on server, skipping"; + continue; + } + + // Гард #3 (rule 4-B): материалы не пушим при локально выключенном профиле. + if (app.status != "active") continue; + + const QList materials = MaterialDAO::getAccounts(dev.id, app.code); + for (const MaterialInfo &mat : materials) { + if (mat.materialId.isEmpty()) continue; + if (!serverMaterialState.contains(mat.materialId)) continue; + + const QString localMatState = (materialStatusToString(mat.status) == "active") + ? "enabled" : "disabled"; + const QString srvMatState = serverMaterialState.value(mat.materialId); + if (srvMatState != localMatState) { + const QString enableStr = (localMatState == "enabled") ? "true" : "false"; + qDebug() << "[syncCurrentProfile] pushing material" << mat.materialId + << "→" << enableStr << "(local=" << localMatState << "srv=" << srvMatState << ")"; + patchJson(m_apiBase + m_pathMaterial + "/" + mat.materialId + "/" + enableStr, + QJsonObject(), true); + ++pushedMaterials; + } + } + } + } + + qDebug() << "[syncCurrentProfile] done — pushed" << pushedProfiles << "profiles," + << pushedMaterials << "materials"; + return true; +} + +void NetworkService::postBankProfile() { + if (!ensureValidToken()) { emit finished(); return; } + + const QJsonObject body = QJsonDocument::fromJson(m_json).object(); + qDebug() << "[NetworkService] postBankProfile payload:" << QJsonDocument(body).toJson(QJsonDocument::Compact); + const QJsonValue result = postJson(m_apiBase + m_pathBankProfile, body, true); + if (result.isNull() || result.isUndefined()) { + qWarning() << "[NetworkService] postBankProfile failed"; + } else { + const QJsonObject data = result.toObject(); + const QString bankProfileId = data["id"].toString(); + const int appId = m_extraData.value("app_id").toInt(); + if (!bankProfileId.isEmpty() && appId > 0) { + BankProfileDAO::updateBankProfileId(appId, bankProfileId); + qDebug() << "[NetworkService] postBankProfile saved bank_profile_id:" << bankProfileId; + } + } + + emit finished(); +} + +void NetworkService::patchBankProfile() { + if (!ensureValidToken()) { emit finished(); return; } + + const QString bankProfileId = m_extraData.value("bank_profile_id").toString(); + if (bankProfileId.isEmpty()) { + qWarning() << "[NetworkService] patchBankProfile: bank_profile_id is empty"; + emit finished(); + return; + } + + const QJsonObject body = QJsonDocument::fromJson(m_json).object(); + patchJson(m_apiBase + m_pathBankProfile + "/" + bankProfileId, body, true); + qDebug() << "[NetworkService] patchBankProfile done for id:" << bankProfileId; + + emit finished(); +} + +void NetworkService::patchMaterial() { + if (!ensureValidToken()) { emit finishedWithResult(-1); emit finished(); return; } + + const QString materialId = m_extraData.value("material_id").toString(); + if (materialId.isEmpty()) { + qWarning() << "[NetworkService] patchMaterial: material_id is empty"; + emit finishedWithResult(-1); emit finished(); + return; + } + + QJsonObject body; + if (m_extraData.contains("account_number")) { + body["account_number"] = m_extraData.value("account_number").toString(); + } + if (m_extraData.contains("state")) { + body["state"] = m_extraData.value("state").toString(); + } + + const QString path = m_apiBase + m_pathMaterial + "/" + materialId; + const QByteArray bodyJson = QJsonDocument(body).toJson(QJsonDocument::Indented); + qDebug().noquote() << "[NetworkService] patchMaterial PATCH" << path + << "\nbody:\n" << bodyJson; + + const QJsonValue result = patchJson(path, body, true); + if (result.isNull() || result.isUndefined()) { + qWarning() << "[NetworkService] patchMaterial failed:" << materialId; + emit finishedWithResult(-1); + } else { + qDebug() << "[NetworkService] patchMaterial done:" << materialId; + emit finishedWithResult(0); + } + + emit finished(); +} + +void NetworkService::toggleMaterial() { + if (!ensureValidToken()) { emit finishedWithResult(-1); emit finished(); return; } + + const QString materialId = m_extraData.value("material_id").toString(); + const bool enable = m_extraData.value("enable").toBool(); + + if (materialId.isEmpty()) { + qWarning() << "[NetworkService] toggleMaterial: material_id is empty"; + emit finishedWithResult(-1); emit finished(); + return; + } + + // Резолвим (deviceId, bankProfileId) по материалу — нужно и для гарда #3, + // и для сериализации через keyMutex. + const MaterialInfo localMaterial = MaterialDAO::findByMaterialId(materialId); + QString deviceId; + QString bankProfileId; + bool profileActive = true; + if (localMaterial.id >= 0) { + deviceId = localMaterial.deviceId; + const BankProfileInfo app = BankProfileDAO::getApplication(localMaterial.deviceId, localMaterial.appCode); + if (app.id >= 0) { + bankProfileId = app.bankProfileId; + profileActive = (app.status == "active"); + } + } + + // Гард #3 (rule 4-B): пока локальный профиль off, на сервер материал + // не пушим — сервер всё равно держит его disabled, любое расхождение + // дотянет периодический sync после включения профиля. + if (!profileActive) { + qDebug() << "[NetworkService] toggleMaterial skipped — local profile is off:" + << materialId << "(device" << deviceId << ")"; + emit finishedWithResult(0); + emit finished(); + return; + } + + QSharedPointer mtx = keyMutex(deviceId, bankProfileId); + QMutexLocker locker(mtx.data()); + + const QString enableStr = enable ? "true" : "false"; + const QString path = m_apiBase + m_pathMaterial + "/" + materialId + "/" + enableStr; + const QJsonValue result = patchJson(path, QJsonObject(), true); + // Локаль — источник истины (правило-инвариант). Ответ сервера не пишем + // обратно в БД, расхождения дотянет периодический sync. + if (result.isNull() || result.isUndefined()) { + qWarning() << "[NetworkService] toggleMaterial failed:" << materialId; + emit finishedWithResult(-1); + } else { + qDebug() << "[NetworkService] toggleMaterial ok:" << materialId << "enable=" << enable; + emit finishedWithResult(0); + } + + emit finished(); +} + +void NetworkService::toggleBankProfile() { + if (!ensureValidToken()) { emit finishedWithResult(-1); emit finished(); return; } + + const QString bankProfileId = m_extraData.value("bank_profile_id").toString(); + const bool enable = m_extraData.value("enable").toBool(); + + if (bankProfileId.isEmpty()) { + qWarning() << "[NetworkService] toggleBankProfile: bank_profile_id is empty"; + emit finishedWithResult(-1); emit finished(); + return; + } + + // Резолвим deviceId для mutex-ключа (правило #5). + QString deviceId; + const QList apps = BankProfileDAO::findAllByBankProfileId(bankProfileId); + if (!apps.isEmpty()) deviceId = apps.first().deviceId; + + QSharedPointer mtx = keyMutex(deviceId, bankProfileId); + QMutexLocker locker(mtx.data()); + + const QString enableStr = enable ? "true" : "false"; + const QString path = m_apiBase + m_pathBankProfile + "/" + bankProfileId + "/" + enableStr; + const QJsonValue result = patchJson(path, QJsonObject(), true); + if (result.isNull() || result.isUndefined()) { + qWarning() << "[NetworkService] toggleBankProfile failed:" << bankProfileId; + emit finishedWithResult(-1); + } else { + qDebug() << "[NetworkService] toggleBankProfile ok:" << bankProfileId << "enable=" << enable; + emit finishedWithResult(0); + } + + emit finished(); +} + +void NetworkService::postMaterial() { + if (!ensureValidToken()) { emit finished(); return; } + + const QJsonObject body = QJsonDocument::fromJson(m_json).object(); + qDebug() << "[NetworkService] postMaterial body:" << QJsonDocument(body).toJson(QJsonDocument::Compact); + const QJsonValue result = postJson(m_apiBase + m_pathMaterial, body, true); + if (result.isNull() || result.isUndefined()) { + qWarning() << "[NetworkService] postMaterial failed, card:" << body["card_number"].toString(); + } else { + const QString materialId = result.toObject()["id"].toString(); + const int accountId = m_extraData.value("account_id").toInt(); + if (!materialId.isEmpty() && accountId > 0) { + MaterialDAO::updateMaterialId(accountId, materialId); + } + qDebug() << "[NetworkService] postMaterial ok, card:" << body["card_number"].toString() << "material_id:" << materialId; + + // Сервер не применяет поле state при POST — отдельный PATCH //. + if (!materialId.isEmpty()) { + const QString state = body.value("state").toString(); + if (!state.isEmpty()) { + const QString enableStr = (state == "enabled") ? "true" : "false"; + const QString togglePath = m_apiBase + m_pathMaterial + "/" + materialId + "/" + enableStr; + const QJsonValue toggleResult = patchJson(togglePath, QJsonObject(), true); + if (toggleResult.isNull() || toggleResult.isUndefined()) { + qWarning() << "[NetworkService] postMaterial: state enforcement failed for" + << materialId << "state=" << state; + } else { + qDebug() << "[NetworkService] postMaterial: state enforced for" + << materialId << "state=" << state; + } + } + } + } + + emit finished(); +} + +void NetworkService::postDeviceScreenshot() { + if (!ensureValidToken()) { emit finished(); return; } + + const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); + if (device.apiId.isEmpty()) { + qWarning() << "[NetworkService] postDeviceScreenshot: no api_id for" << m_deviceId; + emit finished(); + return; + } + if (m_json.isEmpty()) { + qWarning() << "[NetworkService] postDeviceScreenshot: no image data"; + emit finished(); + return; + } + + const QUrl url(m_apiBase + m_pathDeviceScreenshot + device.apiId); + QNetworkRequest request(url); + request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8()); + + QNetworkReply *reply = nullptr; + bool timedOut = true; + for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) { + if (reply) { reply->deleteLater(); reply = nullptr; } + + auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + QHttpPart filePart; + filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg"); + filePart.setHeader( + QNetworkRequest::ContentDispositionHeader, + QStringLiteral("form-data; name=\"file\"; filename=\"%1.jpg\"").arg(m_deviceId) + ); + filePart.setBody(m_json); + multiPart->append(filePart); + + reply = m_manager->post(request, multiPart); + multiPart->setParent(reply); + + if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; } + qWarning() << "[NetworkService] postDeviceScreenshot timeout attempt" + << attempt << "/" << kMaxRequestAttempts << "for device" << device.apiId; + reply->abort(); + } + + if (timedOut) { + qWarning() << "[NetworkService] postDeviceScreenshot timeout after" + << kMaxRequestAttempts << "attempts for device" << device.apiId; + } else if (reply->error() != QNetworkReply::NoError) { + qWarning() << "[NetworkService] postDeviceScreenshot failed:" << reply->errorString(); + } else { + qDebug() << "[NetworkService] postDeviceScreenshot ok for device" << device.apiId; + } + reply->deleteLater(); + emit finished(); +} + +void NetworkService::createDevice() { + const QString desktopId = SettingsDAO::get("desktop_id"); + if (desktopId.isEmpty()) { + qWarning() << "createDevice: desktop_id not set"; + emit finished(); + return; + } + + const QJsonObject body = QJsonDocument::fromJson(m_json).object(); + const QJsonValue result = postJson(m_apiBase + m_pathDevice, body, true); + + if (!result.isNull() && !result.isUndefined()) { + const QString apiId = result.toObject()["id"].toString(); + if (!apiId.isEmpty() && !m_deviceId.isEmpty()) { + DeviceDAO::updateApiId(m_deviceId, apiId); + qDebug() << "createDevice: saved api_id" << apiId << "for" << m_deviceId; + } + } + + emit finished(); +} + +void NetworkService::updateDevice() { + const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); + if (device.apiId.isEmpty()) { + qWarning() << "updateDevice: api_id not set for" << m_deviceId; + emit finished(); + return; + } + + const QJsonObject body = QJsonDocument::fromJson(m_json).object(); + patchJson(m_apiBase + m_pathDevice + device.apiId, body, true); + + emit finished(); +} + +bool NetworkService::refreshToken() { + const QString storedRefreshToken = SettingsDAO::get("refresh_token"); + if (storedRefreshToken.isEmpty()) { + qWarning() << "refreshToken: refresh_token отсутствует"; + return false; + } + + QJsonObject body; + body["refresh_token"] = storedRefreshToken; + const QJsonValue result = postJson(m_apiBase + m_pathAuthRefresh, body); + + if (result.isNull() || result.isUndefined()) { + qCritical() << "refreshToken: не удалось обновить токен"; + if (!m_tokenErrorShown) { + m_tokenErrorShown = true; + emit tokenRefreshFailed(); + } + return false; + } + + const QJsonObject data = result.toObject(); + m_accessToken = data["access_token"].toString(); + if (m_accessToken.isEmpty() || !data.contains("refresh_token") || !data.contains("expires_in")) { + qCritical() << "refreshToken: ответ не содержит обязательных полей"; + return false; + } + SettingsDAO::set("access_token", m_accessToken); + SettingsDAO::set("refresh_token", data["refresh_token"].toString()); + const qint64 expiresIn = static_cast(data["expires_in"].toDouble()); + SettingsDAO::setLong("token_expires_at", QDateTime::currentSecsSinceEpoch() + expiresIn); + + m_tokenErrorShown = false; // сбрасываем если снова заработало + qDebug() << "refreshToken: токен обновлён, истекает через" << expiresIn << "сек"; + return true; +} + +bool NetworkService::ensureValidToken() { + const qint64 expiresAt = SettingsDAO::getLong("token_expires_at"); + const qint64 now = QDateTime::currentSecsSinceEpoch(); + + // Обновляем заранее — за 30 минут до истечения + if (expiresAt - now < 30 * 60) { + qDebug() << "ensureValidToken: токен истекает через" << (expiresAt - now) << "сек, обновляем"; + return refreshToken(); + } + return true; +} + +void NetworkService::scheduleNextRefresh() { + if (!m_running) return; + + const qint64 expiresAt = SettingsDAO::getLong("token_expires_at"); + const qint64 now = QDateTime::currentSecsSinceEpoch(); + + // Обновляем за 30 минут до истечения; если уже пора — сразу + const qint64 secsUntilRefresh = qMax(0, expiresAt - now - 30 * 60); + qDebug() << "scheduleNextRefresh: следующий рефреш через" << secsUntilRefresh << "сек" + << "(" << secsUntilRefresh / 3600 << "ч" << (secsUntilRefresh % 3600) / 60 << "мин)"; + + QTimer::singleShot(secsUntilRefresh * 1000, this, [this]() { + if (!m_running) return; + + if (refreshToken()) { + scheduleNextRefresh(); // успех — планируем следующий + } else { + // Ошибка — повторяем через 60 секунд + qWarning() << "scheduleNextRefresh: повтор через 60 сек"; + QTimer::singleShot(60 * 1000, this, [this]() { + scheduleNextRefresh(); + }); + } + }); +} + +void NetworkService::getTasks(const QStringList &materialIds) { + if (m_fetchingTasks) { + qDebug() << "[NetworkService] getTasks skipped: already fetching"; + return; + } + m_fetchingTasks = true; + + if (!ensureValidToken()) { + m_fetchingTasks = false; + return; + } + + const QString accessToken = SettingsDAO::get("access_token"); + + QUrl url(m_apiBase + m_pathGetTasks); + QUrlQuery urlQuery; + const QStringList ids = materialIds.isEmpty() ? MaterialDAO::getActiveMaterialIds() : materialIds; + if (ids.isEmpty()) { + m_fetchingTasks = false; + return; + } + for (const QString &mid : ids) { + urlQuery.addQueryItem("materials_id", mid); + } + url.setQuery(urlQuery); + QNetworkRequest request(url); + request.setRawHeader("Authorization", ("Bearer " + accessToken).toUtf8()); + + QNetworkReply *reply = m_manager->get(request); + // Таймаут 30 сек: если ответ не пришёл, abort reply чтобы m_fetchingTasks не залип + auto *timeoutTimer = new QTimer(this); + timeoutTimer->setSingleShot(true); + connect(timeoutTimer, &QTimer::timeout, reply, [reply]() { reply->abort(); }); + connect(reply, &QNetworkReply::finished, timeoutTimer, &QTimer::stop); + connect(reply, &QNetworkReply::finished, timeoutTimer, &QObject::deleteLater); + timeoutTimer->start(30000); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + m_fetchingTasks = false; + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "[NetworkService] getTasks error:" << reply->errorString() + << "status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + return; + } + + const QByteArray raw = reply->readAll(); + QJsonParseError err; + const QJsonDocument doc = QJsonDocument::fromJson(raw, &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) { + qWarning() << "[NetworkService] getTasks: invalid JSON"; + return; + } + + const QJsonObject root = doc.object(); + if (!root.value("success").toBool()) { + qWarning() << "[NetworkService] getTasks: success=false" << root; + return; + } + + emit tasksReceived(root.value("data").toArray()); + }); +} + +void NetworkService::postTaskResult() { + const QJsonObject body = QJsonDocument::fromJson(m_json).object(); + const QJsonValue result = postJson(m_apiBase + m_pathTaskResult, body, true); + if (result.isUndefined()) { + qWarning() << "[NetworkService] postTaskResult failed, body:" << QJsonDocument(body).toJson(QJsonDocument::Compact); + emit finishedWithResult(-1); + } else { + emit finishedWithResult(0); + } + emit finished(); +} + +void NetworkService::postMonitoringLog(const QString &type, const QString &event, + const QString &message, const QByteArray &image, + bool toApiBase) { + QNetworkRequest request; + if (toApiBase) { + ensureValidToken(); + request.setUrl(QUrl(m_apiBase + m_pathMonitoringLog)); + if (!m_accessToken.isEmpty()) { + request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8()); + } + } else { + request.setUrl(QUrl(m_monitoringBase + m_pathMonitoringLog)); + if (!m_monitoringToken.isEmpty()) { + request.setRawHeader("X-Monitoring-Token", m_monitoringToken.toUtf8()); + } + } + + const int internalTaskId = m_extraData.value("internal_task_id").toInt(); + + QNetworkReply *reply = nullptr; + bool timedOut = true; + for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) { + if (reply) { reply->deleteLater(); reply = nullptr; } + + auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + addFormField(multiPart, "type", type.toUpper()); + addFormField(multiPart, "event", event); + addFormField(multiPart, "message", message); + if (internalTaskId > 0) { + addFormField(multiPart, "internal_task_id", QString::number(internalTaskId)); + } + if (!image.isEmpty()) { + QHttpPart imagePart; + imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png"); + imagePart.setHeader( + QNetworkRequest::ContentDispositionHeader, + QStringLiteral("form-data; name=\"image\"; filename=\"screenshot.png\"") + ); + imagePart.setBody(image); + multiPart->append(imagePart); + } + + reply = m_manager->post(request, multiPart); + multiPart->setParent(reply); + + if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; } + qWarning() << "[NetworkService] postMonitoringLog timeout attempt" + << attempt << "/" << kMaxRequestAttempts; + reply->abort(); + } + + if (timedOut) { + qWarning() << "[NetworkService] postMonitoringLog timeout after" + << kMaxRequestAttempts << "attempts"; + } else if (reply->error() != QNetworkReply::NoError) { + qWarning() << "[NetworkService] postMonitoringLog failed:" << reply->errorString() + << "\n response body:" << reply->readAll(); + } + reply->deleteLater(); +} + +void NetworkService::postMonitoringDump(const QString &text, const QByteArray &archive, + const QByteArray &image, + const QString &archiveFilename) { + qDebug() << "[NetworkService] postMonitoringDump enter — text.size=" << text.size() + << "archive.size=" << archive.size() << "image.size=" << image.size() + << "archiveFilename=" << archiveFilename; + + const QUrl url(m_monitoringBase + m_pathMonitoringDump); + QNetworkRequest request(url); + if (!m_monitoringToken.isEmpty()) { + request.setRawHeader("X-Monitoring-Token", m_monitoringToken.toUtf8()); + } + + qDebug() << "[NetworkService] postMonitoringDump POST" << url.toString(); + + QNetworkReply *reply = nullptr; + bool timedOut = true; + for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) { + if (reply) { reply->deleteLater(); reply = nullptr; } + + auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + addFormField(multiPart, "text", text); + if (!archive.isEmpty()) { + QHttpPart filePart; + filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/zip"); + filePart.setHeader( + QNetworkRequest::ContentDispositionHeader, + QStringLiteral("form-data; name=\"file\"; filename=\"%1\"").arg(archiveFilename) + ); + filePart.setBody(archive); + multiPart->append(filePart); + } + if (!image.isEmpty()) { + QHttpPart imagePart; + imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png"); + imagePart.setHeader( + QNetworkRequest::ContentDispositionHeader, + QStringLiteral("form-data; name=\"image\"; filename=\"final_screen.png\"") + ); + imagePart.setBody(image); + multiPart->append(imagePart); + } + + reply = m_manager->post(request, multiPart); + multiPart->setParent(reply); + + if (awaitReply(reply, kDumpTimeoutMs)) { timedOut = false; break; } + qWarning() << "[NetworkService] postMonitoringDump timeout attempt" + << attempt << "/" << kMaxRequestAttempts; + reply->abort(); + } + + if (timedOut) { + qWarning() << "[NetworkService] postMonitoringDump timeout after" + << kMaxRequestAttempts << "attempts"; + } else if (reply->error() != QNetworkReply::NoError) { + qWarning() << "[NetworkService] postMonitoringDump failed:" << reply->errorString() + << "\n response body:" << reply->readAll(); + } else { + const QByteArray raw = reply->readAll(); + const QJsonDocument doc = QJsonDocument::fromJson(raw); + if (doc.isObject() && !doc.object().value("success").toBool()) { + qWarning() << "[NetworkService] postMonitoringDump rejected:" << raw; + } else { + qDebug() << "[NetworkService] postMonitoringDump OK:" << raw; + } + } + reply->deleteLater(); +} + +void NetworkService::start() { + m_running = true; + + // Проверяем токен при старте и планируем рефреш по времени жизни + ensureValidToken(); + scheduleNextRefresh(); + + // Периодическую синхронизацию запускает startPeriodicSync(), который должен + // быть вызван явно после firstPassDone от DeviceScreener (см. правило #6 + // в docs/status-sync.md). Без firstPassDone мы не знаем, какие устройства + // действительно онлайн, и можем ошибочно сработать как «device→offline». +} + +void NetworkService::startPeriodicSync() { + if (m_periodicSyncEnabled) return; + m_periodicSyncEnabled = true; + qDebug() << "[NetworkService] periodic sync enabled (first tick in" << (kPeriodicSyncMs / 1000) << "s)"; + scheduleNextStatusSync(); +} + +void NetworkService::scheduleNextStatusSync() { + if (!m_running || !m_periodicSyncEnabled) return; + + QTimer::singleShot(kPeriodicSyncMs, this, [this]() { + if (!m_running) return; + + // Если предыдущий тик ещё не завершился — пропускаем (правило #10). + if (m_syncInProgress.exchange(true)) { + qDebug() << "[NetworkService] periodic sync tick skipped — previous still in progress"; + scheduleNextStatusSync(); + return; + } + + const bool ok = runPeriodicSyncTick(); + m_syncInProgress.store(false); + + if (ok) { + if (m_consecutiveSyncFailures > 0 || m_unhealthyEmitted) { + qDebug() << "[NetworkService] periodic sync recovered after" + << m_consecutiveSyncFailures << "failures"; + } + m_consecutiveSyncFailures = 0; + if (m_unhealthyEmitted) { + m_unhealthyEmitted = false; + emit syncHealthChanged(true, 0); + } + } else { + ++m_consecutiveSyncFailures; + qWarning() << "[NetworkService] periodic sync tick failed; consecutive failures:" + << m_consecutiveSyncFailures; + if (!m_unhealthyEmitted && m_consecutiveSyncFailures >= kSyncFailureThreshold) { + m_unhealthyEmitted = true; + emit syncHealthChanged(false, m_consecutiveSyncFailures); + } + } + + scheduleNextStatusSync(); + }); +} + +bool NetworkService::runPeriodicSyncTick() { + if (!ensureValidToken()) return false; + return syncCurrentProfile(); +} + +QMutex NetworkService::s_keyRegistryMutex; +QMap> NetworkService::s_keyMutexes; + +QSharedPointer NetworkService::keyMutex(const QString &deviceId, const QString &bankProfileId) { + const QString key = deviceId + "|" + bankProfileId; + QMutexLocker locker(&s_keyRegistryMutex); + auto it = s_keyMutexes.find(key); + if (it == s_keyMutexes.end()) { + it = s_keyMutexes.insert(key, QSharedPointer::create()); + } + return *it; +} + +void NetworkService::onDeviceWentOffline(const QString &deviceId) { + // Правило #1 (device→OFFLINE): выключаем bank_profile и локально, и на сервере. + // Материалы локально не трогаем (правило 4-A). + if (deviceId.isEmpty()) return; + if (!ensureValidToken()) return; + + qDebug() << "[NetworkService] onDeviceWentOffline:" << deviceId; + + const QList apps = BankProfileDAO::getApplicationsByDeviceId(deviceId); + + // 1) Сервер: для каждого активного профиля с bank_profile_id → PATCH disable. + for (const BankProfileInfo &app : apps) { + if (app.status != "active") continue; + if (app.bankProfileId.isEmpty()) continue; + + QSharedPointer mtx = keyMutex(deviceId, app.bankProfileId); + QMutexLocker locker(mtx.data()); + + const QString path = m_apiBase + m_pathBankProfile + "/" + app.bankProfileId + "/false"; + patchJson(path, QJsonObject(), true); + qDebug() << "[NetworkService] onDeviceWentOffline: disabled bank_profile on server:" + << app.bankProfileId << "(" << app.code << ")"; + } + + // 2) Локально: все профили устройства → off. + if (!BankProfileDAO::deactivateAppsForDevice(deviceId)) { + qWarning() << "[NetworkService] onDeviceWentOffline: failed to deactivate local apps for device" << deviceId; + } +} + +void NetworkService::onDeviceWentOnline(const QString &deviceId) { + // Правило #1 (device→ONLINE): локаль не меняем, пушим текущее локальное + // состояние bank_profile и его материалов на сервер (с учётом гарда #3). + if (deviceId.isEmpty()) return; + if (!ensureValidToken()) return; + + qDebug() << "[NetworkService] onDeviceWentOnline:" << deviceId; + + const QJsonValue profileResult = getJson(m_apiBase + m_pathCurrentProfile, true); + if (profileResult.isUndefined() || profileResult.isNull()) { + qWarning() << "[NetworkService] onDeviceWentOnline: GET current_profile failed"; + return; + } + + QMap serverProfileState; + QMap serverMaterialState; + const QJsonArray bankProfiles = profileResult.toObject()["bank_profiles"].toArray(); + for (const QJsonValue &bpVal : bankProfiles) { + const QJsonObject bp = bpVal.toObject(); + const QString bpId = bp["id"].toString(); + if (!bpId.isEmpty()) serverProfileState[bpId] = bp["state"].toString(); + for (const QJsonValue &matVal : bp["materials"].toArray()) { + const QJsonObject mat = matVal.toObject(); + const QString matId = mat["id"].toString(); + if (!matId.isEmpty()) serverMaterialState[matId] = mat["state"].toString(); + } + } + + const QList apps = BankProfileDAO::getApplicationsByDeviceId(deviceId); + for (const BankProfileInfo &app : apps) { + if (app.bankProfileId.isEmpty()) continue; + if (!serverProfileState.contains(app.bankProfileId)) { + qWarning() << "[NetworkService] onDeviceWentOnline: server lacks bank_profile" << app.bankProfileId; + continue; + } + + QSharedPointer mtx = keyMutex(deviceId, app.bankProfileId); + QMutexLocker locker(mtx.data()); + + const QString localBpState = (app.status == "active") ? "enabled" : "disabled"; + const QString srvBpState = serverProfileState.value(app.bankProfileId); + if (srvBpState != localBpState) { + const QString enableStr = (app.status == "active") ? "true" : "false"; + patchJson(m_apiBase + m_pathBankProfile + "/" + app.bankProfileId + "/" + enableStr, + QJsonObject(), true); + qDebug() << "[NetworkService] onDeviceWentOnline: pushed bank_profile" << app.bankProfileId + << "→" << enableStr; + } + + // Гард #3: материалы не пушим при локально выключенном профиле. + if (app.status != "active") continue; + + const QList materials = MaterialDAO::getAccounts(deviceId, app.code); + for (const MaterialInfo &mat : materials) { + if (mat.materialId.isEmpty()) continue; + if (!serverMaterialState.contains(mat.materialId)) continue; + + const QString localMatState = (materialStatusToString(mat.status) == "active") + ? "enabled" : "disabled"; + const QString srvMatState = serverMaterialState.value(mat.materialId); + if (srvMatState != localMatState) { + const QString enableStr = (localMatState == "enabled") ? "true" : "false"; + patchJson(m_apiBase + m_pathMaterial + "/" + mat.materialId + "/" + enableStr, + QJsonObject(), true); + qDebug() << "[NetworkService] onDeviceWentOnline: pushed material" << mat.materialId + << "→" << enableStr; + } + } + } +} + +void NetworkService::deleteDeviceFromServer() { + if (!ensureValidToken()) { emit finished(); return; } + + const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); + if (device.apiId.isEmpty()) { + qWarning() << "[deleteDeviceFromServer] no api_id for device" << m_deviceId; + // Удаляем только локально + DeviceDAO::deleteDevice(m_deviceId); + emit finished(); + return; + } + + // Сначала удаляем на сервере все банковские профили девайса, которые уже + // были синхронизированы (есть bank_profile_id). Несинхронизированные — + // пропускаем, на сервере их нет. + const QList profiles = BankProfileDAO::getApplicationsByDeviceId(device.id); + for (const BankProfileInfo &bp : profiles) { + if (bp.bankProfileId.isEmpty()) continue; + const QJsonValue bpResult = deleteJson(m_apiBase + m_pathBankProfile + "/" + bp.bankProfileId, true); + if (bpResult.isUndefined()) { + qWarning() << "[deleteDeviceFromServer] failed to delete bank_profile on server:" << bp.bankProfileId << bp.code; + } else { + qDebug() << "[deleteDeviceFromServer] deleted bank_profile on server:" << bp.bankProfileId << bp.code; + } + } + + const QJsonValue result = deleteJson(m_apiBase + m_pathDevice + device.apiId, true); + if (result.isUndefined()) { + qWarning() << "[deleteDeviceFromServer] failed to delete device on server:" << device.apiId; + } else { + qDebug() << "[deleteDeviceFromServer] deleted device on server:" << device.apiId; + } + + DeviceDAO::deleteDevice(m_deviceId); + emit finished(); +} + +void NetworkService::disableAllProfilesOnServer() { + if (!ensureValidToken()) return; + + const QJsonValue profileResult = getJson(m_apiBase + m_pathCurrentProfile, true); + if (profileResult.isUndefined() || profileResult.isNull()) return; + + const QJsonArray bankProfiles = profileResult.toObject()["bank_profiles"].toArray(); + for (const QJsonValue &bpVal : bankProfiles) { + const QJsonObject bp = bpVal.toObject(); + const QString bpId = bp["id"].toString(); + const QString bpState = bp["state"].toString(); + if (bpId.isEmpty() || bpState == "disabled") continue; + + patchJson(m_apiBase + m_pathBankProfile + "/" + bpId + "/false", QJsonObject(), true); + qDebug() << "[shutdown] disabled bank_profile" << bpId; + + // const QJsonArray materials = bp["materials"].toArray(); + // for (const QJsonValue &matVal : materials) { + // const QJsonObject mat = matVal.toObject(); + // const QString matId = mat["id"].toString(); + // const QString matState = mat["state"].toString(); + // if (matId.isEmpty() || matState == "disabled") continue; + // + // patchJson(m_apiBase + m_pathMaterial + "/" + matId + "/false", QJsonObject(), true); + // qDebug() << "[shutdown] disabled material" << matId; + // } + } +} + +void NetworkService::stop() { + m_running = false; + QMetaObject::invokeMethod(this, [this]() { + disableAllProfilesOnServer(); + emit finished(); + }, Qt::QueuedConnection); +} + +void NetworkService::connectErrorDialog(QWidget *parent, NetworkService *net) { + if (!net) return; + // Если parent есть — connect привязан к нему (авто-disconnect при уничтожении). + // Если nullptr — привязываемся к самому net и берём activeWindow на момент сигнала. + QObject *ctx = parent ? static_cast(parent) : static_cast(net); + QObject::connect(net, &NetworkService::apiPatchError, ctx, + [parent](int httpStatus, const QString &message) { + Q_UNUSED(httpStatus); + QWidget *p = parent ? parent : qApp->activeWindow(); + QMessageBox::warning(p, "Ошибка", message); + }, Qt::QueuedConnection); +} diff --git a/services/net/NetworkService.h b/services/net/NetworkService.h new file mode 100644 index 0000000..2a7761b --- /dev/null +++ b/services/net/NetworkService.h @@ -0,0 +1,182 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +class QWidget; + +class NetworkService final : public QObject { + Q_OBJECT + +public: + explicit NetworkService(QObject *parent = nullptr); + + ~NetworkService() override; + + void setPayload(const QByteArray &json) { m_json = json; } + + void setDeviceId(const QString &deviceId) { + m_deviceId = deviceId; + } + + void addExtra(const QString &key, const QVariant &value); + + // Подвешивает QMessageBox с текстом ошибки от backend (HTTP 409 — "материал + // заблокирован" и подобные) к ближайшему окну. Вызывать после `new NetworkService` + // в UI-коде, до запуска треда. + static void connectErrorDialog(QWidget *parent, NetworkService *net); + +public slots: + void postAccountsData(); + + void postBankProfile(); + + void patchBankProfile(); + + void toggleBankProfile(); + + void postMaterial(); + + void patchMaterial(); + + void toggleMaterial(); + + void postDeviceScreenshot(); + + void getPayments(); + + void postAppStatus(); + + void postEventStatus(); + + void insertTransactionData(); + + void updateTransactionData(); + + void loginAndSetup(); + // true — GET current_profile прошёл; false — таймаут/сеть. Используется + // health-счётчиком в runPeriodicSyncTick (правило #9). + bool syncCurrentProfile(); + + // Включает периодическую синхронизацию статусов (60s). Должно быть вызвано + // ОДИН раз для долгоживущего инстанса после firstPassDone от DeviceScreener. + void startPeriodicSync(); + + // Обрабатывает переход устройства в офлайн (дебаунс-сигнал от DeviceScreener): + // выключает bank_profile локально и на сервере. + void onDeviceWentOffline(const QString &deviceId); + + // Обрабатывает переход устройства в онлайн: пушит текущее локальное + // состояние профиля и материалов на сервер (локаль не меняем). + void onDeviceWentOnline(const QString &deviceId); + + void createDevice(); + + void updateDevice(); + + void getTasks(const QStringList &materialIds = {}); + + void postTaskResult(); + + // toApiBase=true — POST на m_apiBase + /monitoring/log с Bearer-токеном + // (старый эндпоинт, используется для task_success). По умолчанию false — + // monitoringBase + X-Monitoring-Token (Telegram-релей, для ошибок). + void postMonitoringLog(const QString &type, const QString &event, + const QString &message, const QByteArray &image, + bool toApiBase = false); + + void postMonitoringDump(const QString &text, const QByteArray &archive, + const QByteArray &image, + const QString &archiveFilename = QStringLiteral("task_dump.zip")); + + void start(); + void stop(); + void disableAllProfilesOnServer(); + void deleteDeviceFromServer(); + +signals: + void finished(); + void finishedWithResult(int value); + void tokenRefreshFailed(); + void tasksReceived(QJsonArray tasks); + // Эмитится при HTTP-ошибке PATCH-запроса (например, 409 'материал заблокирован'). + // UI подписывается и показывает QMessageBox. + void apiPatchError(int httpStatus, QString message); + // healthy=false при kSyncFailureThreshold подряд неудачных тиков; healthy=true + // при первом успехе после такой серии. consecutiveFailures даёт текущий счёт. + void syncHealthChanged(bool healthy, int consecutiveFailures); + +private: + QByteArray m_json; + QString m_deviceId; + QString m_token; + QString m_accessToken; + QString m_apiBase; + QMap m_extraData = {}; + bool m_running = true; + bool m_tokenErrorShown = false; + + QString m_urlAccountUpdate; + QString m_urlPaymentsGet; + QString m_urlTransactionUpdate; + QString m_urlAppStatusUpdate; + QString m_urlTransactionInsert; + QString m_urlEventStatusUpdate; + QString m_pathAuthLogin; + QString m_pathAuthRefresh; + QString m_pathDesktopCreate; + QString m_pathDevice; + QString m_pathDeviceScreenshot; + QString m_pathBankProfile; + QString m_pathMaterial; + QString m_pathGetTasks; + QString m_pathTaskResult; + QString m_pathMonitoringLog; + QString m_pathMonitoringDump; + QString m_monitoringBase; + QString m_monitoringToken; + QString m_pathCurrentProfile; + QString m_pathProfileGetInfo; + bool m_fetchingTasks = false; + + // Периодическая синхронизация + bool m_periodicSyncEnabled = false; // включается только после firstPassDone + std::atomic m_syncInProgress{false}; // защита от наложения тиков + int m_consecutiveSyncFailures = 0; // счётчик подряд неуспешных тиков + bool m_unhealthyEmitted = false; // чтобы не спамить syncHealthChanged + static constexpr int kSyncFailureThreshold = 3; + static constexpr int kPeriodicSyncMs = 60 * 1000; + + // Реестр мьютексов на пару (deviceId, bankProfileId) — общий для всех + // инстансов NetworkService. Используется для сериализации PATCH-операций. + static QMutex s_keyRegistryMutex; + static QMap> s_keyMutexes; + + QNetworkAccessManager *m_manager; + + QJsonValue post(QHttpMultiPart *multiPart, const QString &path); + QJsonArray get(const QString &path, const QMap ¶ms); + QJsonValue getJson(const QString &path, bool withAuth = false); + QJsonValue postJson(const QString &path, const QJsonObject &body, bool withAuth = false); + QJsonValue patchJson(const QString &path, const QJsonObject &body, bool withAuth = false); + QJsonValue deleteJson(const QString &path, bool withAuth = false); + + bool refreshToken(); + bool ensureValidToken(); + void scheduleNextRefresh(); + void scheduleNextStatusSync(); + bool runPeriodicSyncTick(); // делает один тик, возвращает true при успехе + void syncDevicesFromServer(const QString &desktopId); + void syncBankProfilesFromServer(); + void syncBanksIfEmpty(); + +public: + // Возвращает (лениво создавая) shared mutex для пары (deviceId, bankProfileId). + // Используется через QMutexLocker для сериализации PATCH-ов по одному профилю. + static QSharedPointer keyMutex(const QString &deviceId, const QString &bankProfileId); +}; diff --git a/split_zip.ps1 b/split_zip.ps1 new file mode 100644 index 0000000..6664c79 --- /dev/null +++ b/split_zip.ps1 @@ -0,0 +1,48 @@ +param( + [string]$SourceDir, + [string]$OutputDir, + [string]$Prefix = "ARC_x64_part", + [long]$MaxSize = 48000000 +) + +$SourceDir = $SourceDir.Trim('"', "'", ' ') +$OutputDir = $OutputDir.Trim('"', "'", ' ') + +$part = 1 +$currentSize = 0 +$currentFiles = @() +$allItems = Get-ChildItem -Path $SourceDir -Recurse -File | Sort-Object Length + +function Flush-Part { + param($files, $partNum, $size) + $zipName = Join-Path $OutputDir "$Prefix$partNum.zip" + Write-Host "Creating part $partNum ($([math]::Round($size/1MB,1)) MB, $($files.Count) files)" + $tempDir = Join-Path $env:TEMP "arc_split_$partNum" + if (Test-Path $tempDir) { Remove-Item $tempDir -Recurse -Force } + foreach ($cf in $files) { + $dest = Join-Path $tempDir $cf.Rel + $destDir = Split-Path $dest -Parent + if (!(Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null } + Copy-Item $cf.Full $dest + } + Compress-Archive -Path (Join-Path $tempDir '*') -DestinationPath $zipName -Force + Remove-Item $tempDir -Recurse -Force +} + +foreach ($f in $allItems) { + $rel = $f.FullName.Substring($SourceDir.TrimEnd('\').Length) + if ($currentSize + $f.Length -gt $MaxSize -and $currentFiles.Count -gt 0) { + Flush-Part $currentFiles $part $currentSize + $part++ + $currentSize = 0 + $currentFiles = @() + } + $currentFiles += @{ Full = $f.FullName; Rel = $rel } + $currentSize += $f.Length +} + +if ($currentFiles.Count -gt 0) { + Flush-Part $currentFiles $part $currentSize +} + +Write-Host "Done: $part part(s) created." diff --git a/tessdata/eng.traineddata b/tessdata/eng.traineddata new file mode 100644 index 0000000..f4744c2 Binary files /dev/null and b/tessdata/eng.traineddata differ diff --git a/tessdata/rus.traineddata b/tessdata/rus.traineddata new file mode 100644 index 0000000..8b71c2d Binary files /dev/null and b/tessdata/rus.traineddata differ diff --git a/tests/mock_server/README.md b/tests/mock_server/README.md new file mode 100644 index 0000000..5b0d488 --- /dev/null +++ b/tests/mock_server/README.md @@ -0,0 +1,322 @@ +# Интеграционные тесты задач — mock-сервер + +ARC запускается как обычно (реальные Android-устройства через ADB), но +подключается к локальному Flask-моку вместо боевого бэкенда. Мок раздаёт +задачи по одной на material, собирает результаты и показывает отчёт в браузере. + +--- + +## 1. Требования + +| Компонент | macOS | Windows | +|---|---|---| +| Python | `brew install python` или встроенный | [python.org](https://python.org), галочка «Add to PATH» | +| Flask | `pip install flask` | `pip install flask` | +| ADB | `brew install android-platform-tools` | Используется из `tools\adb\windows\` | +| ARC | `cmake --build build_test` | `cmake --build build --config Release` | +| Устройства | `adb devices` → `device` | `tools\adb\windows\adb.exe devices` | + +--- + +## 2. Генерация тестовых данных из БД + +Перед первым запуском (или при смене устройств/аккаунтов) нужно сгенерировать +мок-данные из локальной базы. Скрипты читают `config.ini → [db] dbPath`. + +### 2.1 Стартовые данные: профиль + FETCH_PROFILE + +```bash +# macOS (из корня проекта) +python3 tests/mock_server/gen_mock_data.py + +# Windows +python tests\mock_server\gen_mock_data.py +``` + +Скрипт: +- Находит `config.ini` в текущей директории (или `build/Release/config.ini`) +- Открывает БД, читает `bank_profiles` с проверенным PIN-кодом (`pin_code_checked = 'yes'`) и активные `materials` +- Генерирует: + - `tests/mock_server/default_profile.json` — профиль для мока + - `tests/mock_server/scenarios/*__fetch_profile.json` — по одному на материал (с номером карты в имени) +- Очищает таблицу `events` в БД + +Пример вывода: +``` +config: config.ini +database: database/app_data.db + deleted 5 old events + wrote default_profile.json (3 bank_profiles) + wrote scenarios/pixel_7_ozon_7919_fetch_profile.json + wrote scenarios/pixel_7_ozon_9865_fetch_profile.json + wrote scenarios/pixel_7_dushanbe_city_bank_3258_fetch_profile.json + wrote scenarios/tecno_km4_ozon_0982_fetch_profile.json + +Devices (2): + pixel_7: ozon/7919 (50393cc1…), ozon/9865 (06a1ae15…), dushanbe_city_bank/3258 (36002ac2…) + tecno_km4: ozon/0982 (7e3a7e4e…) +``` + +Флаги: `--config C:\path\to\config.ini` — если config не в текущей директории. + +### 2.2 Пул FETCH_TRANSACTIONS с живого устройства + +```bash +# macOS +python3 tests/mock_server/gen_fetch_transactions.py + +# Windows +python tests\mock_server\gen_fetch_transactions.py +``` + +Скрипт: +- Читает БД → находит активные материалы +- Находит подключённые ADB-устройства → матчит по `adb_serial` +- Показывает меню → выбор материала или `all` +- Проходит по транзакциям на экране: + - **Ozon**: парсит заголовки дат (Сегодня/Вчера/dd месяца), определяет дату каждой транзакции по Y-позиции, тапает для получения времени из деталей, скроллит вниз для следующих + - **Dushanbe**: парсит Выписку, тап в детали, время Dushanbe (UTC+5) +- Дедупликация по `(amount, created_at)` — одинаковые суммы в один день с разным временем сохраняются как отдельные задачи +- Сохраняет `scenarios/pool/*__ft_*.json` с реальными `amount` (в копейках) и `created_at` (UTC) +- Если мок запущен — сразу инжектит задачи + +Флаги: +- `--serial 148697059H000712` — явный ADB-серийник +- `--kopecks 100` — множитель суммы (по умолчанию 100) +- `--config path` — путь к config.ini + +--- + +## 3. Переключение ARC на мок + +В `config.ini` → `[net]`: + +```ini +[net] +apiBase=http://127.0.0.1:8888 +``` + +Остальные `path*` оставить как есть. + +> На Windows: `config.ini` обычно лежит рядом с `ARC.exe` в `build\Release\`. +> Совет: держите копию `config.ini.mock` и `config.ini.prod`. + +--- + +## 4. Запуск теста + +### Терминал 1 — mock-сервер + +```bash +# macOS — полный цикл (генерация данных + запуск) +cd tests/mock_server && ./run.sh + +# macOS — только определённые банки +cd tests/mock_server && ./run.sh --banks ozon + +# macOS — только FETCH_TRANSACTIONS (без FETCH_PROFILE) +cd tests/mock_server && ./run.sh --types FETCH_TRANSACTIONS --banks ozon + +# macOS — debug режим (auto-reload при изменении кода) +cd tests/mock_server && ./run.sh --banks ozon --debug + +# Windows +cd tests\mock_server +python mock_server.py --port 8888 +``` + +Флаги `mock_server.py`: +- `--banks ozon dushanbe_city_bank` — только указанные банки +- `--types FETCH_TRANSACTIONS` — только указанные типы задач +- `--ft-per-material 3` — сколько FT на материал при бутстрапе (default 3) +- `--seed 42` — фиксированный RNG для воспроизводимости +- `--debug` — Flask debug режим (auto-reload, состояние сохраняется между перезагрузками) + +При `--debug`: +- Изменения в `.py` файлах применяются автоматически без перезапуска +- Состояние задач сохраняется в `.mock_state.json` и восстанавливается при reload +- Скриншоты сохраняются в `.mock_screenshots/` (переживают reload) +- При **первом** запуске — чистый старт (events в БД очищаются, state сбрасывается) + +### Терминал 2 — ARC + +```bash +# macOS (из корня проекта!) +./build_test/ARC + +# Windows +build\Release\ARC.exe +``` + +При логине мок автоматически ставит задачи: +- 1 × FETCH_PROFILE на каждый материал (если `--types` не ограничивает) +- N × FETCH_TRANSACTIONS (случайные из pool) + +Задачи раздаются **по 1 на material за поллинг** — следующая только после +получения результата предыдущей. Это предотвращает expire задач в очереди. + +### Терминал 3 — отчёт + +```bash +# Браузер с авто-обновлением +open http://127.0.0.1:8888/_admin/report/html # macOS +start http://127.0.0.1:8888/_admin/report/html # Windows +``` + +--- + +## 5. HTML-отчёт + +`http://127.0.0.1:8888/_admin/report/html` — веб-страница с: +- Сводкой: всего / выполнено / ошибки / в очереди +- Группировкой по **устройствам** → **банкам** (Pixel 7 → ozon, dushanbe_city_bank) +- Для каждой задачи: + - Task JSON и Result JSON (для успешных — свёрнуты, кнопка "Показать JSON") + - Ошибки — развёрнуты сразу с красным бейджем + - Скриншот (если есть) — третья колонка, клик открывает полноэкранный просмотр + - Кнопка "Копировать" на каждом JSON блоке +- Цветовой индикацией: зелёный = completed, красный = error, жёлтый = pending +- Авто-обновлением каждые 2 секунды (состояние свёрнутых/развёрнутых блоков сохраняется) + +--- + +## 6. Скриншоты + +Приложение отправляет скриншоты через `POST /monitoring/log` с полем `internal_task_id`. +Мок-сервер: +- Сохраняет PNG на диск в `.mock_screenshots/` +- Привязывает к задаче по `internal_task_id` (точное совпадение с `task_result`) +- Отображает в отчёте как третью колонку рядом с Task/Result JSON +- Клик по скриншоту — полноэкранный просмотр (Escape для закрытия) + +--- + +## 7. Ручной инжект задач + +```bash +# Одна задача +curl -X POST http://127.0.0.1:8888/_admin/inject \ + -H "Content-Type: application/json" \ + --data-binary @scenarios/pixel_7_ozon_7919_fetch_profile.json + +# Повторный бутстрап (без перезапуска мока) +curl -X POST http://127.0.0.1:8888/_admin/bootstrap + +# Сброс (очистка очереди + результатов + скриншотов) +curl -X POST http://127.0.0.1:8888/_admin/reset +``` + +--- + +## 8. Админ-эндпойнты мока + +| Метод | Путь | Назначение | +|---|---|---| +| POST | `/_admin/inject` | Положить задачу (или массив) в очередь | +| POST | `/_admin/bootstrap` | Повторный бутстрап | +| POST | `/_admin/seed_profile` | Подменить ответ `current_profile` | +| POST | `/_admin/seed_devices` | Подменить список устройств | +| GET | `/_admin/report` | JSON: полный отчёт | +| GET | `/_admin/report/html` | HTML-отчёт с авто-обновлением | +| GET | `/_admin/report/pending` | Только незавершённые задачи | +| GET | `/_admin/report/fragment` | JSON-фрагмент для JS-обновления | +| GET | `/_admin/screenshot/` | PNG скриншот по ID | +| GET | `/_admin/state` | Счётчики | +| POST | `/_admin/reset` | Очистить всё, сбросить бутстрап | + +--- + +## 9. Полный цикл с нуля + +```bash +# 1. Установка +pip install flask + +# 2. Собрать ARC, запустить с боевым сервером → залогиниться +# Устройства, профили и материалы появятся в БД + +# 3. Переключить config.ini на мок +# [net] +# apiBase=http://127.0.0.1:8888 + +# 4. Сгенерировать стартовые данные из БД +python3 tests/mock_server/gen_mock_data.py + +# 5. Открыть на устройстве банковское приложение → список транзакций +# Сгенерировать пул задач +python3 tests/mock_server/gen_fetch_transactions.py + +# 6. Запустить мок +cd tests/mock_server && ./run.sh --banks ozon --debug + +# 7. Запустить ARC (из корня проекта, в отдельном терминале) +./build_test/ARC + +# 8. Открыть отчёт +open http://127.0.0.1:8888/_admin/report/html + +# 9. Дождаться completed = всего в отчёте + +# 10. Вернуть config.ini на прод +``` + +--- + +## 10. Структура файлов + +``` +tests/mock_server/ +├── mock_server.py # Flask mock-сервер +├── run.sh # запуск (macOS/Linux): gen_mock_data + mock_server +├── gen_mock_data.py # генерация default_profile + scenarios из БД +├── gen_fetch_transactions.py # генерация pool из живого устройства +├── inject.sh # curl-обёртка для инжекта +├── default_profile.json # авто-генерированный профиль для мока +├── README.md # эта инструкция +├── WINDOWS.md # Windows-специфичные нюансы +├── .mock_state.json # состояние задач (debug режим, .gitignore) +├── .mock_screenshots/ # скриншоты с устройств (.gitignore) +├── scenarios/ # FETCH_PROFILE сценарии +│ ├── pixel_7_ozon_7919_fetch_profile.json +│ ├── pixel_7_ozon_9865_fetch_profile.json +│ └── ... +└── scenarios/pool/ # пул FETCH_TRANSACTIONS + ├── pixel_7_ozon_7919_01_minus_108_ft_01.json + ├── pixel_7_ozon_9865_01_minus_108_ft_01.json + ├── pixel_7_dushanbe_city_bank_3258_01_plus_5_ft_01.json + └── ... +``` + +--- + +## 11. Типовые проблемы + +| Симптом | Причина | Решение | +|---|---|---| +| `config.ini not found` | Скрипт запущен не из корня проекта | `cd` в корень или `--config path` | +| `No active materials` | Нет профилей с проверенным PIN | Запустить ARC с боевым сервером, добавить PIN | +| `No ADB devices match` | Устройства не подключены | `adb devices`, проверить USB | +| `App restarted` ошибки в отчёте | Events остались от прошлого запуска | Мок-сервер чистит events при старте; используйте `run.sh` или запускайте сервер **до** ARC | +| Задача `pending` навсегда | material в `_in_flight` | `/_admin/reset` и перезапустить ARC | +| `Transaction not found` | Баг поиска — скролл, видимость кнопок | Анализировать логи (device= в ошибке) | +| `Page load error` | Ozon WebView не загрузился | Проблема интернета/устройства, таймаут 5 мин | +| Скриншот не привязан | Нет `internal_task_id` | Обновить ARC (фикс в EventHandler.cpp) | +| Pool material_id не совпадает | БД обновилась, pool устарел | Перегенерировать pool или обновить material_id в файлах | +| Порт занят | Другой процесс на 8888 | `--port 9999` + поменять `apiBase` | + +--- + +## 12. Что мок эмулирует и что нет + +**Эмулирует:** +- Авторизацию (login/refresh) +- Синхронизацию профилей (current_profile, bank_profile CRUD) +- Выдачу задач (get_tasks) с in-flight контролем +- Приём результатов (task_result) с `internal_task_id` +- Приём скриншотов (monitoring/log) с привязкой к задачам +- Управление устройствами и материалами (toggle) + +**Не эмулирует:** +- Валидацию тел запросов +- Webhook-уведомления банков +- Сетевые задержки/таймауты diff --git a/tests/mock_server/WINDOWS.md b/tests/mock_server/WINDOWS.md new file mode 100644 index 0000000..87419ed --- /dev/null +++ b/tests/mock_server/WINDOWS.md @@ -0,0 +1,255 @@ +# Запуск тестов на Windows + +## Оглавление + +1. [Быстрый старт](#1-быстрый-старт) +2. [Вспомогательные скрипты](#2-вспомогательные-скрипты) +3. [Генерация FETCH_TRANSACTIONS задач](#3-генерация-fetch_transactions-задач) +4. [Ручной запуск (пошагово)](#4-ручной-запуск-пошагово) +5. [Мониторинг и отчёт](#5-мониторинг-и-отчёт) +6. [Ручной инжект задач](#6-ручной-инжект-задач) +7. [ADB на Windows](#7-adb-на-windows) +8. [Типовые проблемы](#8-типовые-проблемы) +9. [Быстрый чеклист](#9-быстрый-чеклист) + +--- + +## 1. Быстрый старт + +```powershell +cd tests\mock_server +powershell -ExecutionPolicy Bypass -File setup.ps1 +``` + +Скрипт `setup.ps1` автоматически: +1. Проверяет / устанавливает **Python 3.12+** (через winget) +2. Устанавливает **Flask** +3. Создаёт вспомогательные скрипты `gen_transactions.ps1` и `run_server.ps1` +4. Находит `config.ini` и проверяет **базу данных** +5. Удаляет старые сгенерированные данные (профиль + scenarios, пул не трогает) +6. Генерирует `default_profile.json` и FETCH_PROFILE scenarios из БД +7. Очищает таблицу `events` в БД +8. Если порт занят — убивает старый процесс +9. Переключает `config.ini` на mock (`apiBase=http://127.0.0.1:8888`) +10. Показывает сводку: устройства, профили, материалы, пул транзакций +11. Запускает mock-сервер и открывает HTML-отчёт в браузере + +### Параметры + +| Параметр | По умолчанию | Описание | +|---|---|---| +| `-Port` | 8888 | Порт mock-сервера | +| `-FtPerMaterial` | 3 | FETCH_TRANSACTIONS на материал при бутстрапе (из пула) | +| `-Banks` | все | Банки через запятую: `ozon`, `dushanbe_city_bank`, `black` | +| `-Types` | все | Типы задач: `FETCH_PROFILE`, `FETCH_TRANSACTIONS` | +| `-SkipInstall` | — | Пропустить установку Python/Flask | +| `-GenerateOnly` | — | Только сгенерировать данные, не запускать сервер | +| `-Clean` | — | Также очистить пул транзакций (`scenarios/pool/`) | +| `-Debug` | — | Flask debug режим (auto-reload при изменении кода) | + +### Примеры + +```powershell +# Всё автоматически +.\setup.ps1 + +# Только Ozon, только FETCH_TRANSACTIONS +.\setup.ps1 -Banks ozon -Types FETCH_TRANSACTIONS + +# Debug режим с 5 транзакциями на материал +.\setup.ps1 -Banks ozon -FtPerMaterial 5 -Debug + +# Полная очистка (включая пул транзакций) +.\setup.ps1 -Clean +``` + +--- + +## 2. Вспомогательные скрипты + +После первого запуска `setup.ps1` в папке `tests\mock_server\` появляются +скрипты с зашитым путём до Python: + +| Скрипт | Назначение | Запуск | +|---|---|---| +| `gen_transactions.ps1` | Генерация FETCH_TRANSACTIONS с устройства | `powershell -ExecutionPolicy Bypass -File gen_transactions.ps1` | +| `run_server.ps1` | Запуск mock-сервера | `powershell -ExecutionPolicy Bypass -File run_server.ps1` | +| `setup.ps1` | Полная установка + генерация + запуск | `powershell -ExecutionPolicy Bypass -File setup.ps1` | + +Скрипты принимают дополнительные аргументы: + +```powershell +# Запуск сервера только для Ozon, только FETCH_TRANSACTIONS +powershell -ExecutionPolicy Bypass -File run_server.ps1 --banks ozon --types FETCH_TRANSACTIONS + +# Генерация с явным серийником +powershell -ExecutionPolicy Bypass -File gen_transactions.ps1 --serial 28291FDH300712 +``` + +--- + +## 3. Генерация FETCH_TRANSACTIONS задач + +FETCH_TRANSACTIONS задачи генерируются **только вручную** с живого устройства. + +### Подготовка + +1. Подключите устройство по USB +2. Убедитесь что ADB видит устройство: + ```powershell + tools\adb\windows\adb.exe devices + ``` +3. Откройте на устройстве банковское приложение на экране **списка транзакций** + +### Запуск + +```powershell +cd tests\mock_server +powershell -ExecutionPolicy Bypass -File gen_transactions.ps1 +``` + +Скрипт: +1. Покажет меню с активными материалами +2. Выберите нужный или `all` +3. Подтвердите что список транзакций открыт → **Enter** +4. Автоматически пройдёт по транзакциям (**не трогайте телефон!**) +5. Для **Ozon**: парсит заголовки дат, определяет дату каждой транзакции по позиции на экране, из деталей берёт только время +6. Для **Dushanbe**: время конвертируется из Asia/Dushanbe (UTC+5) +7. Сохраняет в `scenarios/pool/` с номером карты в имени файла: + ``` + pixel_7_ozon_7919_01_minus_108_ft_01.json + pixel_7_ozon_9865_01_minus_108_ft_01.json + pixel_7_dushanbe_city_bank_3258_01_plus_5_ft_01.json + ``` + +### Важно + +- Если у банка **два материала** (две карты), генерируются **отдельные файлы** для каждого +- При смене устройств/аккаунтов нужно перегенерировать pool — `material_id` в файлах должны совпадать с `default_profile.json` + +--- + +## 4. Ручной запуск (пошагово) + +### 4.1. Установка зависимостей + +```powershell +python --version # Python 3.9+ +pip install flask +``` + +### 4.2. Подготовка config.ini + +```ini +[net] +apiBase=http://127.0.0.1:8888 +``` + +### 4.3. Генерация данных + запуск + +```powershell +# Генерация профиля +python tests\mock_server\gen_mock_data.py + +# Запуск сервера +cd tests\mock_server +python mock_server.py --port 8888 --banks ozon --types FETCH_TRANSACTIONS + +# Debug режим (auto-reload) +python mock_server.py --port 8888 --banks ozon --debug +``` + +### 4.4. Запуск ARC + +```powershell +cd C:\ARC\Ver.18 # или build\Release +ARC.exe +``` + +--- + +## 5. Мониторинг и отчёт + +```powershell +# Открыть отчёт в браузере (авто-обновление каждые 2 сек) +start http://127.0.0.1:8888/_admin/report/html +``` + +Отчёт показывает: +- **Сводку**: всего / выполнено / ошибки / в очереди +- **Группировку**: устройство → банк → задачи +- **Для каждой задачи**: + - Статус (completed / error / pending) + - Task JSON + Result JSON (успешные свёрнуты, ошибки развёрнуты) + - Скриншот (если есть) — клик для полноэкранного просмотра + - Кнопка "Копировать" на каждом JSON блоке +- Авто-обновление без сброса состояния развёрнутых блоков + +```powershell +# Счётчики +(Invoke-WebRequest http://127.0.0.1:8888/_admin/state).Content | ConvertFrom-Json +``` + +--- + +## 6. Ручной инжект задач + +```powershell +# Одна задача +curl -X POST http://127.0.0.1:8888/_admin/inject ^ + -H "Content-Type: application/json" ^ + --data-binary @scenarios\pixel_7_ozon_7919_fetch_profile.json + +# Повторный бутстрап +curl -X POST http://127.0.0.1:8888/_admin/bootstrap + +# Сброс (очистка всего + скриншоты) +curl -X POST http://127.0.0.1:8888/_admin/reset +``` + +--- + +## 7. ADB на Windows + +ARC использует ADB из `tools\adb\windows\`: + +```powershell +tools\adb\windows\adb.exe devices +``` + +Если ADB занят: +```powershell +tools\adb\windows\adb.exe kill-server +tools\adb\windows\adb.exe start-server +``` + +--- + +## 8. Типовые проблемы + +| Проблема | Решение | +|---|---| +| `python` не найден | Запустите `setup.ps1` — установит автоматически | +| Порт 8888 занят | `setup.ps1` убивает автоматически. Вручную: `netstat -ano \| findstr :8888` → `taskkill /PID /F` | +| `App restarted` ошибки | Мок-сервер чистит events в БД при старте. Запускайте сервер **до** ARC | +| Pool material_id не совпадает | `gen_mock_data.py` обновил material_id. Перегенерируйте pool | +| `Transaction not found` | Баг поиска — анализировать логи (device= в ошибке) | +| `Page load error` | Ozon WebView не загрузился — проблема интернета/устройства | +| Скриншот не привязался | Нет `internal_task_id` — обновить ARC | +| Кракозябры в консоли | `chcp 65001` (UTF-8). Скрипты `.ps1` делают автоматически | +| No active materials | Нет профилей с проверенным PIN. Запустить ARC с боевым сервером | +| Firewall | Разрешить `python.exe`. Мок слушает только `127.0.0.1` | + +--- + +## 9. Быстрый чеклист + +``` +[ ] powershell -ExecutionPolicy Bypass -File setup.ps1 -Banks ozon +[ ] Устройства в adb devices → device +[ ] (опционально) gen_transactions.ps1 для пула транзакций +[ ] ARC.exe запущен (отдельный терминал) +[ ] Браузер: http://127.0.0.1:8888/_admin/report/html +[ ] Дождаться completed = всего в отчёте +[ ] Вернуть config.ini: apiBase=http://93.183.75.134:8005 +``` diff --git a/tests/mock_server/debug_detail.xml b/tests/mock_server/debug_detail.xml new file mode 100644 index 0000000..261a109 --- /dev/null +++ b/tests/mock_server/debug_detail.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/default_profile.json b/tests/mock_server/default_profile.json new file mode 100644 index 0000000..0519d5c --- /dev/null +++ b/tests/mock_server/default_profile.json @@ -0,0 +1,24 @@ +{ + "bank_profiles": [ + { + "id": "9b2f1a38-190c-4cb0-b026-eef721db36ed", + "bank_name": "ozon", + "state": "enabled", + "phone_number": "+79538038318", + "first_name": "Галина", + "middle_name": null, + "last_name": "Терехина", + "currency": 643, + "device_id": "cca4ec91-26e4-42e0-81ce-5226382b6cbc", + "device_label": "Pixel 7", + "materials": [ + { + "id": "e1234f11-b99d-4930-9f86-75148601f683", + "state": "enabled", + "card_number": "2204321035207919", + "currency": 643 + } + ] + } + ] +} diff --git a/tests/mock_server/fixtures/pixel7_dushanbe/details/01_minus_213121.png b/tests/mock_server/fixtures/pixel7_dushanbe/details/01_minus_213121.png new file mode 100644 index 0000000..c24558c Binary files /dev/null and b/tests/mock_server/fixtures/pixel7_dushanbe/details/01_minus_213121.png differ diff --git a/tests/mock_server/fixtures/pixel7_dushanbe/details/01_minus_213121.xml b/tests/mock_server/fixtures/pixel7_dushanbe/details/01_minus_213121.xml new file mode 100644 index 0000000..35c91fc --- /dev/null +++ b/tests/mock_server/fixtures/pixel7_dushanbe/details/01_minus_213121.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/pixel7_dushanbe/details/02_minus_212548.png b/tests/mock_server/fixtures/pixel7_dushanbe/details/02_minus_212548.png new file mode 100644 index 0000000..ec54977 Binary files /dev/null and b/tests/mock_server/fixtures/pixel7_dushanbe/details/02_minus_212548.png differ diff --git a/tests/mock_server/fixtures/pixel7_dushanbe/details/02_minus_212548.xml b/tests/mock_server/fixtures/pixel7_dushanbe/details/02_minus_212548.xml new file mode 100644 index 0000000..5a52c33 --- /dev/null +++ b/tests/mock_server/fixtures/pixel7_dushanbe/details/02_minus_212548.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/pixel7_dushanbe/details/03_minus_183606.png b/tests/mock_server/fixtures/pixel7_dushanbe/details/03_minus_183606.png new file mode 100644 index 0000000..f2bd2aa Binary files /dev/null and b/tests/mock_server/fixtures/pixel7_dushanbe/details/03_minus_183606.png differ diff --git a/tests/mock_server/fixtures/pixel7_dushanbe/details/03_minus_183606.xml b/tests/mock_server/fixtures/pixel7_dushanbe/details/03_minus_183606.xml new file mode 100644 index 0000000..4f8da23 --- /dev/null +++ b/tests/mock_server/fixtures/pixel7_dushanbe/details/03_minus_183606.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/pixel7_dushanbe/details/04_minus_181943.png b/tests/mock_server/fixtures/pixel7_dushanbe/details/04_minus_181943.png new file mode 100644 index 0000000..206f89e Binary files /dev/null and b/tests/mock_server/fixtures/pixel7_dushanbe/details/04_minus_181943.png differ diff --git a/tests/mock_server/fixtures/pixel7_dushanbe/details/04_minus_181943.xml b/tests/mock_server/fixtures/pixel7_dushanbe/details/04_minus_181943.xml new file mode 100644 index 0000000..8e77139 --- /dev/null +++ b/tests/mock_server/fixtures/pixel7_dushanbe/details/04_minus_181943.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/pixel7_dushanbe/details/05_plus_205206.png b/tests/mock_server/fixtures/pixel7_dushanbe/details/05_plus_205206.png new file mode 100644 index 0000000..6123805 Binary files /dev/null and b/tests/mock_server/fixtures/pixel7_dushanbe/details/05_plus_205206.png differ diff --git a/tests/mock_server/fixtures/pixel7_dushanbe/details/05_plus_205206.xml b/tests/mock_server/fixtures/pixel7_dushanbe/details/05_plus_205206.xml new file mode 100644 index 0000000..063d0de --- /dev/null +++ b/tests/mock_server/fixtures/pixel7_dushanbe/details/05_plus_205206.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/pixel7_dushanbe/transactions_list.png b/tests/mock_server/fixtures/pixel7_dushanbe/transactions_list.png new file mode 100644 index 0000000..bea3e05 Binary files /dev/null and b/tests/mock_server/fixtures/pixel7_dushanbe/transactions_list.png differ diff --git a/tests/mock_server/fixtures/pixel7_dushanbe/transactions_list.xml b/tests/mock_server/fixtures/pixel7_dushanbe/transactions_list.xml new file mode 100644 index 0000000..0fcc5e4 --- /dev/null +++ b/tests/mock_server/fixtures/pixel7_dushanbe/transactions_list.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/pixel7_ozon/details/01_evgeny_plus_100.png b/tests/mock_server/fixtures/pixel7_ozon/details/01_evgeny_plus_100.png new file mode 100644 index 0000000..939cd3c Binary files /dev/null and b/tests/mock_server/fixtures/pixel7_ozon/details/01_evgeny_plus_100.png differ diff --git a/tests/mock_server/fixtures/pixel7_ozon/details/01_evgeny_plus_100.xml b/tests/mock_server/fixtures/pixel7_ozon/details/01_evgeny_plus_100.xml new file mode 100644 index 0000000..2dd9a78 --- /dev/null +++ b/tests/mock_server/fixtures/pixel7_ozon/details/01_evgeny_plus_100.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/pixel7_ozon/details/02_vlad_minus_100.png b/tests/mock_server/fixtures/pixel7_ozon/details/02_vlad_minus_100.png new file mode 100644 index 0000000..f1fa5e3 Binary files /dev/null and b/tests/mock_server/fixtures/pixel7_ozon/details/02_vlad_minus_100.png differ diff --git a/tests/mock_server/fixtures/pixel7_ozon/details/02_vlad_minus_100.xml b/tests/mock_server/fixtures/pixel7_ozon/details/02_vlad_minus_100.xml new file mode 100644 index 0000000..9fd926a --- /dev/null +++ b/tests/mock_server/fixtures/pixel7_ozon/details/02_vlad_minus_100.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/pixel7_ozon/details/03_vlad_minus_300.png b/tests/mock_server/fixtures/pixel7_ozon/details/03_vlad_minus_300.png new file mode 100644 index 0000000..582867a Binary files /dev/null and b/tests/mock_server/fixtures/pixel7_ozon/details/03_vlad_minus_300.png differ diff --git a/tests/mock_server/fixtures/pixel7_ozon/details/03_vlad_minus_300.xml b/tests/mock_server/fixtures/pixel7_ozon/details/03_vlad_minus_300.xml new file mode 100644 index 0000000..8d2dbaa --- /dev/null +++ b/tests/mock_server/fixtures/pixel7_ozon/details/03_vlad_minus_300.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/pixel7_ozon/details/04_vlad_plus_100.png b/tests/mock_server/fixtures/pixel7_ozon/details/04_vlad_plus_100.png new file mode 100644 index 0000000..8f517e8 Binary files /dev/null and b/tests/mock_server/fixtures/pixel7_ozon/details/04_vlad_plus_100.png differ diff --git a/tests/mock_server/fixtures/pixel7_ozon/details/04_vlad_plus_100.xml b/tests/mock_server/fixtures/pixel7_ozon/details/04_vlad_plus_100.xml new file mode 100644 index 0000000..5ec90f4 --- /dev/null +++ b/tests/mock_server/fixtures/pixel7_ozon/details/04_vlad_plus_100.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/pixel7_ozon/details/05_vlad_plus_100_2.png b/tests/mock_server/fixtures/pixel7_ozon/details/05_vlad_plus_100_2.png new file mode 100644 index 0000000..b774d68 Binary files /dev/null and b/tests/mock_server/fixtures/pixel7_ozon/details/05_vlad_plus_100_2.png differ diff --git a/tests/mock_server/fixtures/pixel7_ozon/details/05_vlad_plus_100_2.xml b/tests/mock_server/fixtures/pixel7_ozon/details/05_vlad_plus_100_2.xml new file mode 100644 index 0000000..870b219 --- /dev/null +++ b/tests/mock_server/fixtures/pixel7_ozon/details/05_vlad_plus_100_2.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/pixel7_ozon/details/06_evgeny_plus_100_2.png b/tests/mock_server/fixtures/pixel7_ozon/details/06_evgeny_plus_100_2.png new file mode 100644 index 0000000..6eac52f Binary files /dev/null and b/tests/mock_server/fixtures/pixel7_ozon/details/06_evgeny_plus_100_2.png differ diff --git a/tests/mock_server/fixtures/pixel7_ozon/details/06_evgeny_plus_100_2.xml b/tests/mock_server/fixtures/pixel7_ozon/details/06_evgeny_plus_100_2.xml new file mode 100644 index 0000000..c44c5db --- /dev/null +++ b/tests/mock_server/fixtures/pixel7_ozon/details/06_evgeny_plus_100_2.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/pixel7_ozon/transactions_list.png b/tests/mock_server/fixtures/pixel7_ozon/transactions_list.png new file mode 100644 index 0000000..4937794 Binary files /dev/null and b/tests/mock_server/fixtures/pixel7_ozon/transactions_list.png differ diff --git a/tests/mock_server/fixtures/pixel7_ozon/transactions_list.xml b/tests/mock_server/fixtures/pixel7_ozon/transactions_list.xml new file mode 100644 index 0000000..bda729e --- /dev/null +++ b/tests/mock_server/fixtures/pixel7_ozon/transactions_list.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/tecno_ozon/details/01_galina_plus_100.png b/tests/mock_server/fixtures/tecno_ozon/details/01_galina_plus_100.png new file mode 100644 index 0000000..f6d4641 Binary files /dev/null and b/tests/mock_server/fixtures/tecno_ozon/details/01_galina_plus_100.png differ diff --git a/tests/mock_server/fixtures/tecno_ozon/details/01_galina_plus_100.xml b/tests/mock_server/fixtures/tecno_ozon/details/01_galina_plus_100.xml new file mode 100644 index 0000000..8d7269e --- /dev/null +++ b/tests/mock_server/fixtures/tecno_ozon/details/01_galina_plus_100.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/tecno_ozon/details/02_galina_plus_300.png b/tests/mock_server/fixtures/tecno_ozon/details/02_galina_plus_300.png new file mode 100644 index 0000000..2fa49ab Binary files /dev/null and b/tests/mock_server/fixtures/tecno_ozon/details/02_galina_plus_300.png differ diff --git a/tests/mock_server/fixtures/tecno_ozon/details/02_galina_plus_300.xml b/tests/mock_server/fixtures/tecno_ozon/details/02_galina_plus_300.xml new file mode 100644 index 0000000..311c63b --- /dev/null +++ b/tests/mock_server/fixtures/tecno_ozon/details/02_galina_plus_300.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/tecno_ozon/details/03_vlad_minus_100.png b/tests/mock_server/fixtures/tecno_ozon/details/03_vlad_minus_100.png new file mode 100644 index 0000000..9f1ec77 Binary files /dev/null and b/tests/mock_server/fixtures/tecno_ozon/details/03_vlad_minus_100.png differ diff --git a/tests/mock_server/fixtures/tecno_ozon/details/03_vlad_minus_100.xml b/tests/mock_server/fixtures/tecno_ozon/details/03_vlad_minus_100.xml new file mode 100644 index 0000000..49be16a --- /dev/null +++ b/tests/mock_server/fixtures/tecno_ozon/details/03_vlad_minus_100.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/tecno_ozon/details/04_vlad_minus_100_2.png b/tests/mock_server/fixtures/tecno_ozon/details/04_vlad_minus_100_2.png new file mode 100644 index 0000000..01dcfce Binary files /dev/null and b/tests/mock_server/fixtures/tecno_ozon/details/04_vlad_minus_100_2.png differ diff --git a/tests/mock_server/fixtures/tecno_ozon/details/04_vlad_minus_100_2.xml b/tests/mock_server/fixtures/tecno_ozon/details/04_vlad_minus_100_2.xml new file mode 100644 index 0000000..24919f5 --- /dev/null +++ b/tests/mock_server/fixtures/tecno_ozon/details/04_vlad_minus_100_2.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/tecno_ozon/details/05_galina_minus_100.png b/tests/mock_server/fixtures/tecno_ozon/details/05_galina_minus_100.png new file mode 100644 index 0000000..aca00f4 Binary files /dev/null and b/tests/mock_server/fixtures/tecno_ozon/details/05_galina_minus_100.png differ diff --git a/tests/mock_server/fixtures/tecno_ozon/details/05_galina_minus_100.xml b/tests/mock_server/fixtures/tecno_ozon/details/05_galina_minus_100.xml new file mode 100644 index 0000000..fa803c6 --- /dev/null +++ b/tests/mock_server/fixtures/tecno_ozon/details/05_galina_minus_100.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/tecno_ozon/details/06_galina_minus_100_2.png b/tests/mock_server/fixtures/tecno_ozon/details/06_galina_minus_100_2.png new file mode 100644 index 0000000..70ed009 Binary files /dev/null and b/tests/mock_server/fixtures/tecno_ozon/details/06_galina_minus_100_2.png differ diff --git a/tests/mock_server/fixtures/tecno_ozon/details/06_galina_minus_100_2.xml b/tests/mock_server/fixtures/tecno_ozon/details/06_galina_minus_100_2.xml new file mode 100644 index 0000000..2b0f93f --- /dev/null +++ b/tests/mock_server/fixtures/tecno_ozon/details/06_galina_minus_100_2.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/tecno_ozon/details/07_vlad_plus_550.png b/tests/mock_server/fixtures/tecno_ozon/details/07_vlad_plus_550.png new file mode 100644 index 0000000..f940b01 Binary files /dev/null and b/tests/mock_server/fixtures/tecno_ozon/details/07_vlad_plus_550.png differ diff --git a/tests/mock_server/fixtures/tecno_ozon/details/07_vlad_plus_550.xml b/tests/mock_server/fixtures/tecno_ozon/details/07_vlad_plus_550.xml new file mode 100644 index 0000000..69fa14b --- /dev/null +++ b/tests/mock_server/fixtures/tecno_ozon/details/07_vlad_plus_550.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/fixtures/tecno_ozon/transactions_list.png b/tests/mock_server/fixtures/tecno_ozon/transactions_list.png new file mode 100644 index 0000000..2df87b4 Binary files /dev/null and b/tests/mock_server/fixtures/tecno_ozon/transactions_list.png differ diff --git a/tests/mock_server/fixtures/tecno_ozon/transactions_list.xml b/tests/mock_server/fixtures/tecno_ozon/transactions_list.xml new file mode 100644 index 0000000..b0a7101 --- /dev/null +++ b/tests/mock_server/fixtures/tecno_ozon/transactions_list.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/mock_server/gen_fetch_transactions.py b/tests/mock_server/gen_fetch_transactions.py new file mode 100755 index 0000000..824aea6 --- /dev/null +++ b/tests/mock_server/gen_fetch_transactions.py @@ -0,0 +1,662 @@ +#!/usr/bin/env python3 +""" +Generate FETCH_TRANSACTIONS pool tasks from a live device screen. + +Reads config.ini to find the DB, queries active materials, shows a menu +to pick which device+bank to scan, then dumps the transactions list from +the real device, taps into each detail, and generates pool JSON files. + +Run from project root while the transactions list is open on the device: + + python tests/mock_server/gen_fetch_transactions.py + +Or specify config and serial: + + python tests/mock_server/gen_fetch_transactions.py --config config.ini -s 148697059H000712 + +On Windows: + + python tests\\mock_server\\gen_fetch_transactions.py +""" + +import argparse +import configparser +import json +import os +import re +import sqlite3 +import subprocess +import sys +import time +from datetime import datetime, timedelta, timezone +from pathlib import Path + +HERE = Path(__file__).parent +PROJECT_ROOT = (HERE / ".." / "..").resolve() +POOL_DIR = HERE / "scenarios" / "pool" + +if sys.platform == "win32": + ADB = str(PROJECT_ROOT / "tools" / "adb" / "windows" / "adb.exe") +elif sys.platform == "darwin": + ADB = str(PROJECT_ROOT / "tools" / "adb" / "macos" / "adb") +else: + ADB = "adb" +BOUNDS_RE = re.compile(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]") + +# ---------------------------------------------------------------- config/db + +def resolve_db_path(config_path: Path) -> Path: + config = configparser.ConfigParser() + config.read(str(config_path), encoding="utf-8") + raw = config.get("db", "dbPath", fallback="") + if not raw: + print(f"ERROR: [db] dbPath not found in {config_path}", file=sys.stderr) + sys.exit(1) + expanded = os.path.expanduser(raw) + p = Path(expanded) + if not p.is_absolute(): + p = config_path.parent / p + return p.resolve() + + +def find_config(hint): + if hint: + p = Path(hint) + if p.exists(): + return p + print(f"ERROR: config not found at {p}", file=sys.stderr) + sys.exit(1) + for c in [Path("config.ini"), Path("build/Release/config.ini"), + Path("build/config.ini"), Path("build/Debug/config.ini")]: + if c.exists(): + return c.resolve() + print("ERROR: config.ini not found. Run from project root or use --config.", file=sys.stderr) + sys.exit(1) + + +def query_active_materials(db_path: Path): + conn = sqlite3.connect(str(db_path)) + conn.row_factory = sqlite3.Row + rows = conn.execute(""" + SELECT d.id AS device_id, d.api_id, d.name AS device_name, + d.adb_serial, + bp.code AS bank_name, bp.bank_profile_id, bp.full_name, + m.material_id, m.last_numbers, m.card_number + FROM devices d + JOIN bank_profiles bp ON bp.device_id = d.id + JOIN materials m ON m.device_id = d.id AND m.app_code = bp.code + WHERE bp.pin_code_checked = 'yes' AND m.status = 'active' + AND m.material_id != '' AND bp.bank_profile_id != '' + ORDER BY d.name, bp.code + """).fetchall() + conn.close() + return [dict(r) for r in rows] + + +# ---------------------------------------------------------------- ADB helpers + +def adb(serial, *args): + cmd = [ADB] + if serial: + cmd += ["-s", serial] + cmd += list(args) + return subprocess.run(cmd, check=True, capture_output=True) + + +PACKAGE_TO_BANK = { + "ru.ozon.fintech.finance": "ozon", + "tj.dc.next1": "dushanbe_city_bank", + "app.blackwallet.ar": "black", +} + + +def get_foreground_package(serial) -> str | None: + """Return the package name of the currently focused app.""" + try: + r = adb(serial, "shell", "dumpsys", "window", "windows") + for line in r.stdout.decode("utf-8", errors="replace").splitlines(): + if "mCurrentFocus" in line or "mFocusedApp" in line: + # e.g. mCurrentFocus=Window{... u0 tj.dc.next1/tj.dc.next1.MainActivity} + m = re.search(r"u\d+\s+(\S+)/", line) + if m: + return m.group(1) + except Exception: + pass + return None + + +def dump_xml(serial) -> str: + try: + adb(serial, "shell", "uiautomator", "dump", "/sdcard/_gen_ft.xml") + r = adb(serial, "exec-out", "cat", "/sdcard/_gen_ft.xml") + return r.stdout.decode("utf-8", errors="replace") + except subprocess.CalledProcessError as e: + print(f" WARNING: dump_xml failed: {e}") + return "" + + +def get_device_tz(serial) -> timezone: + r = adb(serial, "shell", "date", "+%z") + s = r.stdout.decode().strip() + try: + sign = 1 if s[0] == "+" else -1 + return timezone(timedelta(hours=sign * int(s[1:3]), minutes=sign * int(s[3:5]))) + except (ValueError, IndexError): + print(f" warning: can't parse tz '{s}', assuming UTC+7") + return timezone(timedelta(hours=7)) + + +def get_adb_serials(): + r = subprocess.run([ADB, "devices"], check=True, capture_output=True) + serials = [] + for line in r.stdout.decode().splitlines()[1:]: + parts = line.split() + if len(parts) >= 2 and parts[1] == "device": + serials.append(parts[0]) + return serials + + +def tap(serial, x, y): + adb(serial, "shell", "input", "tap", str(x), str(y)) + + +def back(serial): + adb(serial, "shell", "input", "keyevent", "KEYCODE_BACK") + + +def swipe_up(serial, duration=500): + """Swipe up to scroll the list down (small step).""" + adb(serial, "shell", "input", "swipe", "540", "1400", "540", "900", str(duration)) + + +# ---------------------------------------------------------------- Ozon + +MONTH_MAP = { + "января": 1, "февраля": 2, "марта": 3, "апреля": 4, + "мая": 5, "июня": 6, "июля": 7, "августа": 8, + "сентября": 9, "октября": 10, "ноября": 11, "декабря": 12, +} +DATE_HEADER_RE = re.compile(r"(\d{1,2})\s+(\S+),\s+\S+") + + +def parse_ozon_date_headers(xml, device_tz): + """Parse date headers (TextView) and return list of (y, date_str 'dd.MM.yyyy').""" + today = datetime.now(tz=device_tz).date() + headers = [] + for m in re.finditer( + r'text="([^"]*)"[^>]*class="android.widget.TextView"' + r'[^>]*bounds="(\[\d+,\d+\]\[\d+,\d+\])"', xml): + text, bounds = m.group(1).strip(), m.group(2) + bm = BOUNDS_RE.match(bounds) + if not bm: + continue + x1, y1, x2, y2 = map(int, bm.groups()) + h = y2 - y1 + if h < 5 or y1 < 700: + continue # за экраном или в зоне шапки + d = None + if text == "Сегодня": + d = today + elif text == "Вчера": + d = today - timedelta(days=1) + else: + dm = DATE_HEADER_RE.match(text) + if dm: + day = int(dm.group(1)) + month = MONTH_MAP.get(dm.group(2), 0) + if month: + d = today.replace(month=month, day=day) + if d > today: + d = d.replace(year=today.year - 1) + if d: + headers.append({"y": y1, "date": d.strftime("%d.%m.%Y")}) + headers.sort(key=lambda h: h["y"]) + return headers + + +def parse_ozon_rows(xml): + rows = [] + for m in re.finditer( + r'text="([^"]*)"' + r'[^>]*?bounds="(\[\d+,\d+\]\[\d+,\d+\])"', xml): + text, bounds = m.group(1), m.group(2) + bm = BOUNDS_RE.match(bounds) + if not bm: + continue + x1, y1, x2, y2 = map(int, bm.groups()) + h = y2 - y1 + cy = (y1 + y2) // 2 + if h < 50 or h < 5 or cy < 800: + continue + if "Расход" in text or "Доход" in text: + continue + am = re.search(r"([+\-−])\s*(\d[\d\s\u202f]*(?:[,.]\d{1,2})?)\s*₽", text) + if not am: + continue + sign = -1 if am.group(1) in ("-", "−") else 1 + raw = re.sub(r"[\s\u202f]", "", am.group(2)).replace(",", ".") + amount = sign * (float(raw) if "." in raw else int(raw)) + rows.append({"text": text, "cx": (x1+x2)//2, "cy": cy, "amount": amount}) + rows.sort(key=lambda r: r["cy"]) + return rows + + +def assign_dates_to_rows(rows, headers): + """Assign date from nearest preceding header to each row.""" + for row in rows: + row["date"] = None + for h in reversed(headers): + if h["y"] < row["cy"]: + row["date"] = h["date"] + break + + +def is_ozon_list(xml): + return "Операции" in xml + + +def extract_ozon_detail_time(xml): + for m in re.finditer(r'text="([^"]+)"', xml): + dm = re.match(r"(\d{2}\.\d{2}\.\d{4}),?\s+(\d{2}:\d{2})", m.group(1)) + if dm: + return dm.group(1), dm.group(2) + ":00" + return None, None + + +# ---------------------------------------------------------------- Dushanbe + +def parse_dushanbe_rows(xml): + rows = [] + for m in re.finditer( + r']*?content-desc="([^"]*часть P2P/операции[^"]*)"' + r'[^>]*?bounds="(\[(\d+),(\d+)\]\[(\d+),(\d+)\])"', xml): + cd = m.group(1) + x1, y1, x2, y2 = int(m.group(3)), int(m.group(4)), int(m.group(5)), int(m.group(6)) + h = y2 - y1 + if h < 80 or h > 400: + continue + parts = cd.split("\n") if "\n" in cd else cd.split(" ") + amount, time_str = None, None + for p in parts: + p = p.strip() + try: + amount = float(p.replace(",", ".")) + except ValueError: + pass + if re.match(r"^\s*\d{1,2}:\d{2}(:\d{2})?\s*$", p): + time_str = p.strip() + rows.append({"cx": (x1+x2)//2, "cy": (y1+y2)//2, "amount": amount, "time": time_str}) + rows.sort(key=lambda r: r["cy"]) + return rows + + +def is_dushanbe_list(xml): + return "Операции" in xml and "часть P2P" in xml + + +def extract_dushanbe_detail(xml): + m = re.search(r'content-desc="([^"]{30,})"', xml) + if not m: + return None, None, None + parts = [x.strip() for x in m.group(1).split(" ")] + info = {} + for i, x in enumerate(parts): + if x.endswith(":") and i + 1 < len(parts): + info[x] = parts[i + 1] + return info.get("Сумма:"), info.get("Дата:"), info.get("Время:") + + +# ---------------------------------------------------------------- Main + +def short_label(amount, time_str): + sign = "plus" if amount >= 0 else "minus" + t = time_str.replace(":", "") if time_str else "unk" + a = abs(amount) + if isinstance(a, float): + rub = int(a) + kop = int(round((a - rub) * 100)) + amt = f"{rub}_{kop:02d}" if kop else f"{rub}" + else: + amt = f"{a}" + return f"{sign}_{amt}_{t}" + + +def collect_tasks(serial, bank, mid, device_tz, kopecks, prefix): + xml = dump_xml(serial) + # Detect bank by foreground app package first, fall back to XML heuristics + fg_pkg = get_foreground_package(serial) + fg_bank = PACKAGE_TO_BANK.get(fg_pkg) if fg_pkg else None + if fg_bank: + print(f"Foreground app: {fg_pkg} -> {fg_bank}") + is_dush = fg_bank == "dushanbe_city_bank" or "dushanbe" in bank or is_dushanbe_list(xml) + is_ozon = not is_dush and (fg_bank == "ozon" or bank == "ozon" or is_ozon_list(xml)) + + if not is_ozon and not is_dush: + print("ERROR: not on a recognized transactions list", file=sys.stderr) + return [] + + tasks = [] + seen = set() + + try: + return _collect_tasks_inner(serial, bank, mid, device_tz, kopecks, prefix, + xml, is_ozon, is_dush, tasks, seen) + except (subprocess.CalledProcessError, OSError) as e: + print(f"\n ERROR: ADB/device failure: {e}") + print(f" Saving {len(tasks)} tasks collected before crash") + return tasks + + +def _collect_tasks_inner(serial, bank, mid, device_tz, kopecks, prefix, + xml, is_ozon, is_dush, tasks, seen): + if is_ozon: + print("Detected: Ozon") + headers = parse_ozon_date_headers(xml, device_tz) + baseline = parse_ozon_rows(xml) + assign_dates_to_rows(baseline, headers) + print(f"date headers: {[h['date'] for h in headers]}") + print(f"visible rows: {len(baseline)}") + for i, r in enumerate(baseline, 1): + print(f" {i}. ({r['cx']},{r['cy']}) {r['amount']:+.2f} ₽ date={r.get('date','-')} | {r['text'][:40]}") + + task_num = 0 + processed_keys = set() # (amount, created_at) — финальная дедупликация + completed_dates = set() # даты где все строки уже обработаны + date_row_counts = {} # date -> сколько строк видели для этой даты (макс) + max_scrolls = 20 + scroll_count = 0 + + while True: + xml = dump_xml(serial) + if not is_ozon_list(xml): + print("Left list, stop"); break + + headers = parse_ozon_date_headers(xml, device_tz) + rows = parse_ozon_rows(xml) + assign_dates_to_rows(rows, headers) + + if not rows: + print("No rows found, stop"); break + + # Фильтруем строки от уже завершённых дат + rows = [r for r in rows if r.get("date") and r["date"] not in completed_dates] + if not rows: + print("All visible dates already processed, scrolling...") + scroll_count += 1 + if scroll_count >= max_scrolls: + print(f"\nReached max scrolls ({max_scrolls}), stop"); break + swipe_up(serial) + time.sleep(1.5) + continue + + # Считаем строки по датам на текущем экране + screen_date_counts = {} + for row in rows: + d = row["date"] + screen_date_counts[d] = screen_date_counts.get(d, 0) + 1 + + new_on_screen = 0 + for row in rows: + date_from_header = row["date"] + + print(f"\n [{date_from_header}] tap ({row['cx']},{row['cy']}) {row['amount']:+.2f}") + tap(serial, row["cx"], row["cy"]) + time.sleep(3) + + # Ждём деталей — нужно только время (дата уже из заголовка) + time_s = None + for attempt in range(3): + d_xml = dump_xml(serial) + _, time_s = extract_ozon_detail_time(d_xml) + if time_s: + break + if is_ozon_list(d_xml): + print(f" retry {attempt+1}: still on list, re-tapping") + tap(serial, row["cx"], row["cy"]) + time.sleep(2) + else: + print(f" retry {attempt+1}: waiting for time...") + time.sleep(2) + + back(serial); time.sleep(1.5) + + if not time_s: + print(f" SKIP (no time from detail)"); continue + + print(f" {date_from_header} {time_s}") + + dt = datetime.strptime(f"{date_from_header} {time_s}", "%d.%m.%Y %H:%M:%S").replace(tzinfo=device_tz) + cat = dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + dedup_key = (row["amount"], cat) + if dedup_key in processed_keys: + print(" DUPLICATE, skip"); continue + processed_keys.add(dedup_key) + new_on_screen += 1 + + task_num += 1 + label = short_label(row["amount"], time_s) + while label in seen: label += "_2" + seen.add(label) + tasks.append({"bank_name": bank, "material_id": mid, "type": "FETCH_TRANSACTIONS", + "body": {"amount": int(round(row["amount"] * kopecks)), "created_at": cat}, + "_tag": f"{prefix}_{task_num:02d}_{label}"}) + + # Помечаем даты как завершённые: все даты кроме последней на экране + # (последняя может быть обрезана скроллом) + visible_dates = list(dict.fromkeys(r["date"] for r in rows)) # ordered unique + if len(visible_dates) > 1: + for d in visible_dates[:-1]: + completed_dates.add(d) + + if new_on_screen == 0: + # Последняя дата на экране — тоже завершена + if visible_dates: + completed_dates.add(visible_dates[-1]) + print("\nNo new transactions on screen, scrolling past...") + scroll_count += 1 + if scroll_count >= max_scrolls: + print(f"\nReached max scrolls ({max_scrolls}), stop"); break + swipe_up(serial) + time.sleep(1.5) + continue + + scroll_count += 1 + if scroll_count >= max_scrolls: + print(f"\nReached max scrolls ({max_scrolls}), stop"); break + print(f"\n--- scroll {scroll_count}/{max_scrolls} ---") + swipe_up(serial) + time.sleep(1.5) + + elif is_dush: + print("Detected: Dushanbe") + baseline = parse_dushanbe_rows(xml) + print(f"visible rows: {len(baseline)}") + for i, r in enumerate(baseline, 1): + print(f" {i}. ({r['cx']},{r['cy']}) amt={r['amount']} time={r['time']}") + + task_num = 0 + processed_keys = set() + max_scrolls = 30 + scroll_count = 0 + + while True: + xml = dump_xml(serial) + if not is_dushanbe_list(xml): + print("Left list, stop"); break + rows = parse_dushanbe_rows(xml) + if not rows: + print("No rows found, stop"); break + + tapped_any = False + for row in rows: + print(f"\n tap ({row['cx']},{row['cy']}) amt={row['amount']}") + tap(serial, row["cx"], row["cy"]) + time.sleep(2.5) + d_xml = dump_xml(serial) + amt_s, date_s, time_s = extract_dushanbe_detail(d_xml) + print(f" amt={amt_s} {date_s} {time_s}") + back(serial); time.sleep(1.3) + if not date_s or not time_s: + print(" SKIP"); continue + + amount = float(amt_s.replace(",", ".")) if amt_s else (row["amount"] or 0) + fmt = "%d.%m.%Y %H:%M:%S" if len(time_s) > 5 else "%d.%m.%Y %H:%M" + # Душанбе показывает время в Asia/Dushanbe (UTC+5), не в таймзоне устройства + dushanbe_tz = timezone(timedelta(hours=5)) + dt = datetime.strptime(f"{date_s} {time_s}", fmt).replace(tzinfo=dushanbe_tz) + cat = dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + dedup_key = (amount, cat) + if dedup_key in processed_keys: + print(" DUPLICATE, skip"); continue + processed_keys.add(dedup_key) + tapped_any = True + + task_num += 1 + label = short_label(amount, time_s) + while label in seen: label += "_2" + seen.add(label) + tasks.append({"bank_name": bank, "material_id": mid, "type": "FETCH_TRANSACTIONS", + "body": {"amount": int(round(amount * kopecks)), "created_at": cat}, + "_tag": f"{prefix}_{task_num:02d}_{label}"}) + + scroll_count += 1 + if scroll_count >= max_scrolls: + print(f"\nReached max scrolls ({max_scrolls}), stop"); break + if not tapped_any: + print("\nNo new transactions on screen, stop"); break + print(f"\n--- scrolling down ({scroll_count}/{max_scrolls}) ---") + swipe_up(serial) + time.sleep(1.5) + + return tasks + + +def main(): + parser = argparse.ArgumentParser(description="Generate FETCH_TRANSACTIONS pool from live device") + parser.add_argument("--config", "-c", default=None, help="Path to config.ini") + parser.add_argument("--serial", "-s", default=None, help="ADB serial (prompted if multiple)") + parser.add_argument("--kopecks", type=int, default=100, help="Amount multiplier (default 100)") + parser.add_argument("--mock-host", default="127.0.0.1") + parser.add_argument("--mock-port", type=int, default=8888) + args = parser.parse_args() + + # Find config and DB + for c in ([Path(args.config)] if args.config else + [Path("config.ini"), Path("build/Release/config.ini"), + Path("build/config.ini")]): + if c.exists(): + config_path = c.resolve(); break + else: + print("ERROR: config.ini not found", file=sys.stderr); return 1 + + db_path = resolve_db_path(config_path) + print(f"config: {config_path}") + print(f"db: {db_path}") + + materials = query_active_materials(db_path) + if not materials: + print("No active materials in DB"); return 1 + + # Find connected ADB devices + serials = get_adb_serials() + if not serials: + print("ERROR: no ADB devices connected"); return 1 + + # Match DB devices to connected ADB serials + options = [] + for mat in materials: + adb_serial = mat["adb_serial"] + if adb_serial not in serials: + continue + dev_label = re.sub(r"-[a-f0-9]{10,}$", "", mat["device_name"]) + options.append({**mat, "serial": adb_serial, "label": dev_label}) + + if not options: + print("No active materials match connected ADB devices.") + print(f" DB serials: {[m['adb_serial'] for m in materials]}") + print(f" ADB serials: {serials}") + return 1 + + # Menu + print(f"\nAvailable materials ({len(options)}):") + for i, o in enumerate(options, 1): + print(f" {i}. {o['label']} / {o['bank_name']} / {o['full_name'] or '?'} " + f"(material {o['material_id'][:8]}… serial={o['serial']})") + + if len(options) == 1: + choice = 0 + print(f"\n→ auto-selected: {options[0]['label']} / {options[0]['bank_name']}") + else: + try: + inp = input(f"\nPick [1-{len(options)}] or 'all': ").strip() + except (EOFError, KeyboardInterrupt): + return 0 + if inp.lower() == "all": + choice = -1 + else: + choice = int(inp) - 1 + if choice < 0 or choice >= len(options): + print("Invalid choice"); return 1 + + to_scan = options if choice == -1 else [options[choice]] + all_tasks = [] + + for o in to_scan: + serial = args.serial or o["serial"] + bank = o["bank_name"] + mid = o["material_id"] + card = o.get('last_numbers') or o['material_id'][:8] + prefix = f"{o['label'].lower().replace(' ', '_')}_{bank}_{card}" + device_tz = get_device_tz(serial) + print(f"\n{'='*60}") + print(f"Scanning: {o['label']} / {bank} ({mid[:8]}…)") + print(f"Serial: {serial} TZ: UTC{device_tz.utcoffset(None)}") + print(f"{'='*60}") + + input(f"\nOpen {bank} transactions list on {o['label']} and press Enter...") + + tasks = collect_tasks(serial, bank, mid, device_tz, args.kopecks, prefix) + all_tasks.extend(tasks) + + if not all_tasks: + print("\nNo tasks generated"); return 1 + + # Write pool + POOL_DIR.mkdir(parents=True, exist_ok=True) + # Remove old files for scanned prefixes + scanned_prefixes = set() + for o in to_scan: + card = o.get('last_numbers') or o['material_id'][:8] + scanned_prefixes.add(f"{o['label'].lower().replace(' ', '_')}_{o['bank_name']}_{card}") + for old in POOL_DIR.glob("*.json"): + for pfx in scanned_prefixes: + if old.name.startswith(pfx + "_ft_"): + old.unlink(); break + + for i, t in enumerate(all_tasks, 1): + tag = t["_tag"] + fname = f"{tag.rsplit('_', 1)[0]}_ft_{i:02d}.json" + (POOL_DIR / fname).write_text(json.dumps(t, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + + print(f"\n{len(all_tasks)} tasks written to {POOL_DIR}/") + for t in all_tasks: + b = t["body"] + print(f" {t['_tag']}: amount={b['amount']:+.2f} created_at={b['created_at']}") + + # Inject into mock if running + try: + import urllib.request + url = f"http://{args.mock_host}:{args.mock_port}/_admin/inject" + req = urllib.request.Request(url, data=json.dumps(all_tasks).encode(), + headers={"Content-Type": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=3) as resp: + print(f"\nInjected {len(all_tasks)} tasks into mock -> {resp.status}") + except Exception as e: + print(f"\nMock not reachable ({e}) — tasks saved to files only") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/mock_server/gen_mock_data.py b/tests/mock_server/gen_mock_data.py new file mode 100755 index 0000000..c2e0a6a --- /dev/null +++ b/tests/mock_server/gen_mock_data.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +""" +Generate mock test data from the local ARC database. + +Reads config.ini (from project root or --config) to find the DB path, +queries active devices/profiles/materials, and generates: + - default_profile.json (seeded into mock at startup) + - scenarios/*_fetch_profile.json (one FETCH_PROFILE per material) + - Cleans old scenarios that no longer match the DB + +Run from project root: + python tests/mock_server/gen_mock_data.py + +Or specify config explicitly: + python tests\\mock_server\\gen_mock_data.py --config C:\\path\\to\\config.ini + +On Windows: + python tests\\mock_server\\gen_mock_data.py +""" + +import argparse +import configparser +import json +import os +import re +import sqlite3 +import sys +from pathlib import Path + + +def resolve_db_path(config_path: Path) -> Path: + config = configparser.ConfigParser() + config.read(str(config_path), encoding="utf-8-sig") + raw = config.get("db", "dbPath", fallback="") + if not raw: + print(f"ERROR: [db] dbPath not found in {config_path}", file=sys.stderr) + sys.exit(1) + # Expand ~ and make absolute relative to config dir + expanded = os.path.expanduser(raw) + p = Path(expanded) + if not p.is_absolute(): + p = config_path.parent / p + return p.resolve() + + +def find_config(hint: str | None) -> Path: + if hint: + p = Path(hint) + if p.exists(): + return p + print(f"ERROR: config not found at {p}", file=sys.stderr) + sys.exit(1) + # Try common locations relative to CWD + candidates = [ + Path("config.ini"), + Path("build/Release/config.ini"), + Path("build/config.ini"), + Path("build/Debug/config.ini"), + ] + for c in candidates: + if c.exists(): + return c.resolve() + print("ERROR: config.ini not found. Run from project root or use --config.", file=sys.stderr) + sys.exit(1) + + +def query_active_materials(db_path: Path): + """Returns list of dicts with device/profile/material info.""" + if not db_path.exists(): + print(f"ERROR: database not found at {db_path}", file=sys.stderr) + sys.exit(1) + + conn = sqlite3.connect(str(db_path)) + conn.row_factory = sqlite3.Row + rows = conn.execute(""" + SELECT + d.id AS device_id, + d.api_id AS device_api_id, + d.name AS device_name, + bp.code AS bank_name, + bp.bank_profile_id, + bp.full_name, + bp.phone, + bp.currency AS bp_currency, + m.material_id, + m.last_numbers, + m.card_number, + m.currency AS mat_currency + FROM devices d + JOIN bank_profiles bp ON bp.device_id = d.id + JOIN materials m ON m.device_id = d.id AND m.app_code = bp.code + WHERE bp.pin_code IS NOT NULL AND bp.pin_code != '' + AND bp.pin_code_checked = 'yes' + AND m.status = 'active' + AND m.material_id IS NOT NULL AND m.material_id != '' + AND bp.bank_profile_id IS NOT NULL AND bp.bank_profile_id != '' + ORDER BY d.name, bp.code + """).fetchall() + conn.close() + return [dict(r) for r in rows] + + +CURRENCY_MAP = { + "RUB": 643, "USD": 840, "KRW": 410, "AZN": 944, + "ARS": 32, "TJS": 972, "EUR": 978, "GBP": 826, + "CNY": 156, "JPY": 392, "TRY": 949, +} + + +def device_label(name: str) -> str: + """TECNO KM4-c7369f1959ffec79 → TECNO KM4""" + m = re.match(r"^(.+?)-[a-f0-9]{10,}$", name) + return m.group(1) if m else name + + +def cleanup_events(db_path: Path): + """Delete all events from previous runs.""" + conn = sqlite3.connect(str(db_path)) + count = conn.execute("SELECT count(*) FROM events").fetchone()[0] + if count > 0: + conn.execute("DELETE FROM events") + conn.commit() + print(f" deleted {count} old events") + else: + print(" events table clean") + conn.close() + + +def generate(materials: list, out_dir: Path): + out_dir.mkdir(parents=True, exist_ok=True) + scenarios_dir = out_dir / "scenarios" + scenarios_dir.mkdir(exist_ok=True) + + # --- default_profile.json --- + # Group by (device_api_id, bank_name) → bank_profile with materials + bp_map: dict = {} + for row in materials: + key = (row["device_api_id"], row["bank_name"]) + if key not in bp_map: + bp_map[key] = { + "id": row["bank_profile_id"], + "bank_name": row["bank_name"], + "state": "enabled", + "phone_number": row["phone"] or "", + "first_name": (row["full_name"] or "").split()[0] if row["full_name"] else "", + "middle_name": None, + "last_name": " ".join((row["full_name"] or "").split()[1:]) if row["full_name"] else "", + "currency": CURRENCY_MAP.get(row["bp_currency"], 0), + "device_id": row["device_api_id"], + "device_label": device_label(row["device_name"]), + "materials": [], + } + bp_map[key]["materials"].append({ + "id": row["material_id"], + "state": "enabled", + "card_number": row["card_number"] or "", + "currency": CURRENCY_MAP.get(row["mat_currency"], 0), + }) + + profile = {"bank_profiles": list(bp_map.values())} + profile_path = out_dir / "default_profile.json" + profile_path.write_text(json.dumps(profile, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + print(f" wrote {profile_path.name} ({len(bp_map)} bank_profiles)") + + # --- FETCH_PROFILE scenarios --- + # Remove old fetch_profile scenarios + for old in scenarios_dir.glob("*_fetch_profile.json"): + old.unlink() + + devices_seen = {} + for row in materials: + dev_short = device_label(row["device_name"]).lower().replace(" ", "_") + bank = row["bank_name"] + card = row["last_numbers"] or row["material_id"][:8] + fname = f"{dev_short}_{bank}_{card}_fetch_profile.json" + task = { + "bank_name": bank, + "material_id": row["material_id"], + "type": "FETCH_PROFILE", + "body": {}, + } + (scenarios_dir / fname).write_text( + json.dumps(task, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" + ) + print(f" wrote scenarios/{fname}") + + devices_seen.setdefault(dev_short, []).append( + f"{bank}/{card} ({row['material_id'][:8]}…)" + ) + + # Summary + print(f"\nDevices ({len(devices_seen)}):") + for dev, banks in devices_seen.items(): + print(f" {dev}: {', '.join(banks)}") + print(f"\nTotal: {len(materials)} active materials") + print(f"Files in: {out_dir.resolve()}") + + +def main(): + parser = argparse.ArgumentParser( + description="Generate mock test data from local ARC database" + ) + parser.add_argument("--config", "-c", default=None, + help="Path to config.ini (default: auto-detect from CWD)") + args = parser.parse_args() + + config_path = find_config(args.config) + print(f"config: {config_path}") + + db_path = resolve_db_path(config_path) + print(f"database: {db_path}") + + cleanup_events(db_path) + + materials = query_active_materials(db_path) + if not materials: + print("No active materials found in database.", file=sys.stderr) + return 1 + + # Output dir = tests/mock_server (same dir as this script) + out_dir = Path(__file__).parent + generate(materials, out_dir) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/mock_server/gen_transactions.ps1 b/tests/mock_server/gen_transactions.ps1 new file mode 100644 index 0000000..acffc1e --- /dev/null +++ b/tests/mock_server/gen_transactions.ps1 @@ -0,0 +1,16 @@ +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +$_configCandidates = @( + (Join-Path $PSScriptRoot "..\..\cmake-build-debug\config.ini"), + (Join-Path $PSScriptRoot "..\..\config.ini"), + (Join-Path $PSScriptRoot "..\config.ini"), + (Join-Path $PSScriptRoot "config.ini") +) +$_configPath = $null +foreach ($_c in $_configCandidates) { + $_resolved = [System.IO.Path]::GetFullPath($_c) + if (Test-Path $_resolved) { $_configPath = $_resolved; break } +} +if (-not $_configPath) { Write-Host "ERROR: config.ini not found" -ForegroundColor Red; exit 1 } +$_configArg = @("--config", $_configPath) +Write-Host "Config: $_configPath" -ForegroundColor Gray +& "C:\Users\User\AppData\Local\Programs\Python\Python312-arm64\python.exe" "$PSScriptRoot\gen_fetch_transactions.py" @_configArg @args \ No newline at end of file diff --git a/tests/mock_server/inject.sh b/tests/mock_server/inject.sh new file mode 100755 index 0000000..92a1a22 --- /dev/null +++ b/tests/mock_server/inject.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Usage: ./inject.sh scenarios/fetch_profile_ozon.json +set -euo pipefail +FILE="${1:?usage: inject.sh }" +curl -fsS http://127.0.0.1:8888/_admin/inject \ + -H 'Content-Type: application/json' \ + --data-binary @"$FILE" +echo diff --git a/tests/mock_server/inject_random.py b/tests/mock_server/inject_random.py new file mode 100755 index 0000000..9ce72fb --- /dev/null +++ b/tests/mock_server/inject_random.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +""" +Pick N random scenario JSONs from a pool and inject them into the mock server. + +Usage: + ./inject_random.py # 5 random from scenarios/pool/ + ./inject_random.py --count 3 + ./inject_random.py --dir scenarios/pool --count 5 --host 127.0.0.1 --port 8888 + +Each injected task is tagged with a sequential `_tag` field (if present) so +you can match the `postTaskResult` responses back to which pool entry ran. +""" +import argparse +import json +import random +import sys +import urllib.request +from pathlib import Path + + +def main() -> int: + here = Path(__file__).parent + parser = argparse.ArgumentParser() + parser.add_argument("--dir", default=str(here / "scenarios" / "pool"), + help="Directory with scenario JSON files") + parser.add_argument("--count", type=int, default=5, + help="How many scenarios to pick randomly") + parser.add_argument("--host", default="127.0.0.1") + parser.add_argument("--port", type=int, default=8888) + parser.add_argument("--seed", type=int, default=None, + help="Optional RNG seed for reproducibility") + args = parser.parse_args() + + pool_dir = Path(args.dir) + files = sorted(pool_dir.glob("*.json")) + if not files: + print(f"no scenarios in {pool_dir}", file=sys.stderr) + return 1 + if args.count > len(files): + print(f"asked for {args.count} but pool has only {len(files)}", + file=sys.stderr) + return 1 + + if args.seed is not None: + random.seed(args.seed) + picked = random.sample(files, args.count) + + tasks = [] + for p in picked: + with p.open() as f: + tasks.append(json.load(f)) + print(f" picked: {p.name}") + + url = f"http://{args.host}:{args.port}/_admin/inject" + req = urllib.request.Request( + url, + data=json.dumps(tasks).encode(), + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(req, timeout=5) as resp: + print(f"injected {len(tasks)} tasks -> {resp.status} {resp.read().decode()}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/mock_server/mock_server.py b/tests/mock_server/mock_server.py new file mode 100644 index 0000000..4c273a7 --- /dev/null +++ b/tests/mock_server/mock_server.py @@ -0,0 +1,1314 @@ +#!/usr/bin/env python3 +""" +Mock ARC backend for task-delivery integration tests. + +Stubs all endpoints NetworkService hits so a real ARC desktop app can connect, +run real devices, and process tasks that you inject from the outside. + +At startup loads: + - default_profile.json → served as /current_profile (seed bank profiles + with their materials so ARC's sync links them + to real local device/material rows) + - scenarios/pool/*.json → pool of FETCH_TRANSACTIONS tasks, grouped + by material_id for auto-bootstrap + +On the FIRST /auth/login of a run the mock bootstraps the task queue: + - 1 × FETCH_PROFILE per material seen in default_profile.json + - N × FETCH_TRANSACTIONS per material, picked randomly from the pool + (controlled by --ft-per-material, default 3) + +Every injected task gets a unique `_task_id`. Results posted via +POST /task/task_result are paired (FIFO per material+type) to the matching +injected task. GET /_admin/report returns the full list of pairs so you can +see exactly which task JSON went out and what the app returned. + +Usage: + pip install flask + python3 mock_server.py --port 8888 + + # Point ARC at it: in config.ini set + # [net] + # apiBase=http://127.0.0.1:8888 + + # Start ARC → it logs in, mock auto-queues bootstrap tasks, ARC processes + # them on real devices and posts results back. + + # Inspect the full report of task ↔ result pairs: + curl -s http://127.0.0.1:8888/_admin/report | jq +""" + +import argparse +import copy +import json +import os +import random +import re +import shutil +import threading +import time +import uuid +from collections import defaultdict, deque +from datetime import datetime, timedelta, timezone +from pathlib import Path + +from flask import Flask, Response, jsonify, request + +app = Flask(__name__) +app.config["JSON_SORT_KEYS"] = False + +HERE = Path(__file__).parent + +_lock = threading.Lock() + +_task_queue: deque = deque() +# _injected: list of {id, task, injected_at, result, result_at, status} +# status: "pending" | "completed" +_injected: list = [] +# Per (material_id, type) → FIFO of _injected indices still waiting for a result +_pending_by_key: dict = defaultdict(deque) + +_profile_id = "11111111-1111-1111-1111-111111111111" +_desktop_id = "22222222-2222-2222-2222-222222222222" + +_bank_profiles: list = [] # seeded from default_profile.json +_devices_by_desktop: dict = {} +_pool_by_material: dict = defaultdict(list) +_device_label_by_material: dict = {} # material_id -> "TECNO KM4" etc. +_in_flight: set = set() # material_ids with a delivered-but-not-yet-resulted task +_bootstrap_done = False +_bootstrap_ft_count = 3 +_bootstrap_banks: list | None = None # None = all banks +_bootstrap_types: list | None = None # None = all types (FETCH_PROFILE + FETCH_TRANSACTIONS) + +_STATE_FILE = HERE / ".mock_state.json" + + +def _save_state(): + """Persist task state to disk so debug reloader doesn't lose it.""" + try: + state = { + "injected": _injected, + "task_queue": list(_task_queue), + "in_flight": list(_in_flight), + "bootstrap_done": _bootstrap_done, + "pending_by_key": {f"{k[0]}|{k[1]}": v for k, v in _pending_by_key.items()}, + } + _STATE_FILE.write_text(json.dumps(state, default=str), encoding="utf-8") + except Exception as e: + print(f"[mock] failed to save state: {e}") + + +def _load_state() -> bool: + """Restore state from disk. Returns True if loaded.""" + global _bootstrap_done + if not _STATE_FILE.exists(): + return False + try: + state = json.loads(_STATE_FILE.read_text(encoding="utf-8")) + _injected.clear() + _injected.extend(state.get("injected", [])) + _task_queue.clear() + _task_queue.extend(state.get("task_queue", [])) + _in_flight.clear() + _in_flight.update(state.get("in_flight", [])) + _bootstrap_done = state.get("bootstrap_done", False) + _pending_by_key.clear() + for k, v in state.get("pending_by_key", {}).items(): + parts = k.split("|", 1) + if len(parts) == 2: + _pending_by_key[(parts[0], parts[1])] = v + print(f"[mock] restored state: {len(_injected)} injected, " + f"{len(_task_queue)} queued, bootstrap={'done' if _bootstrap_done else 'pending'}") + return True + except Exception as e: + print(f"[mock] failed to load state: {e}") + return False + + +def ok(data=None): + return jsonify({"success": True, "data": data if data is not None else {}}) + + +def log(tag, obj): + text = json.dumps(obj, ensure_ascii=False) + if len(text) > 400: + text = text[:400] + "…" + print(f"[{time.strftime('%H:%M:%S')}] {tag}: {text}") + + +# ---------------------------------------------------------------- startup load +def load_default_profile(): + global _bank_profiles + p = HERE / "default_profile.json" + if not p.exists(): + print(f"[mock] no {p.name} found — profile starts empty") + return + data = json.loads(p.read_text(encoding="utf-8")) + _bank_profiles = data.get("bank_profiles", []) + mats = sum(len(bp.get("materials", [])) for bp in _bank_profiles) + + # Build material_id → device_label map for report grouping + _device_label_by_material.clear() + for bp in _bank_profiles: + label = bp.get("device_label") or (bp.get("device_id", "")[:8] + "…") + for mat in bp.get("materials", []): + mid = mat.get("id") + if mid: + _device_label_by_material[mid] = label + + print(f"[mock] loaded {p.name}: {len(_bank_profiles)} bank_profiles, {mats} materials") + + +def load_pool(): + pool_dir = HERE / "scenarios" / "pool" + if not pool_dir.exists(): + print(f"[mock] pool dir {pool_dir} missing — bootstrap disabled") + return + count = 0 + for f in sorted(pool_dir.glob("*.json")): + try: + task = json.loads(f.read_text(encoding="utf-8")) + except Exception as e: + print(f"[mock] skipping {f.name}: {e}") + continue + if task.get("type") != "FETCH_TRANSACTIONS": + continue + mid = task.get("material_id") + if not mid: + continue + task["_source"] = f.name + _pool_by_material[mid].append(task) + count += 1 + print(f"[mock] loaded pool: {count} FETCH_TRANSACTIONS across " + f"{len(_pool_by_material)} materials") + for mid, lst in _pool_by_material.items(): + print(f" {mid}: {len(lst)} tasks") + + +def enqueue_task(task: dict) -> dict: + """Attach _task_id, store in queue, track in _injected.""" + task = copy.deepcopy(task) + tid = str(uuid.uuid4()) + task["_task_id"] = tid + entry = { + "id": tid, + "task": task, + "injected_at": time.time(), + "result": None, + "result_at": None, + "status": "pending", + } + _injected.append(entry) + idx = len(_injected) - 1 + key = (task.get("material_id"), task.get("type")) + _pending_by_key[key].append(idx) + _task_queue.append(task) + return entry + + +def bootstrap_tasks(): + """Queue 1 FETCH_PROFILE + N FETCH_TRANSACTIONS per seeded material.""" + global _bootstrap_done + if _bootstrap_done: + return + queued = 0 + for bp in _bank_profiles: + bank = bp.get("bank_name") + if _bootstrap_banks and bank not in _bootstrap_banks: + continue + for mat in bp.get("materials", []): + mid = mat.get("id") + if not mid: + continue + # 1 × FETCH_PROFILE + if not _bootstrap_types or "FETCH_PROFILE" in _bootstrap_types: + enqueue_task({ + "bank_name": bank, + "material_id": mid, + "type": "FETCH_PROFILE", + "body": {}, + "_source": "bootstrap", + }) + queued += 1 + + # N × random FETCH_TRANSACTIONS from pool + if not _bootstrap_types or "FETCH_TRANSACTIONS" in _bootstrap_types: + pool = _pool_by_material.get(mid, []) + if not pool: + print(f"[mock] bootstrap: no pool tasks for {mid} ({bank})") + continue + pick = random.sample(pool, min(_bootstrap_ft_count, len(pool))) + for t in pick: + enqueue_task(t) + queued += 1 + _bootstrap_done = True + _save_state() + print(f"[mock] bootstrap queued {queued} tasks") + + +# ---------------------------------------------------------------- auth +@app.post("/api/v1/auth/login") +def auth_login(): + body = request.get_json(silent=True) or {} + log("POST /auth/login", body) + with _lock: + bootstrap_tasks() + return ok({ + "access_token": "mock-access-token", + "refresh_token": "mock-refresh-token", + "expires_in": 3600, + }) + + +@app.post("/api/v1/auth/refresh_token") +def auth_refresh(): + return ok({ + "access_token": "mock-access-token", + "refresh_token": "mock-refresh-token", + "expires_in": 3600, + }) + + +# ---------------------------------------------------------------- desktop +@app.get("/api/v1/desktop/") +def desktop_list(): + return ok([{ + "id": _desktop_id, + "profile_id": _profile_id, + "hidden": False, + "name": "mock-desktop", + }]) + + +@app.post("/api/v1/desktop/") +def desktop_create(): + body = request.get_json(silent=True) or {} + log("POST /desktop/", body) + return ok({"id": _desktop_id, "profile_id": _profile_id}) + + +# ---------------------------------------------------------------- device +@app.get("/api/v1/device/") +def device_list(desktop_id): + return ok(_devices_by_desktop.get(desktop_id, [])) + + +@app.post("/api/v1/device/") +def device_create(): + body = request.get_json(silent=True) or {} + log("POST /device/", body) + api_id = str(uuid.uuid4()) + dev = { + "id": api_id, + "name": body.get("name", "unknown"), + "status": body.get("status", "OFFLINE"), + "screen_width": body.get("screen_width", 0), + "screen_height": body.get("screen_height", 0), + "battery": body.get("battery", 0), + } + _devices_by_desktop.setdefault(_desktop_id, []).append(dev) + return ok(dev) + + +@app.patch("/api/v1/device/") +def device_patch(api_id): + return ok({"id": api_id}) + + +@app.delete("/api/v1/device/") +def device_delete(api_id): + return ok({"id": api_id}) + + +@app.post("/api/v1/device/add_screenshot/") +def device_screenshot(api_id): + return ok({"id": api_id}) + + +# ---------------------------------------------------------------- profile +@app.get("/api/v1/profile/current_profile") +def current_profile(): + return ok({ + "id": _profile_id, + "bank_profiles": _bank_profiles, + }) + + +@app.post("/api/v1/profile/bank_profile") +def bank_profile_create(): + body = request.get_json(silent=True) or {} + bp = dict(body) + bp["id"] = bp.get("id") or str(uuid.uuid4()) + bp.setdefault("materials", []) + _bank_profiles.append(bp) + return ok(bp) + + +@app.patch("/api/v1/profile/bank_profile//") +def bank_profile_toggle(bp_id, state): + return ok({"id": bp_id, "state": state}) + + +@app.patch("/api/v1/profile/bank_profile/") +def bank_profile_update(bp_id): + body = request.get_json(silent=True) or {} + log("PATCH /bank_profile", {"id": bp_id, **body}) + for bp in _bank_profiles: + if bp.get("id") == bp_id: + bp.update(body) + return ok(bp) + # Не найден — создаём + bp = dict(body) + bp["id"] = bp_id + bp.setdefault("materials", []) + _bank_profiles.append(bp) + return ok(bp) + + +@app.post("/api/v1/profile/material") +def material_create(): + body = request.get_json(silent=True) or {} + mat = dict(body) + mat["id"] = mat.get("id") or str(uuid.uuid4()) + return ok(mat) + + +@app.patch("/api/v1/profile/material//") +def material_toggle(mat_id, state): + return ok({"id": mat_id, "state": state}) + + +# ---------------------------------------------------------------- tasks +@app.get("/api/v1/task/get_tasks") +def get_tasks(): + """Deliver at most 1 task per material per poll. + + A material is blocked until the previous task's result arrives via + POST /task/task_result. This prevents the 5-min expire window in + EventHandler from killing tasks that were just waiting in queue. + """ + material_ids = request.args.getlist("materials_id") + with _lock: + if not _task_queue: + return ok([]) + delivered = [] + delivered_mats = set() + kept = deque() + while _task_queue: + t = _task_queue.popleft() + mid = t.get("material_id") + # Filter by requested materials + if material_ids and mid not in material_ids: + kept.append(t) + continue + # Skip if this material already has an in-flight task + if mid in _in_flight or mid in delivered_mats: + kept.append(t) + continue + delivered.append(t) + delivered_mats.add(mid) + _in_flight.add(mid) + _task_queue.extend(kept) + if delivered: + _save_state() + log("GET /task/get_tasks -> delivered", + [{"type": t.get("type"), "material_id": t.get("material_id")} + for t in delivered]) + return ok(delivered) + + +@app.post("/api/v1/task/task_result") +def task_result(): + body = request.get_json(silent=True) or {} + mid = body.get("material_id") + log("POST /task/task_result", + {"type": body.get("type"), "material_id": mid}) + + with _lock: + # Unblock this material for the next task delivery + _in_flight.discard(mid) + + # Клиент присылает ошибки с type="ERROR", но оригинальный тип задачи + # лежит в data.event_type — им и матчимся, иначе ошибка повиснет + # как unsolicited без привязки к задаче/устройству. + rtype = body.get("type") + if rtype == "ERROR": + data = body.get("data") or {} + rtype = data.get("event_type") or rtype + + key = (mid, rtype) + idx = None + if _pending_by_key[key]: + idx = _pending_by_key[key].popleft() + if idx is not None: + _injected[idx]["result"] = body + _injected[idx]["result_at"] = time.time() + _injected[idx]["status"] = "completed" + else: + _injected.append({ + "id": str(uuid.uuid4()), + "task": None, + "injected_at": None, + "result": body, + "result_at": time.time(), + "status": "unsolicited", + }) + _save_state() + return ok({}) + + +# ---------------------------------------------------------------- monitoring +_monitoring_logs: list = [] # {type, event, message, has_image, image_id} +_SCREENSHOTS_DIR = HERE / ".mock_screenshots" + + +@app.post("/monitoring/log") +def monitoring_log(): + mtype = request.form.get("type", "") + mevent = request.form.get("event", "") + message = request.form.get("message", "") + image_file = request.files.get("image") + + image_id = None + if image_file: + image_id = str(uuid.uuid4()) + _SCREENSHOTS_DIR.mkdir(exist_ok=True) + (_SCREENSHOTS_DIR / f"{image_id}.png").write_bytes(image_file.read()) + + entry = { + "type": mtype, + "event": mevent, + "message": message, + "has_image": image_id is not None, + "image_id": image_id, + "timestamp": time.time(), + } + with _lock: + _monitoring_logs.append(entry) + + # Привязываем скриншот к задаче по internal_task_id + internal_tid = request.form.get("internal_task_id", "") + if image_id: + if not internal_tid: + print(f"[mock] WARNING: monitoring_log without internal_task_id, screenshot not linked") + else: + matched = False + with _lock: + for e in reversed(_injected): + r = e.get("result") + if r and str(r.get("internal_task_id", "")) == internal_tid: + e["screenshot_id"] = image_id + matched = True + break + if matched: + _save_state() + else: + print(f"[mock] WARNING: no task found for internal_task_id={internal_tid}") + + log("POST /monitoring/log", {"type": mtype, "event": mevent, + "has_image": image_id is not None, + "message": message[:100]}) + return ok({}) + + +@app.get("/_admin/screenshot/") +def get_screenshot(image_id): + path = _SCREENSHOTS_DIR / f"{image_id}.png" + if not path.exists(): + return "not found", 404 + return Response(path.read_bytes(), mimetype="image/png") + + +# ---------------------------------------------------------------- admin +@app.post("/_admin/inject") +def admin_inject(): + body = request.get_json(silent=True) + if body is None: + return jsonify({"error": "json body required"}), 400 + tasks = body if isinstance(body, list) else [body] + with _lock: + for t in tasks: + enqueue_task(t) + log("_admin/inject", {"queued": len(tasks)}) + return ok({"queued": len(tasks)}) + + +@app.post("/_admin/seed_profile") +def admin_seed_profile(): + body = request.get_json(silent=True) or {} + global _bank_profiles + _bank_profiles = body.get("bank_profiles", []) + return ok({"bank_profiles": len(_bank_profiles)}) + + +@app.post("/_admin/seed_devices") +def admin_seed_devices(): + body = request.get_json(silent=True) or [] + _devices_by_desktop[_desktop_id] = body + return ok({"devices": len(body)}) + + +@app.post("/_admin/bootstrap") +def admin_bootstrap(): + """Re-run bootstrap manually (useful if ARC was already logged in).""" + global _bootstrap_done + with _lock: + _bootstrap_done = False + bootstrap_tasks() + return ok({"queued": sum(1 for e in _injected if e["status"] == "pending")}) + + +@app.get("/_admin/report") +def admin_report(): + """Full task ↔ result pairing report.""" + with _lock: + errors = sum(1 for e in _injected if e["status"] == "completed" and _is_error_entry(e)) + summary = { + "total": len(_injected), + "pending": sum(1 for e in _injected if e["status"] == "pending"), + "completed": sum(1 for e in _injected if e["status"] == "completed") - errors, + "errors": errors, + "unsolicited": sum(1 for e in _injected if e["status"] == "unsolicited"), + } + return jsonify({ + "summary": summary, + "entries": _injected, + }) + + +@app.get("/_admin/report/pending") +def admin_report_pending(): + with _lock: + return jsonify([e for e in _injected if e["status"] == "pending"]) + + +def _is_error_entry(e) -> bool: + """Check if a completed entry has an error in result data.""" + result = e.get("result") + if not result: + return False + data = result.get("data") + if isinstance(data, dict) and data.get("status") == "error": + return True + return False + + +def _snapshot_entries_and_summary(): + entries = list(_injected) + errors = sum(1 for e in entries if e["status"] == "completed" and _is_error_entry(e)) + summary = { + "total": len(entries), + "pending": sum(1 for e in entries if e["status"] == "pending"), + "completed": sum(1 for e in entries if e["status"] == "completed") - errors, + "errors": errors, + "unsolicited": sum(1 for e in entries if e["status"] == "unsolicited"), + } + return entries, summary + + +@app.get("/_admin/report/html") +def admin_report_html(): + """Pretty HTML view of task ↔ result pairs. Auto-refreshes via JS.""" + with _lock: + entries, summary = _snapshot_entries_and_summary() + return Response(render_report_html(entries, summary), mimetype="text/html") + + +@app.get("/_admin/report/fragment") +def admin_report_fragment(): + """JSON fragment for JS auto-refresh: rendered body HTML + summary counts.""" + with _lock: + entries, summary = _snapshot_entries_and_summary() + body = _render_grouped_blocks(entries) or '
Отчёт пуст
' + return jsonify({"html": body, "summary": summary}) + + +def _fmt_ts(ts): + if not ts: + return "—" + return time.strftime("%H:%M:%S", time.localtime(ts)) + + +def _esc(s: str) -> str: + return (s.replace("&", "&").replace("<", "<") + .replace(">", ">").replace('"', """)) + + +def render_entry_html(e) -> str: + import html as _html + status = e["status"] + is_error = _is_error_entry(e) + display_status = "error" if is_error else status + task = e.get("task") or {} + result = e.get("result") + ttype = _html.escape(task.get("type", "?")) + mid = _html.escape((task.get("material_id") or "")[:8]) + src = _html.escape(task.get("_source", "")) + bank = _html.escape(task.get("bank_name", "")) + injected_at = _fmt_ts(e.get("injected_at")) + result_at = _fmt_ts(e.get("result_at")) + + tid = _html.escape(e.get("id", "")) + + def json_block(obj): + if obj is None: + return '' + text = json.dumps(obj, ensure_ascii=False, indent=2) + escaped = _html.escape(text) + # data-json с escaped JSON для копирования + raw = _html.escape(text, quote=True) + return (f'
' + f'' + f'
{escaped}
') + + result_summary = "" + if is_error: + d = result.get("data", {}) if result else {} + result_summary = f'
ERROR: {_html.escape(str(d.get("description", "")))}
' + elif result and isinstance(result.get("data"), list): + result_summary = f'
{len(result["data"])} items
' + + scr_id = e.get("screenshot_id") + scr_pane = "" + if scr_id: + scr_pane = f""" +
+
Screenshot
+ +
""" + + has_screenshot = bool(scr_id) + panes_cls = "panes panes-3" if has_screenshot else "panes" + + # Успешные — вся задача свёрнута, ошибки — развёрнуты + if display_status == "completed": + panes_html = f""" +
+ +
+
+
Task JSON
{json_block(task)}
+
Result JSON
{result_summary}{json_block(result)}
+ {scr_pane} +
+
+
""" + else: + panes_html = f""" +
+
Task JSON
{json_block(task)}
+
Result JSON
{result_summary}{json_block(result)}
+ {scr_pane} +
""" + + return f""" +
+
+ {display_status} + {ttype} + {bank} + {mid}… + {src} + inj {injected_at} → res {result_at} +
+ {panes_html} +
+ """ + + +def render_profile_section() -> str: + """Render loaded bank profiles / materials as an HTML info block.""" + import html as _html + if not _bank_profiles: + return '
Профиль не загружен
' + + # Group by device + devices: dict = {} + for bp in _bank_profiles: + dev_id = bp.get("device_id", "?") + label = bp.get("device_label") or (dev_id[:8] + "…") + if dev_id not in devices: + devices[dev_id] = {"label": label, "profiles": []} + devices[dev_id]["profiles"].append(bp) + + device_blocks = [] + for dev_id, dev in devices.items(): + label = _html.escape(dev["label"]) + dev_mats = sum(len(bp.get("materials", [])) for bp in dev["profiles"]) + bp_cards = [] + for bp in dev["profiles"]: + bank = _html.escape(bp.get("bank_name", "?")) + first = bp.get("first_name") or "" + last = bp.get("last_name") or "" + name = _html.escape(f"{first} {last}".strip() or "—") + phone = _html.escape(bp.get("phone_number") or "—") + mats = bp.get("materials", []) + mat_rows = [] + for m in mats: + mid = _html.escape(m.get("id", "?")[:12]) + card = _html.escape(m.get("card_number") or "—") + cur = m.get("currency", "") + state = _html.escape(m.get("state", "?")) + # Count pool tasks for this material + pool_n = len(_pool_by_material.get(m.get("id", ""), [])) + pool_badge = (f'{pool_n}' + if pool_n > 0 else '0') + mat_rows.append( + f'{mid}…' + f'{card}{cur}' + f'{state}' + f'{pool_badge}' + ) + mat_table = "" + if mat_rows: + mat_table = ( + '' + '' + '' + + "".join(mat_rows) + + '
Material IDКартаВалютаСтатусПул
' + ) + bp_cards.append( + f'
' + f'
' + f'{bank}' + f'{name}' + f'{phone}' + f'
' + f'{mat_table}' + f'
' + ) + + device_blocks.append( + f'
' + f'
' + f'{label}' + f'{len(dev["profiles"])} профилей, {dev_mats} материалов' + f'
' + f'
{"".join(bp_cards)}
' + f'
' + ) + + pool_count = sum(len(v) for v in _pool_by_material.values()) + pool_info = (f'{pool_count} задач в пуле' + if pool_count > 0 + else 'пул пуст') + total_mats = sum(len(bp.get("materials", [])) for bp in _bank_profiles) + + return ( + '
' + 'Загруженные данные ' + f'({len(devices)} устройств, ' + f'{len(_bank_profiles)} профилей, ' + f'{total_mats} материалов, ' + f'{pool_info})' + + "".join(device_blocks) + + '
' + ) + + +def _render_grouped_blocks(entries) -> str: + import html as _html + + # Group by device label → bank_name + devices: dict = defaultdict(lambda: defaultdict(list)) + for e in entries: + task = e.get("task") or {} + mid = task.get("material_id") + label = _device_label_by_material.get(mid) or ( + "(без устройства)" if mid is None else f"{mid[:8]}…" + ) + bank = task.get("bank_name", "unknown") + devices[label][bank].append(e) + + group_blocks = [] + for label in sorted(devices.keys()): + banks = devices[label] + # Device-level stats + all_entries = [e for bl in banks.values() for e in bl] + g_total = len(all_entries) + g_err = sum(1 for e in all_entries if _is_error_entry(e)) + g_done = sum(1 for e in all_entries if e["status"] == "completed") - g_err + g_pend = sum(1 for e in all_entries if e["status"] == "pending") + err_badge = f' ошибки: {g_err}' if g_err else '' + + bank_blocks = [] + for bank in sorted(banks.keys()): + bentries = banks[bank] + b_total = len(bentries) + b_err = sum(1 for e in bentries if _is_error_entry(e)) + b_done = sum(1 for e in bentries if e["status"] == "completed") - b_err + b_pend = sum(1 for e in bentries if e["status"] == "pending") + b_err_badge = f' {b_err}' if b_err else '' + rows_html = "\n".join(render_entry_html(e) for e in bentries) + bank_blocks.append( + f'
' + f'

{_html.escape(bank)}' + f'' + f'{b_done}' + f' / {b_total}' + f'{b_err_badge}' + f' (ожидают: {b_pend})' + f'

' + f'{rows_html}
' + ) + + group_blocks.append(f""" +
+

+ {_html.escape(label)} + + {g_done} + / {g_total} + {err_badge} + (ожидают: {g_pend}) + +

+ {"".join(bank_blocks)} +
+ """) + + return "\n".join(group_blocks) + + +def render_report_html(entries, summary) -> str: + body_rows = _render_grouped_blocks(entries) or '
Отчёт пуст — бутстрап ещё не сработал или результаты ещё не пришли.
' + + return f""" + + + +ARC mock — отчёт + + + +

ARC mock — отчёт (автообновление 2 сек)

+
+
{summary['total']}
всего
+
{summary['completed']}
выполнено
+
{summary['errors']}
ошибки
+
{summary['pending']}
в очереди
+
+ {render_profile_section()} +
+ {body_rows} +
+ + + + + + +""" + + +@app.post("/_admin/reset") +def admin_reset(): + global _bootstrap_done + with _lock: + _task_queue.clear() + _injected.clear() + _pending_by_key.clear() + _in_flight.clear() + _monitoring_logs.clear() + _bootstrap_done = False + if _SCREENSHOTS_DIR.exists(): + shutil.rmtree(_SCREENSHOTS_DIR) + if _STATE_FILE.exists(): + _STATE_FILE.unlink() + return ok({}) + + +@app.get("/_admin/state") +def admin_state(): + with _lock: + return jsonify({ + "queued_tasks": len(_task_queue), + "injected": len(_injected), + "completed": sum(1 for e in _injected if e["status"] == "completed"), + "pending": sum(1 for e in _injected if e["status"] == "pending"), + "bank_profiles": len(_bank_profiles), + "pool_materials": len(_pool_by_material), + "bootstrap_done": _bootstrap_done, + }) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--host", default="127.0.0.1") + parser.add_argument("--port", type=int, default=8888) + parser.add_argument("--ft-per-material", type=int, default=3, + help="How many random FETCH_TRANSACTIONS to bootstrap per material") + parser.add_argument("--banks", nargs="+", default=None, + help="Only bootstrap tasks for these banks (e.g. --banks ozon dushanbe_city_bank)") + parser.add_argument("--seed", type=int, default=None, + help="RNG seed for reproducible bootstrap picks") + parser.add_argument("--types", nargs="+", default=None, + help="Only bootstrap these task types (e.g. --types FETCH_TRANSACTIONS)") + parser.add_argument("--debug", action="store_true", + help="Run Flask in debug mode (auto-reload on code changes)") + args = parser.parse_args() + + _bootstrap_ft_count = args.ft_per_material + _bootstrap_banks = args.banks + _bootstrap_types = args.types + if _bootstrap_banks: + print(f"[mock] bootstrap limited to banks: {_bootstrap_banks}") + if _bootstrap_types: + print(f"[mock] bootstrap limited to types: {_bootstrap_types}") + if args.seed is not None: + random.seed(args.seed) + + load_default_profile() + load_pool() + + is_reload = os.environ.get("WERKZEUG_RUN_MAIN") == "true" + + if args.debug and is_reload and _load_state(): + # Auto-reload в debug — восстанавливаем состояние + print("[mock] auto-reload: state restored from disk") + else: + # Первый запуск (или без --debug) — чистим всё + _task_queue.clear() + _injected.clear() + _pending_by_key.clear() + _in_flight.clear() + _monitoring_logs.clear() + _bootstrap_done = False + if _STATE_FILE.exists(): + _STATE_FILE.unlink() + import shutil + if _SCREENSHOTS_DIR.exists(): + shutil.rmtree(_SCREENSHOTS_DIR) + # Чистим events в БД приложения чтобы не было "App restarted" при старте + try: + import configparser + import sqlite3 + project_root = (HERE / ".." / "..").resolve() + for cfg in [project_root / "config.ini", project_root / "build_test" / "config.ini"]: + if cfg.exists(): + cp = configparser.ConfigParser() + cp.read(str(cfg), encoding="utf-8") + raw = cp.get("db", "dbPath", fallback="") + if raw: + db = Path(os.path.expanduser(raw)) + if not db.is_absolute(): + db = cfg.parent / db + if db.exists(): + conn = sqlite3.connect(str(db)) + n = conn.execute("SELECT count(*) FROM events").fetchone()[0] + if n > 0: + conn.execute("DELETE FROM events") + conn.commit() + print(f"[mock] cleaned {n} events from {db.name}") + conn.close() + break + except Exception as e: + print(f"[mock] warning: could not clean events DB: {e}") + print("[mock] starting fresh (clean slate)") + + app.run(host=args.host, port=args.port, threaded=True, debug=args.debug) diff --git a/tests/mock_server/run.sh b/tests/mock_server/run.sh new file mode 100755 index 0000000..9b6e012 --- /dev/null +++ b/tests/mock_server/run.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Start the mock ARC backend. Requires `flask` in the active Python env. +set -euo pipefail +cd "$(dirname "$0")" + +# Find config.ini (project root is two levels up) +PROJECT_ROOT="$(cd ../.. && pwd)" +CONFIG="" +for c in "$PROJECT_ROOT/config.ini" "$PROJECT_ROOT/build/config.ini" "$PROJECT_ROOT/build/Release/config.ini"; do + if [ -f "$c" ]; then CONFIG="$c"; break; fi +done + +if [ -n "$CONFIG" ]; then + echo "[run.sh] generating mock data + cleaning events..." + python3 gen_mock_data.py --config "$CONFIG" + echo "" +fi + +exec python3 mock_server.py --host 127.0.0.1 --port 8888 "$@" diff --git a/tests/mock_server/run_server.ps1 b/tests/mock_server/run_server.ps1 new file mode 100644 index 0000000..aa4dff6 --- /dev/null +++ b/tests/mock_server/run_server.ps1 @@ -0,0 +1,2 @@ +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +& "C:\Users\User\AppData\Local\Programs\Python\Python312-arm64\python.exe" "$PSScriptRoot\mock_server.py" --port 8888 @args \ No newline at end of file diff --git a/tests/mock_server/seed_profile.example.json b/tests/mock_server/seed_profile.example.json new file mode 100644 index 0000000..db9bf5c --- /dev/null +++ b/tests/mock_server/seed_profile.example.json @@ -0,0 +1,23 @@ +{ + "bank_profiles": [ + { + "id": "bp-ozon-0001", + "bank_name": "ozon", + "state": "active", + "phone_number": "+79990000000", + "first_name": "Ivan", + "middle_name": "", + "last_name": "Petrov", + "currency": 643, + "device_id": "REPLACE_WITH_DEVICE_API_ID", + "materials": [ + { + "id": "c340cd2e-cefd-48d6-a813-c4be097ae493", + "state": "active", + "bank_account_id": "40817810000000000001", + "currency": 643 + } + ] + } + ] +} diff --git a/tests/mock_server/setup.ps1 b/tests/mock_server/setup.ps1 new file mode 100644 index 0000000..73abb27 --- /dev/null +++ b/tests/mock_server/setup.ps1 @@ -0,0 +1,567 @@ +<# +.SYNOPSIS + Установка зависимостей, генерация тестовых данных и запуск mock-сервера. + +.DESCRIPTION + Скрипт выполняет: + 1. Проверяет/устанавливает Python 3.12+ + 2. Устанавливает Flask (pip install flask) + 3. Находит config.ini и БД + 4. Проверяет наличие БД и активных данных (devices, bank_profiles, materials) + 5. Генерирует default_profile.json и scenarios из БД + 6. Переключает config.ini на mock (apiBase=http://127.0.0.1:PORT) + 7. Показывает сводку по данным из БД + 8. Запускает mock-сервер и открывает отчёт в браузере + +.PARAMETER Port + Порт mock-сервера (default: 8888) + +.PARAMETER FtPerMaterial + Сколько FETCH_TRANSACTIONS на материал при бутстрапе (default: 3) + +.PARAMETER SkipInstall + Пропустить установку Python/Flask (если уже установлены) + +.PARAMETER GenerateOnly + Только сгенерировать данные, не запускать сервер + +.EXAMPLE + .\setup.ps1 + .\setup.ps1 -Port 9999 -FtPerMaterial 5 + .\setup.ps1 -SkipInstall + .\setup.ps1 -GenerateOnly +#> + +param( + [int]$Port = 8888, + [int]$FtPerMaterial = 3, + [string]$Banks = "", + [string]$Types = "", + [switch]$SkipInstall, + [switch]$GenerateOnly, + [switch]$Clean, + [switch]$Debug +) + +$ErrorActionPreference = "Continue" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +$Host.UI.RawUI.WindowTitle = "ARC Mock Server Setup" + +# ─── Helpers ─────────────────────────────────────────────────────────────── + +function Write-Step([string]$n, [string]$msg) { + Write-Host "`n=== [$n] $msg ===" -ForegroundColor Cyan +} + +function Write-Ok([string]$msg) { + Write-Host " OK: $msg" -ForegroundColor Green +} + +function Write-Warn([string]$msg) { + Write-Host " WARN: $msg" -ForegroundColor Yellow +} + +function Write-Err([string]$msg) { + Write-Host " ERROR: $msg" -ForegroundColor Red +} + +function Exit-WithError([string]$msg) { + Write-Err $msg + Write-Host "`nPress any key to exit..." -ForegroundColor Gray + $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") + exit 1 +} + +# ─── 1. Find script and project directories ─────────────────────────────── + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition + +$configPath = $null +$configCandidates = @( + (Join-Path $scriptDir "..\..\cmake-build-debug\config.ini"), + (Join-Path $scriptDir "..\..\config.ini"), + (Join-Path $scriptDir "..\config.ini"), + (Join-Path $scriptDir "config.ini") +) +foreach ($c in $configCandidates) { + $resolved = [System.IO.Path]::GetFullPath($c) + if (Test-Path $resolved) { + $configPath = $resolved + $projectRoot = Split-Path -Parent $resolved + break + } +} + +if (-not $configPath) { + Exit-WithError "config.ini not found. Run from project root or Release/tests/mock_server/" +} + +Write-Host "Project root : $projectRoot" -ForegroundColor Gray +Write-Host "Config : $configPath" -ForegroundColor Gray +Write-Host "Script dir : $scriptDir" -ForegroundColor Gray + +# ─── 2. Python ───────────────────────────────────────────────────────────── + +Write-Step "1/6" "Python" + +$pythonExe = $null + +function Find-Python { + foreach ($cmd in @("python", "python3", "py")) { + try { + $ver = & $cmd --version 2>&1 + if ($ver -match "Python\s+3\.(\d+)") { + if ([int]$Matches[1] -ge 9) { + return (Get-Command $cmd).Source + } + } + } catch {} + } + $paths = @( + "$env:LOCALAPPDATA\Programs\Python\Python3*\python.exe", + "$env:LOCALAPPDATA\Programs\Python\Python31*\python.exe", + "C:\Python3*\python.exe" + ) + foreach ($pattern in $paths) { + $found = Get-ChildItem -Path $pattern -ErrorAction SilentlyContinue | + Sort-Object Name -Descending | Select-Object -First 1 + if ($found) { + $ver = & $found.FullName --version 2>&1 + if ($ver -match "Python\s+3\.(\d+)" -and [int]$Matches[1] -ge 9) { + return $found.FullName + } + } + } + return $null +} + +if (-not $SkipInstall) { + $pythonExe = Find-Python + if ($pythonExe) { + $ver = & $pythonExe --version 2>&1 + Write-Ok "Found: $ver ($pythonExe)" + } else { + Write-Warn "Python not found - installing via winget..." + try { + winget install Python.Python.3.12 --accept-source-agreements --accept-package-agreements --silent 2>&1 | Out-Null + $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + + [System.Environment]::GetEnvironmentVariable("Path", "User") + $pythonExe = Find-Python + if ($pythonExe) { + $ver = & $pythonExe --version 2>&1 + Write-Ok "Installed: $ver" + } else { + Exit-WithError "Python installed but not in PATH. Restart terminal and try again." + } + } catch { + Exit-WithError "Failed to install Python. Install manually: https://python.org" + } + } +} else { + $pythonExe = Find-Python + if (-not $pythonExe) { + Exit-WithError "Python not found. Remove -SkipInstall or install manually." + } + Write-Ok "Python: $pythonExe" +} + +# ─── Generate helper scripts ────────────────────────────────────────────── + +$genFtPs1 = Join-Path $scriptDir "gen_transactions.ps1" +$runServerPs1 = Join-Path $scriptDir "run_server.ps1" + +# Shared config-finder block for helper scripts +$configFinderBlock = @' +$_configCandidates = @( + (Join-Path $PSScriptRoot "..\..\cmake-build-debug\config.ini"), + (Join-Path $PSScriptRoot "..\..\config.ini"), + (Join-Path $PSScriptRoot "..\config.ini"), + (Join-Path $PSScriptRoot "config.ini") +) +$_configPath = $null +foreach ($_c in $_configCandidates) { + $_resolved = [System.IO.Path]::GetFullPath($_c) + if (Test-Path $_resolved) { $_configPath = $_resolved; break } +} +if (-not $_configPath) { Write-Host "ERROR: config.ini not found" -ForegroundColor Red; exit 1 } +$_configArg = @("--config", $_configPath) +'@ + +# gen_transactions.ps1 +$genFtContent = @" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +$configFinderBlock +Write-Host "Config: `$_configPath" -ForegroundColor Gray +& "$pythonExe" "`$PSScriptRoot\gen_fetch_transactions.py" @_configArg @args +"@ +[System.IO.File]::WriteAllText($genFtPs1, $genFtContent, [System.Text.UTF8Encoding]::new($false)) + +# run_server.ps1 +$runServerContent = @" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +& "$pythonExe" "`$PSScriptRoot\mock_server.py" --port 8888 @args +"@ +[System.IO.File]::WriteAllText($runServerPs1, $runServerContent, [System.Text.UTF8Encoding]::new($false)) + +Write-Ok "Created gen_transactions.ps1, run_server.ps1" + +# ─── 3. Flask ────────────────────────────────────────────────────────────── + +Write-Step "2/6" "Flask" + +$flaskInstalled = $false +try { + $pipOut = & $pythonExe -m pip show flask 2>&1 + if ($pipOut -match "Name:\s*Flask") { + $flaskVer = ($pipOut | Select-String "Version:\s*(.+)").Matches.Groups[1].Value + Write-Ok "Flask $flaskVer already installed" + $flaskInstalled = $true + } +} catch {} + +if (-not $flaskInstalled) { + Write-Host " Installing Flask..." -ForegroundColor Gray + & $pythonExe -m pip install flask --quiet 2>&1 + if ($LASTEXITCODE -ne 0) { + Exit-WithError "Failed to install Flask. Try: $pythonExe -m pip install flask" + } + Write-Ok "Flask installed" +} + +# ─── 4. Database check ──────────────────────────────────────────────────── + +Write-Step "3/6" "Database" + +$configContent = Get-Content $configPath -Encoding UTF8 +$dbMatch = $configContent | Select-String "dbPath\s*=\s*(.+)" +if (-not $dbMatch) { + Exit-WithError "[db] dbPath not found in config.ini" +} +$dbPathRaw = $dbMatch.Matches.Groups[1].Value.Trim() + +if ([System.IO.Path]::IsPathRooted($dbPathRaw)) { + $dbPath = $dbPathRaw +} else { + $dbPath = [System.IO.Path]::GetFullPath((Join-Path $projectRoot $dbPathRaw)) +} + +Write-Host " DB path: $dbPath" -ForegroundColor Gray + +if (-not (Test-Path $dbPath)) { + Exit-WithError ("DB not found: $dbPath`n`n" + + "To create the DB:`n" + + " 1. Set apiBase to production server in config.ini`n" + + " 2. Run ARC.exe and log in`n" + + " 3. Devices, profiles and materials will appear in the DB`n" + + " 4. Re-run this script") +} + +Write-Ok "DB found: $dbPath" + +# Write a temp Python helper that queries DB and outputs structured data +$dbHelperPy = Join-Path $env:TEMP "arc_db_check.py" +@' +import sqlite3, sys, os + +sys.stdout.reconfigure(encoding='utf-8') +db_path = sys.argv[1] +mode = sys.argv[2] if len(sys.argv) > 2 else "count" +pool_dir = sys.argv[3] if len(sys.argv) > 3 else "" + +conn = sqlite3.connect(db_path) +conn.row_factory = sqlite3.Row + +if mode == "count": + cnt = conn.execute(""" + SELECT COUNT(*) FROM devices d + JOIN bank_profiles bp ON bp.device_id = d.id + JOIN materials m ON m.device_id = d.id AND m.app_code = bp.code + WHERE bp.status = 'active' AND m.status = 'active' + AND m.material_id != '' AND bp.bank_profile_id != '' + """).fetchone()[0] + conn.close() + print(cnt) + sys.exit(0 if cnt > 0 else 1) + +elif mode == "clean_events": + try: + cnt = conn.execute("SELECT COUNT(*) FROM events").fetchone()[0] + conn.execute("DELETE FROM events") + conn.commit() + print(cnt) + except Exception: + print(0) + conn.close() + sys.exit(0) + +elif mode == "summary": + devices = conn.execute("SELECT id, name, api_id, adb_serial FROM devices").fetchall() + profiles = conn.execute(""" + SELECT bp.code, bp.full_name, bp.phone, bp.status, d.name AS device + FROM bank_profiles bp JOIN devices d ON d.id = bp.device_id + WHERE bp.status = 'active' AND bp.bank_profile_id != '' + ORDER BY d.name, bp.code + """).fetchall() + materials = conn.execute(""" + SELECT m.material_id, m.card_number, m.last_numbers, m.currency, m.status, + m.app_code, d.name AS device + FROM materials m JOIN devices d ON d.id = m.device_id + WHERE m.status = 'active' AND m.material_id != '' + ORDER BY d.name, m.app_code + """).fetchall() + conn.close() + + print("DEVICES|" + str(len(devices))) + for d in devices: + serial = d['adb_serial'] or '-' + api = d['api_id'] or '-' + print(f" DEV|{d['name']}|{serial}|{api}") + + print("PROFILES|" + str(len(profiles))) + for p in profiles: + name = p['full_name'] or '-' + phone = p['phone'] or '-' + print(f" PRF|{p['device']}|{p['code']}|{name}|{phone}") + + print("MATERIALS|" + str(len(materials))) + for m in materials: + card = m['card_number'] or ('****' + m['last_numbers'] if m['last_numbers'] else '-') + print(f" MAT|{m['device']}|{m['app_code']}|{card}|{m['currency']}|{m['material_id'][:12]}") + + pool_n = 0 + if pool_dir and os.path.isdir(pool_dir): + pool_n = len([f for f in os.listdir(pool_dir) if f.endswith('.json')]) + print(f"POOL|{pool_n}") +'@ | Set-Content -Path $dbHelperPy -Encoding UTF8 + +# Check active materials count +$matCount = & $pythonExe $dbHelperPy $dbPath "count" 2>&1 +if ($LASTEXITCODE -ne 0) { + Exit-WithError ("DB exists but no active materials found.`n`n" + + "To populate:`n" + + " 1. Set apiBase to production server`n" + + " 2. Run ARC.exe, log in, add devices and profiles`n" + + " 3. Re-run this script") +} + +Write-Ok "Active materials in DB: $matCount" + +# Clean events table +$eventsCleared = & $pythonExe $dbHelperPy $dbPath "clean_events" 2>&1 +if ($eventsCleared -and [int]$eventsCleared -gt 0) { + Write-Ok "Cleared $eventsCleared events from DB" +} else { + Write-Ok "Events table already empty" +} + +# ─── 5. Generate mock data ──────────────────────────────────────────────── + +Write-Step "4/6" "Generate test data" + +# Clean old generated data +$oldProfile = Join-Path $scriptDir "default_profile.json" +if (Test-Path $oldProfile) { + Remove-Item $oldProfile -Force + Write-Host " Cleaned: default_profile.json" -ForegroundColor Gray +} +$scenariosDir = Join-Path $scriptDir "scenarios" +if (Test-Path $scenariosDir) { + $oldScenarios = Get-ChildItem "$scenariosDir\*.json" -ErrorAction SilentlyContinue + if ($oldScenarios) { + $oldScenarios | Remove-Item -Force + Write-Host " Cleaned: scenarios/ ($($oldScenarios.Count) files)" -ForegroundColor Gray + } +} +# -Clean: also wipe the transaction pool +$poolDir = Join-Path $scriptDir "scenarios\pool" +if ($Clean -and (Test-Path $poolDir)) { + $oldPool = Get-ChildItem "$poolDir\*.json" -ErrorAction SilentlyContinue + if ($oldPool) { + $oldPool | Remove-Item -Force + Write-Host " Cleaned: scenarios/pool/ ($($oldPool.Count) files)" -ForegroundColor Gray + } +} + +$genScript = Join-Path $scriptDir "gen_mock_data.py" +if (-not (Test-Path $genScript)) { + Exit-WithError "gen_mock_data.py not found in $scriptDir" +} + +$genOutput = & $pythonExe $genScript --config $configPath 2>&1 +$genOutput | ForEach-Object { Write-Host " $_" } +if ($LASTEXITCODE -ne 0) { + Exit-WithError "Data generation failed" +} + +$profileJson = Join-Path $scriptDir "default_profile.json" +if (Test-Path $profileJson) { + Write-Ok "default_profile.json created" +} else { + Exit-WithError "default_profile.json was not created" +} + +$poolDir = Join-Path $scriptDir "scenarios\pool" +$poolCount = 0 +if (Test-Path $poolDir) { + $poolFiles = Get-ChildItem "$poolDir\*.json" -ErrorAction SilentlyContinue + if ($poolFiles) { $poolCount = $poolFiles.Count } +} +if ($poolCount -gt 0) { + Write-Ok "Transaction pool: $poolCount files" +} else { + Write-Warn "Transaction pool is empty - bootstrap will only create FETCH_PROFILE tasks" + Write-Host " To generate pool run: $pythonExe gen_fetch_transactions.py" -ForegroundColor Gray +} + +# ─── 6. Switch config to mock ───────────────────────────────────────────── + +Write-Step "5/6" "Switch config.ini to mock" + +$mockBase = "http://127.0.0.1:$Port" +$configText = Get-Content $configPath -Raw -Encoding UTF8 + +# Find the active (non-commented) apiBase line +$configLines = Get-Content $configPath -Encoding UTF8 +$activeIdx = -1 +$currentBase = "" +for ($i = 0; $i -lt $configLines.Count; $i++) { + if ($configLines[$i] -match "^\s*apiBase\s*=\s*(.+)") { + $activeIdx = $i + $currentBase = $Matches[1].Trim() + break + } +} + +if ($activeIdx -eq -1) { + Exit-WithError "apiBase not found in config.ini" +} + +if ($currentBase -eq $mockBase) { + Write-Ok "apiBase already points to mock: $mockBase" +} else { + $backupPath = "$configPath.prod_backup" + if (-not (Test-Path $backupPath)) { + Copy-Item $configPath $backupPath + Write-Host " Backup saved: $backupPath" -ForegroundColor Gray + } + # Comment out old line, insert mock line after it + $configLines[$activeIdx] = "#$($configLines[$activeIdx])" + $before = $configLines[0..$activeIdx] + $after = if ($activeIdx + 1 -lt $configLines.Count) { $configLines[($activeIdx + 1)..($configLines.Count - 1)] } else { @() } + $configLines = $before + @("apiBase=$mockBase") + $after + $newContent = ($configLines -join "`r`n") + "`r`n" + [System.IO.File]::WriteAllText($configPath, $newContent, [System.Text.UTF8Encoding]::new($false)) + Write-Ok "apiBase switched: $currentBase -> $mockBase" +} + +if ($GenerateOnly) { + Write-Host "`n=== Done (generate only) ===" -ForegroundColor Green + Write-Host "To start server: $pythonExe mock_server.py --port $Port" -ForegroundColor Gray + exit 0 +} + +# ─── 7. Start mock server ───────────────────────────────────────────────── + +Write-Step "6/6" "Start mock server" + +# Kill old process on this port if any +$portCheck = netstat -ano 2>$null | Select-String ":${Port}\s.*LISTENING" +if ($portCheck) { + $oldPid = ($portCheck -split '\s+')[-1] + if ($oldPid -and $oldPid -ne "0") { + Write-Warn "Port $Port occupied by PID $oldPid - killing..." + taskkill /PID $oldPid /F 2>$null | Out-Null + Start-Sleep -Seconds 1 + Write-Ok "Old process killed" + } +} + +$mockScript = Join-Path $scriptDir "mock_server.py" + +# ─── Summary banner with DB data ────────────────────────────────────────── + +$poolDirArg = Join-Path $scriptDir "scenarios\pool" +$bannerLines = & $pythonExe $dbHelperPy $dbPath "summary" $poolDirArg 2>&1 + +Write-Host "" +Write-Host "================================================================" -ForegroundColor Cyan +Write-Host " ARC Mock Server - Summary " -ForegroundColor Cyan +Write-Host "================================================================" -ForegroundColor Cyan + +foreach ($line in $bannerLines) { + $l = $line.ToString().Trim() + + if ($l -match "^DEVICES\|(\d+)") { + Write-Host "" + Write-Host " Devices ($($Matches[1])):" -ForegroundColor White + Write-Host " ----------------------------------------------------------" -ForegroundColor DarkGray + Write-Host (" {0,-30} {1,-20} {2}" -f "Name", "ADB Serial", "API ID") -ForegroundColor DarkGray + } + elseif ($l -match "^PROFILES\|(\d+)") { + Write-Host "" + Write-Host " Bank profiles ($($Matches[1])):" -ForegroundColor White + Write-Host " ----------------------------------------------------------" -ForegroundColor DarkGray + Write-Host (" {0,-25} {1,-20} {2,-20} {3}" -f "Device", "Bank", "Name", "Phone") -ForegroundColor DarkGray + } + elseif ($l -match "^MATERIALS\|(\d+)") { + Write-Host "" + Write-Host " Materials ($($Matches[1])):" -ForegroundColor White + Write-Host " ----------------------------------------------------------" -ForegroundColor DarkGray + Write-Host (" {0,-25} {1,-20} {2,-20} {3,-5} {4}" -f "Device", "Bank", "Card", "Cur.", "Material ID") -ForegroundColor DarkGray + } + elseif ($l -match "^POOL\|(\d+)") { + Write-Host "" + if ([int]$Matches[1] -gt 0) { + Write-Host " Transaction pool: $($Matches[1]) tasks" -ForegroundColor Green + } else { + Write-Host " Transaction pool: empty (only FETCH_PROFILE at bootstrap)" -ForegroundColor Yellow + } + } + elseif ($l -match "^\s*DEV\|([^|]+)\|([^|]+)\|(.+)") { + Write-Host (" {0,-30} {1,-20} {2}" -f $Matches[1], $Matches[2], $Matches[3]) -ForegroundColor Gray + } + elseif ($l -match "^\s*PRF\|([^|]+)\|([^|]+)\|([^|]+)\|(.+)") { + Write-Host (" {0,-25} {1,-20} {2,-20} {3}" -f $Matches[1], $Matches[2], $Matches[3], $Matches[4]) -ForegroundColor Gray + } + elseif ($l -match "^\s*MAT\|([^|]+)\|([^|]+)\|([^|]+)\|([^|]+)\|(.+)") { + Write-Host (" {0,-25} {1,-20} {2,-20} {3,-5} {4}" -f $Matches[1], $Matches[2], $Matches[3], $Matches[4], $Matches[5]) -ForegroundColor Gray + } +} + +Write-Host "" +Write-Host " ==========================================================" -ForegroundColor Cyan +Write-Host "" +Write-Host " Mock server: " -NoNewline -ForegroundColor White +Write-Host "http://127.0.0.1:$Port" -ForegroundColor Green +Write-Host " HTML report: " -NoNewline -ForegroundColor White +Write-Host "http://127.0.0.1:${Port}/_admin/report/html" -ForegroundColor Green +Write-Host " State: " -NoNewline -ForegroundColor White +Write-Host "http://127.0.0.1:${Port}/_admin/state" -ForegroundColor Green +Write-Host "" +Write-Host " FT/material: $FtPerMaterial" -ForegroundColor Gray +Write-Host " Config: $configPath" -ForegroundColor Gray +Write-Host "" +Write-Host " Ctrl+C to stop the server" -ForegroundColor Yellow +Write-Host " After testing restore: apiBase=http://93.183.75.134:8005" -ForegroundColor Yellow +Write-Host "" + +# Cleanup temp helper +Remove-Item $dbHelperPy -ErrorAction SilentlyContinue + +# Open report in browser +Start-Process "http://127.0.0.1:${Port}/_admin/report/html" + +# Launch server (blocks until Ctrl+C) +$serverArgs = @("--host", "127.0.0.1", "--port", $Port, "--ft-per-material", $FtPerMaterial) +if ($Banks) { + $serverArgs += "--banks" + $serverArgs += ($Banks -split '[,\s]+') +} +if ($Types) { + $serverArgs += "--types" + $serverArgs += ($Types -split '[,\s]+') +} +if ($Debug) { + $serverArgs += "--debug" +} +& $pythonExe $mockScript @serverArgs diff --git a/tools/adb/macos/adb b/tools/adb/macos/adb new file mode 100755 index 0000000..3bfe1a1 Binary files /dev/null and b/tools/adb/macos/adb differ diff --git a/tools/adb/macos/libc++.dylib b/tools/adb/macos/libc++.dylib new file mode 100755 index 0000000..95f6b9d Binary files /dev/null and b/tools/adb/macos/libc++.dylib differ diff --git a/tools/adb/windows/AdbWinApi.dll b/tools/adb/windows/AdbWinApi.dll new file mode 100644 index 0000000..c32abf5 Binary files /dev/null and b/tools/adb/windows/AdbWinApi.dll differ diff --git a/tools/adb/windows/AdbWinUsbApi.dll b/tools/adb/windows/AdbWinUsbApi.dll new file mode 100644 index 0000000..d10d1e1 Binary files /dev/null and b/tools/adb/windows/AdbWinUsbApi.dll differ diff --git a/tools/adb/windows/adb.exe b/tools/adb/windows/adb.exe new file mode 100644 index 0000000..10784b3 Binary files /dev/null and b/tools/adb/windows/adb.exe differ diff --git a/tools/adb/windows/libwinpthread-1.dll b/tools/adb/windows/libwinpthread-1.dll new file mode 100644 index 0000000..f6599c7 Binary files /dev/null and b/tools/adb/windows/libwinpthread-1.dll differ diff --git a/tutorials/image_1.jpg b/tutorials/image_1.jpg new file mode 100644 index 0000000..e06b248 Binary files /dev/null and b/tutorials/image_1.jpg differ diff --git a/tutorials/image_2.jpg b/tutorials/image_2.jpg new file mode 100644 index 0000000..3973818 Binary files /dev/null and b/tutorials/image_2.jpg differ diff --git a/tutorials/ui_info.md b/tutorials/ui_info.md new file mode 100644 index 0000000..3d4125e --- /dev/null +++ b/tutorials/ui_info.md @@ -0,0 +1,38 @@ +# Описание интерфейса + +--- + +## Главный экран — Устройство + + + + + + +
+

1 — Название устройства и ID

+

2 — Кнопка настроек устройств (там вы увидите какие банки поддерживаются приложением)

+

3 — Кнопка удаления устройства (удалит все данные устройства и если подключите снова, создаст новое)

+

4 — Статус банковского профиля, красный - выключен, зеленый - включен

+

5 — Настройки банковского профиля. Если нет то самый первый раз будет мастер настроеек профиля.

+

6 — Имя профиля и номер телефона банковского профиля (если настроен)

+

7 — Кнопка включения/выключения банковского профиля

+
+ +--- + +## Настройки банка + + + + + + +
+

1 — Включить / выключить профиль банка

+

2 — Редактировать пин-код

+

3 — Проверить пин-код на устройстве

+

4 — Включить / выключить счёт

+

5 — Обновить профиль (ФИО, телефон, баланс)

+

6 — Проверить аккаунты (карты и счета)

+
diff --git a/views/DeviceWidget.cpp b/views/DeviceWidget.cpp deleted file mode 100644 index 5e73e5e..0000000 --- a/views/DeviceWidget.cpp +++ /dev/null @@ -1,52 +0,0 @@ -#include "DeviceWidget.h" -#include -#include -#include -#include -#include - -DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(parent) { - auto *layout = new QVBoxLayout(this); - - this->setStyleSheet("background-color: lightgray;padding: 10px;"); - - QImage img; - img.loadFromData(device.image, "JPG"); - - auto *imageLabel = new QLabel(this); - // qDebug() << "imageLabel" << img.width() ; - imageLabel->setPixmap(QPixmap::fromImage(img).scaled(200, 350, Qt::KeepAspectRatio, Qt::SmoothTransformation)); - imageLabel->setAlignment(Qt::AlignCenter); - layout->addWidget(imageLabel); - - const QString result = QString("%1, Android %2, battery %3% [%4x%5]") - .arg(device.name).arg(device.android).arg(device.battery). - arg(device.screenWidth).arg(device.screenHeight); - - auto *statusLabel = new QLabel(result, this); - statusLabel->setAlignment(Qt::AlignCenter); - statusLabel->setMaximumWidth(200); - statusLabel->setWordWrap(true); - - const QString styleSheet = QString("color: %1; font-weight: bold;") - .arg(device.status == "online" ? "green" : "red"); - statusLabel->setStyleSheet(styleSheet); - - // layout->setContentsMargins(10, 10, 10, 10); - layout->setSpacing(0); - layout->addWidget(statusLabel); -} - -void DeviceWidget::mouseDoubleClickEvent(QMouseEvent *event) { - if (event->button() == Qt::LeftButton) { - if (scrcpyWindow == nullptr) { - scrcpyWindow = new ScrcpyWindow(); - scrcpyWindow->show(); - - // Подключаем сигнал закрытия окна - connect(scrcpyWindow, &ScrcpyWindow::destroyed, this, [this]() { - scrcpyWindow = nullptr; // Освобождаем память, когда окно закрыто - }); - } - } -} diff --git a/views/DeviceWidget.h b/views/DeviceWidget.h deleted file mode 100644 index e74ee17..0000000 --- a/views/DeviceWidget.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once -#include -#include "db/DeviceInfo.h" -#include "scrcpy/ScrcpyWindow.h" - -class DeviceWidget final : public QWidget { - Q_OBJECT - -public: - explicit DeviceWidget(const DeviceInfo& device, QWidget* parent = nullptr); - -private: - ScrcpyWindow *scrcpyWindow = nullptr; - -protected: - void mouseDoubleClickEvent(QMouseEvent *event) override; -}; diff --git a/views/MainWindow.cpp b/views/MainWindow.cpp index 2009158..5dde497 100644 --- a/views/MainWindow.cpp +++ b/views/MainWindow.cpp @@ -1,19 +1,38 @@ #include "MainWindow.h" -#include "AccountWindow.h" +#include "bank/AccountWindow.h" #include +#include +#include #include #include #include -#include -#include +#include +#include +#include + +#include "TutorialWindow.h" +#include "bank/BankListWindow.h" +#include "event/EventWindow.h" +#include "log/FileLogWindow.h" +#include "dao/GeneralLogDAO.h" +#include "net/NetworkService.h" #include "db/DeviceInfo.h" -#include "DeviceWidget.h" +#include "device/DeviceWidget.h" +#include "dao/BankProfileDAO.h" #include "dao/DeviceDAO.h" -#include "widget/FlowLayout.h" +#include "dao/MaterialDAO.h" +#include "dao/SettingsDAO.h" +#include +#include +#include +#include "widget/common/FlowLayout.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { + const QSettings settings("config.ini", QSettings::IniFormat); + const QString version = settings.value("common/version").toString(); + setWindowTitle(version.isEmpty() ? "ARC" : QString("ARC v%1").arg(version)); // Инициализируем таймер обновления timer = new QTimer(this); @@ -23,12 +42,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { setCentralWidget(stackedWidget); createDevicePage(); - stackedWidget->addWidget(devicePage); - stackedWidget->setCurrentWidget(devicePage); + stackedWidget->addWidget(m_devicesWindow); + stackedWidget->setCurrentWidget(m_devicesWindow); // FIXME переделать на ленивое создание - accountPage = new AccountWindow(this); - stackedWidget->addWidget(accountPage); + // m_accountsWindow = new AccountWindow(this); + // stackedWidget->addWidget(m_accountsWindow); // stackedWidget->setCurrentWidget(accountPage); resize(800, 600); @@ -36,62 +55,163 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { // Работа с меню QMenu *menu = menuBar()->addMenu("Меню"); auto *devicesPageAction = new QAction("Активные устройства", this); - auto *accountPageAction = new QAction("Аккаунты", this); + // auto *accountPageAction = new QAction("Аккаунты", this); + auto *tutorialPageAction = new QAction("Инструкция", this); + auto *eventAction = new QAction("События", this); + auto *fileLogAction = new QAction("Логи", this); menu->addAction(devicesPageAction); - menu->addAction(accountPageAction); + // menu->addAction(accountPageAction); + menu->addAction(eventAction); + menu->addAction(tutorialPageAction); + menu->addAction(fileLogAction); connect(devicesPageAction, &QAction::triggered, this, [=]() { - if (!devicePage) { + if (!m_devicesWindow) { createDevicePage(); } - stackedWidget->setCurrentWidget(devicePage); + stackedWidget->setCurrentWidget(m_devicesWindow); }); - connect(accountPageAction, &QAction::triggered, this, [=]() { - stackedWidget->setCurrentWidget(accountPage); - deleteDevicePage(); // освобождаем ресурсы + // connect(accountPageAction, &QAction::triggered, this, [=]() { + // stackedWidget->setCurrentWidget(m_accountsWindow); + // deleteDevicePage(); // освобождаем ресурсы + // }); + + connect(tutorialPageAction, &QAction::triggered, this, [=]() { + if (!m_tutorialWindow) { + m_tutorialWindow = new TutorialWindow(this); + } + m_tutorialWindow->show(); + m_tutorialWindow->raise(); + m_tutorialWindow->activateWindow(); }); + + connect(eventAction, &QAction::triggered, this, [=]() { + if (!m_eventWindow) { + m_eventWindow = new EventWindow(this); + } + m_eventWindow->show(); + m_eventWindow->raise(); + m_eventWindow->activateWindow(); + }); + + connect(fileLogAction, &QAction::triggered, this, [this]() { + QMessageBox msgBox(this); + msgBox.setWindowTitle("Логи"); + msgBox.setText("Отправить логи на сервер?"); + auto *yesBtn = msgBox.addButton("Отправить", QMessageBox::YesRole); + msgBox.addButton("Отмена", QMessageBox::NoRole); + msgBox.exec(); + if (msgBox.clickedButton() == yesBtn) { + if (!m_fileLogWindow) { + m_fileLogWindow = new FileLogWindow(this); + } + m_fileLogWindow->sendLogsToBackend(); + } + }); + + if (SettingsDAO::get("tutorial_shown").isEmpty()) { + QTimer::singleShot(0, this, [this]() { + m_tutorialWindow = new TutorialWindow(this); + m_tutorialWindow->show(); + m_tutorialWindow->raise(); + m_tutorialWindow->activateWindow(); + SettingsDAO::set("tutorial_shown", "1"); + }); + } + + // Sync-health индикатор в статус-баре (правило #9). По умолчанию скрыт — + // показывается только когда NetworkService сообщил unhealthy. + m_syncHealthLabel = new QLabel(this); + m_syncHealthLabel->setVisible(false); + m_syncHealthLabel->setStyleSheet("color: #d9534f; padding: 0 8px;"); + statusBar()->addPermanentWidget(m_syncHealthLabel); +} + +void MainWindow::setSyncHealth(bool healthy, int consecutiveFailures) { + if (!m_syncHealthLabel) return; + if (healthy) { + m_syncHealthLabel->setVisible(false); + m_syncHealthLabel->setToolTip(QString()); + return; + } + m_syncHealthLabel->setText(QStringLiteral("⚠ Нет связи с сервером")); + m_syncHealthLabel->setToolTip( + QStringLiteral("Подряд %1 неудачных синхронизаций. Проверьте подключение.") + .arg(consecutiveFailures)); + m_syncHealthLabel->setVisible(true); } void MainWindow::createDevicePage() { - if (devicePage) { + if (m_devicesWindow) { return; } - auto *scrollArea = new QScrollArea(this); - auto *contentWidget = new QWidget; - flowLayout = new FlowLayout(contentWidget); - flowLayout->setContentsMargins(0, 0, 0, 0); - flowLayout->setSpacing(0); + auto *container = new QWidget(this); + auto *containerLayout = new QVBoxLayout(container); + containerLayout->setContentsMargins(0, 0, 0, 0); - QVector devices = DeviceDAO::getAllDevices(true); + m_noDevicesLabel = new QLabel(container); + m_noDevicesPixmap = QPixmap(QCoreApplication::applicationDirPath() + "/no_devices.png"); + m_noDevicesLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + m_noDevicesLabel->setAlignment(Qt::AlignCenter); + containerLayout->addWidget(m_noDevicesLabel); + + m_scrollArea = new QScrollArea(container); + auto *contentWidget = new QWidget; + flowLayout = new FlowLayout(contentWidget, 0, 0, 0); + + QVector devices = DeviceDAO::getAllDevices(false); for (const auto &device: devices) { - auto *deviceWidget = new DeviceWidget(device); - deviceWidget->setFixedSize(220, 400); - flowLayout->addWidget(deviceWidget); + flowLayout->addWidget(new DeviceWidget(device)); } contentWidget->setLayout(flowLayout); - scrollArea->setWidget(contentWidget); - scrollArea->setWidgetResizable(true); + static_cast(m_scrollArea)->setWidget(contentWidget); + static_cast(m_scrollArea)->setWidgetResizable(true); + containerLayout->addWidget(m_scrollArea); - devicePage = scrollArea; - stackedWidget->addWidget(devicePage); + m_noDevicesLabel->setVisible(devices.isEmpty()); + m_scrollArea->setVisible(!devices.isEmpty()); + + // Панель логов внизу + m_logList = new QListWidget(container); + m_logList->setMaximumHeight(120); + m_logList->setStyleSheet( + "QListWidget { background-color: #1e1e1e; color: #cccccc; font-family: monospace; font-size: 11px; border-top: 1px solid #444; }" + "QListWidget::item { padding: 1px 4px; }" + ); + m_logList->setSelectionMode(QAbstractItemView::NoSelection); + m_logList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + containerLayout->addWidget(m_logList); + updateLogPanel(); + + m_devicesWindow = container; + stackedWidget->addWidget(m_devicesWindow); timer->start(1000); + + if (devices.isEmpty()) { + QTimer::singleShot(0, this, [this]() { + if (m_noDevicesLabel && !m_noDevicesPixmap.isNull()) { + m_noDevicesLabel->setPixmap(m_noDevicesPixmap.scaled( + m_noDevicesLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); + } + }); + } } void MainWindow::deleteDevicePage() { - if (!devicePage) { + if (!m_devicesWindow) { return; } timer->stop(); - stackedWidget->removeWidget(devicePage); - devicePage->deleteLater(); - devicePage = nullptr; + stackedWidget->removeWidget(m_devicesWindow); + m_devicesWindow->deleteLater(); + m_devicesWindow = nullptr; flowLayout = nullptr; } @@ -100,32 +220,188 @@ void MainWindow::deleteDevicePage() { * Для того, чтобы не нарушать константность, этот метод должен быть неконстантным (без const в сигнатуре). */ void MainWindow::loadDevices() { - const auto devices = DeviceDAO::getAllDevices(true); + const auto devices = DeviceDAO::getAllDevices(false); // Очистить старые виджеты QLayoutItem *item; while ((item = flowLayout->takeAt(0)) != nullptr) { if (item->widget()) { - delete item->widget(); // Удаляем виджет + item->widget()->deleteLater(); } - delete item; // Удаляем сам элемент из layout + delete item; } // Добавляем новые виджеты для каждого устройства for (const auto &device: devices) { flowLayout->addWidget(new DeviceWidget(device)); } + + m_noDevicesLabel->setVisible(devices.isEmpty()); + m_scrollArea->setVisible(!devices.isEmpty()); + + updateLogPanel(); } -void MainWindow::closeEvent(QCloseEvent *event) { - closeAllWindows(); // Закрываем все окна - // event->accept(); // Разрешаем закрытие окна +void MainWindow::updateLogPanel() { + if (!m_logList) return; + + const QList logs = GeneralLogDAO::getLogs(0, 5, {"WARNING", "CRITICAL", "FATAL"}, 10); + m_logList->clear(); + for (const auto &entry : logs) { + // Преобразуем в человекопонятные сообщения + QString msg = entry.message; + + // Извлекаем контекст: банк, устройство + QString bank; + QString device; + { + // bank=ozon + QRegularExpression bankRe(R"(bank=(\w+))"); + if (auto m = bankRe.match(msg); m.hasMatch()) { + const QString b = m.captured(1); + if (b == "ozon") bank = "Ozon"; + else if (b == "black") bank = "Black"; + else if (b == "dushanbe_city_bank") bank = "Dushanbe"; + else bank = b; + } + // Устройство: device=ID или "SERIAL" + QRegularExpression devEqRe(R"(device=(\S+))"); + if (auto m = devEqRe.match(msg); m.hasMatch()) { + const QString devId = m.captured(1); + const DeviceInfo dev = DeviceDAO::getDeviceById(devId); + if (!dev.name.isEmpty()) device = dev.name; + else { + const DeviceInfo dev2 = DeviceDAO::findByAdbSerial(devId); + device = dev2.name.isEmpty() ? devId.left(10) + "..." : dev2.name; + } + } + if (device.isEmpty()) { + QRegularExpression devRe(R"x("?([A-Z0-9]{12,})"?)x"); + if (auto m = devRe.match(msg); m.hasMatch()) { + const DeviceInfo dev = DeviceDAO::findByAdbSerial(m.captured(1)); + device = dev.name.isEmpty() ? m.captured(1).left(8) + "..." : dev.name; + } + } + // UUID в URL — ищем bank_profile или material по ID + if (bank.isEmpty() || device.isEmpty()) { + QRegularExpression uuidRe(R"(([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}))"); + auto it = uuidRe.globalMatch(msg); + while (it.hasNext() && (bank.isEmpty() || device.isEmpty())) { + const QString uuid = it.next().captured(1); + if (bank.isEmpty()) { + const QList bpList = BankProfileDAO::findAllByBankProfileId(uuid); + const BankProfileInfo bp = bpList.isEmpty() ? BankProfileInfo() : bpList.first(); + if (bp.id >= 0) { + if (bank.isEmpty()) { + const QString b = bp.code; + if (b == "ozon") bank = "Ozon"; + else if (b == "black") bank = "Black"; + else if (b == "dushanbe_city_bank") bank = "Dushanbe"; + else bank = b; + } + if (device.isEmpty() && !bp.deviceId.isEmpty()) { + const DeviceInfo dev = DeviceDAO::getDeviceById(bp.deviceId); + device = dev.name.isEmpty() ? bp.deviceId.left(8) + "..." : dev.name; + } + } + } + if (bank.isEmpty()) { + const MaterialInfo mat = MaterialDAO::findByMaterialId(uuid); + if (mat.id >= 0) { + const QString b = mat.appCode; + if (b == "ozon") bank = "Ozon"; + else if (b == "black") bank = "Black"; + else if (b == "dushanbe_city_bank") bank = "Dushanbe"; + else bank = b; + if (device.isEmpty() && !mat.deviceId.isEmpty()) { + const DeviceInfo dev = DeviceDAO::getDeviceById(mat.deviceId); + device = dev.name.isEmpty() ? mat.deviceId.left(8) + "..." : dev.name; + } + } + } + } + } + } + // Формируем префикс [девайс | банк] + QString ctxPrefix; + if (!device.isEmpty() || !bank.isEmpty()) { + QStringList parts; + if (!device.isEmpty()) parts << device; + if (!bank.isEmpty()) parts << bank; + ctxPrefix = "[" + parts.join(" | ") + "] "; + } + + if (msg.contains("Failed to send result to server")) + msg = ctxPrefix + "Не удалось отправить результат на сервер"; + else if (msg.contains("Device is offline")) + msg = ctxPrefix + "Устройство не подключено"; + else if (msg.contains("bank_transaction_id is empty")) + msg = ctxPrefix + "Не удалось распознать ID транзакции"; + else if (msg.contains("Transaction not found")) + msg = ctxPrefix + "Транзакция не найдена"; + else if (msg.contains("Cannot reach home screen") || msg.contains("Cant open the home screen")) + msg = ctxPrefix + "Не удалось открыть приложение"; + else if (msg.contains("Card input screen not loaded")) + msg = ctxPrefix + "Экран ввода карты не загрузился"; + else if (msg.contains("Payment result not received")) + msg = ctxPrefix + "Результат платежа не получен (таймаут)"; + else if (msg.contains("Transfer failed")) + msg = ctxPrefix + "Перевод не удался"; + else if (msg.contains("PIN code") || msg.contains("Пин-код")) + msg = ctxPrefix + "Ошибка проверки пин-кода"; + else if (msg.contains("postTaskResult failed")) + msg = ctxPrefix + "Ошибка отправки результата задачи"; + else if (msg.contains("Network error") || msg.contains("network error")) { + QRegularExpression urlRe(R"((https?://[^\s\"]+))"); + QRegularExpression codeRe(R"(server replied:\s*(.+?)[\"\n])"); + QString url, code; + if (auto m = urlRe.match(msg); m.hasMatch()) url = m.captured(1); + if (auto m = codeRe.match(msg); m.hasMatch()) code = m.captured(1).trimmed(); + + // Преобразуем путь API в читаемое название + QString endpoint; + if (url.contains("/bank_profile")) endpoint = "обновление профиля банка"; + else if (url.contains("/material")) endpoint = "обновление карты/счёта"; + else if (url.contains("/task_result")) endpoint = "отправка результата задачи"; + else if (url.contains("/get_tasks")) endpoint = "получение задач"; + else if (url.contains("/device")) endpoint = "обновление устройства"; + else if (url.contains("/auth")) endpoint = "авторизация"; + else if (url.contains("/current_profile")) endpoint = "получение профиля"; + else if (url.contains("/monitoring")) endpoint = "отправка логов"; + else { + const int pathIdx = url.indexOf('/', url.indexOf("//") + 2); + endpoint = pathIdx > 0 ? url.mid(pathIdx) : url; + } + + msg = "Ошибка: " + endpoint; + if (!code.isEmpty()) msg += " [" + code + "]"; + msg = ctxPrefix + msg; + } + else if (msg.contains("App restarted")) + msg = ctxPrefix + "Приложение перезапущено, задача отменена"; + else if (msg.contains("Task expired")) + msg = ctxPrefix + "Задача просрочена"; + else if (msg.contains("Bank profile disabled") || msg.contains("Bank profile is disabled")) + msg = ctxPrefix + "Банковский профиль выключен"; + else if (msg.contains("Скриншот пустой")) + msg = ctxPrefix + "Скриншот устройства пустой"; + + const QString time = entry.timestamp.toLocalTime().toString("HH:mm:ss"); + const QString line = QStringLiteral("[%1] %2").arg(time, msg); + + auto *item = new QListWidgetItem(line, m_logList); + if (entry.level == "CRITICAL" || entry.level == "FATAL") { + item->setForeground(QColor("#ff6b6b")); + } else if (entry.level == "WARNING") { + item->setForeground(QColor("#ffa94d")); + } else { + item->setForeground(QColor("#cccccc")); + } + } } -void MainWindow::closeAllWindows() { - // QList topLevelWidgets = QApplication::topLevelWidgets(); // Получаем все верхние окна - - // for (QWidget *widget : topLevelWidgets) { - // widget->close(); // Закрываем каждое окно - // } - QApplication::quit(); - // QCoreApplication::exit(0); +void MainWindow::resizeEvent(QResizeEvent *event) { + QMainWindow::resizeEvent(event); + if (m_noDevicesLabel && m_noDevicesLabel->isVisible() && !m_noDevicesPixmap.isNull()) { + m_noDevicesLabel->setPixmap(m_noDevicesPixmap.scaled( + m_noDevicesLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); + } } diff --git a/views/MainWindow.h b/views/MainWindow.h index ca7cb5d..1e7e637 100644 --- a/views/MainWindow.h +++ b/views/MainWindow.h @@ -1,31 +1,52 @@ #pragma once +#include #include #include #include #include +#include +#include + +#include "TutorialWindow.h" +#include "bank/AccountWindow.h" #include "db/DeviceInfo.h" -#include "widget/FlowLayout.h" +#include "log/FileLogWindow.h" +#include "widget/common/FlowLayout.h" class MainWindow final : public QMainWindow { Q_OBJECT + public: explicit MainWindow(QWidget *parent = nullptr); + void loadDevices(); -protected: - void closeEvent(QCloseEvent *event) override; +public slots: + // Подключается к NetworkService::syncHealthChanged. healthy=false показывает + // индикатор «нет связи» в status bar, healthy=true прячет (правило #9). + void setSyncHealth(bool healthy, int consecutiveFailures); private: - QWidget* central; - FlowLayout* flowLayout; - QTimer* timer; - QStackedWidget* stackedWidget; + QWidget *central; + FlowLayout *flowLayout; + QTimer *timer; + QStackedWidget *stackedWidget; - QWidget* devicePage = nullptr; - QWidget* accountPage = nullptr; + QLabel *m_noDevicesLabel = nullptr; + QPixmap m_noDevicesPixmap; + QWidget *m_scrollArea = nullptr; + QWidget *m_devicesWindow = nullptr; + QWidget *m_accountsWindow = nullptr; + QWidget *m_tutorialWindow = nullptr; + QWidget *m_bankListWindow = nullptr; + FileLogWindow *m_fileLogWindow = nullptr; + QWidget *m_eventWindow = nullptr; + + QListWidget *m_logList = nullptr; + QLabel *m_syncHealthLabel = nullptr; void createDevicePage(); void deleteDevicePage(); - - void closeAllWindows(); + void updateLogPanel(); + void resizeEvent(QResizeEvent *event) override; }; diff --git a/views/TransactionWindow.cpp b/views/TransactionWindow.cpp deleted file mode 100644 index a5e3766..0000000 --- a/views/TransactionWindow.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "TransactionWindow.h" -#include -#include - -#include "dao/AccountDAO.h" - -TransactionWindow::TransactionWindow(const int accountId, QWidget *parent): QDialog(parent) { - setWindowTitle("Транзакции"); - setMinimumSize(300, 200); // минимальный размер - setMaximumSize(800, 400); // максимальный размер - - auto *layout = new QVBoxLayout(this); - layout->setAlignment(Qt::AlignCenter); - - AccountInfo account = AccountDAO::getAccountById(accountId); - - QLabel *label = new QLabel(QString("*%1 | %2").arg(account.cardName, account.description), this); - layout->addWidget(label); - - setModal(true); -} diff --git a/views/TutorialWindow.cpp b/views/TutorialWindow.cpp new file mode 100644 index 0000000..738a1af --- /dev/null +++ b/views/TutorialWindow.cpp @@ -0,0 +1,64 @@ +#include "TutorialWindow.h" +#include +#include +#include +#include +#include +#include +#include "widget/common/CollapsibleSection.h" + +TutorialWindow::TutorialWindow(QWidget *parent) + : QWidget(parent, + Qt::Window + | Qt::WindowTitleHint + | Qt::WindowSystemMenuHint + | Qt::WindowCloseButtonHint + | Qt::WindowMinMaxButtonsHint) { + setWindowTitle(tr("Инструкция")); + setMinimumSize(800, 400); + resize(1000, 600); + + // Основной контейнер со всеми разделами + auto *container = new QWidget(this); + auto *layout = new QVBoxLayout(container); + layout->setSpacing(10); + + const QString tutorialsPath = QCoreApplication::applicationDirPath() + "/tutorials"; + // Загружаем Markdown-файлы + const QDir dir(tutorialsPath); + const QStringList files = dir.entryList({"*.md"}, QDir::Files, QDir::Name); + for (int i = 0; i < files.size(); ++i) { + const QString &fileName = files.at(i); + + QFile file(dir.absoluteFilePath(fileName)); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + continue; + QTextStream in(&file); + QString titleLine = in.readLine().trimmed(); + QString content = in.readAll().trimmed(); + file.close(); + + QString title = !titleLine.isEmpty() ? titleLine.replace("#", "") : QFileInfo(file).baseName(); + + auto *section = new CollapsibleSection(title, content, container, i == 0); + layout->addWidget(section); + + if (i == 0) { + } + } + layout->addStretch(); + + QSpacerItem *spacer = new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding); + layout->addItem(spacer); + + // Скролл вокруг контейнера + auto *scroll = new QScrollArea(this); + scroll->setWidget(container); + scroll->setWidgetResizable(true); + scroll->setAlignment(Qt::AlignTop | Qt::AlignLeft); + + auto *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->addWidget(scroll); + setLayout(mainLayout); +} diff --git a/views/TutorialWindow.h b/views/TutorialWindow.h new file mode 100644 index 0000000..cfbc3d1 --- /dev/null +++ b/views/TutorialWindow.h @@ -0,0 +1,9 @@ +#pragma once +#include + +class TutorialWindow final : public QWidget { + Q_OBJECT + +public: + explicit TutorialWindow(QWidget *parent = nullptr); +}; diff --git a/views/bank/AccountSettingsWindow.cpp b/views/bank/AccountSettingsWindow.cpp new file mode 100644 index 0000000..36b38fe --- /dev/null +++ b/views/bank/AccountSettingsWindow.cpp @@ -0,0 +1,789 @@ +#include "AccountSettingsWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ozon/LoginAndCheckAccountsScript.h" +#include "ozon/GetProfileInfoScript.h" +#include "ozon/GetCardInfoScript.h" +#include "black/LoginAndCheckAccountsScript.h" +#include "black/GetProfileInfoScript.h" +#include "black/GetCardInfoScript.h" +#include "dushanbe/LoginAndCheckAccountsScript.h" +#include "dushanbe/GetProfileInfoScript.h" +#include "dushanbe/GetCardInfoScript.h" +#include "dao/BankProfileDAO.h" +#include "dao/MaterialDAO.h" +#include "net/NetworkService.h" + +AccountSettingsWindow::AccountSettingsWindow( + QWidget *parent, QString deviceId, QString deviceName, const BankProfileInfo &app +) : QWidget(parent, + Qt::Window + | Qt::WindowTitleHint + | Qt::WindowSystemMenuHint + | Qt::WindowCloseButtonHint + | Qt::WindowMinMaxButtonsHint), + m_deviceId(std::move(deviceId)), + m_deviceName(std::move(deviceName)), + m_app(app) { + setWindowTitle("Настройки банка [" + m_app.name + "]"); + setMinimumSize(700, 400); + resize(750, 500); + + auto *mainLayout = new QVBoxLayout(this); + + auto *scrollArea = new QScrollArea(this); + scrollArea->setWidgetResizable(true); + auto *content = new QWidget; + auto *layout = new QVBoxLayout(content); + layout->setContentsMargins(8, 8, 8, 8); + layout->setAlignment(Qt::AlignTop); + + // ── Статус профиля ───────────────────────────────────────────────── + auto *statusRow = new QHBoxLayout; + statusRow->addWidget(new QLabel("Статус профиля:")); + m_statusLabel = new QLabel; + m_statusLabel->setTextFormat(Qt::RichText); + statusRow->addWidget(m_statusLabel); + statusRow->addStretch(); + + m_toggleBtn = new QPushButton; + m_toggleBtn->setFixedWidth(120); + connect(m_toggleBtn, &QPushButton::clicked, this, &AccountSettingsWindow::toggleProfileStatus); + statusRow->addWidget(m_toggleBtn); + layout->addLayout(statusRow); + + // ── Пин-код ──────────────────────────────────────────────────────── + auto *pinRow = new QHBoxLayout; + pinRow->addWidget(new QLabel("Пин-код:")); + m_pinLabel = new QLabel; + m_pinLabel->setTextFormat(Qt::RichText); + pinRow->addWidget(m_pinLabel); + pinRow->addStretch(); + + auto *editPinBtn = new QPushButton("Редактировать"); + editPinBtn->setStyleSheet("background-color: green; color: white; padding: 4px 12px;"); + connect(editPinBtn, &QPushButton::clicked, this, &AccountSettingsWindow::editPinCode); + pinRow->addWidget(editPinBtn); + + auto *checkPinBtn = new QPushButton("Проверить пин-код"); + checkPinBtn->setStyleSheet("background-color: orange; color: white; padding: 4px 12px;"); + connect(checkPinBtn, &QPushButton::clicked, this, &AccountSettingsWindow::startCheckPinCode); + pinRow->addWidget(checkPinBtn); + layout->addLayout(pinRow); + + // ── Профиль ──────────────────────────────────────────────────────── + auto *profileRow = new QHBoxLayout; + m_profileLabel = new QLabel; + m_profileLabel->setTextFormat(Qt::RichText); + m_profileLabel->setStyleSheet("margin: 4px 0;"); + profileRow->addWidget(m_profileLabel); + + m_copyPhoneBtn = new QPushButton; + m_copyPhoneBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/copy.png")); + m_copyPhoneBtn->setIconSize(QSize(16, 16)); + m_copyPhoneBtn->setToolTip("Копировать телефон"); + m_copyPhoneBtn->setFixedSize(28, 24); + m_copyPhoneBtn->setStyleSheet("padding: 2px;"); + m_copyPhoneBtn->hide(); + connect(m_copyPhoneBtn, &QPushButton::clicked, this, [this]() { + if (m_app.phone.isEmpty()) return; + QGuiApplication::clipboard()->setText(m_app.phone); + QToolTip::showText(m_copyPhoneBtn->mapToGlobal(QPoint(0, m_copyPhoneBtn->height())), + "Скопировано", m_copyPhoneBtn, {}, 1500); + }); + profileRow->addWidget(m_copyPhoneBtn); + profileRow->addStretch(); + layout->addLayout(profileRow); + + // ── Кнопки обновления ────────────────────────────────────────────── + auto *actionsRow = new QHBoxLayout; + + m_profileBtn = new QPushButton("Обновить профиль"); + m_profileBtn->setStyleSheet("background-color: #1565C0; color: white; padding: 4px 12px;"); + connect(m_profileBtn, &QPushButton::clicked, this, &AccountSettingsWindow::startGetProfileInfo); + actionsRow->addWidget(m_profileBtn); + + m_cardsBtn = new QPushButton("Проверить аккаунты"); + m_cardsBtn->setStyleSheet("background-color: #1565C0; color: white; padding: 4px 12px;"); + connect(m_cardsBtn, &QPushButton::clicked, this, &AccountSettingsWindow::startGetCardInfo); + m_cardsBtn->hide(); + actionsRow->addWidget(m_cardsBtn); + + actionsRow->addStretch(); + layout->addLayout(actionsRow); + + // ── Таблица карт ─────────────────────────────────────────────────── + layout->addSpacing(8); + layout->addWidget(new QLabel("Счета")); + + m_cardsTable = new QTableWidget; + m_cardsTable->setSelectionMode(QAbstractItemView::NoSelection); + m_cardsTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_cardsTable->setFocusPolicy(Qt::NoFocus); + layout->addWidget(m_cardsTable); + + content->setLayout(layout); + scrollArea->setWidget(content); + mainLayout->addWidget(scrollArea); + + // Overlay (скрыт по умолчанию) + m_overlay = new QWidget(this); + m_overlay->setStyleSheet("background-color: rgba(0, 0, 0, 150);"); + m_overlay->hide(); + + m_overlayText = new QLabel(m_overlay); + m_overlayText->setAlignment(Qt::AlignCenter); + m_overlayText->setStyleSheet("color: white; font-size: 16px; font-weight: bold;"); + auto *overlayLayout = new QVBoxLayout(m_overlay); + overlayLayout->addWidget(m_overlayText); + + m_overlayTimer = new QTimer(this); + m_overlayTimer->setSingleShot(true); + + updateStatusUI(); + reloadCards(); +} + +// ── Overlay ──────────────────────────────────────────────────────────────── + +void AccountSettingsWindow::resizeEvent(QResizeEvent *event) { + QWidget::resizeEvent(event); + if (m_overlay) m_overlay->setGeometry(rect()); +} + +void AccountSettingsWindow::showOverlay(const QString &text) { + m_overlayText->setText(text); + m_overlay->setGeometry(rect()); + m_overlay->raise(); + m_overlay->show(); +} + +void AccountSettingsWindow::hideOverlay(bool success, const QString &message, std::function onHidden) { + m_overlayText->setText(message); + m_overlayText->setStyleSheet(success + ? "color: #4CAF50; font-size: 16px; font-weight: bold;" + : "color: #F44336; font-size: 16px; font-weight: bold;"); + m_overlayTimer->disconnect(); + connect(m_overlayTimer, &QTimer::timeout, this, [this, onHidden]() { + m_overlay->hide(); + m_overlayText->setStyleSheet("color: white; font-size: 16px; font-weight: bold;"); + if (onHidden) onHidden(); + }); + m_overlayTimer->start(1500); +} + +// ── UI update ────────────────────────────────────────────────────────────── + +void AccountSettingsWindow::updateStatusUI() { + // Статус профиля + const bool active = m_app.status == "active"; + m_statusLabel->setText(active + ? "Активен" + : "Неактивен"); + m_toggleBtn->setText(active ? "Выключить" : "Включить"); + m_toggleBtn->setStyleSheet(active + ? "background-color: red; color: white; padding: 4px 12px;" + : "background-color: green; color: white; padding: 4px 12px;"); + + // Пин-код + const QString pinDisplay = m_app.pinCode.isEmpty() ? "не задан" : m_app.pinCode; + const QString pinStatus = m_app.pinCodeChecked + ? "" + pinDisplay + " (проверен)" + : "" + pinDisplay + " (не проверен)"; + m_pinLabel->setText(pinStatus); + + // Профиль + if (m_app.fullName.isEmpty() && m_app.phone.isEmpty()) { + m_profileLabel->setText("Данные профиля не получены"); + } else { + QString text; + if (!m_app.fullName.isEmpty()) text += m_app.fullName; + if (!m_app.phone.isEmpty()) text += " | " + m_app.phone; + if (!m_app.email.isEmpty()) text += " | " + m_app.email; + + const QString syncText = m_app.bankProfileId.isEmpty() + ? " ● Не синхронизирован" + : " ● Синхронизирован"; + m_profileLabel->setText(text + syncText); + } + m_copyPhoneBtn->setVisible(!m_app.phone.isEmpty()); +} + +// ── Toggle On/Off ────────────────────────────────────────────────────────── + +void AccountSettingsWindow::toggleProfileStatus() { + const bool isActive = m_app.status == "active"; + const QString newStatus = isActive ? "off" : "active"; + + // Если включаем — проверяем что есть хотя бы одна активная карта + if (!isActive) { + const QList materials = MaterialDAO::getAccounts(m_deviceId, m_app.code); + bool hasActive = false; + for (const auto &m : materials) { + if (m.status == MaterialStatus::Active) { hasActive = true; break; } + } + if (!hasActive) { + QMessageBox::warning(this, "Невозможно включить", + "Ни одна карта не активна.\nВключите хотя бы одну карту перед включением профиля."); + return; + } + } + + if (!BankProfileDAO::update(m_app.id, m_app.pinCode, m_app.comment, newStatus)) + return; + + m_app.status = newStatus; + showOverlay(isActive ? "Выключение профиля..." : "Включение профиля..."); + sendAppStatus(newStatus); + + // При включении профиля — включаем на сервере все локально активные материалы + if (!isActive) { + const QList materials = MaterialDAO::getAccounts(m_deviceId, m_app.code); + for (const auto &mat : materials) { + if (mat.status == MaterialStatus::Active && !mat.materialId.isEmpty()) { + auto *thread = new QThread; + auto *net = new NetworkService; + net->moveToThread(thread); + net->addExtra("material_id", mat.materialId); + net->addExtra("enable", true); + NetworkService::connectErrorDialog(this, net); + connect(thread, &QThread::started, net, &NetworkService::toggleMaterial); + connect(net, &NetworkService::finished, thread, &QThread::quit); + connect(net, &NetworkService::finished, net, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); + qDebug() << "[AccountSettings] Enabling material on server:" << mat.materialId; + } + } + } +} + +// ── Edit PIN ─────────────────────────────────────────────────────────────── + +void AccountSettingsWindow::editPinCode() { + bool ok; + const QString pin = QInputDialog::getText(this, "Пин-код", + "Введите пин-код:", QLineEdit::Normal, m_app.pinCode, &ok); + if (!ok) return; + + if (BankProfileDAO::update(m_app.id, pin, m_app.comment, m_app.status)) { + m_app.pinCode = pin; + m_app.pinCodeChecked = false; + BankProfileDAO::updatePinCodeStatus(m_app.id, "no", ""); + updateStatusUI(); + } +} + +// ── Check PIN ────────────────────────────────────────────────────────────── + +void AccountSettingsWindow::startCheckPinCode() { + if (m_app.pinCode.isEmpty()) { + QMessageBox::warning(this, "Пин-код не задан", + "Сначала укажите пин-код через кнопку \"Редактировать\"."); + return; + } + + showOverlay("Проверка пин-кода..."); + pauseActiveProfiles(); + + auto *thread = new QThread(nullptr); + const QString deviceId = m_deviceId; + const QString appCode = m_app.code; + const int appId = m_app.id; + + auto onResult = [this, appId](const QString &result) { + QMetaObject::invokeMethod(this, [this, appId, result]() { + resumePausedProfiles(); + if (result.isEmpty()) { + BankProfileDAO::updatePinCodeStatus(appId, "yes", ""); + m_app.pinCodeChecked = true; + updateStatusUI(); + hideOverlay(true, "Пин-код проверен"); + } else { + hideOverlay(false, "Ошибка: " + result); + } + }, Qt::QueuedConnection); + }; + + if (appCode == "ozon") { + auto *worker = new Ozon::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, false, true); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &Ozon::LoginAndCheckAccountsScript::start); + connect(worker, &Ozon::LoginAndCheckAccountsScript::finishedWithResult, this, onResult, Qt::QueuedConnection); + connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, thread, &QThread::quit); + connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + } else if (appCode == "black") { + auto *worker = new Black::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, true); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &Black::LoginAndCheckAccountsScript::start); + connect(worker, &Black::LoginAndCheckAccountsScript::finishedWithResult, this, onResult, Qt::QueuedConnection); + connect(worker, &Black::LoginAndCheckAccountsScript::finished, thread, &QThread::quit); + connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + } else if (appCode == "dushanbe_city_bank") { + auto *worker = new Dushanbe::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, true); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &Dushanbe::LoginAndCheckAccountsScript::start); + connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finishedWithResult, this, onResult, Qt::QueuedConnection); + connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finished, thread, &QThread::quit); + connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + } else { + resumePausedProfiles(); + hideOverlay(false, "Банк не поддерживается"); + thread->deleteLater(); + return; + } + + thread->start(); +} + +// ── Get Profile ──────────────────────────────────────────────────────────── + +void AccountSettingsWindow::startGetProfileInfo() { + m_profileBtn->setEnabled(false); + m_cardsBtn->setEnabled(false); + showOverlay("Обновление профиля..."); + pauseActiveProfiles(); + + auto *thread = new QThread(nullptr); + const QString deviceId = m_deviceId; + const QString appCode = m_app.code; + + auto onDone = [this, appCode]() { + QMetaObject::invokeMethod(this, [this, appCode]() { + m_profileBtn->setEnabled(true); + m_cardsBtn->setEnabled(true); + resumePausedProfiles(); + m_app = BankProfileDAO::getApplication(m_deviceId, appCode); + updateStatusUI(); + hideOverlay(true, "Профиль обновлён"); + }, Qt::QueuedConnection); + }; + + if (appCode == "ozon") { + auto *worker = new Ozon::GetProfileInfoScript(deviceId, appCode, nullptr); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &Ozon::GetProfileInfoScript::start); + connect(worker, &Ozon::GetProfileInfoScript::finished, this, onDone, Qt::QueuedConnection); + connect(worker, &Ozon::GetProfileInfoScript::finished, thread, &QThread::quit); + connect(worker, &Ozon::GetProfileInfoScript::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + } else if (appCode == "black") { + auto *worker = new Black::GetProfileInfoScript(deviceId, appCode, nullptr); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &Black::GetProfileInfoScript::start); + connect(worker, &Black::GetProfileInfoScript::finished, this, onDone, Qt::QueuedConnection); + connect(worker, &Black::GetProfileInfoScript::finished, thread, &QThread::quit); + connect(worker, &Black::GetProfileInfoScript::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + } else if (appCode == "dushanbe_city_bank") { + auto *worker = new Dushanbe::GetProfileInfoScript(deviceId, appCode, nullptr); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &Dushanbe::GetProfileInfoScript::start); + connect(worker, &Dushanbe::GetProfileInfoScript::finished, this, onDone, Qt::QueuedConnection); + connect(worker, &Dushanbe::GetProfileInfoScript::finished, thread, &QThread::quit); + connect(worker, &Dushanbe::GetProfileInfoScript::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + } else { + thread->deleteLater(); + return; + } + + thread->start(); +} + +// ── Get Cards ────────────────────────────────────────────────────────────── + +void AccountSettingsWindow::startGetCardInfo() { + m_profileBtn->setEnabled(false); + m_cardsBtn->setEnabled(false); + showOverlay("Проверка аккаунтов..."); + pauseActiveProfiles(); + + auto *thread = new QThread(nullptr); + const QString deviceId = m_deviceId; + const QString appCode = m_app.code; + + auto onDone = [this]() { + QMetaObject::invokeMethod(this, [this]() { + m_profileBtn->setEnabled(true); + m_cardsBtn->setEnabled(true); + resumePausedProfiles(); + reloadCards(); + hideOverlay(true, "Аккаунты обновлены"); + }, Qt::QueuedConnection); + }; + + if (appCode == "ozon") { + auto *worker = new Ozon::GetCardInfoScript(deviceId, appCode, "", nullptr); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &Ozon::GetCardInfoScript::start); + connect(worker, &Ozon::GetCardInfoScript::finished, this, onDone, Qt::QueuedConnection); + connect(worker, &Ozon::GetCardInfoScript::finished, thread, &QThread::quit); + connect(worker, &Ozon::GetCardInfoScript::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + } else if (appCode == "black") { + auto *worker = new Black::GetCardInfoScript(deviceId, appCode, nullptr); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &Black::GetCardInfoScript::start); + connect(worker, &Black::GetCardInfoScript::finished, this, onDone, Qt::QueuedConnection); + connect(worker, &Black::GetCardInfoScript::finished, thread, &QThread::quit); + connect(worker, &Black::GetCardInfoScript::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + } else if (appCode == "dushanbe_city_bank") { + auto *worker = new Dushanbe::GetCardInfoScript(deviceId, appCode, nullptr); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &Dushanbe::GetCardInfoScript::start); + connect(worker, &Dushanbe::GetCardInfoScript::finished, this, onDone, Qt::QueuedConnection); + connect(worker, &Dushanbe::GetCardInfoScript::finished, thread, &QThread::quit); + connect(worker, &Dushanbe::GetCardInfoScript::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + } else { + thread->deleteLater(); + return; + } + + thread->start(); +} + +// ── Send status to server ────────────────────────────────────────────────── + +void AccountSettingsWindow::sendAppStatus(const QString &status) { + if (m_app.bankProfileId.isEmpty()) { + qWarning() << "[AccountSettings] sendAppStatus: no bankProfileId for" << m_app.code; + hideOverlay(true, "Сохранено локально", [this]() { updateStatusUI(); }); + return; + } + + const bool enable = (status == "active"); + auto *thread = new QThread; + auto *net = new NetworkService; + net->moveToThread(thread); + net->addExtra("bank_profile_id", m_app.bankProfileId); + net->addExtra("enable", enable); + NetworkService::connectErrorDialog(this, net); + connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile); + connect(net, &NetworkService::finishedWithResult, this, [this, enable](int result) { + if (result == 0) { + hideOverlay(true, "Успешно", [this]() { updateStatusUI(); }); + return; + } + // Правило #3: при неудачном включении — откатываем локаль в off и + // сообщаем юзеру. При неудачном выключении — локаль остаётся off, + // сервер дотянется периодическим sync-ом (правило #2). + if (enable) { + BankProfileDAO::update(m_app.id, m_app.pinCode, m_app.comment, "off"); + m_app.status = "off"; + hideOverlay(false, "Не удалось включить — возвращено в выключено", + [this]() { updateStatusUI(); }); + } else { + hideOverlay(false, "Локально выключено, сервер дотянется автоматически", + [this]() { updateStatusUI(); }); + } + }, Qt::QueuedConnection); + connect(net, &NetworkService::finished, thread, &QThread::quit); + connect(net, &NetworkService::finished, net, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +// ── Toggle card status ───────────────────────────────────────────────────── + +void AccountSettingsWindow::toggleMaterialStatus(const MaterialInfo &account) { + const bool isActive = account.status == MaterialStatus::Active; + + QList deactivated; // другие активные, которые будут выключены при включении этой + if (!isActive) { + const QList materials = MaterialDAO::getAccounts(m_deviceId, m_app.code); + for (const auto &m : materials) { + if (m.id != account.id && m.status == MaterialStatus::Active) { + deactivated.append(m); + } + } + if (!deactivated.isEmpty()) { + QStringList names; + for (const auto &m : deactivated) { + names << (m.cardNumber.isEmpty() ? m.lastNumbers : m.cardNumber); + } + const auto reply = QMessageBox::question(this, "Включение карты", + "В этом банке уже активны другие карты:\n• " + names.join("\n• ") + + "\n\nОни будут выключены. Продолжить?", + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (reply != QMessageBox::Yes) return; + } + } + + if (isActive) { + if (!MaterialDAO::updateStatus(account.id, "off")) { + qWarning() << "[AccountSettings] Failed to update material status:" << account.id; + return; + } + } else { + const auto actuallyDeactivated = + MaterialDAO::activateExclusive(m_deviceId, m_app.code, account.id); + // Синхронизируем deactivated с реальным результатом транзакции + deactivated = actuallyDeactivated; + } + + showOverlay(isActive ? "Выключение карты..." : "Включение карты..."); + + auto finishToggle = [this, isActive]() { + // Если выключили — проверяем остались ли активные + if (isActive) { + const QList materials = MaterialDAO::getAccounts(m_deviceId, m_app.code); + bool hasActive = false; + for (const auto &m : materials) { + if (m.status == MaterialStatus::Active) { hasActive = true; break; } + } + if (!hasActive && m_app.status == "active") { + BankProfileDAO::update(m_app.id, m_app.pinCode, m_app.comment, "off"); + m_app.status = "off"; + sendAppStatus("off"); + return; // sendAppStatus покажет свой overlay + } + } + reloadCards(); + }; + + // Сервер не даёт включать материалы при выключенном профиле — возвращает их + // в `disabled`, и client-side sync затирает локальный active. Поэтому пока + // профиль off, синхронизация только локальная; материалы зальются на сервер + // когда юзер включит профиль (см. toggleProfileStatus). + const bool profileActive = m_app.status == "active"; + if (!profileActive) { + hideOverlay(true, "Сохранено локально", finishToggle); + return; + } + + // Выключаем на сервере остальные (fire-and-forget) — только при включении этой карты + if (!isActive) { + for (const MaterialInfo &m : deactivated) { + if (m.materialId.isEmpty()) continue; + auto *t = new QThread; + auto *n = new NetworkService; + n->moveToThread(t); + n->addExtra("material_id", m.materialId); + n->addExtra("enable", false); + NetworkService::connectErrorDialog(this, n); + connect(t, &QThread::started, n, &NetworkService::toggleMaterial); + connect(n, &NetworkService::finished, t, &QThread::quit); + connect(n, &NetworkService::finished, n, &QObject::deleteLater); + connect(t, &QThread::finished, t, &QThread::deleteLater); + t->start(); + } + } + + if (!account.materialId.isEmpty()) { + auto *thread = new QThread; + auto *net = new NetworkService; + net->moveToThread(thread); + net->addExtra("material_id", account.materialId); + net->addExtra("enable", !isActive); + NetworkService::connectErrorDialog(this, net); + connect(thread, &QThread::started, net, &NetworkService::toggleMaterial); + const bool willEnable = !isActive; + const int materialDbId = account.id; + connect(net, &NetworkService::finishedWithResult, this, + [this, finishToggle, willEnable, materialDbId](int result) { + if (result == 0) { + hideOverlay(true, "Успешно", finishToggle); + return; + } + // Правило #3: при неудачном включении карты — откат локально в off. + if (willEnable) { + MaterialDAO::updateStatus(materialDbId, "off"); + hideOverlay(false, "Не удалось включить — возвращено в выключено", finishToggle); + } else { + hideOverlay(false, "Локально выключено, сервер дотянется автоматически", finishToggle); + } + }, Qt::QueuedConnection); + connect(net, &NetworkService::finished, thread, &QThread::quit); + connect(net, &NetworkService::finished, net, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); + } else { + hideOverlay(true, "Сохранено локально", finishToggle); + } +} + +// ── Cards table ──────────────────────────────────────────────────────────── + +void AccountSettingsWindow::reloadCards() { + const QList accounts = MaterialDAO::getAccounts(m_deviceId, m_app.code); + + // setRowCount(0) перед re-fill удаляет и items, и cell widgets. + // clearContents() оставляет setCellWidget'ы висеть — старые кнопки с + // замкнутой устаревшей `acc` остаются в строках, ставших active. + m_cardsTable->setRowCount(0); + m_cardsTable->setRowCount(accounts.size()); + m_cardsTable->setColumnCount(8); + m_cardsTable->setHorizontalHeaderLabels({ + "Сервер", "Статус", "Время обновления", "Номер карты", "Сумма", "Валюта", "", "" + }); + m_cardsTable->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter); + + // Правило 4-A: при выключенном профиле материалы фактически недоступны — + // отображаем приглушённо (gray) и добавляем подсказку, локальный status в БД не трогаем. + const bool profileOff = (m_app.status != "active"); + const QBrush dimmedBrush = QBrush(QColor(128, 128, 128)); + const QString dimmedTooltip = profileOff + ? QStringLiteral("Профиль банка выключен — карта фактически недоступна. " + "Включите профиль, чтобы карта заработала.") + : QString(); + + for (int k = 0; k < accounts.size(); ++k) { + const MaterialInfo &acc = accounts[k]; + + // Сервер + auto *syncItem = new QTableWidgetItem(acc.materialId.isEmpty() ? "Не синхр." : "Синхр."); + syncItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + if (profileOff) { + syncItem->setForeground(dimmedBrush); + syncItem->setToolTip(dimmedTooltip); + } else { + syncItem->setForeground(acc.materialId.isEmpty() ? QBrush(Qt::red) : QBrush(Qt::darkGreen)); + } + m_cardsTable->setItem(k, 0, syncItem); + + // Статус + const bool isActive = acc.status == MaterialStatus::Active; + auto *statusItem = new QTableWidgetItem(materialStatusToUiString(acc.status)); + statusItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + if (profileOff) { + statusItem->setForeground(dimmedBrush); + statusItem->setToolTip(dimmedTooltip); + } else { + statusItem->setForeground(isActive ? QBrush(Qt::darkGreen) : QBrush(Qt::red)); + } + m_cardsTable->setItem(k, 1, statusItem); + + // Время обновления + auto *timeItem = new QTableWidgetItem(acc.updateTime.toString("dd.MM.yyyy HH:mm:ss")); + timeItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + m_cardsTable->setItem(k, 2, timeItem); + + // Номер карты (текст + иконка копирования) + const QString displayNumber = acc.cardNumber.isEmpty() ? acc.lastNumbers : acc.cardNumber; + const QString fullCardNumber = acc.cardNumber.isEmpty() ? acc.lastNumbers : acc.cardNumber; + auto *cardCell = new QWidget(m_cardsTable); + auto *cardLayout = new QHBoxLayout(cardCell); + cardLayout->setContentsMargins(4, 0, 4, 0); + cardLayout->setSpacing(6); + auto *cardLabel = new QLabel(displayNumber, cardCell); + cardLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + cardLayout->addWidget(cardLabel); + if (!fullCardNumber.isEmpty()) { + auto *copyCardBtn = new QPushButton(cardCell); + copyCardBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/copy.png")); + copyCardBtn->setIconSize(QSize(14, 14)); + copyCardBtn->setToolTip("Копировать номер карты"); + copyCardBtn->setFixedSize(22, 20); + copyCardBtn->setStyleSheet("padding: 1px; border: none;"); + connect(copyCardBtn, &QPushButton::clicked, this, [copyCardBtn, fullCardNumber]() { + QGuiApplication::clipboard()->setText(fullCardNumber); + QToolTip::showText(copyCardBtn->mapToGlobal(QPoint(0, copyCardBtn->height())), + "Скопировано", copyCardBtn, {}, 1500); + }); + cardLayout->addWidget(copyCardBtn); + } + cardLayout->addStretch(); + m_cardsTable->setCellWidget(k, 3, cardCell); + + // Сумма + auto *amountItem = new QTableWidgetItem(QString::number(acc.amount)); + amountItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + m_cardsTable->setItem(k, 4, amountItem); + + // Валюта + auto *currItem = new QTableWidgetItem(acc.currency); + currItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + m_cardsTable->setItem(k, 5, currItem); + + // Кнопка переключения: только для неактивных — включит этот и выключит остальные + if (!isActive) { + auto *toggleBtn = new QPushButton("Переключить", m_cardsTable); + toggleBtn->setStyleSheet("background-color: green; color: white;"); + m_cardsTable->setCellWidget(k, 6, toggleBtn); + + connect(toggleBtn, &QPushButton::clicked, this, [this, acc]() { + toggleMaterialStatus(acc); + }); + } + + // Кнопка копирования номера счёта + if (!acc.accountNumber.isEmpty()) { + auto *copyAcctBtn = new QPushButton(m_cardsTable); + copyAcctBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/copy.png")); + copyAcctBtn->setIconSize(QSize(16, 16)); + copyAcctBtn->setToolTip("Копировать номер счёта"); + copyAcctBtn->setFixedSize(28, 24); + copyAcctBtn->setStyleSheet("padding: 2px;"); + m_cardsTable->setCellWidget(k, 7, copyAcctBtn); + + connect(copyAcctBtn, &QPushButton::clicked, this, [this, copyAcctBtn, acc]() { + QGuiApplication::clipboard()->setText(acc.accountNumber); + QToolTip::showText(copyAcctBtn->mapToGlobal(QPoint(0, copyAcctBtn->height())), + "Скопировано", copyAcctBtn, {}, 1500); + }); + } + } + + m_cardsTable->resizeColumnsToContents(); + m_cardsTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); + m_cardsTable->resizeRowsToContents(); +} + +// ── Pause/Resume active profiles ─────────────────────────────────────────── + +static void toggleProfileOnServer(const BankProfileInfo &app, bool enable) { + if (app.bankProfileId.isEmpty()) return; + auto *thread = new QThread; + auto *net = new NetworkService; + net->moveToThread(thread); + net->addExtra("bank_profile_id", app.bankProfileId); + net->addExtra("enable", enable); + QObject::connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile); + QObject::connect(net, &NetworkService::finished, thread, &QThread::quit); + QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +void AccountSettingsWindow::pauseActiveProfiles() { + const QList apps = BankProfileDAO::getApplicationsByDeviceId(m_deviceId); + for (const BankProfileInfo &app : apps) { + if (app.status == "active") { + m_pausedProfiles.append(app.code); + BankProfileDAO::update(app.id, app.pinCode, app.comment, "off"); + toggleProfileOnServer(app, false); + qDebug() << "[AccountSettings] Paused profile:" << app.code; + } + } +} + +void AccountSettingsWindow::resumePausedProfiles() { + for (const QString &code : m_pausedProfiles) { + const BankProfileInfo app = BankProfileDAO::getApplication(m_deviceId, code); + if (app.id >= 0 && app.status == "off") { + BankProfileDAO::update(app.id, app.pinCode, app.comment, "active"); + toggleProfileOnServer(app, true); + qDebug() << "[AccountSettings] Resumed profile:" << app.code; + } + } + m_pausedProfiles.clear(); +} diff --git a/views/bank/AccountSettingsWindow.h b/views/bank/AccountSettingsWindow.h new file mode 100644 index 0000000..d292cee --- /dev/null +++ b/views/bank/AccountSettingsWindow.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "db/MaterialInfo.h" +#include "db/BankProfileInfo.h" + +class AccountSettingsWindow final : public QWidget { + Q_OBJECT + +public: + AccountSettingsWindow(QWidget *parent, QString deviceId, QString deviceName, const BankProfileInfo &app); + +private: + QString m_deviceId; + QString m_deviceName; + BankProfileInfo m_app; + + QLabel *m_statusLabel; + QLabel *m_pinLabel; + QLabel *m_profileLabel; + QPushButton *m_copyPhoneBtn = nullptr; + QPushButton *m_toggleBtn; + QPushButton *m_profileBtn; + QPushButton *m_cardsBtn; + QTableWidget *m_cardsTable; + + // Overlay + QWidget *m_overlay = nullptr; + QLabel *m_overlayText = nullptr; + QTimer *m_overlayTimer = nullptr; + + void showOverlay(const QString &text); + void hideOverlay(bool success, const QString &message, std::function onHidden = nullptr); + + void updateStatusUI(); + void toggleProfileStatus(); + void editPinCode(); + void startCheckPinCode(); + void startGetProfileInfo(); + void startGetCardInfo(); + void sendAppStatus(const QString &status); + void toggleMaterialStatus(const MaterialInfo &account); + void reloadCards(); + + QStringList m_pausedProfiles; + void pauseActiveProfiles(); + void resumePausedProfiles(); + +protected: + void resizeEvent(QResizeEvent *event) override; +}; diff --git a/views/AccountWindow.cpp b/views/bank/AccountWindow.cpp similarity index 94% rename from views/AccountWindow.cpp rename to views/bank/AccountWindow.cpp index 6f80ca9..aa8586c 100644 --- a/views/AccountWindow.cpp +++ b/views/bank/AccountWindow.cpp @@ -7,7 +7,7 @@ #include #include "TransactionWindow.h" -#include "dao/AccountDAO.h" +#include "dao/MaterialDAO.h" #include "dao/DeviceDAO.h" AccountWindow::AccountWindow(QWidget *parent): QWidget(parent) { @@ -30,7 +30,7 @@ AccountWindow::AccountWindow(QWidget *parent): QWidget(parent) { QList devices = DeviceDAO::getAllDevices(false); for (int i = 0; i < devices.size(); ++i) { DeviceInfo device = devices[i]; - QList accounts = AccountDAO::getAccounts(device.id, "bank"); + QList accounts = MaterialDAO::getAccounts(device.id, "ozon"); // Текст перед первой таблицей QString title = QString("%1 [%2] | %3").arg(device.name, device.id, device.status); QLabel *label = new QLabel(title, this); @@ -54,14 +54,14 @@ AccountWindow::AccountWindow(QWidget *parent): QWidget(parent) { for (int k = 0; k < accounts.size(); ++k) { - AccountInfo account = accounts[k]; - QTableWidgetItem *item = new QTableWidgetItem("*" + account.cardName); + MaterialInfo account = accounts[k]; + QTableWidgetItem *item = new QTableWidgetItem("*" + account.lastNumbers); item->setData(Qt::UserRole, account.id); tableWidget->setItem(k, 0, item); tableWidget->setItem(k, 1, new QTableWidgetItem(QString::number(account.amount))); tableWidget->setItem(k, 2, new QTableWidgetItem(account.updateTime.toString("dd.MM.yyyy HH:mm:ss"))); tableWidget->setItem(k, 3, new QTableWidgetItem(account.description)); - tableWidget->setItem(k, 4, new QTableWidgetItem(account.status)); + tableWidget->setItem(k, 4, new QTableWidgetItem(materialStatusToString(account.status))); totalHeight += tableWidget->rowHeight(k); } tableWidget->setFixedHeight(totalHeight + 2); @@ -93,7 +93,7 @@ void AccountWindow::onItemDoubleClicked(QTableWidgetItem *item) { int column = item->column(); if (column == 0) { int accountId = item->data(Qt::UserRole).toInt(); - // qDebug() << accountId; + qDebug() << accountId; TransactionWindow *detailWindow = new TransactionWindow(accountId, this); detailWindow->exec(); // Ожидаем закрытия окна } diff --git a/views/AccountWindow.h b/views/bank/AccountWindow.h similarity index 100% rename from views/AccountWindow.h rename to views/bank/AccountWindow.h diff --git a/views/bank/BankListWindow.cpp b/views/bank/BankListWindow.cpp new file mode 100644 index 0000000..630a337 --- /dev/null +++ b/views/bank/BankListWindow.cpp @@ -0,0 +1,148 @@ +#include "BankListWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dao/BankDAO.h" + +BankListWindow::BankListWindow(QWidget *parent) + : QWidget(parent, + Qt::Window + | Qt::WindowTitleHint + | Qt::WindowSystemMenuHint + | Qt::WindowCloseButtonHint + | Qt::WindowMinMaxButtonsHint) { + setWindowTitle("Банки"); + setMinimumSize(600, 400); + resize(700, 500); + + auto *layout = new QVBoxLayout(this); + + auto *topRow = new QHBoxLayout(); + auto *syncBtn = new QPushButton("Синхронизировать"); + syncBtn->setStyleSheet("background-color: #1565C0; color: white;"); + syncBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + topRow->addWidget(syncBtn); + auto *searchEdit = new QLineEdit(this); + searchEdit->setPlaceholderText("Поиск..."); + searchEdit->setClearButtonEnabled(true); + topRow->addWidget(searchEdit); + + topRow->addStretch(); + layout->addLayout(topRow); + + connect(syncBtn, &QPushButton::clicked, this, &BankListWindow::syncBanks); + connect(searchEdit, &QLineEdit::textChanged, this, &BankListWindow::filterTable); + + m_tableWidget = new QTableWidget(this); + m_tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_tableWidget->setSelectionMode(QAbstractItemView::SingleSelection); + m_tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows); + m_tableWidget->setColumnCount(3); + m_tableWidget->setHorizontalHeaderLabels({"Alias", "Название", "NSPK"}); + m_tableWidget->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter); + m_tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + + layout->addWidget(m_tableWidget); + + reloadTable(); +} + +void BankListWindow::reloadTable() { + QList banks = BankDAO::getAll(); + + m_tableWidget->setRowCount(banks.size()); + for (int i = 0; i < banks.size(); ++i) { + const BankInfo &b = banks[i]; + + auto *alias = new QTableWidgetItem(b.alias); + alias->setFlags(alias->flags() & ~Qt::ItemIsEditable); + m_tableWidget->setItem(i, 0, alias); + + auto *name = new QTableWidgetItem(b.name); + name->setFlags(name->flags() & ~Qt::ItemIsEditable); + m_tableWidget->setItem(i, 1, name); + + auto *nspk = new QTableWidgetItem(b.nspkName); + nspk->setFlags(nspk->flags() & ~Qt::ItemIsEditable); + m_tableWidget->setItem(i, 2, nspk); + } +} + +void BankListWindow::syncBanks() { + if (!QSslSocket::supportsSsl()) { + QMessageBox::warning(this, "Ошибка", + "SSL не доступен. HTTPS-запросы невозможны.\n\n" + "Убедитесь, что libssl и libcrypto DLL находятся рядом с исполняемым файлом."); + qWarning() << "[BankListWindow] SSL not supported. Build:" + << QSslSocket::sslLibraryBuildVersionString() + << "Runtime:" << QSslSocket::sslLibraryVersionString(); + return; + } + + auto *manager = new QNetworkAccessManager(this); + + QNetworkRequest request(QUrl("https://api.brtservice.io/api/rest/info/")); + QNetworkReply *reply = manager->get(request); + + connect(reply, &QNetworkReply::finished, this, [this, reply, manager]() { + reply->deleteLater(); + manager->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + QMessageBox::warning(this, "Ошибка", "Не удалось загрузить банки: " + reply->errorString()); + return; + } + + const QByteArray data = reply->readAll(); + const QJsonDocument doc = QJsonDocument::fromJson(data); + + QJsonArray array; + if (doc.isArray()) { + array = doc.array(); + } else if (doc.isObject() && doc.object().contains("data")) { + array = doc.object()["data"].toArray(); + } + + QList banks; + for (const QJsonValue &val : array) { + QJsonObject obj = val.toObject(); + BankInfo bank; + bank.alias = obj["alias"].toString(); + bank.name = obj["name"].toString(); + bank.nspkName = obj["nspk_name"].toString(); + if (!bank.alias.isEmpty()) { + banks.append(bank); + } + } + + BankDAO::replaceAll(banks); + reloadTable(); + + qDebug() << "[BankListWindow] Synced" << banks.size() << "banks"; + }); +} + +void BankListWindow::filterTable(const QString &text) { + for (int i = 0; i < m_tableWidget->rowCount(); ++i) { + bool match = false; + for (int col = 0; col < m_tableWidget->columnCount(); ++col) { + QTableWidgetItem *item = m_tableWidget->item(i, col); + if (item && item->text().contains(text, Qt::CaseInsensitive)) { + match = true; + break; + } + } + m_tableWidget->setRowHidden(i, !match); + } +} diff --git a/views/bank/BankListWindow.h b/views/bank/BankListWindow.h new file mode 100644 index 0000000..5647950 --- /dev/null +++ b/views/bank/BankListWindow.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +class BankListWindow final : public QWidget { + Q_OBJECT + +public: + explicit BankListWindow(QWidget *parent = nullptr); + +private: + QTableWidget *m_tableWidget; + + void reloadTable(); + void syncBanks(); + void filterTable(const QString &text); +}; diff --git a/views/bank/BankSettingsWindow.cpp b/views/bank/BankSettingsWindow.cpp new file mode 100644 index 0000000..be58e73 --- /dev/null +++ b/views/bank/BankSettingsWindow.cpp @@ -0,0 +1,409 @@ +#include "BankSettingsWindow.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dao/BankProfileDAO.h" +#include "dao/EventDAO.h" +#include "db/BankProfileInfo.h" +#include "net/NetworkService.h" +#include "common/PaddedItemDelegate.h" +#include "device/EditBankDialog.h" +#include "ozon/LoginAndCheckAccountsScript.h" +#include "black/LoginAndCheckAccountsScript.h" +#include "dushanbe/LoginAndCheckAccountsScript.h" + +QTableWidgetItem *BankSettingsWindow::createTableWidget(const QString &text) { + QTableWidgetItem *item = new QTableWidgetItem(text); + item->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + return item; +} + +QString parseStatus(const BankProfileInfo &app) { + if (!app.install) { + return "не установлен"; + } + if (app.status == "active") { + if (!app.pinCodeChecked) { + return app.pinCode.isEmpty() ? "нет пин-код" : "пин-код не проверен"; + } + return "работает"; + } else if (app.status == "off") { + return "выключено"; + } else { + return app.status; + } +} + +void BankSettingsWindow::startCheckPinCode(const QString &appCode) { + QThread *thread = new QThread(nullptr); + const QString deviceId = m_deviceId; + + auto onResult = [deviceId, appCode](const QString &result) { + if (result.isEmpty()) { + const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode); + if (app.id >= 0) { + BankProfileDAO::updatePinCodeStatus(app.id, "yes", ""); + } + } + }; + + if (appCode == "ozon") { + auto *worker = new Ozon::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &Ozon::LoginAndCheckAccountsScript::start); + connect(worker, &Ozon::LoginAndCheckAccountsScript::finishedWithResult, worker, onResult); + connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, thread, &QThread::quit); + connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + } else if (appCode == "black") { + auto *worker = new Black::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &Black::LoginAndCheckAccountsScript::start); + connect(worker, &Black::LoginAndCheckAccountsScript::finishedWithResult, worker, onResult); + connect(worker, &Black::LoginAndCheckAccountsScript::finished, thread, &QThread::quit); + connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + } else if (appCode == "dushanbe_city_bank") { + auto *worker = new Dushanbe::LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr); + worker->moveToThread(thread); + connect(thread, &QThread::started, worker, &Dushanbe::LoginAndCheckAccountsScript::start); + connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finishedWithResult, worker, onResult); + connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finished, thread, &QThread::quit); + connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + } else { + thread->deleteLater(); + return; + } + + thread->start(); +} + +static void sendTaskError(const QString &type, const QString &materialId, const QString &bankName) { + QJsonObject errData; + errData["status"] = "error"; + errData["event_type"] = type; + errData["description"] = "Bank profile disabled"; + + QJsonObject obj; + obj["type"] = "ERROR"; + obj["material_id"] = materialId; + obj["bank_name"] = bankName; + obj["data"] = errData; + + auto *thread = new QThread; + auto *net = new NetworkService; + net->moveToThread(thread); + net->setPayload(QJsonDocument(obj).toJson()); + QObject::connect(thread, &QThread::started, net, &NetworkService::postTaskResult); + QObject::connect(net, &NetworkService::finished, thread, &QThread::quit); + QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +void sendAppStatus(const QString &deviceId, const QString &code, const QString &status) { + const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, code); + if (app.bankProfileId.isEmpty()) { + qWarning() << "[BankSettings] sendAppStatus: no bankProfileId for" << code; + return; + } + + // При выключении — отменяем ожидающие задачи + if (status != "active") { + const QList cancelled = EventDAO::cancelWaitingByBank(deviceId, code); + for (const EventInfo &e : cancelled) { + sendTaskError(eventTypeToString(e.type), e.externalId, e.bankName); + EventDAO::markSentToServer(e.id); + qDebug() << "[BankSettings] cancelled task" << e.id << eventTypeToString(e.type); + } + } + + const bool enable = (status == "active"); + auto *thread = new QThread; + auto *net = new NetworkService; + net->moveToThread(thread); + net->addExtra("bank_profile_id", app.bankProfileId); + net->addExtra("enable", enable); + NetworkService::connectErrorDialog(nullptr, net); + QObject::connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile); + QObject::connect(net, &NetworkService::finished, thread, &QThread::quit); + QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) { + QList applications = BankProfileDAO::getApplicationsByDeviceId(m_deviceId); + + tableWidget->setRowCount(applications.length()); + tableWidget->setColumnCount(8); + tableWidget->setHorizontalHeaderLabels({ + "", + "Имя", + "Пин-код", + "Статус", + "Сервер", + "Комментарий", + "", + "" + }); + + tableWidget->setWordWrap(true); + tableWidget->setTextElideMode(Qt::ElideNone); + + tableWidget->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter); + for (int i = 0; i < 8; ++i) { + tableWidget->horizontalHeader()->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } + for (int i = 0; i < applications.length(); ++i) { + tableWidget->verticalHeader()->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } + tableWidget->horizontalHeader()->setSectionResizeMode(5, QHeaderView::Stretch); + tableWidget->setColumnWidth(4, 140); + + + for (int k = 0; k < applications.size(); ++k) { + const BankProfileInfo &app = applications[k]; + + + auto *icon = new QLabel(tableWidget); + QPixmap pixmap(QCoreApplication::applicationDirPath() + "/images/" + app.code + "_icon.png"); + icon->setPixmap(pixmap); + icon->setPixmap(pixmap.scaled(20, 20, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + icon->setStyleSheet( + "background-color: transparent;" + "text-align: center;" + "padding:0 4px 0 0;" + ); + icon->setMinimumSize(24, 20); + tableWidget->setCellWidget(k, 0, icon); + + tableWidget->setItem(k, 1, createTableWidget(app.name)); + + + // QTableWidgetItem *picCodeItem = new QTableWidgetItem(app.pinCode); + // picCodeItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + auto *picCode = new QLabel(app.pinCode); + if (!app.pinCode.isEmpty() && !app.pinCodeChecked) { + picCode->setStyleSheet( + "background-color: orange;" + "color: white;" + ); + } + picCode->setAlignment(Qt::AlignCenter); + tableWidget->setCellWidget(k, 2, picCode); + + tableWidget->setItem(k, 3, createTableWidget(parseStatus(app))); + + // Статус синхронизации с сервером + auto *syncItem = new QTableWidgetItem(app.bankProfileId.isEmpty() ? "Не синхронизирован" : "Синхронизирован"); + syncItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + syncItem->setForeground(app.bankProfileId.isEmpty() ? QBrush(Qt::red) : QBrush(Qt::darkGreen)); + tableWidget->setItem(k, 4, syncItem); + + QTableWidgetItem *item = new QTableWidgetItem(app.comment); + item->setTextAlignment(Qt::AlignLeft | Qt::AlignTop); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + tableWidget->setItem(k, 5, item); + + + if (app.install) { + if (!app.pinCode.isEmpty() && !app.pinCodeChecked) { + auto *pinCodeBtn = new QPushButton(" Проверить пин-код ", tableWidget); + pinCodeBtn->setProperty("btnClass", "pincode"); + tableWidget->setCellWidget(k, 6, pinCodeBtn); + + connect(pinCodeBtn, &QPushButton::clicked, this, [this, app]() { + QMessageBox msgBox(this); + msgBox.setWindowTitle(tr("Проверка пин-код")); + msgBox.setText("Сейчас запустится скрипт проверки пин-код в приложении: " + app.name); + + QPushButton *deleteBtn = msgBox.addButton(tr("Запустить"), QMessageBox::AcceptRole); + QPushButton *closeBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole); + msgBox.setDefaultButton(closeBtn); + msgBox.setModal(true); + msgBox.exec(); + + if (msgBox.clickedButton() == deleteBtn) { + startCheckPinCode(app.code); + } + }); + } else { + if (app.status == "active" && !app.pinCode.isEmpty()) { + auto *deleteBtn = new QPushButton(" Отключить ", tableWidget); + deleteBtn->setProperty("btnClass", "delete"); + // deleteBtn->setStyleSheet("padding: 4px 8px;"); + // deleteBtn->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); + tableWidget->setCellWidget(k, 6, deleteBtn); + + connect(deleteBtn, &QPushButton::clicked, this, [this,tableWidget, app]() { + QMessageBox msgBox(this); + msgBox.setWindowTitle(tr("Подтверждение")); + msgBox.setText("Вы уверены, что хотите отключить от работы данный банк: " + app.name + "?"); + + QPushButton *deleteBtn = msgBox.addButton(tr("Отключить"), QMessageBox::AcceptRole); + QPushButton *closeBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole); + msgBox.setDefaultButton(closeBtn); + msgBox.setModal(true); + msgBox.exec(); + + if (msgBox.clickedButton() == deleteBtn) { + if (!BankProfileDAO::update(app.id, app.pinCode, app.comment, "off")) { + qDebug() << "Cant update bank data"; + } else { + sendAppStatus(app.deviceId, app.code, "off"); + tableWidget->clearContents(); + reloadContent(tableWidget); + tableWidget->resizeRowsToContents(); + } + } else { + // просто закрыли — ничего не делаем + } + }); + } + } + + + // auto *editBtn = new QTableWidgetItem; + // tableWidget->setItem(k, 5, editBtn); + auto *btn = new QPushButton(" Редактировать ", tableWidget); + btn->setProperty("btnClass", "edit"); + // btn->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); + tableWidget->setCellWidget(k, 7, btn); + connect(btn, &QPushButton::clicked, this, [this, tableWidget, k,app]() { + BankEditDialog dlg(app, this); + if (dlg.exec() == QDialog::Accepted) { + const QString pinCode = dlg.firstValue(); + const QString comment = dlg.secondValue(); + bool on = dlg.toggled(); + + if (!BankProfileDAO::update(app.id, pinCode, comment, on ? "active" : "off")) { + qDebug() << "Cant update bank data"; + } else { + sendAppStatus(app.deviceId, app.code, on ? "active" : "off"); + tableWidget->clearContents(); + reloadContent(tableWidget); + tableWidget->resizeRowsToContents(); + } + } else { + // Пользователь нажал «Cancel» или закрыл окно + } + }); + } + } +} + +BankSettingsWindow::BankSettingsWindow(QWidget *parent, QString deviceId, QString deviceName, const BankProfileInfo &app) + : QWidget(parent, + Qt::Window + | Qt::WindowTitleHint + | Qt::WindowSystemMenuHint + | Qt::WindowCloseButtonHint + | Qt::WindowMinMaxButtonsHint), + m_deviceId(std::move(deviceId)), + m_deviceName(std::move(deviceName) + ) { + setWindowTitle("Настройки банков [" + m_deviceName + "]"); + setMinimumSize(800, 400); + setMaximumSize(1000, 800); + resize(800, 400); + + auto *mainLayout = new QVBoxLayout(this); + + // Скролл-область + auto *scrollArea = new QScrollArea(this); + scrollArea->setWidgetResizable(true); + + // Виджет-контейнер, в котором будет layout с таблицами и лейблами + auto *contentWidget = new QWidget; + auto *layout = new QVBoxLayout(contentWidget); + + // Убираем отступы и рамку + contentWidget->setContentsMargins(0, 0, 0, 0); // Убираем отступы + layout->setContentsMargins(0, 0, 0, 0); // Убираем отступы у layout + scrollArea->setStyleSheet("border: none;"); // Убираем рамку + layout->setAlignment(Qt::AlignTop); + + + QTableWidget *tableWidget = new QTableWidget(this); + tableWidget->setSelectionMode(QAbstractItemView::NoSelection); + tableWidget->setFocusPolicy(Qt::NoFocus); + tableWidget->setStyleSheet( + "QTableWidget {" + " border: 1px solid grey;" + // " gridline-color: lightgrey;" + // " background-color: #f9f9f9;" + " font-size: 14px;" + "}" + "QHeaderView::section {" + // " background-color: #e0e0e0;" + // " border: 1px solid grey;" + " padding: 4px;" + " font-weight: bold;" + " text-align: left center;" + "}" + "QTableWidget::item {" + " padding: 2px;" + " border: none;" + " text-align: left;" + " vertical-align: middle; " + "}" + "QTableWidget::item:selected {" + // " background-color: #cce5ff;" + // " color: #003366;" + "}" + "QTableCornerButton::section {" + // " background-color: #e0e0e0;" + // " border: 1px solid grey;" + "}" + "QPushButton {" + "border-radius: 4px;" + "height: 26px;" + "}" + "QPushButton[btnClass='delete'] {" + "background-color: red;" + "}" + "QPushButton[btnClass='pincode'] {" + "background-color: orange;" + "}" + "QPushButton[btnClass='edit'] {" + "background-color: green;" + "}" + "QPushButton[btnClass='delete']:pressed {" + "background-color: blue;" + "}" + "QPushButton[btnClass='edit']:pressed {" + "background-color: blue;" + "}" + ); + + tableWidget->clearContents(); + reloadContent(tableWidget); + tableWidget->resizeColumnsToContents(); + + layout->addWidget(tableWidget); + tableWidget->resizeRowsToContents(); + + // Добавим вертикальный Spacer в конец, чтобы прокручиваемая область не растягивала контент + // QSpacerItem *spacer = new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding); + // layout->addItem(spacer); + + contentWidget->setLayout(layout); + scrollArea->setWidget(contentWidget); + mainLayout->addWidget(scrollArea); + setLayout(mainLayout); +} diff --git a/views/bank/BankSettingsWindow.h b/views/bank/BankSettingsWindow.h new file mode 100644 index 0000000..e6ca4f2 --- /dev/null +++ b/views/bank/BankSettingsWindow.h @@ -0,0 +1,21 @@ +#pragma once +#include +#include +#include "db/BankProfileInfo.h" + +class BankSettingsWindow final : public QWidget { + Q_OBJECT + +public: + BankSettingsWindow(QWidget *parent, QString deviceId, QString deviceName, const BankProfileInfo &app); + +private: + QString m_deviceId; + QString m_deviceName; + + QTableWidgetItem *createTableWidget(const QString &text); + + void startCheckPinCode(const QString &appCode); + + void reloadContent(QTableWidget *tableWidget); +}; diff --git a/views/bank/BankSetupWizard.cpp b/views/bank/BankSetupWizard.cpp new file mode 100644 index 0000000..3a346a1 --- /dev/null +++ b/views/bank/BankSetupWizard.cpp @@ -0,0 +1,695 @@ +#include "BankSetupWizard.h" + +#include +#include +#include +#include +#include + +#include "ozon/LoginAndCheckAccountsScript.h" +#include "ozon/GetProfileInfoScript.h" +#include "ozon/GetCardInfoScript.h" +#include "black/LoginAndCheckAccountsScript.h" +#include "black/GetProfileInfoScript.h" +#include "black/GetCardInfoScript.h" +#include "dushanbe/LoginAndCheckAccountsScript.h" +#include "dushanbe/GetProfileInfoScript.h" +#include "dushanbe/GetCardInfoScript.h" +#include "dao/BankProfileDAO.h" +#include "dao/DeviceDAO.h" +#include "dao/MaterialDAO.h" +#include "net/NetworkService.h" + +BankSetupWizard::BankSetupWizard(QWidget *parent, const QString &deviceId, const BankProfileInfo &app) + : QDialog(parent), m_deviceId(deviceId), m_app(app) { + setWindowTitle("Настройка банка — " + app.name); + setMinimumSize(500, 350); + resize(520, 380); + + auto *mainLayout = new QVBoxLayout(this); + m_stack = new QStackedWidget(this); + + m_stack->addWidget(createStep1()); // 0 + m_stack->addWidget(createStep2()); // 1 + m_stack->addWidget(createStep3()); // 2 + m_stack->addWidget(createStep4Success()); // 3 + m_stack->addWidget(createStep4Error()); // 4 + + m_stack->setCurrentIndex(0); + mainLayout->addWidget(m_stack); + + pauseActiveProfiles(); +} + +BankSetupWizard::~BankSetupWizard() { + stopWorkerThread(); + resumePausedProfiles(); +} + +// ── Step 1: Intro ────────────────────────────────────────────────────────── + +QWidget *BankSetupWizard::createStep1() { + auto *page = new QWidget; + auto *layout = new QVBoxLayout(page); + layout->setAlignment(Qt::AlignTop); + + auto *icon = new QLabel; + QPixmap pixmap(QCoreApplication::applicationDirPath() + "/images/" + m_app.code + "_icon.png"); + if (!pixmap.isNull()) + icon->setPixmap(pixmap.scaled(48, 48, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + icon->setAlignment(Qt::AlignCenter); + layout->addWidget(icon); + + auto *title = new QLabel("

" + m_app.name + "

"); + title->setAlignment(Qt::AlignCenter); + layout->addWidget(title); + + auto *desc = new QLabel( + "Мастер поможет настроить банковский профиль для приложения " + m_app.name + ".

" + "Будут выполнены следующие шаги:
" + "1. Ввод пин-кода
" + "2. Проверка входа в приложение
" + "3. Получение данных профиля и карт" + ); + desc->setWordWrap(true); + desc->setAlignment(Qt::AlignCenter); + layout->addWidget(desc); + + layout->addStretch(); + + auto *btnLayout = new QHBoxLayout; + btnLayout->addStretch(); + auto *nextBtn = new QPushButton("Далее"); + nextBtn->setStyleSheet("background-color: green; color: white; padding: 6px 24px;"); + connect(nextBtn, &QPushButton::clicked, this, [this]() { + m_stack->setCurrentIndex(1); + }); + btnLayout->addWidget(nextBtn); + layout->addLayout(btnLayout); + + return page; +} + +// ── Step 2: PIN Code ─────────────────────────────────────────────────────── + +QWidget *BankSetupWizard::createStep2() { + auto *page = new QWidget; + auto *layout = new QVBoxLayout(page); + layout->setAlignment(Qt::AlignTop); + + auto *title = new QLabel("

Введите пин-код

"); + title->setAlignment(Qt::AlignCenter); + layout->addWidget(title); + + auto *desc = new QLabel("Укажите пин-код для входа в приложение " + m_app.name + "."); + desc->setWordWrap(true); + desc->setAlignment(Qt::AlignCenter); + layout->addWidget(desc); + + layout->addSpacing(16); + + m_pinCodeEdit = new QLineEdit; + m_pinCodeEdit->setPlaceholderText("Пин-код"); + m_pinCodeEdit->setMaxLength(10); + m_pinCodeEdit->setAlignment(Qt::AlignCenter); + m_pinCodeEdit->setFixedWidth(200); + + auto *editLayout = new QHBoxLayout; + editLayout->addStretch(); + editLayout->addWidget(m_pinCodeEdit); + editLayout->addStretch(); + layout->addLayout(editLayout); + + layout->addStretch(); + + auto *btnLayout = new QHBoxLayout; + auto *backBtn = new QPushButton("Назад"); + connect(backBtn, &QPushButton::clicked, this, [this]() { + m_stack->setCurrentIndex(0); + }); + btnLayout->addWidget(backBtn); + btnLayout->addStretch(); + auto *nextBtn = new QPushButton("Далее"); + nextBtn->setStyleSheet("background-color: green; color: white; padding: 6px 24px;"); + connect(nextBtn, &QPushButton::clicked, this, [this]() { + if (m_pinCodeEdit->text().trimmed().isEmpty()) return; + m_app.pinCode = m_pinCodeEdit->text().trimmed(); + m_stack->setCurrentIndex(2); + startVerification(); + }); + btnLayout->addWidget(nextBtn); + layout->addLayout(btnLayout); + + return page; +} + +// ── Step 3: Progress ─────────────────────────────────────────────────────── + +QWidget *BankSetupWizard::createStep3() { + auto *page = new QWidget; + auto *layout = new QVBoxLayout(page); + layout->setAlignment(Qt::AlignTop); + + auto *title = new QLabel("

Идёт проверка банка

"); + title->setAlignment(Qt::AlignCenter); + layout->addWidget(title); + + auto *desc = new QLabel( + "Пожалуйста, не закрывайте приложение и не трогайте телефон.\n" + "Проверка может занять некоторое время." + ); + desc->setWordWrap(true); + desc->setAlignment(Qt::AlignCenter); + desc->setStyleSheet("color: gray; margin-bottom: 12px;"); + layout->addWidget(desc); + + m_progressBar = new QProgressBar; + m_progressBar->setRange(0, 100); + m_progressBar->setValue(0); + m_progressBar->setTextVisible(true); + layout->addWidget(m_progressBar); + + m_progressLabel = new QLabel("Подготовка..."); + m_progressLabel->setAlignment(Qt::AlignCenter); + m_progressLabel->setStyleSheet("margin-top: 8px;"); + layout->addWidget(m_progressLabel); + + layout->addStretch(); + + auto *btnLayout = new QHBoxLayout; + m_backBtn3 = new QPushButton("Назад"); + connect(m_backBtn3, &QPushButton::clicked, this, [this]() { + stopWorkerThread(); + m_progressBar->setValue(0); + m_progressLabel->setText("Подготовка..."); + m_stack->setCurrentIndex(1); + }); + btnLayout->addWidget(m_backBtn3); + btnLayout->addStretch(); + layout->addLayout(btnLayout); + + return page; +} + +// ── Step 4a: Success ─────────────────────────────────────────────────────── + +QWidget *BankSetupWizard::createStep4Success() { + m_successPage = new QWidget; + auto *layout = new QVBoxLayout(m_successPage); + layout->setAlignment(Qt::AlignTop); + + auto *title = new QLabel("

Проверка прошла успешно

"); + title->setAlignment(Qt::AlignCenter); + layout->addWidget(title); + + m_resultLabel = new QLabel; + m_resultLabel->setWordWrap(true); + m_resultLabel->setTextFormat(Qt::RichText); + layout->addWidget(m_resultLabel); + + auto *cardsTitle = new QLabel("Найденные карты: выберите одну активную"); + layout->addWidget(cardsTitle); + + m_cardsTable = new QTableWidget; + m_cardsTable->setSelectionMode(QAbstractItemView::NoSelection); + m_cardsTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_cardsTable->setFocusPolicy(Qt::NoFocus); + m_cardsTable->setColumnCount(4); + m_cardsTable->setHorizontalHeaderLabels({"Номер", "Сумма", "Валюта", "Активна"}); + m_cardsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + layout->addWidget(m_cardsTable); + + m_cardsGroup = new QButtonGroup(this); + m_cardsGroup->setExclusive(true); + connect(m_cardsGroup, &QButtonGroup::idClicked, this, [this](int id) { + m_selectedMaterialId = id; + m_finishBtn->setEnabled(true); + }); + + layout->addStretch(); + + auto *btnLayout = new QHBoxLayout; + btnLayout->addStretch(); + m_finishBtn = new QPushButton("Завершить"); + m_finishBtn->setStyleSheet("background-color: green; color: white; padding: 6px 24px;"); + m_finishBtn->setEnabled(false); + connect(m_finishBtn, &QPushButton::clicked, this, [this]() { + applyActiveCardSelection(); + accept(); + }); + btnLayout->addWidget(m_finishBtn); + layout->addLayout(btnLayout); + + return m_successPage; +} + +// ── Step 4b: Error ───────────────────────────────────────────────────────── + +QWidget *BankSetupWizard::createStep4Error() { + m_errorPage = new QWidget; + auto *layout = new QVBoxLayout(m_errorPage); + layout->setAlignment(Qt::AlignTop); + + auto *title = new QLabel("

Ошибка

"); + title->setAlignment(Qt::AlignCenter); + layout->addWidget(title); + + m_errorText = new QLabel; + m_errorText->setWordWrap(true); + m_errorText->setStyleSheet("color: red; margin: 12px 0;"); + m_errorText->setAlignment(Qt::AlignCenter); + layout->addWidget(m_errorText); + + layout->addStretch(); + + auto *btnLayout = new QHBoxLayout; + auto *backBtn = new QPushButton("Назад"); + connect(backBtn, &QPushButton::clicked, this, [this]() { + m_stack->setCurrentIndex(1); + }); + btnLayout->addWidget(backBtn); + btnLayout->addStretch(); + auto *restartBtn = new QPushButton("Начать заново"); + restartBtn->setStyleSheet("background-color: orange; color: white; padding: 6px 24px;"); + connect(restartBtn, &QPushButton::clicked, this, [this]() { + // Очищаем данные из БД если ещё не отправлены на сервер + const BankProfileInfo current = BankProfileDAO::getApplication(m_deviceId, m_app.code); + if (current.id >= 0 && current.bankProfileId.isEmpty()) { + MaterialDAO::deleteAccountsByApp(m_deviceId, m_app.code); + BankProfileDAO::deleteProfile(m_deviceId, m_app.code); + m_app.id = -1; + m_app.pinCodeChecked = false; + m_app.pinCode.clear(); + } + m_pinCodeEdit->clear(); + m_progressBar->setValue(0); + m_progressLabel->setText("Подготовка..."); + m_stack->setCurrentIndex(0); + }); + btnLayout->addWidget(restartBtn); + layout->addLayout(btnLayout); + + return m_errorPage; +} + +// ── Pause/Resume active profiles ─────────────────────────────────────────── + +static void toggleProfileOnServer(const BankProfileInfo &app, bool enable) { + if (app.bankProfileId.isEmpty()) return; + auto *thread = new QThread; + auto *net = new NetworkService; + net->moveToThread(thread); + net->addExtra("bank_profile_id", app.bankProfileId); + net->addExtra("enable", enable); + QObject::connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile); + QObject::connect(net, &NetworkService::finished, thread, &QThread::quit); + QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +void BankSetupWizard::pauseActiveProfiles() { + const QList apps = BankProfileDAO::getApplicationsByDeviceId(m_deviceId); + for (const BankProfileInfo &app : apps) { + if (app.status == "active") { + m_pausedProfiles.append(app.code); + BankProfileDAO::update(app.id, app.pinCode, app.comment, "off"); + toggleProfileOnServer(app, false); + qDebug() << "[BankSetupWizard] Paused profile:" << app.code; + } + } +} + +void BankSetupWizard::resumePausedProfiles() { + for (const QString &code : m_pausedProfiles) { + const BankProfileInfo app = BankProfileDAO::getApplication(m_deviceId, code); + if (app.id >= 0 && app.status == "off") { + BankProfileDAO::update(app.id, app.pinCode, app.comment, "active"); + toggleProfileOnServer(app, true); + qDebug() << "[BankSetupWizard] Resumed profile:" << app.code; + } + } + m_pausedProfiles.clear(); +} + +// ── Verification flow ────────────────────────────────────────────────────── + +void BankSetupWizard::setProgress(int value, const QString &text) { + m_progressBar->setValue(value); + m_progressLabel->setText(text); +} + +void BankSetupWizard::stopWorkerThread() { + if (!m_workerThread) return; + + QThread *t = m_workerThread; + m_workerThread = nullptr; + + t->requestInterruption(); + t->quit(); + if (t->wait(3000)) { + t->deleteLater(); + return; + } + // Воркер залип (скорее всего в AdbUtils::startProcess / QProcess::waitForFinished). + // Отсоединяем QThread от виджета, чтобы ~QWidget не удалил работающий поток + // (это триггерит qFatal в ~QThread), и откладываем удаление до естественного завершения. + qWarning() << "[BankSetupWizard] worker thread still running after 3s, detaching"; + t->setParent(nullptr); + connect(t, &QThread::finished, t, &QObject::deleteLater); +} + +void BankSetupWizard::startVerification() { + // Проверяем что устройство онлайн + const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); + if (device.status != "device") { + onVerificationError("Устройство не подключено.\nУбедитесь, что телефон подключён и попробуйте снова."); + return; + } + + // Save pin code to DB first via upsert + m_app.status = "off"; + m_app.install = true; + BankProfileDAO::upsertApplication(m_app); + + // Re-read to get the id + m_app = BankProfileDAO::getApplication(m_deviceId, m_app.code); + + setProgress(5, "Запуск проверки пин-кода..."); + m_backBtn3->setEnabled(true); + + runPinCheck(); +} + +void BankSetupWizard::runPinCheck() { + stopWorkerThread(); + m_workerThread = new QThread(this); + const QString deviceId = m_deviceId; + const QString appCode = m_app.code; + const int appId = m_app.id; + + if (appCode == "ozon") { + auto *worker = new Ozon::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, false, true); + worker->moveToThread(m_workerThread); + connect(m_workerThread, &QThread::started, worker, &Ozon::LoginAndCheckAccountsScript::start); + connect(worker, &Ozon::LoginAndCheckAccountsScript::finishedWithResult, this, + [this, appId, appCode](const QString &result) { + if (!result.isEmpty()) { + onVerificationError("Ошибка проверки пин-кода: " + result); + return; + } + BankProfileDAO::updatePinCodeStatus(appId, "yes", ""); + m_app.pinCodeChecked = true; + setProgress(33, "Пин-код проверен. Получение профиля..."); + runProfileCheck(); + }, Qt::QueuedConnection); + connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); + connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, m_workerThread, &QThread::quit); + } else if (appCode == "black") { + auto *worker = new Black::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, true); + worker->moveToThread(m_workerThread); + connect(m_workerThread, &QThread::started, worker, &Black::LoginAndCheckAccountsScript::start); + connect(worker, &Black::LoginAndCheckAccountsScript::finishedWithResult, this, + [this, appId](const QString &result) { + if (!result.isEmpty()) { + onVerificationError("Ошибка проверки пин-кода: " + result); + return; + } + BankProfileDAO::updatePinCodeStatus(appId, "yes", ""); + m_app.pinCodeChecked = true; + setProgress(33, "Пин-код проверен. Получение профиля..."); + runProfileCheck(); + }, Qt::QueuedConnection); + connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); + connect(worker, &Black::LoginAndCheckAccountsScript::finished, m_workerThread, &QThread::quit); + } else if (appCode == "dushanbe_city_bank") { + auto *worker = new Dushanbe::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, true); + worker->moveToThread(m_workerThread); + connect(m_workerThread, &QThread::started, worker, &Dushanbe::LoginAndCheckAccountsScript::start); + connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finishedWithResult, this, + [this, appId](const QString &result) { + if (!result.isEmpty()) { + onVerificationError("Ошибка проверки пин-кода: " + result); + return; + } + BankProfileDAO::updatePinCodeStatus(appId, "yes", ""); + m_app.pinCodeChecked = true; + setProgress(33, "Пин-код проверен. Получение профиля..."); + runProfileCheck(); + }, Qt::QueuedConnection); + connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); + connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finished, m_workerThread, &QThread::quit); + } else { + onVerificationError("Банк " + appCode + " не поддерживается для автоматической настройки."); + return; + } + + m_workerThread->start(); +} + +void BankSetupWizard::runProfileCheck() { + stopWorkerThread(); + m_workerThread = new QThread(this); + const QString deviceId = m_deviceId; + const QString appCode = m_app.code; + + auto onProfileResult = [this, deviceId, appCode](const QString &result) { + if (!result.isEmpty()) { + onVerificationError("Ошибка получения профиля: " + result); + return; + } + // Проверяем что bank_profile_id получен от сервера + const BankProfileInfo updated = BankProfileDAO::getApplication(deviceId, appCode); + if (updated.bankProfileId.isEmpty()) { + onVerificationError("Не удалось зарегистрировать банковский профиль на сервере.\n" + "Возможно, банк ещё не поддерживается сервером."); + return; + } + setProgress(66, "Профиль получен. Проверка карт..."); + runCardCheck(); + }; + + if (appCode == "ozon") { + auto *worker = new Ozon::GetProfileInfoScript(deviceId, appCode, nullptr); + worker->moveToThread(m_workerThread); + connect(m_workerThread, &QThread::started, worker, &Ozon::GetProfileInfoScript::start); + connect(worker, &Ozon::GetProfileInfoScript::finishedWithResult, this, onProfileResult, Qt::QueuedConnection); + connect(worker, &Ozon::GetProfileInfoScript::finished, worker, &QObject::deleteLater); + connect(worker, &Ozon::GetProfileInfoScript::finished, m_workerThread, &QThread::quit); + } else if (appCode == "black") { + auto *worker = new Black::GetProfileInfoScript(deviceId, appCode, nullptr); + worker->moveToThread(m_workerThread); + connect(m_workerThread, &QThread::started, worker, &Black::GetProfileInfoScript::start); + connect(worker, &Black::GetProfileInfoScript::finishedWithResult, this, onProfileResult, Qt::QueuedConnection); + connect(worker, &Black::GetProfileInfoScript::finished, worker, &QObject::deleteLater); + connect(worker, &Black::GetProfileInfoScript::finished, m_workerThread, &QThread::quit); + } else if (appCode == "dushanbe_city_bank") { + auto *worker = new Dushanbe::GetProfileInfoScript(deviceId, appCode, nullptr); + worker->moveToThread(m_workerThread); + connect(m_workerThread, &QThread::started, worker, &Dushanbe::GetProfileInfoScript::start); + connect(worker, &Dushanbe::GetProfileInfoScript::finishedWithResult, this, onProfileResult, Qt::QueuedConnection); + connect(worker, &Dushanbe::GetProfileInfoScript::finished, worker, &QObject::deleteLater); + connect(worker, &Dushanbe::GetProfileInfoScript::finished, m_workerThread, &QThread::quit); + } else { + onVerificationError("Банк " + appCode + " не поддерживается."); + return; + } + + m_workerThread->start(); +} + +void BankSetupWizard::runCardCheck() { + stopWorkerThread(); + m_workerThread = new QThread(this); + const QString deviceId = m_deviceId; + const QString appCode = m_app.code; + + auto onCardResult = [this, deviceId, appCode](const QString &result) { + if (!result.isEmpty()) { + onVerificationError("Ошибка получения карт: " + result); + return; + } + // Проверяем что хотя бы одна карта синхронизирована с сервером + const QList materials = MaterialDAO::getAccounts(deviceId, appCode); + bool hasSynced = false; + for (const auto &m : materials) { + if (!m.materialId.isEmpty()) { hasSynced = true; break; } + } + if (!hasSynced && !materials.isEmpty()) { + onVerificationError("Не удалось отправить данные карт на сервер.\n" + "Проверьте подключение к серверу."); + return; + } + setProgress(100, "Готово!"); + onVerificationSuccess(); + }; + + if (appCode == "ozon") { + auto *worker = new Ozon::GetCardInfoScript(deviceId, appCode, "", nullptr); + worker->moveToThread(m_workerThread); + connect(m_workerThread, &QThread::started, worker, &Ozon::GetCardInfoScript::start); + connect(worker, &Ozon::GetCardInfoScript::finishedWithResult, this, onCardResult, Qt::QueuedConnection); + connect(worker, &Ozon::GetCardInfoScript::finished, worker, &QObject::deleteLater); + connect(worker, &Ozon::GetCardInfoScript::finished, m_workerThread, &QThread::quit); + } else if (appCode == "black") { + auto *worker = new Black::GetCardInfoScript(deviceId, appCode, nullptr); + worker->moveToThread(m_workerThread); + connect(m_workerThread, &QThread::started, worker, &Black::GetCardInfoScript::start); + connect(worker, &Black::GetCardInfoScript::finishedWithResult, this, onCardResult, Qt::QueuedConnection); + connect(worker, &Black::GetCardInfoScript::finished, worker, &QObject::deleteLater); + connect(worker, &Black::GetCardInfoScript::finished, m_workerThread, &QThread::quit); + } else if (appCode == "dushanbe_city_bank") { + auto *worker = new Dushanbe::GetCardInfoScript(deviceId, appCode, nullptr); + worker->moveToThread(m_workerThread); + connect(m_workerThread, &QThread::started, worker, &Dushanbe::GetCardInfoScript::start); + connect(worker, &Dushanbe::GetCardInfoScript::finishedWithResult, this, onCardResult, Qt::QueuedConnection); + connect(worker, &Dushanbe::GetCardInfoScript::finished, worker, &QObject::deleteLater); + connect(worker, &Dushanbe::GetCardInfoScript::finished, m_workerThread, &QThread::quit); + } else { + onVerificationError("Банк " + appCode + " не поддерживается."); + return; + } + + m_workerThread->start(); +} + +void BankSetupWizard::onVerificationSuccess() { + // Re-read profile from DB + m_app = BankProfileDAO::getApplication(m_deviceId, m_app.code); + + QString profileHtml; + if (!m_app.fullName.isEmpty()) + profileHtml += "ФИО: " + m_app.fullName + "
"; + if (!m_app.phone.isEmpty()) + profileHtml += "Телефон: " + m_app.phone + "
"; + if (!m_app.email.isEmpty()) + profileHtml += "Email: " + m_app.email + "
"; + if (profileHtml.isEmpty()) + profileHtml = "Данные профиля не получены"; + m_resultLabel->setText(profileHtml); + + // Load cards + const QList cards = MaterialDAO::getAccounts(m_deviceId, m_app.code); + + for (const MaterialInfo &c : cards) { + if (c.cardNumber.isEmpty()) { + onVerificationError("Не удалось получить полный номер карты (распознаны только последние 4 цифры).\n" + "Попробуйте повторить настройку."); + return; + } + } + + // Очистка предыдущих радио-кнопок, если мастер перезапускался + for (QAbstractButton *b : m_cardsGroup->buttons()) { + m_cardsGroup->removeButton(b); + b->deleteLater(); + } + m_selectedMaterialId = -1; + + m_cardsTable->setRowCount(cards.size()); + for (int i = 0; i < cards.size(); ++i) { + const MaterialInfo &c = cards[i]; + m_cardsTable->setItem(i, 0, new QTableWidgetItem(c.cardNumber)); + m_cardsTable->setItem(i, 1, new QTableWidgetItem(QString::number(c.amount))); + m_cardsTable->setItem(i, 2, new QTableWidgetItem(c.currency)); + + auto *radio = new QRadioButton; + auto *wrapper = new QWidget; + auto *h = new QHBoxLayout(wrapper); + h->addWidget(radio); + h->setAlignment(Qt::AlignCenter); + h->setContentsMargins(0, 0, 0, 0); + m_cardsTable->setCellWidget(i, 3, wrapper); + m_cardsGroup->addButton(radio, c.id); + } + m_cardsTable->resizeColumnsToContents(); + + if (cards.isEmpty()) { + m_cardsTable->setRowCount(1); + auto *item = new QTableWidgetItem("Карты не найдены"); + item->setForeground(QBrush(Qt::gray)); + m_cardsTable->setItem(0, 0, item); + m_cardsTable->setSpan(0, 0, 1, 4); + m_finishBtn->setEnabled(true); // нечего выбирать — выйти можно + } else if (cards.size() == 1) { + // Одна карта — автовыбор, скрываем колонку с радио + m_selectedMaterialId = cards.first().id; + m_cardsTable->setColumnHidden(3, true); + m_finishBtn->setEnabled(true); + } else { + m_cardsTable->setColumnHidden(3, false); + m_selectedMaterialId = cards.first().id; + if (auto *first = m_cardsGroup->button(m_selectedMaterialId)) { + first->setChecked(true); + } + m_finishBtn->setEnabled(true); + } + + m_stack->setCurrentIndex(3); +} + +void BankSetupWizard::applyActiveCardSelection() { + if (m_selectedMaterialId < 0) return; + + const QList deactivated = + MaterialDAO::activateExclusive(m_deviceId, m_app.code, m_selectedMaterialId); + + const MaterialInfo selected = MaterialDAO::getAccountById(m_selectedMaterialId); + + auto sendToggle = [this](const QString &materialId, bool enable) { + if (materialId.isEmpty()) return; + auto *thread = new QThread; + auto *net = new NetworkService; + net->moveToThread(thread); + net->addExtra("material_id", materialId); + net->addExtra("enable", enable); + NetworkService::connectErrorDialog(this, net); + QObject::connect(thread, &QThread::started, net, &NetworkService::toggleMaterial); + QObject::connect(net, &NetworkService::finished, thread, &QThread::quit); + QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); + }; + + sendToggle(selected.materialId, true); + for (const MaterialInfo &m : deactivated) { + sendToggle(m.materialId, false); + } +} + +static QString humanReadableError(const QString &error) { + // Определяем этап + QString stage; + if (error.contains("пин-кода")) + stage = "Проверка пин-кода: "; + else if (error.contains("профиля")) + stage = "Получение профиля: "; + else if (error.contains("карт")) + stage = "Получение карт: "; + + if (error.contains("Device or app not found")) + return stage + "Устройство или приложение не найдено.\nУбедитесь, что телефон подключён и приложение установлено."; + if (error.contains("Cant open the home screen") || error.contains("Cannot reach home screen")) + return stage + "Не удалось открыть главный экран приложения.\nПопробуйте перезапустить приложение на телефоне и повторить."; + if (error.contains("Cannot find user name") || error.contains("Cannot find Hola button")) + return stage + "Не удалось найти данные профиля на главном экране.\nВозможно, приложение обновилось или экран выглядит иначе."; + if (error.contains("Profile screen not detected") || error.contains("Profile page not detected")) + return stage + "Не удалось открыть страницу профиля.\nПопробуйте повторить позже."; + if (error.contains("Side menu not detected")) + return stage + "Не удалось открыть боковое меню приложения.\nПопробуйте повторить."; + if (error.contains("Cannot find Tu Informaci") || error.contains("Cannot find Tu CVU")) + return stage + "Не удалось найти нужный раздел в меню приложения.\nВозможно, приложение обновилось."; + if (error.contains("CVU screen not detected") || error.contains("Cannot parse CVU")) + return stage + "Не удалось получить данные карты.\nПопробуйте повторить."; + if (error.contains("No cards found")) + return stage + "Карты не найдены на главном экране.\nУбедитесь, что в приложении есть привязанные карты."; + if (error.contains("не поддерживается") || error.contains("не подключено")) + return error; + return stage + "Произошла непредвиденная ошибка.\nПопробуйте повторить позже."; +} + +void BankSetupWizard::onVerificationError(const QString &error) { + qWarning() << "[BankSetupWizard] Error:" << error; + m_errorText->setText(humanReadableError(error)); + m_stack->setCurrentIndex(4); +} diff --git a/views/bank/BankSetupWizard.h b/views/bank/BankSetupWizard.h new file mode 100644 index 0000000..f91e455 --- /dev/null +++ b/views/bank/BankSetupWizard.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "db/BankProfileInfo.h" + +class BankSetupWizard final : public QDialog { + Q_OBJECT + +public: + BankSetupWizard(QWidget *parent, const QString &deviceId, const BankProfileInfo &app); + ~BankSetupWizard() override; + +private: + QString m_deviceId; + BankProfileInfo m_app; + + QStackedWidget *m_stack; + + // Step 2 + QLineEdit *m_pinCodeEdit; + + // Step 3 + QLabel *m_progressLabel; + QProgressBar *m_progressBar; + QPushButton *m_backBtn3; + + // Step 4 + QLabel *m_resultLabel; + QTableWidget *m_cardsTable; + QWidget *m_successPage; + QWidget *m_errorPage; + QLabel *m_errorText; + QPushButton *m_finishBtn; + QButtonGroup *m_cardsGroup; + int m_selectedMaterialId = -1; + + QThread *m_workerThread = nullptr; + QStringList m_pausedProfiles; // коды профилей, выключенных на время мастера + + void pauseActiveProfiles(); + void resumePausedProfiles(); + + QWidget *createStep1(); + QWidget *createStep2(); + QWidget *createStep3(); + QWidget *createStep4Success(); + QWidget *createStep4Error(); + + void startVerification(); + void runPinCheck(); + void runProfileCheck(); + void runCardCheck(); + + void onVerificationSuccess(); + void onVerificationError(const QString &error); + void stopWorkerThread(); + void applyActiveCardSelection(); + + void setProgress(int value, const QString &text); +}; diff --git a/views/bank/TransactionWindow.cpp b/views/bank/TransactionWindow.cpp new file mode 100644 index 0000000..b141339 --- /dev/null +++ b/views/bank/TransactionWindow.cpp @@ -0,0 +1,191 @@ +#include "TransactionWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dao/MaterialDAO.h" +#include "dao/TransactionDAO.h" + +class AmountDelegate : public QStyledItemDelegate { +public: + explicit AmountDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) { + } + + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + + // Получаем значение суммы + QString text = index.data().toString(); + bool isNegative = text.toDouble() < 0; + + // Настраиваем цвета + if (isNegative) { + opt.backgroundBrush = QColor(255, 230, 230); // Светло-красный фон + opt.palette.setColor(QPalette::Text, QColor(255, 0, 0)); // Красный текст + } else { + opt.backgroundBrush = QColor(230, 255, 230); // Светло-зеленый фон + opt.palette.setColor(QPalette::Text, QColor(0, 128, 0)); // Зеленый текст + } + + // Рисуем ячейку + QStyledItemDelegate::paint(painter, opt, index); + } +}; + +QTableWidgetItem *createTableWidget(const QString &text) { + QTableWidgetItem *item = new QTableWidgetItem(text); + item->setTextAlignment(Qt::AlignLeft | Qt::AlignTop); + return item; +} + +TransactionWindow::TransactionWindow(const int accountId, QWidget *parent): QDialog(parent) { + setWindowTitle("Транзакции"); + setMinimumSize(800, 600); + + auto *mainLayout = new QVBoxLayout(this); + + // Скролл-область + auto *scrollArea = new QScrollArea(this); + scrollArea->setWidgetResizable(true); + + // Виджет-контейнер, в котором будет layout с таблицами и лейблами + auto *contentWidget = new QWidget; + auto *layout = new QVBoxLayout(contentWidget); + + // Убираем отступы и рамку + contentWidget->setContentsMargins(0, 0, 0, 0); // Убираем отступы + layout->setContentsMargins(0, 0, 0, 0); // Убираем отступы у layout + scrollArea->setStyleSheet("border: none;"); // Убираем рамку + layout->setAlignment(Qt::AlignTop); + + MaterialInfo account = MaterialDAO::getAccountById(accountId); + QList transactions = TransactionDAO::getTransactions(accountId); + + QTableWidget *tableWidget = new QTableWidget(this); + tableWidget->setStyleSheet( + "QTableWidget {" + " border: 1px solid grey;" + " gridline-color: lightgrey;" + " background-color: #f9f9f9;" + " font-size: 14px;" + "}" + "QHeaderView::section {" + " background-color: #e0e0e0;" + " border: 1px solid grey;" + " padding: 4px;" + " font-weight: bold;" + " text-align: left center;" + "}" + "QTableWidget::item {" + " padding: 4px;" + " border: none;" + " text-align: left top;" + "}" + "QTableWidget::item:selected {" + " background-color: #cce5ff;" + " color: #003366;" + "}" + "QTableCornerButton::section {" + " background-color: #e0e0e0;" + " border: 1px solid grey;" + "}" + ); + + tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); + + tableWidget->setRowCount(transactions.length()); + tableWidget->setColumnCount(7); + tableWidget->setHorizontalHeaderLabels({ + "Дата и время", + "Сумма", + "Банк", + "Телефон", + "Статус", + "Описание", + "" + }); + tableWidget->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter); + for (int i = 0; i < 5; ++i) { + tableWidget->horizontalHeader()->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } + tableWidget->horizontalHeader()->setSectionResizeMode(5, QHeaderView::Stretch); + + tableWidget->setItemDelegateForColumn(1, new AmountDelegate(tableWidget)); + + for (int k = 0; k < transactions.size(); ++k) { + TransactionInfo tr = transactions[k]; + tableWidget->setItem(k, 0, createTableWidget(tr.bankTime)); + tableWidget->setItem(k, 1, createTableWidget(QString::number(tr.amount))); + tableWidget->setItem(k, 2, createTableWidget(tr.bankName)); + tableWidget->setItem(k, 3, createTableWidget(tr.phone)); + tableWidget->setItem(k, 4, createTableWidget(transactionStatusToString(tr.status))); + + QTableWidgetItem *item = new QTableWidgetItem(tr.description); + item->setTextAlignment(Qt::AlignLeft | Qt::AlignTop); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + item->setSizeHint(QSize(100, tr.description.length() / 2)); + tableWidget->setItem(k, 5, item); + + if (!tr.receiptImage.isEmpty()) { + auto *docBtn = new QPushButton("Документы", tableWidget); + docBtn->setStyleSheet("background-color: #1565C0; color: white;"); + tableWidget->setCellWidget(k, 6, docBtn); + + connect(docBtn, &QPushButton::clicked, this, [this, tr]() { + QPixmap pixmap; + pixmap.loadFromData(tr.receiptImage); + if (pixmap.isNull()) return; + + auto *imgDialog = new QDialog(this); + imgDialog->setWindowTitle("Документ"); + imgDialog->setAttribute(Qt::WA_DeleteOnClose); + auto *dlgLayout = new QVBoxLayout(imgDialog); + auto *imgLabel = new QLabel(imgDialog); + imgLabel->setPixmap(pixmap.scaled(600, 1000, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + dlgLayout->addWidget(imgLabel); + imgDialog->resize(imgLabel->pixmap().size()); + imgDialog->show(); + }); + } + } + layout->addWidget(tableWidget); + tableWidget->resizeColumnsToContents(); + tableWidget->resizeRowsToContents(); + + // Подгоняем ширину окна под содержимое таблицы + int totalWidth = tableWidget->verticalHeader()->width() + tableWidget->frameWidth() * 2 + 20; + for (int i = 0; i < tableWidget->columnCount(); ++i) { + totalWidth += tableWidget->columnWidth(i); + } + resize(qMax(800, totalWidth), 600); + + // for (int row = 0; row < tableWidget->rowCount(); ++row) { + // for (int col = 0; col < tableWidget->columnCount(); ++col) { + // QTableWidgetItem *item = tableWidget->item(row, col); + // if (item) { + // item->setTextAlignment(Qt::AlignLeft | Qt::AlignTop); + // } + // } + // } + + // Добавим вертикальный Spacer в конец, чтобы прокручиваемая область не растягивала контент + // QSpacerItem *spacer = new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding); + // layout->addItem(spacer); + + contentWidget->setLayout(layout); + scrollArea->setWidget(contentWidget); + mainLayout->addWidget(scrollArea); + setLayout(mainLayout); + setWindowTitle("Транзакции"); + + setModal(true); +} diff --git a/views/TransactionWindow.h b/views/bank/TransactionWindow.h similarity index 100% rename from views/TransactionWindow.h rename to views/bank/TransactionWindow.h diff --git a/views/common/PaddedItemDelegate.h b/views/common/PaddedItemDelegate.h new file mode 100644 index 0000000..7ff99bc --- /dev/null +++ b/views/common/PaddedItemDelegate.h @@ -0,0 +1,35 @@ +#pragma once +#include +#include + +class PaddedItemDelegate final : public QStyledItemDelegate { + Q_OBJECT + +public: + explicit PaddedItemDelegate(const QMargins &margins, QObject *parent = nullptr) + : QStyledItemDelegate(parent), m_margins(margins) { + } + + void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override { + QStyledItemDelegate::initStyleOption(option, index); + // «вырезаем» рамки со всех сторон + option->rect = option->rect.adjusted( + m_margins.left(), + m_margins.top(), + -m_margins.right(), + -m_margins.bottom() + ); + } + + // расширяем sizeHint — чтобы ResizeToContents «увидел» отступы + [[nodiscard]] QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override { + const QSize baseSize = QStyledItemDelegate::sizeHint(option, index); + return QSize( + baseSize.width() + m_margins.left() + m_margins.right(), + baseSize.height() + m_margins.top() + m_margins.bottom() + ); + } + +private: + QMargins m_margins; +}; diff --git a/views/device/DeviceSettingsWindow.cpp b/views/device/DeviceSettingsWindow.cpp new file mode 100644 index 0000000..5df5f20 --- /dev/null +++ b/views/device/DeviceSettingsWindow.cpp @@ -0,0 +1,194 @@ +#include "DeviceSettingsWindow.h" + +#include +#include +#include +#include +#include +#include + +#include +#include "bank/AccountSettingsWindow.h" +#include "bank/BankSetupWizard.h" +#include "dao/BankProfileDAO.h" +#include "dao/SettingsDAO.h" +#include "db/BankProfileInfo.h" + +static QString formatStatus(const BankProfileInfo &app) { + if (!app.install) return "не установлен"; + if (app.status == "active") { + if (!app.pinCodeChecked) + return app.pinCode.isEmpty() ? "нет пин-код" : "пин-код не проверен"; + return "работает"; + } + if (app.status == "off") return "выключено"; + return app.status; +} + +DeviceSettingsWindow::DeviceSettingsWindow(QWidget *parent, const QString &deviceId, const QString &deviceName) + : QWidget(parent, + Qt::Window + | Qt::WindowTitleHint + | Qt::WindowSystemMenuHint + | Qt::WindowCloseButtonHint + | Qt::WindowMinMaxButtonsHint), + m_deviceId(deviceId), + m_deviceName(deviceName) { + setWindowTitle("Настройки устройства [" + m_deviceName + "]"); + setMinimumSize(600, 300); + resize(700, 400); + + auto *mainLayout = new QVBoxLayout(this); + + auto *scrollArea = new QScrollArea(this); + scrollArea->setWidgetResizable(true); + + auto *contentWidget = new QWidget; + auto *layout = new QVBoxLayout(contentWidget); + layout->setContentsMargins(4, 4, 4, 4); + layout->setAlignment(Qt::AlignTop); + + auto *tableWidget = new QTableWidget(this); + tableWidget->setSelectionMode(QAbstractItemView::NoSelection); + tableWidget->setFocusPolicy(Qt::NoFocus); + tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); + + reloadContent(tableWidget); + tableWidget->resizeColumnsToContents(); + tableWidget->resizeRowsToContents(); + + layout->addWidget(tableWidget); + + contentWidget->setLayout(layout); + scrollArea->setWidget(contentWidget); + mainLayout->addWidget(scrollArea); + setLayout(mainLayout); +} + +void DeviceSettingsWindow::reloadContent(QTableWidget *tableWidget) { + QList apps = BankProfileDAO::getAllApplicationsByDeviceId(m_deviceId); + + const QSettings configSettings("config.ini", QSettings::IniFormat); + const QString profileCountry = SettingsDAO::get("country"); + + // Фильтруем уже сохранённые в БД банки по стране профиля + if (!profileCountry.isEmpty()) { + for (auto it = apps.begin(); it != apps.end();) { + const QString bankCountry = configSettings.value(it->code + "/country").toString(); + if (bankCountry.compare(profileCountry, Qt::CaseInsensitive) != 0) { + it = apps.erase(it); + } else { + ++it; + } + } + } + + // Дополняем список банками из config.ini, которых нет в БД + const QStringList banks = configSettings.value("apps/list").toStringList(); + QSet existingCodes; + for (const BankProfileInfo &a : apps) { + existingCodes.insert(a.code); + } + for (const QString &rawBank : banks) { + const QString bank = rawBank.trimmed(); + if (bank.isEmpty() || existingCodes.contains(bank)) continue; + if (!profileCountry.isEmpty()) { + const QString bankCountry = configSettings.value(bank + "/country").toString(); + if (bankCountry.compare(profileCountry, Qt::CaseInsensitive) != 0) continue; + } + BankProfileInfo stub; + stub.id = -1; + stub.deviceId = m_deviceId; + stub.code = bank; + stub.name = configSettings.value(bank + "/name", bank).toString(); + stub.package = configSettings.value(bank + "/android_package_name").toString(); + stub.currency = configSettings.value(bank + "/currency").toString(); + stub.status = "off"; + stub.install = false; + apps.append(stub); + } + + tableWidget->clearContents(); + tableWidget->setRowCount(apps.size()); + tableWidget->setColumnCount(8); + tableWidget->setHorizontalHeaderLabels({"", "Приложение", "Пакет", "Валюта", "Статус", "Версия на телефоне", "Версия ARC", ""}); + + tableWidget->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter); + for (int i = 0; i < 8; ++i) + tableWidget->horizontalHeader()->setSectionResizeMode(i, QHeaderView::ResizeToContents); + tableWidget->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch); + + for (int k = 0; k < apps.size(); ++k) { + const BankProfileInfo &app = apps[k]; + + // Иконка + auto *icon = new QLabel(tableWidget); + QPixmap pixmap(QCoreApplication::applicationDirPath() + "/images/" + app.code + "_icon.png"); + icon->setPixmap(pixmap.scaled(20, 20, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + icon->setStyleSheet("background-color: transparent; padding: 0 4px 0 0;"); + icon->setMinimumSize(24, 20); + tableWidget->setCellWidget(k, 0, icon); + + // Название + auto *nameItem = new QTableWidgetItem(app.name); + nameItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + tableWidget->setItem(k, 1, nameItem); + + // Пакет + auto *packageItem = new QTableWidgetItem(app.package); + packageItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + tableWidget->setItem(k, 2, packageItem); + + // Валюта + auto *currencyItem = new QTableWidgetItem(app.currency); + currencyItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + tableWidget->setItem(k, 3, currencyItem); + + // Статус + auto *statusItem = new QTableWidgetItem(formatStatus(app)); + statusItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + if (app.status != "active" || !app.pinCodeChecked) + statusItem->setForeground(QBrush(Qt::red)); + else + statusItem->setForeground(QBrush(Qt::darkGreen)); + tableWidget->setItem(k, 4, statusItem); + + // Версия на телефоне (из БД, обновляется через adb) + auto *deviceVersionItem = new QTableWidgetItem(app.appVersion); + deviceVersionItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + tableWidget->setItem(k, 5, deviceVersionItem); + + // Версия ARC (из config.ini) + const QString arcVersion = configSettings.value(app.code + "/app_version").toString(); + auto *arcVersionItem = new QTableWidgetItem(arcVersion); + arcVersionItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); + tableWidget->setItem(k, 6, arcVersionItem); + + // Кнопка "Настроить" + if (app.install) { + auto *settingsBtn = new QPushButton("Настроить", tableWidget); + settingsBtn->setStyleSheet("background-color: green; color: white; border-radius: 4px; height: 26px;"); + tableWidget->setCellWidget(k, 7, settingsBtn); + + connect(settingsBtn, &QPushButton::clicked, this, [this, app, tableWidget]() { + const BankProfileInfo dbApp = BankProfileDAO::getApplication(m_deviceId, app.code); + if (dbApp.id < 0 || (!dbApp.pinCodeChecked && dbApp.bankProfileId.isEmpty())) { + auto *wizard = new BankSetupWizard(this, m_deviceId, dbApp.id >= 0 ? dbApp : app); + wizard->setAttribute(Qt::WA_DeleteOnClose); + connect(wizard, &QDialog::finished, this, [this, tableWidget]() { + reloadContent(tableWidget); + tableWidget->resizeColumnsToContents(); + }); + wizard->show(); + } else { + auto *window = new AccountSettingsWindow(nullptr, m_deviceId, m_deviceName, dbApp); + window->setWindowFlags(window->windowFlags() | Qt::Window); + window->setAttribute(Qt::WA_DeleteOnClose); + window->show(); + window->raise(); + window->activateWindow(); + } + }); + } + } +} diff --git a/views/device/DeviceSettingsWindow.h b/views/device/DeviceSettingsWindow.h new file mode 100644 index 0000000..aa6a7c4 --- /dev/null +++ b/views/device/DeviceSettingsWindow.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +class DeviceSettingsWindow final : public QWidget { + Q_OBJECT + +public: + DeviceSettingsWindow(QWidget *parent, const QString &deviceId, const QString &deviceName); + +private: + QString m_deviceId; + QString m_deviceName; + + void reloadContent(QTableWidget *tableWidget); +}; diff --git a/views/device/DeviceWidget.cpp b/views/device/DeviceWidget.cpp new file mode 100644 index 0000000..01e3f6c --- /dev/null +++ b/views/device/DeviceWidget.cpp @@ -0,0 +1,699 @@ +#include "DeviceWidget.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bank/AccountSettingsWindow.h" +#include "bank/BankSetupWizard.h" +#include "dao/BankProfileDAO.h" +#include "bank/BankSettingsWindow.h" +#include "DeviceSettingsWindow.h" +#include "dao/BankProfileDAO.h" +#include "dao/MaterialDAO.h" +#include "dao/DeviceDAO.h" +#include "dao/EventDAO.h" +#include "dao/SettingsDAO.h" +#include "db/BankProfileInfo.h" +#include "net/NetworkService.h" +#include "adb/AdbUtils.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +QString getColorByStatus(const BankProfileInfo &app) { + if (!app.install) { + return "black"; + } + if (app.status == "active") { + if (!app.pinCodeChecked) { + return "orange"; + } + return "green"; + } else if (app.status == "off") { + return "red"; + } else { + return "red"; + } +} + +static void sendTaskErrorToServer(const QString &type, const QString &materialId, const QString &bankName) { + QJsonObject errData; + errData["status"] = "error"; + errData["event_type"] = type; + errData["description"] = "Bank profile disabled"; + + QJsonObject obj; + obj["type"] = "ERROR"; + obj["material_id"] = materialId; + obj["bank_name"] = bankName; + obj["data"] = errData; + + auto *thread = new QThread; + auto *net = new NetworkService; + net->moveToThread(thread); + net->setPayload(QJsonDocument(obj).toJson()); + QObject::connect(thread, &QThread::started, net, &NetworkService::postTaskResult); + QObject::connect(net, &NetworkService::finished, thread, &QThread::quit); + QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +void sendBankStatus(const QString &deviceId, const QString &code, const QString &status, + const std::function &onResult = nullptr) { + const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, code); + if (app.bankProfileId.isEmpty()) { + qWarning() << "[DeviceWidget] sendBankStatus: no bankProfileId for" << code; + if (onResult) onResult(-1); + return; + } + + // При выключении — отменяем ожидающие задачи + if (status != "active") { + const QList cancelled = EventDAO::cancelWaitingByBank(deviceId, code); + for (const EventInfo &e : cancelled) { + sendTaskErrorToServer(eventTypeToString(e.type), e.externalId, e.bankName); + EventDAO::markSentToServer(e.id); + } + } + + const bool enable = (status == "active"); + auto *thread = new QThread; + auto *net = new NetworkService; + net->moveToThread(thread); + net->addExtra("bank_profile_id", app.bankProfileId); + net->addExtra("enable", enable); + QObject::connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile); + if (onResult) { + QObject::connect(net, &NetworkService::finishedWithResult, net, + [onResult](int result) { onResult(result); }, + Qt::QueuedConnection); + } + QObject::connect(net, &NetworkService::finished, thread, &QThread::quit); + QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +void DeviceWidget::buildBankRow(const BankProfileInfo &app, QWidget *rowWidget, QWidget *parent, bool isOffline) { + rowWidget->setContentsMargins(0, 0, 0, 0); + rowWidget->setStyleSheet( + "background-color: rgba(0, 0, 0, 0.6);" // полупрозрачный фон + "border-radius: 4px;" // скруглённые углы, если нужно + ); + + // вешаем на контейнер ваш QHBoxLayout + auto *hbox = new QHBoxLayout(rowWidget); + hbox->setContentsMargins(2, 1, 1, 1); + hbox->setSpacing(0); + + auto *icon = new QLabel(parent); + QPixmap pixmap(QString("%1/images/%2_icon.png").arg(QCoreApplication::applicationDirPath(), app.code)); + icon->setPixmap(pixmap); + icon->setPixmap(pixmap.scaled(18, 18, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + icon->setStyleSheet( + "background-color: transparent;" + "margin: 0px;" + ); + + + QString labelText; + if (!app.fullName.isEmpty()) { + labelText = app.fullName; + if (!app.phone.isEmpty()) { + labelText += "\n" + app.phone; + } + } else { + labelText = app.name; + } + auto *label = new QLabel(labelText, parent); + label->setStyleSheet( + "background-color: transparent;" + "margin: 0px;" + "font-size: 10px;" + "color: white;" + ); + + auto *circle = new QLabel(parent); + circle->setFixedSize(12, 12); + const QString color = getColorByStatus(app); + circle->setStyleSheet( + "margin: 0px;" + "background-color: " + color + "; " + "border-radius: 6px;" + ); + // собираем + if (app.install && app.pinCodeChecked && (app.status == "active" || app.status == "off")) { + auto *btn1 = new QPushButton(parent); + bool turnOn = app.pinCodeChecked && app.status == "active"; + QIcon btnIcon( + QCoreApplication::applicationDirPath() + (turnOn ? "/images/turn_off.png" : "/images/turn_on.png")); + btn1->setIcon(btnIcon); + btn1->setIconSize(QSize(16, 16)); + btn1->setFlat(true); + btn1->setStyleSheet( + "margin: 0px;" + "background-color: transparent;" + "border: none;" + ); + + if (isOffline) { + btn1->setEnabled(false); + } + connect(btn1, &QPushButton::clicked, this, [app]() { + const QString newStatus = app.status == "active" ? "off" : "active"; + QMessageBox msgBox(nullptr); + msgBox.setWindowTitle(newStatus == "off" ? "Отключить банк?" : "Включить банк?"); + msgBox.setText( + "Вы уверены, что хотите " + + QString(newStatus == "off" ? "отключить от работы" : "включить") + + " данный банк: " + + app.name + "?"); + QPushButton *confirmBtn = msgBox.addButton(newStatus == "off" ? "Отключить" : "Включить", + QMessageBox::AcceptRole); + QPushButton *cancelBtn = msgBox.addButton("Отмена", QMessageBox::RejectRole); + msgBox.setDefaultButton(cancelBtn); + msgBox.setModal(true); + msgBox.exec(); + + if (msgBox.clickedButton() == confirmBtn) { + // При включении — проверяем что есть хотя бы одна активная карта + if (newStatus == "active") { + const QList materials = MaterialDAO::getAccounts(app.deviceId, app.code); + bool hasActive = false; + for (const auto &m : materials) { + if (m.status == MaterialStatus::Active) { hasActive = true; break; } + } + if (!hasActive) { + QMessageBox::warning(nullptr, "Невозможно включить", + "Ни одна карта не активна.\nВключите хотя бы одну карту перед включением профиля."); + return; + } + } + if (!BankProfileDAO::update(app.id, app.pinCode, app.comment, newStatus)) { + qDebug() << "Cant update bank data"; + } else { + // Правило #3: при неудачном включении — откат локально + сообщение. + // При неудачном выключении — локаль остаётся off (источник истины), + // 60s sync дотянет сервер. + const bool enable = (newStatus == "active"); + sendBankStatus(app.deviceId, app.code, newStatus, + [app, enable](int result) { + if (result == 0) return; + if (enable) { + BankProfileDAO::update(app.id, app.pinCode, app.comment, "off"); + QMessageBox::warning(nullptr, "Ошибка", + "Не удалось включить банк на сервере.\n" + "Локальное состояние возвращено в выключено."); + } else { + qWarning() << "[DeviceWidget] PATCH disable failed for" + << app.code << "— sync retries via 60s timer"; + } + }); + // При включении — включаем на сервере все локально активные материалы + if (newStatus == "active") { + const QList materials = MaterialDAO::getAccounts(app.deviceId, app.code); + for (const auto &mat : materials) { + if (mat.status == MaterialStatus::Active && !mat.materialId.isEmpty()) { + auto *thr = new QThread; + auto *ns = new NetworkService; + ns->moveToThread(thr); + ns->addExtra("material_id", mat.materialId); + ns->addExtra("enable", true); + QObject::connect(thr, &QThread::started, ns, &NetworkService::toggleMaterial); + QObject::connect(ns, &NetworkService::finished, thr, &QThread::quit); + QObject::connect(ns, &NetworkService::finished, ns, &QObject::deleteLater); + QObject::connect(thr, &QThread::finished, thr, &QThread::deleteLater); + thr->start(); + } + } + } + } + } + }); + hbox->addWidget(btn1); + } + + auto *btn2 = new QPushButton(parent); + QIcon btn2Icon(QCoreApplication::applicationDirPath() + "/images/cogwheel.png"); + btn2->setIcon(btn2Icon); + btn2->setIconSize(QSize(16, 16)); + btn2->setFlat(true); + btn2->setStyleSheet( + "margin: 0px;" + "background-color: transparent;" + "border: none;" + ); + // в конструкторе окна или сразу после создания btn2: + connect(btn2, &QPushButton::pressed, this, [btn2] { + auto anim = new QPropertyAnimation(btn2, "geometry"); + anim->setDuration(120); + QRect orig = btn2->geometry(); + QRect smaller = orig.adjusted(orig.width() * 0.05, + orig.height() * 0.05, + -orig.width() * 0.05, + -orig.height() * 0.05); + anim->setStartValue(orig); + anim->setKeyValueAt(0.5, smaller); + anim->setEndValue(orig); + anim->setEasingCurve(QEasingCurve::InOutQuad); + anim->start(QAbstractAnimation::DeleteWhenStopped); + }); + + setOpenBanksSettings(btn2, app); + + hbox->addWidget(icon); + hbox->addWidget(label, 1); + hbox->addWidget(circle); + hbox->addWidget(btn2); + + // hbox->addStretch(); +} + +void DeviceWidget::setOpenBanksSettings(QPushButton *button, const BankProfileInfo &app) { + QString deviceId = m_device.id; + QString deviceName = m_device.name; + connect(button, &QPushButton::clicked, this, [deviceId, deviceName, app]() { + const BankProfileInfo dbApp = BankProfileDAO::getApplication(deviceId, app.code); + if (dbApp.id < 0 || (!dbApp.pinCodeChecked && dbApp.bankProfileId.isEmpty())) { + auto *wizard = new BankSetupWizard(nullptr, deviceId, dbApp.id >= 0 ? dbApp : app); + wizard->setAttribute(Qt::WA_DeleteOnClose); + wizard->show(); + } else { + static QPointer bankSettingsWindow; + if (!bankSettingsWindow) { + bankSettingsWindow = new AccountSettingsWindow(nullptr, deviceId, deviceName, dbApp); + bankSettingsWindow->setWindowFlags(bankSettingsWindow->windowFlags() | Qt::Window); + bankSettingsWindow->setAttribute(Qt::WA_DeleteOnClose); + } + bankSettingsWindow->show(); + bankSettingsWindow->raise(); + bankSettingsWindow->activateWindow(); + } + }); +} + +DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(parent), m_device(device) { + auto *layout = new QGridLayout(this); + layout->setContentsMargins(2, 2, 2, 2); + layout->setSpacing(0); + this->setStyleSheet("background-color: lightgray;padding: 2px;"); + + auto *imageLabel = new QLabel(this); + if (!device.image.isEmpty()) { + QImage img; + img.loadFromData(device.image, "JPG"); + imageLabel->setPixmap(QPixmap::fromImage(img).scaled(300, 450, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + } else { + imageLabel->setFixedSize(300, 450); + imageLabel->setStyleSheet("background-color: #555;"); + } + imageLabel->setAlignment(Qt::AlignCenter); + layout->addWidget(imageLabel, 0, 0); + + auto *statusLabel = new QLabel(QString("%1 %2%").arg(device.name).arg(device.battery), this); + statusLabel->setWordWrap(true); + statusLabel->setMaximumWidth(300); + statusLabel->setStyleSheet( + "background-color: rgba(0, 0, 0, 0.5);" + "color: white;" + "font-size: 10px;" + ); + layout->addWidget(statusLabel, 0, 0, Qt::AlignTop | Qt::AlignLeft); + + // Кнопки настроек и удаления (справа сверху) + auto *topRightWidget = new QWidget(this); + topRightWidget->setStyleSheet("background: transparent;"); + auto *topRightLayout = new QVBoxLayout(topRightWidget); + topRightLayout->setContentsMargins(0, 0, 0, 0); + topRightLayout->setSpacing(2); + + auto *settingsBtn = new QPushButton(this); + settingsBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/cogwheel.png")); + settingsBtn->setIconSize(QSize(20, 20)); + settingsBtn->setFixedSize(28, 28); + settingsBtn->setFlat(true); + settingsBtn->setStyleSheet( + "background-color: rgba(0, 0, 0, 0.5);" + "border: none;" + "border-radius: 4px;" + ); + connect(settingsBtn, &QPushButton::clicked, this, [this]() { + auto *window = new DeviceSettingsWindow(nullptr, m_device.id, m_device.name); + window->setAttribute(Qt::WA_DeleteOnClose); + window->show(); + window->raise(); + window->activateWindow(); + }); + + auto *deleteBtn = new QPushButton(this); + deleteBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/delete.png")); + deleteBtn->setIconSize(QSize(20, 20)); + deleteBtn->setFixedSize(28, 28); + deleteBtn->setFlat(true); + deleteBtn->setStyleSheet( + "background-color: rgba(0, 0, 0, 0.5);" + "border: none;" + "border-radius: 4px;" + ); + connect(deleteBtn, &QPushButton::clicked, this, [this]() { + const QString deviceId = m_device.id; + const QString deviceName = m_device.name; + QPointer guard(this); + + QMessageBox msgBox(nullptr); + msgBox.setWindowTitle(tr("Удалить устройство?")); + msgBox.setText( + "Будет удалено устройство " + deviceName + + " и все связанные данные: банки, аккаунты, транзакции и события."); + QPushButton *confirmBtn = msgBox.addButton(tr("Удалить"), QMessageBox::AcceptRole); + QPushButton *cancelBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole); + msgBox.setDefaultButton(cancelBtn); + msgBox.setModal(true); + msgBox.exec(); + + if (msgBox.clickedButton() == confirmBtn) { + auto *thread = new QThread; + auto *net = new NetworkService; + net->setDeviceId(deviceId); + net->moveToThread(thread); + QObject::connect(thread, &QThread::started, net, &NetworkService::deleteDeviceFromServer); + QObject::connect(net, &NetworkService::finished, thread, &QThread::quit); + QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); + } + }); + + auto *codeBtn = new QPushButton(this); + codeBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/code.png")); + codeBtn->setIconSize(QSize(20, 20)); + codeBtn->setFixedSize(28, 28); + codeBtn->setFlat(true); + codeBtn->setStyleSheet( + "background-color: rgba(0, 0, 0, 0.5);" + "border: none;" + "border-radius: 4px;" + ); + connect(codeBtn, &QPushButton::clicked, this, [this, codeBtn]() { + if (m_device.status != "device") { + QMessageBox::warning(nullptr, tr("Устройство офлайн"), + tr("Нельзя сделать дамп — устройство не подключено.")); + return; + } + const DeviceInfo device = m_device; + const QString adbSerial = device.adbSerial.isEmpty() ? device.id : device.adbSerial; + + QSettings settings(QCoreApplication::applicationDirPath() + "/config.ini", + QSettings::IniFormat); + const QString apiBase = settings.value("net/apiBase").toString(); + const QString monitoringBase = settings.value("net/monitoringBase", apiBase).toString(); + const QString monitoringToken = settings.value("net/monitoringToken").toString(); + const QString apiToken = SettingsDAO::get("api_key"); + const QString path = settings.value("net/pathMonitoringDump", + "/monitoring/dump").toString(); + if (monitoringBase.isEmpty()) { + QMessageBox::warning(nullptr, tr("Ошибка"), + tr("В config.ini не задан net/monitoringBase (или net/apiBase как fallback)")); + return; + } + + codeBtn->setEnabled(false); + qDebug() << "[Dump] start for device" << adbSerial; + + auto *progress = new QProgressDialog(tr("Сбор дампа..."), QString(), 0, 0, nullptr); + progress->setWindowTitle(tr("Дамп")); + progress->setWindowModality(Qt::ApplicationModal); + progress->setCancelButton(nullptr); + progress->setMinimumDuration(0); + progress->setAutoClose(false); + progress->setAutoReset(false); + progress->setWindowFlags(progress->windowFlags() + & ~Qt::WindowCloseButtonHint + & ~Qt::WindowContextHelpButtonHint); + progress->show(); + + // ADB-захват блокирует поток на несколько секунд — выносим в воркер, + // чтобы прогресс-диалог не замораживался. + struct DumpData { QByteArray screenshot; QString xml; }; + auto *data = new DumpData; + auto *worker = QThread::create([data, adbSerial]() { + qDebug() << "[Dump] worker: capturing screenshot"; + data->screenshot = AdbUtils::captureScreenshotBytes(adbSerial); + qDebug() << "[Dump] worker: screenshot size=" << data->screenshot.size(); + qDebug() << "[Dump] worker: capturing xml"; + data->xml = AdbUtils::getRawScreenDumpAsXml(adbSerial); + qDebug() << "[Dump] worker: xml size=" << data->xml.size(); + }); + + // Общий флаг "уже обработано" — чтобы страховочный таймаут не сработал + // одновременно с finished. Шарится через shared_ptr, чтобы пережить лямбды. + auto handled = std::make_shared(false); + QPointer btnGuard(codeBtn); + QPointer progressGuard(progress); + + auto closeAndReenable = [progressGuard, btnGuard]() { + if (progressGuard) { + progressGuard->reset(); + progressGuard->hide(); + progressGuard->deleteLater(); + } + if (btnGuard) btnGuard->setEnabled(true); + }; + + // Страховочный таймаут на весь процесс (ADB + upload). Если что-то висит — + // диалог гарантированно закроется, и пользователь получит ошибку. + // Важно: владелец таймера и приёмник колбэков — qApp, а НЕ `this`. + // DeviceWidget пересоздаётся DeviceScreener-ом при каждом опросе устройств, + // и если повесить connect(this, ...), при пересборке списка соединение + // тихо рвётся, колбэк не запускается, диалог живёт вечно. + auto *safetyTimer = new QTimer; + safetyTimer->setSingleShot(true); + QObject::connect(safetyTimer, &QTimer::timeout, qApp, + [handled, closeAndReenable, worker, safetyTimer]() { + if (*handled) return; + *handled = true; + qWarning() << "[Dump] timeout — aborting"; + if (worker && worker->isRunning()) { + worker->terminate(); + worker->wait(2000); + } + if (worker) worker->deleteLater(); + safetyTimer->deleteLater(); + closeAndReenable(); + QMessageBox::warning(nullptr, QObject::tr("Таймаут"), + QObject::tr("Сбор/отправка дампа заняли больше 90 секунд")); + }); + safetyTimer->start(90000); + + QObject::connect(worker, &QThread::finished, qApp, + [worker, data, device, monitoringBase, monitoringToken, apiToken, path, + handled, closeAndReenable, safetyTimer, progressGuard]() { + qDebug() << "[Dump] worker finished"; + if (*handled) { + delete data; + worker->deleteLater(); + return; + } + + const QByteArray screenshot = data->screenshot; + const QString xml = data->xml; + delete data; + worker->deleteLater(); + + if (screenshot.isEmpty() && xml.isEmpty()) { + *handled = true; + safetyTimer->stop(); + safetyTimer->deleteLater(); + closeAndReenable(); + QMessageBox::warning(nullptr, QObject::tr("Ошибка"), + QObject::tr("Не удалось получить дамп экрана")); + return; + } + + if (progressGuard) progressGuard->setLabelText(QObject::tr("Отправка дампа...")); + qDebug() << "[Dump] uploading: screenshot=" << screenshot.size() + << "xml=" << xml.size(); + + const QString text = apiToken; + + auto *mp = new QHttpMultiPart(QHttpMultiPart::FormDataType); + + QHttpPart textPart; + textPart.setHeader(QNetworkRequest::ContentDispositionHeader, + R"(form-data; name="text")"); + textPart.setBody(text.toUtf8()); + mp->append(textPart); + + if (!xml.isEmpty()) { + QString safeName = device.name; + safeName.replace(QRegularExpression(R"([^\w\-.])"), "_"); + const QString ts = QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm-ss"); + const QString xmlFilename = "DUMP_" + safeName + "_" + ts + ".xml"; + + QHttpPart filePart; + filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/xml"); + filePart.setHeader(QNetworkRequest::ContentDispositionHeader, + QString(R"(form-data; name="file"; filename="%1")").arg(xmlFilename).toUtf8()); + filePart.setBody(xml.toUtf8()); + mp->append(filePart); + } + + if (!screenshot.isEmpty()) { + QHttpPart imagePart; + imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg"); + imagePart.setHeader(QNetworkRequest::ContentDispositionHeader, + R"(form-data; name="image"; filename="screen.jpg")"); + imagePart.setBody(screenshot); + mp->append(imagePart); + } + + const QUrl url(monitoringBase + path); + QNetworkRequest req(url); + if (!monitoringToken.isEmpty()) { + req.setRawHeader("X-Monitoring-Token", monitoringToken.toUtf8()); + } + + auto *manager = new QNetworkAccessManager; + QNetworkReply *reply = manager->post(req, mp); + mp->setParent(reply); + + QObject::connect(reply, &QNetworkReply::finished, reply, + [reply, manager, closeAndReenable, handled, safetyTimer]() { + if (*handled) { + reply->deleteLater(); + manager->deleteLater(); + return; + } + *handled = true; + safetyTimer->stop(); + safetyTimer->deleteLater(); + + const QByteArray body = reply->readAll(); + bool ok = reply->error() == QNetworkReply::NoError; + QString errorDetail; + if (!ok) { + errorDetail = reply->errorString(); + } else { + const QJsonDocument doc = QJsonDocument::fromJson(body); + if (doc.isObject() && !doc.object().value("success").toBool()) { + ok = false; + errorDetail = QString::fromUtf8(body.left(200)); + } + } + qDebug() << "[Dump] reply finished: ok=" << ok << "detail=" << errorDetail; + reply->deleteLater(); + manager->deleteLater(); + closeAndReenable(); + if (ok) { + QMessageBox::information(nullptr, QObject::tr("Готово"), + QObject::tr("Дамп отправлен")); + } else { + QMessageBox::warning(nullptr, QObject::tr("Ошибка отправки"), + QObject::tr("Не удалось отправить дамп:\n%1").arg(errorDetail)); + } + }); + }); + worker->start(); + }); + + topRightLayout->addWidget(settingsBtn); + topRightLayout->addWidget(deleteBtn); + topRightLayout->addWidget(codeBtn); + layout->addWidget(topRightWidget, 0, 0, Qt::AlignTop | Qt::AlignRight); + + if (device.status != "device") { + auto *offlineLabel = new QLabel("Офлайн", this); + offlineLabel->setAlignment(Qt::AlignCenter); + offlineLabel->setStyleSheet( + "background-color: rgba(0, 0, 0, 0.6);" + "color: white;" + "font-size: 24px;" + "font-weight: bold;" + "padding: 10px 20px;" + "border-radius: 8px;" + ); + layout->addWidget(offlineLabel, 0, 0, Qt::AlignCenter); + } + + + auto *appsView = new QWidget(this); + appsView->setAttribute(Qt::WA_TransparentForMouseEvents, false); + appsView->setStyleSheet("background: transparent;"); + + auto *appsBoxLayout = new QVBoxLayout(appsView); + appsBoxLayout->setContentsMargins(2, 0, 0, 2); + appsBoxLayout->setSpacing(2); + + const bool isOffline = device.status != "device"; + + // Пока устройство не зарегистрировано на сервере (нет apiId) — банки не показываем + QList apps; + if (!device.apiId.isEmpty()) { + apps = BankProfileDAO::getApplicationsByDeviceId(device.id); + } + + // Фильтр по стране профиля + const QString profileCountry = SettingsDAO::get("country"); + if (!profileCountry.isEmpty()) { + const QSettings configSettings("config.ini", QSettings::IniFormat); + for (auto it = apps.begin(); it != apps.end();) { + const QString bankCountry = configSettings.value(it->code + "/country").toString(); + if (bankCountry.compare(profileCountry, Qt::CaseInsensitive) != 0) { + it = apps.erase(it); + } else { + ++it; + } + } + } + + for (BankProfileInfo &app: apps) { + if (app.install) { + auto *rowWidget = new QWidget(appsView); + buildBankRow(app, rowWidget, appsView, isOffline); + appsBoxLayout->addWidget(rowWidget, 1); + } + } + // appsBoxLayout->addStretch(); + + // кладём overlay в ту же ячейку, что и картинку, + // но выравниваем его по левому-нижнему углу + layout->setColumnStretch(0, 1); + layout->addWidget(appsView, 0, 0, Qt::AlignLeft | Qt::AlignBottom); + // layout->addWidget(appsView, 0, 0, Qt::AlignLeft); TODO так на всю ширину + + // auto *addBankBtn = new QPushButton(this); + // addBankBtn->setMaximumWidth(300); + // addBankBtn->setText("Добавить банк +"); + // addBankBtn->setStyleSheet( + // "background-color: #fff;" + // "margin-top: 2px;" + // "padding: 4px;" + // "color: #000;" + // ); + + // setOpenBanksSettings(addBankBtn); + // layout->addWidget(addBankBtn, 1, 0); +} diff --git a/views/device/DeviceWidget.h b/views/device/DeviceWidget.h new file mode 100644 index 0000000..d1958cb --- /dev/null +++ b/views/device/DeviceWidget.h @@ -0,0 +1,25 @@ +#pragma once +#include +#include +#include + +#include "db/BankProfileInfo.h" +#include "db/DeviceInfo.h" + +class DeviceWidget final : public QWidget { + Q_OBJECT + +public: + explicit DeviceWidget(const DeviceInfo &device, QWidget *parent = nullptr); + +private: + const DeviceInfo m_device; + + void buildBankRow(const BankProfileInfo &app, QWidget *rowWidget, QWidget *parent, bool isOffline = false); + + void setOpenBanksSettings(QPushButton *button, const BankProfileInfo &app); + +signals: + void deleteRequested(const QString &deviceId); + void settingsRequested(const QString &deviceId); +}; diff --git a/views/device/EditBankAccountDialog.h b/views/device/EditBankAccountDialog.h new file mode 100644 index 0000000..aea73f1 --- /dev/null +++ b/views/device/EditBankAccountDialog.h @@ -0,0 +1,63 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#include "db/MaterialInfo.h" + +class EditBankAccountDialog final : public QDialog { + Q_OBJECT + +public: + explicit EditBankAccountDialog(const MaterialInfo &account, QWidget *parent = nullptr) : QDialog(parent) { + if (account.id > 0) { + setWindowTitle("Настройка аккаунта *" + account.lastNumbers); + } else { + setWindowTitle("Настройка нового аккаунта"); + } + + setModal(true); + resize(300, 160); + + const QSettings settings("config.ini", QSettings::IniFormat); + QStringList currencies = settings.value("common/currencies", QStringList()).toStringList(); + + auto *form = new QFormLayout(this); + cardNumber = new QLineEdit(this); + cardNumber->setText(account.description); + currency = new QComboBox(this); + for (QString c: currencies) { + currency->addItem(c); + } + currency->setCurrentText(account.currency); + + toggle = new QCheckBox(tr("В работе"), this); + toggle->setChecked(account.status == MaterialStatus::Active); + + form->addRow(tr("Номер карты:"), cardNumber); + form->addRow(tr("Валюта:"), currency); + form->addRow(QString(), toggle); + + auto *buttons = new QDialogButtonBox(Qt::Horizontal, this); + buttons->addButton(tr("Сохранить"), QDialogButtonBox::AcceptRole); + buttons->addButton(tr("Отмена"), QDialogButtonBox::RejectRole); + form->addRow(buttons); + + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + } + + + QString cardNumberValue() const { return cardNumber->text(); } + QString currencyValue() const { return currency->currentText(); } + bool toggled() const { return toggle->isChecked(); } + +private: + QLineEdit *cardNumber; + QComboBox *currency; + QCheckBox *toggle; +}; diff --git a/views/device/EditBankDialog.h b/views/device/EditBankDialog.h new file mode 100644 index 0000000..61f5bc2 --- /dev/null +++ b/views/device/EditBankDialog.h @@ -0,0 +1,49 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include "db/BankProfileInfo.h" + +class BankEditDialog final : public QDialog { + Q_OBJECT + +public: + explicit BankEditDialog(const BankProfileInfo &app, QWidget *parent = nullptr) : QDialog(parent) { + setWindowTitle("Настройка " + app.name); + setModal(true); + + auto *form = new QFormLayout(this); + firstEdit = new QLineEdit(this); + firstEdit->setText(app.pinCode); + secondEdit = new QTextEdit(this); + secondEdit->setMaximumHeight(100); + secondEdit->setPlainText(app.comment); + toggle = new QCheckBox(tr("Включен"), this); + toggle->setChecked(app.status == "active"); + + form->addRow(tr("Пин-код:"), firstEdit); + form->addRow(tr("Комментарий:"), secondEdit); + form->addRow(QString(), toggle); + + auto *buttons = new QDialogButtonBox(Qt::Horizontal, this); + buttons->addButton(tr("Сохранить"), QDialogButtonBox::AcceptRole); + buttons->addButton(tr("Отмена"), QDialogButtonBox::RejectRole); + form->addRow(buttons); + + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + } + + + QString firstValue() const { return firstEdit->text(); } + QString secondValue() const { return secondEdit->toPlainText(); } + bool toggled() const { return toggle->isChecked(); } + +private: + QLineEdit *firstEdit; + QTextEdit *secondEdit; + QCheckBox *toggle; +}; diff --git a/views/event/EventWindow.cpp b/views/event/EventWindow.cpp new file mode 100644 index 0000000..373537a --- /dev/null +++ b/views/event/EventWindow.cpp @@ -0,0 +1,411 @@ +#include "EventWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dao/EventDAO.h" +#include "dao/SettingsDAO.h" +#include "net/NetworkService.h" + +static constexpr int COL_ID = 0; +static constexpr int COL_TIME = 1; +static constexpr int COL_TYPE = 2; +static constexpr int COL_BANK = 3; +static constexpr int COL_DEVICE = 4; +static constexpr int COL_STATUS = 5; +static constexpr int COL_AMOUNT = 6; +static constexpr int COL_COMMENT = 7; +static constexpr int COL_JSON = 8; +static constexpr int COL_SENT = 9; +static constexpr int COL_IMG = 10; +static constexpr int COL_ACTION = 11; + +static void sendErrorToServer(const QString &type, const QString &materialId, const QString &bankName) { + QJsonObject errData; + errData["status"] = "error"; + errData["description"] = "Closed manually"; + + QJsonObject obj; + obj["type"] = type; + obj["material_id"] = materialId; + obj["bank_name"] = bankName; + obj["data"] = errData; + + auto *thread = new QThread; + auto *networkService = new NetworkService; + networkService->moveToThread(thread); + networkService->setPayload(QJsonDocument(obj).toJson()); + QObject::connect(thread, &QThread::started, networkService, &NetworkService::postTaskResult); + QObject::connect(networkService, &NetworkService::finished, thread, &QThread::quit); + QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater); + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +static QString statusDisplayName(EventStatus s) { + switch (s) { + case EventStatus::Wait: return "Ожидание"; + case EventStatus::InProgress: return "В работе"; + case EventStatus::Complete: return "Завершено"; + case EventStatus::Error: return "Ошибка"; + default: return "—"; + } +} + +static QString typeDisplayName(EventType t) { + switch (t) { + case EventType::FetchProfile: return "Профиль"; + case EventType::FetchTransactions: return "Транзакции"; + case EventType::CreateTransaction: return "Платёж"; + default: return "—"; + } +} + +EventWindow::EventWindow(QWidget *parent) : QDialog(parent) { + setWindowTitle("События"); + resize(1400, 700); + + // ── Таблица ─────────────────────────────────────────────────────────────── + m_table = new QTableWidget(this); + m_table->setColumnCount(12); + m_table->setHorizontalHeaderLabels({ + "#", "Время", "Тип", "Банк", "Устройство", "Статус", "Сумма", "Комментарий", "JSON", "Отпр.", "Скрин", "" + }); + m_table->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_table->setSelectionBehavior(QAbstractItemView::SelectRows); + m_table->setSelectionMode(QAbstractItemView::SingleSelection); + m_table->verticalHeader()->hide(); + m_table->horizontalHeader()->setStretchLastSection(false); + m_table->setColumnWidth(COL_ID, 45); + m_table->setColumnWidth(COL_TIME, 145); + m_table->setColumnWidth(COL_TYPE, 100); + m_table->setColumnWidth(COL_BANK, 90); + m_table->setColumnWidth(COL_DEVICE, 120); + m_table->setColumnWidth(COL_STATUS, 100); + m_table->setColumnWidth(COL_AMOUNT, 80); + m_table->setColumnHidden(COL_JSON, true); + m_table->setColumnHidden(COL_ACTION, true); + m_table->setColumnWidth(COL_SENT, 45); + m_table->setColumnWidth(COL_IMG, 45); + m_table->horizontalHeader()->setSectionResizeMode(COL_COMMENT, QHeaderView::Stretch); + + // ── Панель скриншота ────────────────────────────────────────────────────── + m_screenshotLabel = new QLabel(this); + m_screenshotLabel->setAlignment(Qt::AlignCenter); + m_screenshotLabel->setMinimumWidth(300); + m_screenshotLabel->setStyleSheet("background-color: #f0f0f0; border: 1px solid #ccc;"); + m_screenshotLabel->setText("Выберите событие\nдля просмотра скриншота"); + + auto *splitter = new QSplitter(Qt::Horizontal, this); + splitter->addWidget(m_table); + splitter->addWidget(m_screenshotLabel); + splitter->setStretchFactor(0, 3); + splitter->setStretchFactor(1, 1); + + // ── Пагинация ───────────────────────────────────────────────────────────── + m_prevBtn = new QPushButton("←", this); + m_nextBtn = new QPushButton("→", this); + m_pageLabel = new QLabel(this); + m_pageLabel->setAlignment(Qt::AlignCenter); + m_pageLabel->setMinimumWidth(140); + + auto *refreshBtn = new QPushButton("Обновить", this); + m_sendBtn = new QPushButton("Отправить", this); + m_sendBtn->setStyleSheet("background-color: #0088cc; color: white; border-radius: 4px; padding: 4px 12px;"); + + auto *paginationLayout = new QHBoxLayout; + paginationLayout->addWidget(m_prevBtn); + paginationLayout->addWidget(m_pageLabel); + paginationLayout->addWidget(m_nextBtn); + paginationLayout->addStretch(); + paginationLayout->addWidget(refreshBtn); + paginationLayout->addWidget(m_sendBtn); + + auto *mainLayout = new QVBoxLayout(this); + mainLayout->addWidget(splitter, 1); + mainLayout->addLayout(paginationLayout); + + // ── Сигналы ─────────────────────────────────────────────────────────────── + connect(m_prevBtn, &QPushButton::clicked, this, [this]() { + if (m_page > 0) { --m_page; loadPage(); } + }); + connect(m_nextBtn, &QPushButton::clicked, this, [this]() { + if (m_page < m_totalPages - 1) { ++m_page; loadPage(); } + }); + connect(refreshBtn, &QPushButton::clicked, this, [this]() { + m_page = 0; + loadPage(); + }); + connect(m_table, &QTableWidget::cellClicked, this, [this](int row, int) { + if (row >= 0 && row < m_eventIds.size()) { + showScreenshot(m_eventIds[row]); + } + }); + connect(m_sendBtn, &QPushButton::clicked, this, &EventWindow::sendEventsToCsv); + + connect(m_table, &QTableWidget::cellDoubleClicked, this, [this](int row, int col) { + if (col != COL_JSON) return; + auto *item = m_table->item(row, col); + if (!item) return; + const QString json = item->toolTip(); + if (json.isEmpty()) return; + + auto *dlg = new QDialog(this); + dlg->setWindowTitle("JSON"); + dlg->resize(600, 400); + auto *edit = new QPlainTextEdit(dlg); + edit->setReadOnly(true); + edit->setPlainText(QJsonDocument::fromJson(json.toUtf8()).toJson(QJsonDocument::Indented)); + auto *layout = new QVBoxLayout(dlg); + layout->addWidget(edit); + dlg->setAttribute(Qt::WA_DeleteOnClose); + dlg->show(); + }); + + loadPage(); +} + +void EventWindow::loadPage() { + const int total = EventDAO::getTotalCount(); + m_totalPages = qMax(1, (total + m_pageSize - 1) / m_pageSize); + m_page = qBound(0, m_page, m_totalPages - 1); + + const QList events = EventDAO::getEvents(m_page, m_pageSize); + m_eventIds.clear(); + + m_table->setRowCount(static_cast(events.size())); + for (int i = 0; i < events.size(); ++i) { + m_eventIds.append(events[i].id); + const auto &e = events[i]; + m_table->setItem(i, COL_ID, new QTableWidgetItem(QString::number(e.id))); + m_table->setItem(i, COL_TIME, new QTableWidgetItem( + e.timestamp.toLocalTime().toString("dd.MM.yy HH:mm:ss"))); + m_table->setItem(i, COL_TYPE, new QTableWidgetItem(typeDisplayName(e.type))); + m_table->setItem(i, COL_BANK, new QTableWidgetItem(e.bankName)); + m_table->setItem(i, COL_DEVICE, new QTableWidgetItem(e.deviceId)); + m_table->setItem(i, COL_STATUS, new QTableWidgetItem(statusDisplayName(e.status))); + m_table->setItem(i, COL_AMOUNT, new QTableWidgetItem( + e.amount > 0 ? QString::number(e.amount, 'f', 2) : "—")); + m_table->setItem(i, COL_COMMENT, new QTableWidgetItem(e.comment)); + + auto *jsonItem = new QTableWidgetItem(e.json.left(80)); + jsonItem->setToolTip(e.json); + m_table->setItem(i, COL_JSON, jsonItem); + + m_table->setItem(i, COL_SENT, new QTableWidgetItem(e.sentToServer ? "+" : "")); + m_table->setItem(i, COL_IMG, new QTableWidgetItem(e.hasScreenshot ? "+" : "")); + + // Подсветка строки по статусу + QColor bg; + switch (e.status) { + case EventStatus::Error: bg = QColor(255, 230, 230); break; + case EventStatus::Complete: bg = QColor(230, 255, 230); break; + case EventStatus::InProgress: bg = QColor(230, 240, 255); break; + default: bg = QColor(255, 255, 230); break; + } + for (int col = 0; col < m_table->columnCount(); ++col) { + if (auto *item = m_table->item(i, col)) + item->setBackground(bg); + } + + // Кнопка "Закрыть" только для активных задач + if (e.status == EventStatus::Wait || e.status == EventStatus::InProgress) { + auto *closeBtn = new QPushButton("Закрыть"); + closeBtn->setStyleSheet("background: #e74c3c; color: white; border: none; padding: 3px 8px; border-radius: 3px;"); + const int eventId = e.id; + const QString externalId = e.externalId; + const QString bankName = e.bankName; + const QString typeStr = eventTypeToString(e.type); + connect(closeBtn, &QPushButton::clicked, this, [this, eventId, externalId, bankName, typeStr]() { + EventDAO::updateEvent(eventId, EventStatus::Error, "Closed manually"); + sendErrorToServer(typeStr, externalId, bankName); + loadPage(); + }); + m_table->setCellWidget(i, COL_ACTION, closeBtn); + } + } + + updatePagination(); +} + +void EventWindow::showScreenshot(int eventId) { + const QByteArray data = EventDAO::getScreenshot(eventId); + if (data.isEmpty()) { + m_screenshotLabel->setText("Нет скриншота"); + return; + } + + QPixmap pixmap; + pixmap.loadFromData(data); + if (pixmap.isNull()) { + m_screenshotLabel->setText("Ошибка загрузки"); + return; + } + + m_screenshotLabel->setPixmap(pixmap.scaled( + m_screenshotLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); +} + +void EventWindow::updatePagination() { + m_pageLabel->setText(QString("Страница %1 из %2").arg(m_page + 1).arg(m_totalPages)); + m_prevBtn->setEnabled(m_page > 0); + m_nextBtn->setEnabled(m_page < m_totalPages - 1); +} + +void EventWindow::sendEventsToCsv() { + const QList events = EventDAO::getRecentEvents(48); + if (events.isEmpty()) { + QMessageBox::information(this, "События", "Нет событий за последние 48 часов."); + return; + } + + m_sendBtn->setEnabled(false); + m_sendBtn->setText("Отправка..."); + + // Формируем CSV + QString csv; + csv += "id;time;type;bank;device;status;amount;phone;comment;sent\n"; + for (const auto &e : events) { + csv += QString("%1;%2;%3;%4;%5;%6;%7;%8;%9;%10\n") + .arg(e.id) + .arg(e.timestamp.toLocalTime().toString("dd.MM.yyyy HH:mm:ss")) + .arg(eventTypeToString(e.type)) + .arg(e.bankName) + .arg(e.deviceId) + .arg(eventStatusToString(e.status)) + .arg(e.amount, 0, 'f', 2) + .arg(e.phone) + .arg(QString(e.comment).replace(';', ',')) + .arg(e.sentToServer ? "yes" : "no"); + } + + // Собираем временную папку + const QString tmpDir = QDir::tempPath() + "/arc_events_export"; + QDir().mkpath(tmpDir); + QDir().mkpath(tmpDir + "/screenshots"); + + // Сохраняем CSV + { + QFile csvFile(tmpDir + "/events.csv"); + if (csvFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + csvFile.write(csv.toUtf8()); + csvFile.close(); + } + } + + // Сохраняем скриншоты + int screenshotCount = 0; + for (const auto &e : events) { + const QByteArray screenshot = EventDAO::getScreenshot(e.id); + if (screenshot.isEmpty()) continue; + const QString fileName = QString("screenshots/%1_%2_%3.png") + .arg(e.id) + .arg(eventTypeToString(e.type)) + .arg(e.timestamp.toString("yyyyMMdd_HHmmss")); + QFile imgFile(tmpDir + "/" + fileName); + if (imgFile.open(QIODevice::WriteOnly)) { + imgFile.write(screenshot); + imgFile.close(); + ++screenshotCount; + } + } + + // Создаём архив +#ifdef Q_OS_WIN + const QString archivePath = QDir::tempPath() + "/arc_events.zip"; + QFile::remove(archivePath); + QProcess archiver; + archiver.setWorkingDirectory(tmpDir); + archiver.start("powershell", QStringList() + << "-NoProfile" << "-Command" + << QString("Compress-Archive -Path '%1/*' -DestinationPath '%2' -Force").arg(tmpDir, archivePath)); + archiver.waitForFinished(30000); +#else + const QString archivePath = QDir::tempPath() + "/arc_events.zip"; + QFile::remove(archivePath); + QProcess archiver; + archiver.setWorkingDirectory(tmpDir); + archiver.start("zip", QStringList() << "-r" << archivePath << "."); + archiver.waitForFinished(30000); +#endif + + QDir(tmpDir).removeRecursively(); + + if (!QFile::exists(archivePath)) { + QMessageBox::warning(this, "Ошибка", "Не удалось создать архив"); + m_sendBtn->setEnabled(true); + m_sendBtn->setText("Отправить"); + return; + } + + // Отправляем в Telegram + const QString botToken = "8256716069:AAFgZRB_0Y6KTpqFQmCxIlZiWdY5m2dR2D8"; + const QString chatId = "-5177086220"; + const QString desktopId = SettingsDAO::get("desktop_id"); + const QString caption = "Events (48h) from desktop: " + desktopId + + "\nTotal: " + QString::number(events.size()) + + "\nScreenshots: " + QString::number(screenshotCount); + + auto *sendFile = new QFile(archivePath); + if (!sendFile->open(QIODevice::ReadOnly)) { + QMessageBox::warning(this, "Ошибка", "Не удалось открыть архив"); + delete sendFile; + m_sendBtn->setEnabled(true); + m_sendBtn->setText("Отправить"); + return; + } + + const QUrl url("https://api.telegram.org/bot" + botToken + "/sendDocument"); + auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + + QHttpPart chatPart; + chatPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="chat_id")"); + chatPart.setBody(chatId.toUtf8()); + multiPart->append(chatPart); + + QHttpPart captionPart; + captionPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="caption")"); + captionPart.setBody(caption.toUtf8()); + multiPart->append(captionPart); + + QHttpPart filePart; + filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/zip"); + filePart.setHeader(QNetworkRequest::ContentDispositionHeader, + R"(form-data; name="document"; filename="arc_events.zip")"); + filePart.setBodyDevice(sendFile); + sendFile->setParent(multiPart); + multiPart->append(filePart); + + auto *manager = new QNetworkAccessManager(this); + QNetworkReply *reply = manager->post(QNetworkRequest(url), multiPart); + multiPart->setParent(reply); + + connect(reply, &QNetworkReply::finished, this, [this, reply, manager, archivePath]() { + reply->deleteLater(); + manager->deleteLater(); + QFile::remove(archivePath); + + m_sendBtn->setEnabled(true); + m_sendBtn->setText("Отправить"); + + if (reply->error() != QNetworkReply::NoError) { + QMessageBox::warning(this, "Ошибка", "Не удалось отправить: " + reply->errorString()); + return; + } + + QMessageBox::information(this, "Готово", "События отправлены."); + }); +} diff --git a/views/event/EventWindow.h b/views/event/EventWindow.h new file mode 100644 index 0000000..79bae0c --- /dev/null +++ b/views/event/EventWindow.h @@ -0,0 +1,33 @@ +#pragma once +#include +#include +#include +#include +#include + +class EventWindow final : public QDialog { + Q_OBJECT + +public: + explicit EventWindow(QWidget *parent = nullptr); + +private: + QTableWidget *m_table; + QLabel *m_pageLabel; + QLabel *m_screenshotLabel; + QPushButton *m_prevBtn; + QPushButton *m_nextBtn; + QPushButton *m_sendBtn; + + int m_page = 0; + int m_pageSize = 50; + int m_totalPages = 1; + + // Кешируем event id текущей страницы для загрузки скриншота по клику + QList m_eventIds; + + void loadPage(); + void updatePagination(); + void showScreenshot(int eventId); + void sendEventsToCsv(); +}; diff --git a/views/log/FileLogWindow.cpp b/views/log/FileLogWindow.cpp new file mode 100644 index 0000000..3a1db67 --- /dev/null +++ b/views/log/FileLogWindow.cpp @@ -0,0 +1,305 @@ +#include "FileLogWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dump/TaskDumpRecorder.h" +#include "dao/GeneralLogDAO.h" +#include "dao/SettingsDAO.h" +#include "log/LogDetailWindow.h" +#include "net/NetworkService.h" + +static constexpr int COL_ID = 0; +static constexpr int COL_TIME = 1; +static constexpr int COL_LEVEL = 2; +static constexpr int COL_SOURCE = 3; +static constexpr int COL_MESSAGE = 4; + +FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) { + setWindowTitle("Логи"); + resize(1200, 650); + + m_table = new QTableWidget(this); + m_table->setColumnCount(5); + m_table->setHorizontalHeaderLabels({"#", "Время", "Уровень", "Источник", "Сообщение"}); + m_table->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_table->setSelectionBehavior(QAbstractItemView::SelectRows); + m_table->setSelectionMode(QAbstractItemView::SingleSelection); + m_table->verticalHeader()->hide(); + m_table->horizontalHeader()->setStretchLastSection(true); + m_table->setColumnWidth(COL_ID, 45); + m_table->setColumnWidth(COL_TIME, 145); + m_table->setColumnWidth(COL_LEVEL, 80); + m_table->setColumnWidth(COL_SOURCE, 250); + + // Панель скриншота + m_screenshotLabel = new QLabel(this); + m_screenshotLabel->setAlignment(Qt::AlignCenter); + m_screenshotLabel->setMinimumWidth(300); + m_screenshotLabel->setStyleSheet("background-color: #f0f0f0; border: 1px solid #ccc;"); + m_screenshotLabel->setText("Выберите запись\nдля просмотра скриншота"); + + auto *splitter = new QSplitter(Qt::Horizontal, this); + splitter->addWidget(m_table); + splitter->addWidget(m_screenshotLabel); + splitter->setStretchFactor(0, 3); + splitter->setStretchFactor(1, 1); + + m_showAllCheck = new QCheckBox("Показать все (включая DEBUG/INFO)", this); + + m_prevBtn = new QPushButton("←", this); + m_nextBtn = new QPushButton("→", this); + m_pageLabel = new QLabel(this); + m_pageLabel->setAlignment(Qt::AlignCenter); + m_pageLabel->setMinimumWidth(140); + + auto *refreshBtn = new QPushButton("Обновить", this); + m_sendBtn = new QPushButton("Отправить", this); + m_sendBtn->setStyleSheet("background-color: #0088cc; color: white; border-radius: 4px; padding: 4px 12px;"); + + auto *paginationLayout = new QHBoxLayout; + paginationLayout->addWidget(m_prevBtn); + paginationLayout->addWidget(m_pageLabel); + paginationLayout->addWidget(m_nextBtn); + paginationLayout->addStretch(); + paginationLayout->addWidget(m_showAllCheck); + paginationLayout->addWidget(refreshBtn); + paginationLayout->addWidget(m_sendBtn); + + auto *mainLayout = new QVBoxLayout(this); + mainLayout->addWidget(splitter, 1); + mainLayout->addLayout(paginationLayout); + + connect(m_prevBtn, &QPushButton::clicked, this, [this]() { + if (m_page > 0) { --m_page; loadPage(); } + }); + connect(m_nextBtn, &QPushButton::clicked, this, [this]() { + if (m_page < m_totalPages - 1) { ++m_page; loadPage(); } + }); + connect(refreshBtn, &QPushButton::clicked, this, [this]() { + m_page = 0; + loadPage(); + }); + connect(m_showAllCheck, &QCheckBox::toggled, this, [this]() { + m_page = 0; + loadPage(); + }); + connect(m_table, &QTableWidget::cellClicked, this, [this](int row, int) { + showScreenshot(row); + }); + connect(m_table, &QTableWidget::cellDoubleClicked, this, [this](int row, int) { + if (row >= 0 && row < m_entries.size()) { + auto *detail = new LogDetailWindow(m_entries[row], this); + detail->setAttribute(Qt::WA_DeleteOnClose); + detail->show(); + } + }); + connect(m_sendBtn, &QPushButton::clicked, this, &FileLogWindow::sendLogsToBackend); + + loadPage(); +} + +QStringList FileLogWindow::currentLevelFilter() const { + if (m_showAllCheck->isChecked()) + return {}; + return {"WARNING", "CRITICAL", "FATAL"}; +} + +void FileLogWindow::loadPage() { + const QStringList levels = currentLevelFilter(); + const int total = GeneralLogDAO::getTotalCount(levels); + m_totalPages = qMax(1, (total + m_pageSize - 1) / m_pageSize); + m_page = qBound(0, m_page, m_totalPages - 1); + + m_entries = GeneralLogDAO::getLogs(m_page, m_pageSize, levels); + + m_table->setRowCount(static_cast(m_entries.size())); + for (int i = 0; i < m_entries.size(); ++i) { + const auto &e = m_entries[i]; + m_table->setItem(i, COL_ID, new QTableWidgetItem(QString::number(e.id))); + m_table->setItem(i, COL_TIME, new QTableWidgetItem( + e.timestamp.toLocalTime().toString("dd.MM.yy HH:mm:ss"))); + m_table->setItem(i, COL_LEVEL, new QTableWidgetItem(e.level)); + m_table->setItem(i, COL_SOURCE, new QTableWidgetItem(e.source)); + m_table->setItem(i, COL_MESSAGE, new QTableWidgetItem(e.message)); + } + + updatePagination(); +} + +void FileLogWindow::showScreenshot(int row) { + if (row < 0 || row >= m_entries.size()) return; + const QByteArray &data = m_entries[row].screenshot; + if (data.isEmpty()) { + m_screenshotLabel->setText("Нет скриншота"); + return; + } + + QPixmap pixmap; + pixmap.loadFromData(data); + if (pixmap.isNull()) { + m_screenshotLabel->setText("Ошибка загрузки"); + return; + } + + m_screenshotLabel->setPixmap(pixmap.scaled( + m_screenshotLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); +} + +void FileLogWindow::updatePagination() { + m_pageLabel->setText(QString("Страница %1 из %2").arg(m_page + 1).arg(m_totalPages)); + m_prevBtn->setEnabled(m_page > 0); + m_nextBtn->setEnabled(m_page < m_totalPages - 1); +} + +void FileLogWindow::sendLogsToBackend() { + qDebug() << "[sendLogsToBackend] step 1: checking log file"; + const QString logPath = QCoreApplication::applicationDirPath() + "/application.log"; + if (!QFile::exists(logPath)) { + QMessageBox::warning(this, "Ошибка", "Файл application.log не найден"); + return; + } + + // Резолвим путь к БД из config.ini (как DatabaseManager): [db]/dbPath, + // с разворотом '~' в домашнюю директорию и приведением к абсолютному пути + // относительно applicationDirPath, чтобы не зависеть от текущего CWD. + const QSettings settings("config.ini", QSettings::IniFormat); + QString dbPath = settings.value("db/dbPath").toString(); + if (dbPath.startsWith("~")) { + dbPath.replace(0, 1, QDir::homePath()); + } + if (!dbPath.isEmpty() && QFileInfo(dbPath).isRelative()) { + dbPath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(dbPath); + } + if (dbPath.isEmpty() || !QFile::exists(dbPath)) { + QMessageBox::warning(this, "Ошибка", + "Файл БД не найден: " + (dbPath.isEmpty() ? QStringLiteral("<пусто>") : dbPath)); + return; + } + + NetworkService *svc = TaskDumpRecorder::networkService(); + if (!svc) { + QMessageBox::warning(this, "Ошибка", + "NetworkService не инициализирован — отправка на бэкенд недоступна."); + return; + } + + m_sendBtn->setEnabled(false); + m_sendBtn->setText("Подготовка..."); + + qDebug() << "[sendLogsToBackend] step 2: creating tmp dir"; + const QString tmpDir = QDir::tempPath() + "/arc_logs_export"; + QDir(tmpDir).removeRecursively(); + QDir().mkpath(tmpDir); + + // 1. application.log целиком + QFile::copy(logPath, tmpDir + "/application.log"); + + // 2. БД + WAL/SHM-сайдкары (если есть) — иначе при включённом WAL + // снимок будет рассогласованным. + const QString dbBaseName = QFileInfo(dbPath).fileName(); + QFile::copy(dbPath, tmpDir + "/" + dbBaseName); + for (const QString &suffix : {QStringLiteral("-wal"), QStringLiteral("-shm")}) { + const QString sidecar = dbPath + suffix; + if (QFile::exists(sidecar)) { + QFile::copy(sidecar, tmpDir + "/" + dbBaseName + suffix); + } + } + + qDebug() << "[sendLogsToBackend] step 3: creating archive"; + const QString archivePath = QDir::tempPath() + "/arc_logs.zip"; + QFile::remove(archivePath); +#ifdef Q_OS_WIN + QProcess archiver; + archiver.setWorkingDirectory(tmpDir); + archiver.start("powershell", QStringList() + << "-NoProfile" << "-Command" + << QString("Compress-Archive -Path '%1/*' -DestinationPath '%2' -Force").arg(tmpDir, archivePath)); + if (!archiver.waitForFinished(30000)) { + qWarning() << "[sendLogsToBackend] powershell archiver timeout/error:" << archiver.errorString(); + } + qDebug() << "[sendLogsToBackend] archiver exit code:" << archiver.exitCode() + << "stdout:" << archiver.readAllStandardOutput() + << "stderr:" << archiver.readAllStandardError(); +#else + QProcess archiver; + archiver.setWorkingDirectory(tmpDir); + archiver.start("zip", QStringList() << "-r" << archivePath << "."); + archiver.waitForFinished(30000); +#endif + + QDir(tmpDir).removeRecursively(); + + if (!QFile::exists(archivePath)) { + QMessageBox::warning(this, "Ошибка", "Не удалось создать архив"); + m_sendBtn->setEnabled(true); + m_sendBtn->setText("Отправить"); + return; + } + + QFile zipFile(archivePath); + if (!zipFile.open(QIODevice::ReadOnly)) { + QMessageBox::warning(this, "Ошибка", "Не удалось открыть архив"); + QFile::remove(archivePath); + m_sendBtn->setEnabled(true); + m_sendBtn->setText("Отправить"); + return; + } + const QByteArray archiveBytes = zipFile.readAll(); + zipFile.close(); + QFile::remove(archivePath); + + if (archiveBytes.isEmpty()) { + QMessageBox::warning(this, "Ошибка", "Архив пуст — отправлять нечего"); + m_sendBtn->setEnabled(true); + m_sendBtn->setText("Отправить"); + return; + } + + const QString apiToken = SettingsDAO::get("api_key"); + const QDateTime now = QDateTime::currentDateTime(); + const QString timestamp = now.toString(Qt::ISODate); + QString header = QStringLiteral("[LOGS]"); + if (!apiToken.isEmpty()) header += QStringLiteral(" [%1]").arg(apiToken.toHtmlEscaped()); + const QString text = QStringLiteral("%1\n%2").arg(header, timestamp.toHtmlEscaped()); + + QString filenameSafeToken = apiToken; + filenameSafeToken.replace(QRegularExpression(R"([^\w\-.])"), "_"); + const QString filenameStamp = now.toString("yyyy-MM-dd_HH-mm-ss"); + const QString archiveFilename = filenameSafeToken.isEmpty() + ? QStringLiteral("LOGS_%1.zip").arg(filenameStamp) + : QStringLiteral("LOGS_%1_%2.zip").arg(filenameSafeToken, filenameStamp); + + qDebug() << "[sendLogsToBackend] dispatching to NetworkService::postMonitoringDump" + << "archive.size=" << archiveBytes.size() << "filename=" << archiveFilename; + const bool dispatched = QMetaObject::invokeMethod(svc, "postMonitoringDump", + Qt::QueuedConnection, + Q_ARG(QString, text), + Q_ARG(QByteArray, archiveBytes), + Q_ARG(QByteArray, QByteArray()), + Q_ARG(QString, archiveFilename)); + + m_sendBtn->setEnabled(true); + m_sendBtn->setText("Отправить"); + + if (!dispatched) { + QMessageBox::warning(this, "Ошибка", + "Не удалось поставить отправку в очередь NetworkService."); + return; + } + + QMessageBox::information(this, "Готово", + "Логи поставлены в очередь на отправку на бэкенд."); +} diff --git a/views/log/FileLogWindow.h b/views/log/FileLogWindow.h new file mode 100644 index 0000000..2d9c9c7 --- /dev/null +++ b/views/log/FileLogWindow.h @@ -0,0 +1,37 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +#include "dao/GeneralLogDAO.h" + +class FileLogWindow final : public QDialog { + Q_OBJECT + +public: + explicit FileLogWindow(QWidget *parent = nullptr); + void sendLogsToBackend(); + +private: + QTableWidget *m_table; + QLabel *m_screenshotLabel; + QLabel *m_pageLabel; + QPushButton *m_prevBtn; + QPushButton *m_nextBtn; + QPushButton *m_sendBtn; + QCheckBox *m_showAllCheck; + + int m_page = 0; + int m_pageSize = 20; + int m_totalPages = 1; + + QList m_entries; + + QStringList currentLevelFilter() const; + void loadPage(); + void updatePagination(); + void showScreenshot(int row); +}; diff --git a/views/log/LogDetailWindow.cpp b/views/log/LogDetailWindow.cpp new file mode 100644 index 0000000..110949d --- /dev/null +++ b/views/log/LogDetailWindow.cpp @@ -0,0 +1,95 @@ +#include "LogDetailWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LogDetailWindow::LogDetailWindow(const GeneralLogEntry &entry, QWidget *parent) + : QDialog(parent) { + setWindowTitle("Лог #" + QString::number(entry.id)); + resize(700, 500); + + auto *layout = new QVBoxLayout(this); + + // Заголовок + auto *headerLayout = new QHBoxLayout; + auto *idLabel = new QLabel("#" + QString::number(entry.id) + ""); + headerLayout->addWidget(idLabel); + + auto *levelLabel = new QLabel(entry.level); + if (entry.level == "WARNING") + levelLabel->setStyleSheet("color: orange; font-weight: bold;"); + else if (entry.level == "CRITICAL" || entry.level == "FATAL") + levelLabel->setStyleSheet("color: red; font-weight: bold;"); + else + levelLabel->setStyleSheet("color: gray;"); + headerLayout->addWidget(levelLabel); + + auto *timeLabel = new QLabel(entry.timestamp.toLocalTime().toString("dd.MM.yyyy HH:mm:ss")); + timeLabel->setStyleSheet("color: gray;"); + headerLayout->addWidget(timeLabel); + headerLayout->addStretch(); + layout->addLayout(headerLayout); + + // Источник + auto *sourceLabel = new QLabel("Источник: " + entry.source); + sourceLabel->setWordWrap(true); + sourceLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + layout->addWidget(sourceLabel); + + // Сообщение + layout->addWidget(new QLabel("Сообщение:")); + auto *messageEdit = new QTextEdit; + messageEdit->setPlainText(entry.message); + messageEdit->setReadOnly(true); + layout->addWidget(messageEdit, 1); + + // Скриншот + if (!entry.screenshot.isEmpty()) { + QPixmap pixmap; + pixmap.loadFromData(entry.screenshot); + if (!pixmap.isNull()) { + layout->addWidget(new QLabel("Скриншот:")); + auto *imgLabel = new QLabel; + imgLabel->setPixmap(pixmap.scaledToWidth( + qMin(pixmap.width(), 600), Qt::SmoothTransformation)); + imgLabel->setAlignment(Qt::AlignCenter); + + auto *scrollArea = new QScrollArea; + scrollArea->setWidget(imgLabel); + scrollArea->setWidgetResizable(false); + scrollArea->setMaximumHeight(300); + layout->addWidget(scrollArea); + } + } + + // Кнопки + auto *btnLayout = new QHBoxLayout; + + auto *copyBtn = new QPushButton("Копировать"); + copyBtn->setStyleSheet("background-color: #1565C0; color: white; padding: 4px 16px;"); + connect(copyBtn, &QPushButton::clicked, this, [entry]() { + const QString text = QString("ID: %1\nУровень: %2\nВремя: %3\nИсточник: %4\nСообщение: %5") + .arg(entry.id) + .arg(entry.level) + .arg(entry.timestamp.toLocalTime().toString("dd.MM.yyyy HH:mm:ss")) + .arg(entry.source) + .arg(entry.message); + QApplication::clipboard()->setText(text); + }); + btnLayout->addWidget(copyBtn); + + btnLayout->addStretch(); + + auto *closeBtn = new QPushButton("Закрыть"); + connect(closeBtn, &QPushButton::clicked, this, &QDialog::close); + btnLayout->addWidget(closeBtn); + + layout->addLayout(btnLayout); +} diff --git a/views/log/LogDetailWindow.h b/views/log/LogDetailWindow.h new file mode 100644 index 0000000..661b4d4 --- /dev/null +++ b/views/log/LogDetailWindow.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include "dao/GeneralLogDAO.h" + +class LogDetailWindow final : public QDialog { + Q_OBJECT + +public: + explicit LogDetailWindow(const GeneralLogEntry &entry, QWidget *parent = nullptr); +}; diff --git a/views/scrcpy/ScrcpyWindow.cpp b/views/scrcpy/ScrcpyWindow.cpp deleted file mode 100644 index 8aadcd3..0000000 --- a/views/scrcpy/ScrcpyWindow.cpp +++ /dev/null @@ -1,169 +0,0 @@ -#include "ScrcpyWindow.h" -#include -#include -#include -#include -#include -#include - -#ifdef Q_OS_WIN -#include -#include -#endif - -ScrcpyWindow::ScrcpyWindow(QWidget *parent) : QWidget(parent), m_process(nullptr) { - // setWindowFlag(Qt::WindowStaysOnTopHint); - startScrcpy(); -} - -ScrcpyWindow::~ScrcpyWindow() { - stopScrcpy(); -} - -void ScrcpyWindow::closeEvent(QCloseEvent *event) { - qDebug() << "closeEvent triggered"; - stopScrcpy(); - event->accept(); -} - -void ScrcpyWindow::startScrcpy() { - int deviceWidth = 720; - int deviceHeight = 1600; - - QScreen *screen = QGuiApplication::primaryScreen(); - int screenHeight = screen->availableGeometry().height(); - int screenWidth = screen->availableGeometry().width(); - - // Вычисляем масштаб так, чтобы устройство влезло в экран (и по ширине, и по высоте) - double scaleW = static_cast(screenWidth) / deviceWidth; - double scaleH = static_cast(screenHeight) / deviceHeight; - double scale = qMin(scaleW, scaleH) * 0.8; // 90% от доступного размера, с отступами - - int windowWidth = static_cast(deviceWidth * scale); - int windowHeight = static_cast(deviceHeight * scale); - setFixedSize(windowWidth + 100, windowHeight); - // setWindowFlags(windowFlags() | Qt::MSWindowsFixedSizeDialogHint); - - m_process = new QProcess(this); - m_process->start("scrcpy", {"--window-title", "scrcpy", "--always-on-top", "--window-borderless"}); - - embedPlatformSpecific(); -} - -void ScrcpyWindow::stopScrcpy() { - m_process->kill(); // Принудительное завершение процесса - delete m_process; - m_process = nullptr; - -#ifdef Q_OS_WIN - HWND hwndScrcpy = FindWindowW(nullptr, L"scrcpy"); - if (hwndScrcpy) { - PostMessage(hwndScrcpy, WM_CLOSE, 0, 0); - } -#endif -} - -#ifdef Q_OS_WIN -LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { - OutputDebugString(L"WM_CLOSE received\n"); - - HWND hwndScrcpy = FindWindowW(nullptr, L"scrcpy"); - if (!hwndScrcpy) - return DefWindowProc(hwnd, msg, wParam, lParam); - - if (msg == WM_CLOSE) { - qDebug() << "WM_CLOSE received, closing scrcpy window."; - // Возможно, необходимо обработать конкретно это сообщение - // или сделать дополнительные проверки. - } - switch (msg) { - case WM_LBUTTONDOWN: - case WM_LBUTTONUP: - case WM_MOUSEMOVE: - case WM_RBUTTONDOWN: - case WM_RBUTTONUP: - case WM_MBUTTONDOWN: - case WM_MBUTTONUP: - case WM_MOUSEWHEEL: - { - // Перевод координат мыши в координаты окна scrcpy - POINT pt; - pt.x = GET_X_LPARAM(lParam); - pt.y = GET_Y_LPARAM(lParam); - qDebug() << "Before ClientToScreen: " << pt.x << ", " << pt.y; - ClientToScreen(hwnd, &pt); - qDebug() << "After ClientToScreen: " << pt.x << ", " << pt.y; - ScreenToClient(hwndScrcpy, &pt); - qDebug() << "After ScreenToClient: " << pt.x << ", " << pt.y; - - LPARAM newLParam = MAKELPARAM(pt.x, pt.y); - - SendMessage(hwndScrcpy, msg, wParam, newLParam); - return 0; - } - - case WM_KEYDOWN: - case WM_KEYUP: - case WM_CHAR: - SendMessage(hwndScrcpy, msg, wParam, lParam); - return 0; - } - - return DefWindowProc(hwnd, msg, wParam, lParam); -} - -void ScrcpyWindow::embedPlatformSpecific() { - HWND hwndScrcpy = FindWindowW(nullptr, L"scrcpy"); - - int attempts = 0; - while (!hwndScrcpy && attempts < 20) { - hwndScrcpy = FindWindowW(nullptr, L"scrcpy"); - Sleep(100); // Задержка 100 мс - attempts++; - } - - if (!hwndScrcpy) { - qDebug() << "scrcpy window not found"; - return; - } - - Sleep(1000); - - // Получаем hwnd текущего окна - HWND hwndTarget = (HWND)this->winId(); - - // Устанавливаем окно scrcpy как дочернее - SetParent(hwndScrcpy, hwndTarget); - - // Убираем рамки и заголовок - LONG style = GetWindowLong(hwndScrcpy, GWL_STYLE); - style &= ~(WS_CAPTION | WS_THICKFRAME | WS_BORDER); - SetWindowLong(hwndScrcpy, GWL_STYLE, style); - - // Получаем размеры клиентской области - RECT rect; - GetClientRect(hwndTarget, &rect); - - qDebug() << "Client area size: " << rect.right << ", " << rect.bottom; - - // int deviceWidth = 720; - // int deviceHeight = 1600; - // Client area size: 676 , 1502 - - - // Получаем размеры клиентской области - // RECT rect; - // GetClientRect(hwndTarget, &rect); - SetWindowPos(hwndScrcpy, HWND_TOP, 0, 0, - rect.right - rect.left - 200, rect.bottom - rect.top, - SWP_SHOWWINDOW | SWP_FRAMECHANGED); - - // rect.right - rect.left, rect.bottom - rect.top, - - - // Сохраняем старую оконную процедуру - // LONG_PTR prevWndProc = GetWindowLongPtr(hwndTarget, GWLP_WNDPROC); - - // SetWindowLongPtr(hwndTarget, GWLP_WNDPROC, (LONG_PTR)wndProc); -} -#endif diff --git a/views/scrcpy/ScrcpyWindow.h b/views/scrcpy/ScrcpyWindow.h deleted file mode 100644 index 5724bc7..0000000 --- a/views/scrcpy/ScrcpyWindow.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include -#include -#include - -class ScrcpyWindow final : public QWidget { - Q_OBJECT - -public: - explicit ScrcpyWindow(QWidget *parent = nullptr); - - ~ScrcpyWindow() override; - -protected: - void closeEvent(QCloseEvent *event) override; - -private: - QProcess *m_process; - - void startScrcpy(); - - void stopScrcpy(); - - void embedPlatformSpecific(); -}; diff --git a/views/widget/common/CollapsibleSection.cpp b/views/widget/common/CollapsibleSection.cpp new file mode 100644 index 0000000..faaaa6b --- /dev/null +++ b/views/widget/common/CollapsibleSection.cpp @@ -0,0 +1,111 @@ +#include "CollapsibleSection.h" +#include +#include +#include +#include +#include +#include +#include + +CollapsibleSection::CollapsibleSection( + const QString &title, + const QString &markdownContent, + QWidget *parent, + const bool expanded +): QWidget(parent) { + QColor bg = qApp->palette().color(QPalette::Window); + bool dark = bg.lightness() < 128; + + // Кнопка заголовка с стрелкой + m_toggleButton = new QToolButton(this); + m_toggleButton->setText(title); + m_toggleButton->setCheckable(true); + m_toggleButton->setChecked(expanded); + m_toggleButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + m_toggleButton->setArrowType(expanded ? Qt::DownArrow : Qt::RightArrow); + + connect(m_toggleButton, &QToolButton::toggled, this, &CollapsibleSection::onToggled); + + // Контент без собственного скрола, займёт доступное пространство + m_contentBrowser = new QTextBrowser(this); + m_contentBrowser->setSearchPaths({QCoreApplication::applicationDirPath() + "/tutorials"}); + if (markdownContent.contains(" + html.replace(QRegularExpression(R"(^## (.+)$)", QRegularExpression::MultilineOption), R"(

\1

)"); + // --- →
+ html.replace(QRegularExpression(R"(^---$)", QRegularExpression::MultilineOption), "
"); + // **text** → text + html.replace(QRegularExpression(R"(\*\*([^*]+)\*\*)"), R"(\1)"); + m_contentBrowser->setHtml(html); + } else { + m_contentBrowser->setMarkdown(markdownContent); + } + m_contentBrowser->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_contentBrowser->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_contentBrowser->setFrameShape(QFrame::NoFrame); + m_contentBrowser->setVisible(expanded); + m_contentBrowser->document()->adjustSize(); + m_contentBrowser->setReadOnly(true); + m_contentBrowser->setOpenLinks(false); + + // 2) Подключаемся к клику по ссылке + connect(m_contentBrowser, &QTextBrowser::anchorClicked, + this, [](const QUrl &url) { + QDesktopServices::openUrl(url); + }); + + if (dark) { + m_contentBrowser->setStyleSheet(R"( + QTextBrowser { + background-color: #2b2b2b; + border: 1px solid #ccc; + color: #dddddd; + border-radius:3px; + } + )"); + } else { + m_contentBrowser->setStyleSheet(R"( + QTextBrowser { + background-color: white; + border: 1px solid #ccc; + color: black; + border-radius: 3px; + } + )"); + } + updateContentHeight(); + m_contentBrowser->viewport()->installEventFilter(this); + + auto *layout = new QVBoxLayout(this); + layout->setSpacing(0); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(m_toggleButton); + layout->addSpacing(5); + layout->addWidget(m_contentBrowser); +} + +bool CollapsibleSection::eventFilter(QObject *watched, QEvent *event) { + if (watched == m_contentBrowser->viewport() && event->type() == QEvent::Resize) { + updateContentHeight(); + } + return QWidget::eventFilter(watched, event); +} + +void CollapsibleSection::updateContentHeight() const { + const qreal w = m_contentBrowser->viewport()->width(); + m_contentBrowser->document()->setTextWidth(w); + const qreal h = m_contentBrowser->document()->size().height(); + // Можно добавить +1 пиксел на погрешность: + m_contentBrowser->setFixedHeight(static_cast(h) + 5); +} + +void CollapsibleSection::onToggled(const bool checked) const { + m_toggleButton->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow); + m_contentBrowser->setVisible(checked); + + if (checked) { + updateContentHeight(); + } +} diff --git a/views/widget/common/CollapsibleSection.h b/views/widget/common/CollapsibleSection.h new file mode 100644 index 0000000..aac9bf7 --- /dev/null +++ b/views/widget/common/CollapsibleSection.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +class CollapsibleSection final : public QWidget { + Q_OBJECT + +public: + explicit CollapsibleSection( + const QString &title, + const QString &markdownContent, + QWidget *parent = nullptr, + bool expanded = false + ); + +private slots: + void onToggled(bool checked) const; + +private: + QToolButton *m_toggleButton; + QTextBrowser *m_contentBrowser; + + void updateContentHeight() const; + + bool eventFilter(QObject *watched, QEvent *event) override; +}; diff --git a/views/widget/FlowLayout.cpp b/views/widget/common/FlowLayout.cpp similarity index 100% rename from views/widget/FlowLayout.cpp rename to views/widget/common/FlowLayout.cpp diff --git a/views/widget/FlowLayout.h b/views/widget/common/FlowLayout.h similarity index 100% rename from views/widget/FlowLayout.h rename to views/widget/common/FlowLayout.h diff --git a/views/widget/loader/ErrorWindow.cpp b/views/widget/loader/ErrorWindow.cpp new file mode 100644 index 0000000..1e06124 --- /dev/null +++ b/views/widget/loader/ErrorWindow.cpp @@ -0,0 +1,27 @@ +#include "ErrorWindow.h" +#include +#include +#include +#include + +ErrorWindow::ErrorWindow(const QString &errorMessage, bool showRetry, QWidget *parent) : QWidget(parent) { + setWindowTitle("Ошибка"); + resize(300, 150); + + QVBoxLayout *layout = new QVBoxLayout(this); + + QLabel *label = new QLabel(errorMessage); + label->setWordWrap(true); + layout->addWidget(label); + + if (showRetry) { + QPushButton *retryButton = new QPushButton("Повторить"); + layout->addWidget(retryButton); + connect(retryButton, &QPushButton::clicked, this, &ErrorWindow::retryRequested); + } + + QPushButton *closeButton = new QPushButton("Закрыть"); + layout->addWidget(closeButton); + + connect(closeButton, &QPushButton::clicked, qApp, &QApplication::quit); +} diff --git a/views/widget/loader/ErrorWindow.h b/views/widget/loader/ErrorWindow.h new file mode 100644 index 0000000..4c89874 --- /dev/null +++ b/views/widget/loader/ErrorWindow.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +class ErrorWindow final : public QWidget { + Q_OBJECT + +public: + explicit ErrorWindow(const QString &errorMessage, bool showRetry = false, QWidget *parent = nullptr); + +signals: + void retryRequested(); +}; diff --git a/views/widget/loader/LoaderDialog.cpp b/views/widget/loader/LoaderDialog.cpp new file mode 100644 index 0000000..8da95f9 --- /dev/null +++ b/views/widget/loader/LoaderDialog.cpp @@ -0,0 +1,18 @@ +#include "LoaderDialog.h" +#include +#include +#include + +LoaderDialog::LoaderDialog(QWidget *parent) + : QDialog(parent) +{ + setWindowTitle("Загрузка..."); + setModal(true); + setFixedSize(200, 100); + + QVBoxLayout *layout = new QVBoxLayout(this); + + QLabel *label = new QLabel("Пожалуйста, подождите..."); + label->setAlignment(Qt::AlignCenter); + layout->addWidget(label); +} diff --git a/views/widget/loader/LoaderDialog.h b/views/widget/loader/LoaderDialog.h new file mode 100644 index 0000000..5c96855 --- /dev/null +++ b/views/widget/loader/LoaderDialog.h @@ -0,0 +1,12 @@ +#pragma once +#include + +class QLabel; +class QMovie; + +class LoaderDialog : public QDialog { + Q_OBJECT + +public: + explicit LoaderDialog(QWidget *parent = nullptr); +}; diff --git a/views/widget/loader/LoaderWindow.cpp b/views/widget/loader/LoaderWindow.cpp new file mode 100644 index 0000000..cce138a --- /dev/null +++ b/views/widget/loader/LoaderWindow.cpp @@ -0,0 +1,61 @@ +#include "LoaderWindow.h" +#include +#include +#include + +#include "net/NetworkService.h" + +LoaderWindow::LoaderWindow(QWidget *parent) : QWidget(parent) { + setWindowTitle("Проверка приложения"); + setFixedSize(300, 100); + + QVBoxLayout *layout = new QVBoxLayout(this); + + QLabel *label = new QLabel("Пожалуйста, подождите..."); + layout->addWidget(label); + + progressBar = new QProgressBar(); + progressBar->setRange(0, 100); + layout->addWidget(progressBar); + + timer = new QTimer(this); + connect(timer, &QTimer::timeout, this, &LoaderWindow::updateProgress); + timer->start(25); + + auto *thread = new QThread; + auto *networkService = new NetworkService; + networkService->moveToThread(thread); + + connect(thread, &QThread::started, networkService, &NetworkService::loginAndSetup); + connect(networkService, &NetworkService::finished, thread, &QThread::quit); + connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + + connect(networkService, &NetworkService::finishedWithResult, this, [this](const int result) { + m_checkResult = result; + if (m_checkResult == -1) { + if (timer) { + timer->stop(); + } + emit loadingFinished(m_checkResult); + } else if (progressBar->value() >= 100) { + emit loadingFinished(m_checkResult); + } + }, Qt::QueuedConnection); + thread->start(); +} + +void LoaderWindow::updateProgress() { + const int value = progressBar->value(); + if (value < 99) { + progressBar->setValue(value + 1); + } else if (m_checkResult != 1000) { + // Результат получен — завершаем + progressBar->setValue(100); + if (timer) { + timer->stop(); + } + emit loadingFinished(m_checkResult); + } + // Иначе ждём на 99 пока сеть не ответит +} diff --git a/views/widget/loader/LoaderWindow.h b/views/widget/loader/LoaderWindow.h new file mode 100644 index 0000000..2257c51 --- /dev/null +++ b/views/widget/loader/LoaderWindow.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +class LoaderWindow final : public QWidget { + Q_OBJECT + +public: + explicit LoaderWindow(QWidget *parent = nullptr); + +signals: + void loadingFinished(int status); + +private slots: + void updateProgress(); + +private: + QProgressBar *progressBar; + QTimer *timer; + int m_checkResult = 1000; +}; diff --git a/views/widget/loader/RegistrationWindow.cpp b/views/widget/loader/RegistrationWindow.cpp new file mode 100644 index 0000000..650ea27 --- /dev/null +++ b/views/widget/loader/RegistrationWindow.cpp @@ -0,0 +1,78 @@ +#include "RegistrationWindow.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "LoaderDialog.h" +#include "dao/SettingsDAO.h" +#include "net/NetworkService.h" + +RegistrationWindow::RegistrationWindow(QWidget *parent): QWidget(parent) { + setWindowTitle("Вход"); + resize(300, 130); + + auto *layout = new QVBoxLayout(this); + + layout->addWidget(new QLabel("Введите ваш идентификатор:")); + + QLineEdit *apiKeyEdit = new QLineEdit(); + apiKeyEdit->setPlaceholderText("Идентификатор"); + layout->addWidget(apiKeyEdit); + + QHBoxLayout *buttonLayout = new QHBoxLayout(); + + QPushButton *loginButton = new QPushButton("Войти"); + QPushButton *closeButton = new QPushButton("Закрыть"); + + buttonLayout->addWidget(loginButton); + buttonLayout->addWidget(closeButton); + + layout->addLayout(buttonLayout); + + connect(closeButton, &QPushButton::clicked, qApp, &QApplication::quit); + connect(loginButton, &QPushButton::clicked, this, [=]() { + const QString apiKey = apiKeyEdit->text().trimmed(); + if (apiKey.isEmpty()) { + QMessageBox::warning(this, "Ошибка", "Введите ваш идентификатор"); + return; + } + + SettingsDAO::set("api_key", apiKey); + + auto *loader = new LoaderDialog(this); + loader->show(); + + auto *thread = new QThread; + auto *networkService = new NetworkService; + networkService->moveToThread(thread); + + connect(thread, &QThread::started, networkService, &NetworkService::loginAndSetup); + connect(networkService, &NetworkService::finished, thread, &QThread::quit); + connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + + connect(networkService, &NetworkService::finishedWithResult, this, [=](int result) { + QMetaObject::invokeMethod(this, [=]() { + loader->close(); + loader->deleteLater(); + + if (result == 1) { + emit onRegisteredSuccess(); + } else if (result == 0) { + SettingsDAO::remove("api_key"); + QMessageBox::critical(this, "Ошибка входа", "Неверный идентификатор. Проверьте и попробуйте снова."); + } else { + QMessageBox::critical(this, "Ошибка подключения", + "Нет подключения к интернету или сервер не отвечает.\n\nПожалуйста, попробуйте позже."); + } + }, Qt::QueuedConnection); + }); + thread->start(); + }); +} diff --git a/views/widget/loader/RegistrationWindow.h b/views/widget/loader/RegistrationWindow.h new file mode 100644 index 0000000..9d386d8 --- /dev/null +++ b/views/widget/loader/RegistrationWindow.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +class RegistrationWindow final : public QWidget { + Q_OBJECT + +public: + explicit RegistrationWindow(QWidget *parent = nullptr); + +signals: + void onRegisteredSuccess(); +};