diff --git a/CMakeLists.txt b/CMakeLists.txt index d8a708a..4a78487 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,7 +75,7 @@ if (WIN32) # Копирование изображений в директорию сборки file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/database/app_data.db" DESTINATION "${CMAKE_BINARY_DIR}") - file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/images" DESTINATION "${CMAKE_BINARY_DIR}") + file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/tutorials" DESTINATION "${CMAKE_BINARY_DIR}") file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/rus.traineddata" DESTINATION "${CMAKE_BINARY_DIR}") file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/config.ini" DESTINATION "${CMAKE_BINARY_DIR}") diff --git a/android/rshb/PayByPhoneScript.cpp b/android/rshb/PayByPhoneScript.cpp index d0e8be6..9a6b23e 100644 --- a/android/rshb/PayByPhoneScript.cpp +++ b/android/rshb/PayByPhoneScript.cpp @@ -81,8 +81,7 @@ void PayByPhoneScript::makePayment( } xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); - QString payBankName; - payBankName = m_bankName == "Россельхозбанк" ? "Россельхозбанк" : "Другой банк через СБП"; + QString payBankName = m_bankName == QStringLiteral("Россельхозбанк") ? "Россельхозбанк" : "Другой банк через СБП"; Node payOtherBankBtn = xmlScreenParser.findButtonNode(payBankName, false); if (!payOtherBankBtn.isEmpty()) { AdbUtils::makeTap(deviceId, payOtherBankBtn.x(), payOtherBankBtn.y()); @@ -93,48 +92,56 @@ void PayByPhoneScript::makePayment( return; } - xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); - for (int i = 0; i < 5; ++i) { - Node allBanksTitle = xmlScreenParser.findTextNode("Банки участники"); - if (!allBanksTitle.isEmpty() && allBanksTitle.y() < 300) { - Node inputBankName = xmlScreenParser.findFirstEditText(); - AdbUtils::makeTap(deviceId, inputBankName.x(), inputBankName.y()); - QThread::msleep(500); - AdbUtils::enableAdbIMEKeyboard(deviceId); - AdbUtils::inputAdbIMEText(deviceId, m_bankName); - AdbUtils::disableAdbIMEKeyboard(deviceId); - QThread::msleep(1000); - break; - } else { - qDebug() << "--- allBanksTitle not found, i: " << i; - } - QThread::msleep(1000); + if (m_bankName != QStringLiteral("Россельхозбанк")) { xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); - } + for (int i = 0; i < 5; ++i) { + Node allBanksTitle = xmlScreenParser.findTextNode("Банки участники"); + if (!allBanksTitle.isEmpty() && allBanksTitle.y() < 300) { + Node inputBankName = xmlScreenParser.findFirstEditText(); + AdbUtils::makeTap(deviceId, inputBankName.x(), inputBankName.y()); + QThread::msleep(500); + AdbUtils::enableAdbIMEKeyboard(deviceId); + AdbUtils::inputAdbIMEText(deviceId, m_bankName); + AdbUtils::disableAdbIMEKeyboard(deviceId); + QThread::msleep(1000); + break; + } else { + qDebug() << "--- allBanksTitle not found, i: " << i; + } + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + } - xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); - for (int i = 0; i < 5; ++i) { - Node bankNameListItem = xmlScreenParser.findTextNode(m_bankName); - if (!bankNameListItem.isEmpty()) { - AdbUtils::makeTap(deviceId, bankNameListItem.x(), bankNameListItem.y()); - QThread::msleep(1000); - break; - } else { - qDebug() << "--- bankNameListItem not found, i: " << i; - } - QThread::msleep(1000); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + for (int i = 0; i < 5; ++i) { + Node bankNameListItem = xmlScreenParser.findTextNode(m_bankName); + if (!bankNameListItem.isEmpty()) { + AdbUtils::makeTap(deviceId, bankNameListItem.x(), bankNameListItem.y()); + QThread::msleep(1000); + break; + } else { + qDebug() << "--- bankNameListItem not found, i: " << i; + } + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + } } xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); - Node payByPhoneScreenTitle = xmlScreenParser.findTextNode("Перевод по телефону"); + const bool isRshbBank = m_bankName == QStringLiteral("Россельхозбанк"); + QString payByPhoneScreenTitleText = isRshbBank ? "Перевод клиенту РСХБ" : "Перевод по телефону"; + Node payByPhoneScreenTitle = xmlScreenParser.findTextNode(payByPhoneScreenTitleText); Node continuePayBtn = xmlScreenParser.findButtonNode("Продолжить", false); - if (!payByPhoneScreenTitle.isEmpty() && payByPhoneScreenTitle.y() < 300 && !continuePayBtn.isEmpty()) { + + if (isRshbBank || + !payByPhoneScreenTitle.isEmpty() && payByPhoneScreenTitle.y() < 300 && !continuePayBtn.isEmpty()) { Node sumBtn = xmlScreenParser.findButtonNode("0", false); - if (!sumBtn.isEmpty()) { - AdbUtils::makeTap(deviceId, sumBtn.x(), sumBtn.y()); - QThread::msleep(500); + if (isRshbBank || !sumBtn.isEmpty()) { + if (!isRshbBank) { + AdbUtils::makeTap(deviceId, sumBtn.x(), sumBtn.y()); + QThread::msleep(500); + } AdbUtils::inputText(deviceId, QString::number(m_amount)); QThread::msleep(500); @@ -143,6 +150,11 @@ void PayByPhoneScript::makePayment( if (!readyBtn.isEmpty()) { AdbUtils::makeTap(deviceId, readyBtn.x(), readyBtn.y()); QThread::msleep(500); + if (isRshbBank) { + QThread::msleep(1000); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + continuePayBtn = xmlScreenParser.findButtonNode("Продолжить", false); + } AdbUtils::makeTap(deviceId, continuePayBtn.x(), continuePayBtn.y()); QThread::msleep(1000); } else { @@ -156,7 +168,7 @@ void PayByPhoneScript::makePayment( return; } } else { - m_error = "Не смогли найти 'Перевод по телефону'"; + m_error = "Не смогли найти заголовок: " + payByPhoneScreenTitleText; qDebug() << m_error; return; } @@ -187,7 +199,7 @@ void PayByPhoneScript::makePayment( xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); } - if (!inputedPinCode) { + if (!isRshbBank && !inputedPinCode) { m_error = "Не смогли ввести пин-код!'"; qDebug() << m_error; return; @@ -204,6 +216,9 @@ void PayByPhoneScript::makePayment( // TODO считываем все данные по переводу TransactionInfo transaction = xmlScreenParser.parseTransactionInfo(width, height); + if (isRshbBank) { + transaction.bankName = m_bankName; + } transaction.accountId = m_account.id; transaction.amount = -m_amount; transaction.type = TransactionType::Phone; @@ -217,6 +232,7 @@ void PayByPhoneScript::makePayment( for (int k = 0; k < 10; ++k) { Node paymentCompleted = xmlScreenParser.findTextNode("Исполнен", 0, 0, width, height / 2); if (!paymentCompleted.isEmpty()) { + transaction.status = TransactionStatus::Complete; if (!TransactionDAO::updateTransactionStatus(transaction.id, TransactionStatus::Complete)) { qDebug() << "Not updated: " << transaction.id; } @@ -227,6 +243,8 @@ void PayByPhoneScript::makePayment( QThread::msleep(1000); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); } + + postNewTransactionData(m_account, transaction); break; } else { qWarning() << "--- moreDetailBtn not found, i: " << i; @@ -245,39 +263,49 @@ void PayByPhoneScript::doStart() { const ApplicationInfo app = ApplicationDAO::getApplicationsByDeviceId(m_account.deviceId, m_account.appName); if (device.id.isEmpty() || app.id.isEmpty()) { - qDebug() << "Device or app not found...."; + m_error = "Device or app not found...."; + qDebug() << m_error; + emit finishedWithResult(m_error); return; } - if (runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { - if (goToAllProducts(device.id, device.screenWidth, device.screenHeight)) { - qDebug() << "======== 1. All products"; - QList > accounts = xmlScreenParser.parseAccountsInfo(); - for (auto &[account, node]: accounts) { - account.deviceId = m_account.deviceId; - account.appName = m_account.appName; - if (!AccountDAO::upsertAccount(account)) { - qDebug() << "Not updated: " << account.lastNumbers; - } - } - for (auto &[account, node]: accounts) { - // FIXME будем считать что числа последние везде уникальные - if (m_account.lastNumbers == account.lastNumbers) { - qDebug() << "======== 2. Tap by card"; - AdbUtils::makeTap(device.id, node.x(), node.y()); - QThread::msleep(1000); - break; - } - } - - qDebug() << "======== 3. Make payment"; - makePayment(device.id, app.pinCode, device.screenWidth, device.screenHeight); - } else { - m_error = "Не смогли открыть 'Все продукты'"; + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_account.deviceId, ++m_counter)); + if (!xmlScreenParser.isHomeScreen()) { + if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { + m_error = "Не смогли дойти до домашней страницы...."; qDebug() << m_error; + emit finishedWithResult(m_error); + return; } + } + + if (goToAllProducts(device.id, device.screenWidth, device.screenHeight)) { + qDebug() << "======== 1. All products"; + QList > accounts = xmlScreenParser.parseAccountsInfo(); + for (auto &[account, node]: accounts) { + account.deviceId = m_account.deviceId; + account.appName = m_account.appName; + if (!AccountDAO::upsertAccount(account)) { + qDebug() << "Not updated: " << account.lastNumbers; + } + } + + postAccountsData(accounts); + + for (auto &[account, node]: accounts) { + // FIXME будем считать что числа последние везде уникальные + if (m_account.lastNumbers == account.lastNumbers) { + qDebug() << "======== 2. Tap by card"; + AdbUtils::makeTap(device.id, node.x(), node.y()); + QThread::msleep(1000); + break; + } + } + + qDebug() << "======== 3. Make payment"; + makePayment(device.id, app.pinCode, device.screenWidth, device.screenHeight); } else { - m_error = "Не смогли дойти до домашней страницы...."; + m_error = "Не смогли открыть 'Все продукты'"; qDebug() << m_error; } diff --git a/android/rshb/SyncAccountDataScript.cpp b/android/rshb/SyncAccountDataScript.cpp new file mode 100644 index 0000000..ceb973f --- /dev/null +++ b/android/rshb/SyncAccountDataScript.cpp @@ -0,0 +1,15 @@ +#include "SyncAccountDataScript.h" + +SyncAccountDataScript::SyncAccountDataScript( + QString deviceId, + QObject *parent +) : CommonScript(parent), + m_deviceId(std::move(deviceId)) { +} + +SyncAccountDataScript::~SyncAccountDataScript() = default; + +void SyncAccountDataScript::doStart() { + + // 1. +} \ No newline at end of file diff --git a/android/rshb/SyncAccountDataScript.h b/android/rshb/SyncAccountDataScript.h new file mode 100644 index 0000000..c9da879 --- /dev/null +++ b/android/rshb/SyncAccountDataScript.h @@ -0,0 +1,21 @@ +#pragma once +#include "CommonScript.h" + +class SyncAccountDataScript final : public CommonScript { + Q_OBJECT + +public: + ~SyncAccountDataScript() override; + + explicit SyncAccountDataScript( + QString deviceId, + QObject *parent = nullptr + ); + +protected: + void doStart() override; + +private: + const QString m_deviceId; +}; + diff --git a/database/app_data.db b/database/app_data.db index 7f38617..6adda89 100644 Binary files a/database/app_data.db and b/database/app_data.db differ diff --git a/database/dao/AccountDAO.cpp b/database/dao/AccountDAO.cpp index 45af597..c9fc019 100644 --- a/database/dao/AccountDAO.cpp +++ b/database/dao/AccountDAO.cpp @@ -10,8 +10,8 @@ bool AccountDAO::upsertAccount(const AccountInfo &info) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - INSERT INTO accounts (device_id, app_name, numbers, last_numbers, amount, update_time, status, description) - VALUES (:device_id, :app_name, :numbers, :last_numbers, :amount, :update_time, :status, :description) + INSERT INTO accounts (device_id, app_name, last_numbers, amount, update_time, status, description) + VALUES (:device_id, :app_name, :last_numbers, :amount, :update_time, :status, :description) ON CONFLICT(device_id, app_name, last_numbers) DO UPDATE SET amount = excluded.amount, @@ -22,7 +22,6 @@ bool AccountDAO::upsertAccount(const AccountInfo &info) { query.bindValue(":device_id", info.deviceId); query.bindValue(":app_name", info.appName); - query.bindValue(":numbers", info.numbers); query.bindValue(":last_numbers", info.lastNumbers); query.bindValue(":amount", info.amount); query.bindValue(":update_time", DateUtils::toUtcString(info.updateTime)); @@ -50,7 +49,6 @@ AccountInfo parseAccount(const QSqlQuery &query) { acc.deviceId = query.value("device_id").toString(); acc.appName = query.value("app_name").toString(); acc.lastNumbers = query.value("last_numbers").toString(); - acc.numbers = query.value("numbers").toString(); acc.amount = query.value("amount").toDouble(); acc.updateTime = DateUtils::fromUtcString(query.value("update_time").toString()); acc.status = accountStatusFromString(query.value("status").toString()); @@ -63,7 +61,7 @@ QList AccountDAO::getAccounts(const QString &deviceId, const QStrin QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - SELECT id, device_id, app_name, numbers, last_numbers, amount, update_time, status, description + SELECT id, device_id, app_name, last_numbers, amount, update_time, status, description FROM accounts WHERE device_id = :device_id AND app_name = :app_name )"); @@ -87,7 +85,7 @@ AccountInfo AccountDAO::getAccountById(const int accountId) { QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - SELECT id, device_id, app_name, numbers, last_numbers, amount, update_time, status, description + SELECT id, device_id, app_name, last_numbers, amount, update_time, status, description FROM accounts WHERE id = :id LIMIT 1 @@ -110,7 +108,7 @@ AccountInfo AccountDAO::findAppAccount(const QString &deviceId, const QString &a QSqlQuery query(DatabaseManager::instance().database()); query.prepare(R"( - SELECT id, device_id, app_name, numbers, last_numbers, amount, update_time, status, description + SELECT id, device_id, app_name, last_numbers, amount, update_time, status, description FROM accounts WHERE app_name = :appName AND last_numbers = :lastNumber AND device_id = :deviceId LIMIT 1 diff --git a/database/dao/EventDAO.cpp b/database/dao/EventDAO.cpp index a3c7d28..15645d4 100644 --- a/database/dao/EventDAO.cpp +++ b/database/dao/EventDAO.cpp @@ -40,7 +40,7 @@ int EventDAO::insertEvent(const EventInfo &event) { query.bindValue(":amount", event.amount); query.bindValue(":phone", event.phone); query.bindValue(":bank_name", event.bankName); - query.bindValue(":timestamp", DateUtils::toUtcString(DateUtils::getUtcNow())); + query.bindValue(":timestamp", event.timestamp); if (!query.exec()) { qCritical() << "Ошибка при вставке event:" << query.lastError().text(); diff --git a/main.cpp b/main.cpp index edbb7c9..70996a5 100644 --- a/main.cpp +++ b/main.cpp @@ -11,7 +11,7 @@ #include "automation_script/Script.h" #include -#include +#include #include #include #include @@ -191,35 +191,38 @@ void messageHandler( int main(int argc, char *argv[]) { QApplication app(argc, argv); - logFile.setFileName("application.log"); - if (!logFile.open(QIODevice::Append | QIODevice::Text)) { - qWarning() << "Не удалось открыть лог-файл для записи"; - } - // Устанавливаем свой обработчик - qInstallMessageHandler(messageHandler); + // Выбрать стиль Fusion + app.setStyle(QStyleFactory::create("Fusion")); - auto *thread = new QThread; - auto *worker = new EventHandler; - worker->moveToThread(thread); - QObject::connect(thread, &QThread::started, worker, &EventHandler::start); - QObject::connect(worker, &EventHandler::finished, thread, &QThread::quit); - QObject::connect(worker, &EventHandler::finished, worker, &QObject::deleteLater); - QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); - thread->start(); - - auto *paymentThread = new QThread; - auto *networkService = new NetworkService; - - // FIXME доставить десктоп ид из настроеек - networkService->setDeviceData("DT88bokcwQ", ""); - 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); - paymentThread->start(); - - setupWorkerAndThread(app); + // logFile.setFileName("application.log"); + // if (!logFile.open(QIODevice::Append | QIODevice::Text)) { + // qWarning() << "Не удалось открыть лог-файл для записи"; + // } + // // Устанавливаем свой обработчик + // qInstallMessageHandler(messageHandler); + // + // auto *thread = new QThread; + // auto *worker = new EventHandler; + // worker->moveToThread(thread); + // QObject::connect(thread, &QThread::started, worker, &EventHandler::start); + // QObject::connect(worker, &EventHandler::finished, thread, &QThread::quit); + // QObject::connect(worker, &EventHandler::finished, worker, &QObject::deleteLater); + // QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + // thread->start(); + // + // auto *paymentThread = new QThread; + // auto *networkService = new NetworkService; + // + // // FIXME доставить десктоп ид из настроеек + // networkService->setDeviceData("DT88bokcwQ", ""); + // 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); + // paymentThread->start(); + // + // setupWorkerAndThread(app); MainWindow window; window.show(); diff --git a/migrations.sql b/migrations.sql index d995f42..3b89184 100644 --- a/migrations.sql +++ b/migrations.sql @@ -31,7 +31,6 @@ CREATE TABLE IF NOT EXISTS accounts id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT, app_name TEXT, - numbers TEXT, last_numbers TEXT, description TEXT, amount REAL, diff --git a/models/db/AccountInfo.h b/models/db/AccountInfo.h index cf66d89..25916b3 100644 --- a/models/db/AccountInfo.h +++ b/models/db/AccountInfo.h @@ -26,7 +26,6 @@ struct AccountInfo { QString deviceId; QString appName; QString lastNumbers; - QString numbers; double amount = 0.0; QDateTime updateTime; AccountStatus status = AccountStatus::Active; @@ -42,7 +41,6 @@ inline QString convertAccountToString(const AccountInfo &info) { if (info.id != -1) lines << " id: " + QString::number(info.id); else emptyFields << "id"; if (!info.deviceId.isEmpty()) lines << " deviceId: " + info.deviceId; else emptyFields << "deviceId"; if (!info.appName.isEmpty()) lines << " appName: " + info.appName; else emptyFields << "appName"; - if (!info.numbers.isEmpty()) lines << " numbers: " + info.numbers; else emptyFields << "numbers"; if (!info.lastNumbers.isEmpty()) lines << " lastNumbers: " + info.lastNumbers; else emptyFields << "lastNumbers"; if (info.amount != 0.0) lines << " amount: " + QString::number(info.amount, 'f', 2); else emptyFields << "amount"; if (info.updateTime.isValid()) lines << " updateTime: " + info.updateTime.toString(Qt::ISODate); else emptyFields << "updateTime"; diff --git a/services/net/NetworkService.cpp b/services/net/NetworkService.cpp index e850f84..9c18397 100644 --- a/services/net/NetworkService.cpp +++ b/services/net/NetworkService.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "dao/AccountDAO.h" #include "dao/EventDAO.h" @@ -144,6 +145,7 @@ void NetworkService::getPayments() { EventInfo eventInfo; eventInfo.status = EventStatus::Wait; eventInfo.externalId = 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(); diff --git a/tutorials/test.md b/tutorials/test.md new file mode 100644 index 0000000..abe8d15 --- /dev/null +++ b/tutorials/test.md @@ -0,0 +1,17 @@ +# Руководство: Запуск приложения TutorialWindow + +Этот файл объясняет, как собрать и запустить ваше Qt6-приложение с окном TutorialWindow. + +--- + +## 1. Требования + +- Qt 6 (рекомендуется версия 6.2 или выше) +- C++17-совместимый компилятор (GCC, Clang, MSVC) +- CMake (версия ≥ 3.16) или qmake + +--- + +## 2. Структура проекта +[Текст ссылки](https://example.com) + diff --git a/tutorials/test2.md b/tutorials/test2.md new file mode 100644 index 0000000..21604fb --- /dev/null +++ b/tutorials/test2.md @@ -0,0 +1,16 @@ +# Руководство: Запуск приложения TutorialWindow + +Этот файл объясняет, как собрать и запустить ваше Qt6-приложение с окном TutorialWindow. + +--- + +## 1. Требования + +- Qt 6 (рекомендуется версия 6.2 или выше) +- C++17-совместимый компилятор (GCC, Clang, MSVC) +- CMake (версия ≥ 3.16) или qmake + +--- + +## 2. Структура проекта + diff --git a/tutorials/test3.md b/tutorials/test3.md new file mode 100644 index 0000000..21604fb --- /dev/null +++ b/tutorials/test3.md @@ -0,0 +1,16 @@ +# Руководство: Запуск приложения TutorialWindow + +Этот файл объясняет, как собрать и запустить ваше Qt6-приложение с окном TutorialWindow. + +--- + +## 1. Требования + +- Qt 6 (рекомендуется версия 6.2 или выше) +- C++17-совместимый компилятор (GCC, Clang, MSVC) +- CMake (версия ≥ 3.16) или qmake + +--- + +## 2. Структура проекта + diff --git a/views/MainWindow.cpp b/views/MainWindow.cpp index 73b1f18..fccd975 100644 --- a/views/MainWindow.cpp +++ b/views/MainWindow.cpp @@ -1,15 +1,16 @@ #include "MainWindow.h" -#include "AccountWindow.h" +#include "bank/AccountWindow.h" #include #include #include #include +#include "TutorialWindow.h" #include "db/DeviceInfo.h" -#include "DeviceWidget.h" +#include "device/DeviceWidget.h" #include "dao/DeviceDAO.h" -#include "widget/FlowLayout.h" +#include "widget/common/FlowLayout.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { // Инициализируем таймер обновления @@ -20,12 +21,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); @@ -34,25 +35,43 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { QMenu *menu = menuBar()->addMenu("Меню"); auto *devicesPageAction = new QAction("Активные устройства", this); auto *accountPageAction = new QAction("Аккаунты", this); + auto *tutorialPageAction = new QAction("Инструкция", this); menu->addAction(devicesPageAction); menu->addAction(accountPageAction); + menu->addAction(tutorialPageAction); 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); + 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(); + }); + + m_tutorialWindow = new TutorialWindow(this); + m_tutorialWindow->show(); + QTimer::singleShot(0, m_tutorialWindow, [w = m_tutorialWindow]() { + w->raise(); + w->activateWindow(); + }); } void MainWindow::createDevicePage() { - if (devicePage) { + if (m_devicesWindow) { return; } @@ -73,22 +92,22 @@ void MainWindow::createDevicePage() { scrollArea->setWidget(contentWidget); scrollArea->setWidgetResizable(true); - devicePage = scrollArea; - stackedWidget->addWidget(devicePage); + m_devicesWindow = scrollArea; + stackedWidget->addWidget(m_devicesWindow); timer->start(1000); } 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; } diff --git a/views/MainWindow.h b/views/MainWindow.h index 685b957..b4e7db9 100644 --- a/views/MainWindow.h +++ b/views/MainWindow.h @@ -3,24 +3,31 @@ #include #include #include + +#include "TutorialWindow.h" +#include "bank/AccountWindow.h" #include "db/DeviceInfo.h" -#include "widget/FlowLayout.h" +#include "widget/common/FlowLayout.h" class MainWindow final : public QMainWindow { Q_OBJECT + public: explicit MainWindow(QWidget *parent = nullptr); + void loadDevices(); private: - QWidget* central; - FlowLayout* flowLayout; - QTimer* timer; - QStackedWidget* stackedWidget; + QWidget *central; + FlowLayout *flowLayout; + QTimer *timer; + QStackedWidget *stackedWidget; - QWidget* devicePage = nullptr; - QWidget* accountPage = nullptr; + QWidget *m_devicesWindow = nullptr; + QWidget *m_accountsWindow = nullptr; + QWidget *m_tutorialWindow = nullptr; void createDevicePage(); + void deleteDevicePage(); }; diff --git a/views/TutorialWindow.cpp b/views/TutorialWindow.cpp new file mode 100644 index 0000000..e24fa23 --- /dev/null +++ b/views/TutorialWindow.cpp @@ -0,0 +1,63 @@ +#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::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/AccountWindow.cpp b/views/bank/AccountWindow.cpp similarity index 100% rename from views/AccountWindow.cpp rename to views/bank/AccountWindow.cpp 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/TransactionWindow.cpp b/views/bank/TransactionWindow.cpp similarity index 100% rename from views/TransactionWindow.cpp rename to views/bank/TransactionWindow.cpp 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/DeviceWidget.cpp b/views/device/DeviceWidget.cpp similarity index 100% rename from views/DeviceWidget.cpp rename to views/device/DeviceWidget.cpp diff --git a/views/DeviceWidget.h b/views/device/DeviceWidget.h similarity index 100% rename from views/DeviceWidget.h rename to views/device/DeviceWidget.h diff --git a/views/widget/common/CollapsibleSection.cpp b/views/widget/common/CollapsibleSection.cpp new file mode 100644 index 0000000..ff1f003 --- /dev/null +++ b/views/widget/common/CollapsibleSection.cpp @@ -0,0 +1,85 @@ +#include "CollapsibleSection.h" +#include +#include +#include +#include + +CollapsibleSection::CollapsibleSection( + const QString &title, + const QString &markdownContent, + QWidget *parent, + const bool expanded +): QWidget(parent) { + // Кнопка заголовка с стрелкой + 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->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(); + // после создания textBrowser: + m_contentBrowser->setReadOnly(true); + + // 1) Отключаем встроенные переходы + m_contentBrowser->setOpenLinks(false); + + // 2) Подключаемся к клику по ссылке + connect(m_contentBrowser, &QTextBrowser::anchorClicked, + this, [](const QUrl &url) { + QDesktopServices::openUrl(url); + }); + m_contentBrowser->setStyleSheet( + "QTextBrowser {" + " border: 1px solid #ccc;" // рамка (по желанию) + " border-radius: 3px;" // радиус скругления + " background-color: white;" // фон (по желанию) + "}" + ); + 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