1
0
forked from BRT/arc

Refactor account handling and add TutorialWindow feature

Removed redundant "numbers" field from Account model and database queries to simplify account structure. Introduced a TutorialWindow class for displaying application tutorials, integrated into the main window, and updated file paths accordingly. Improved string handling and logic in Payment routines and enhanced overall UI structure.
This commit is contained in:
slava 2025-05-23 19:25:02 +07:00
parent 5cb32ec4f1
commit 27ce69d094
28 changed files with 453 additions and 129 deletions

View File

@ -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}/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}/rus.traineddata" DESTINATION "${CMAKE_BINARY_DIR}")
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/config.ini" DESTINATION "${CMAKE_BINARY_DIR}") file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/config.ini" DESTINATION "${CMAKE_BINARY_DIR}")

View File

@ -81,8 +81,7 @@ void PayByPhoneScript::makePayment(
} }
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
QString payBankName; QString payBankName = m_bankName == QStringLiteral("Россельхозбанк") ? "Россельхозбанк" : "Другой банк через СБП";
payBankName = m_bankName == "Россельхозбанк" ? "Россельхозбанк" : "Другой банк через СБП";
Node payOtherBankBtn = xmlScreenParser.findButtonNode(payBankName, false); Node payOtherBankBtn = xmlScreenParser.findButtonNode(payBankName, false);
if (!payOtherBankBtn.isEmpty()) { if (!payOtherBankBtn.isEmpty()) {
AdbUtils::makeTap(deviceId, payOtherBankBtn.x(), payOtherBankBtn.y()); AdbUtils::makeTap(deviceId, payOtherBankBtn.x(), payOtherBankBtn.y());
@ -93,6 +92,7 @@ void PayByPhoneScript::makePayment(
return; return;
} }
if (m_bankName != QStringLiteral("Россельхозбанк")) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
for (int i = 0; i < 5; ++i) { for (int i = 0; i < 5; ++i) {
Node allBanksTitle = xmlScreenParser.findTextNode("Банки участники"); Node allBanksTitle = xmlScreenParser.findTextNode("Банки участники");
@ -125,16 +125,23 @@ void PayByPhoneScript::makePayment(
QThread::msleep(1000); QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
} }
}
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); 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); Node sumBtn = xmlScreenParser.findButtonNode("0", false);
if (!sumBtn.isEmpty()) { if (isRshbBank || !sumBtn.isEmpty()) {
if (!isRshbBank) {
AdbUtils::makeTap(deviceId, sumBtn.x(), sumBtn.y()); AdbUtils::makeTap(deviceId, sumBtn.x(), sumBtn.y());
QThread::msleep(500); QThread::msleep(500);
}
AdbUtils::inputText(deviceId, QString::number(m_amount)); AdbUtils::inputText(deviceId, QString::number(m_amount));
QThread::msleep(500); QThread::msleep(500);
@ -143,6 +150,11 @@ void PayByPhoneScript::makePayment(
if (!readyBtn.isEmpty()) { if (!readyBtn.isEmpty()) {
AdbUtils::makeTap(deviceId, readyBtn.x(), readyBtn.y()); AdbUtils::makeTap(deviceId, readyBtn.x(), readyBtn.y());
QThread::msleep(500); 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()); AdbUtils::makeTap(deviceId, continuePayBtn.x(), continuePayBtn.y());
QThread::msleep(1000); QThread::msleep(1000);
} else { } else {
@ -156,7 +168,7 @@ void PayByPhoneScript::makePayment(
return; return;
} }
} else { } else {
m_error = "Не смогли найти 'Перевод по телефону'"; m_error = "Не смогли найти заголовок: " + payByPhoneScreenTitleText;
qDebug() << m_error; qDebug() << m_error;
return; return;
} }
@ -187,7 +199,7 @@ void PayByPhoneScript::makePayment(
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
} }
if (!inputedPinCode) { if (!isRshbBank && !inputedPinCode) {
m_error = "Не смогли ввести пин-код!'"; m_error = "Не смогли ввести пин-код!'";
qDebug() << m_error; qDebug() << m_error;
return; return;
@ -204,6 +216,9 @@ void PayByPhoneScript::makePayment(
// TODO считываем все данные по переводу // TODO считываем все данные по переводу
TransactionInfo transaction = xmlScreenParser.parseTransactionInfo(width, height); TransactionInfo transaction = xmlScreenParser.parseTransactionInfo(width, height);
if (isRshbBank) {
transaction.bankName = m_bankName;
}
transaction.accountId = m_account.id; transaction.accountId = m_account.id;
transaction.amount = -m_amount; transaction.amount = -m_amount;
transaction.type = TransactionType::Phone; transaction.type = TransactionType::Phone;
@ -217,6 +232,7 @@ void PayByPhoneScript::makePayment(
for (int k = 0; k < 10; ++k) { for (int k = 0; k < 10; ++k) {
Node paymentCompleted = xmlScreenParser.findTextNode("Исполнен", 0, 0, width, height / 2); Node paymentCompleted = xmlScreenParser.findTextNode("Исполнен", 0, 0, width, height / 2);
if (!paymentCompleted.isEmpty()) { if (!paymentCompleted.isEmpty()) {
transaction.status = TransactionStatus::Complete;
if (!TransactionDAO::updateTransactionStatus(transaction.id, TransactionStatus::Complete)) { if (!TransactionDAO::updateTransactionStatus(transaction.id, TransactionStatus::Complete)) {
qDebug() << "Not updated: " << transaction.id; qDebug() << "Not updated: " << transaction.id;
} }
@ -227,6 +243,8 @@ void PayByPhoneScript::makePayment(
QThread::msleep(1000); QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
} }
postNewTransactionData(m_account, transaction);
break; break;
} else { } else {
qWarning() << "--- moreDetailBtn not found, i: " << i; qWarning() << "--- moreDetailBtn not found, i: " << i;
@ -245,11 +263,22 @@ void PayByPhoneScript::doStart() {
const ApplicationInfo app = ApplicationDAO::getApplicationsByDeviceId(m_account.deviceId, m_account.appName); const ApplicationInfo app = ApplicationDAO::getApplicationsByDeviceId(m_account.deviceId, m_account.appName);
if (device.id.isEmpty() || app.id.isEmpty()) { 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; return;
} }
if (runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { 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)) { if (goToAllProducts(device.id, device.screenWidth, device.screenHeight)) {
qDebug() << "======== 1. All products"; qDebug() << "======== 1. All products";
QList<QPair<AccountInfo, Node> > accounts = xmlScreenParser.parseAccountsInfo(); QList<QPair<AccountInfo, Node> > accounts = xmlScreenParser.parseAccountsInfo();
@ -260,6 +289,9 @@ void PayByPhoneScript::doStart() {
qDebug() << "Not updated: " << account.lastNumbers; qDebug() << "Not updated: " << account.lastNumbers;
} }
} }
postAccountsData(accounts);
for (auto &[account, node]: accounts) { for (auto &[account, node]: accounts) {
// FIXME будем считать что числа последние везде уникальные // FIXME будем считать что числа последние везде уникальные
if (m_account.lastNumbers == account.lastNumbers) { if (m_account.lastNumbers == account.lastNumbers) {
@ -276,10 +308,6 @@ void PayByPhoneScript::doStart() {
m_error = "Не смогли открыть 'Все продукты'"; m_error = "Не смогли открыть 'Все продукты'";
qDebug() << m_error; qDebug() << m_error;
} }
} else {
m_error = "Не смогли дойти до домашней страницы....";
qDebug() << m_error;
}
emit finishedWithResult(m_error); emit finishedWithResult(m_error);
} }

View File

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

View File

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

Binary file not shown.

View File

@ -10,8 +10,8 @@ bool AccountDAO::upsertAccount(const AccountInfo &info) {
QSqlQuery query(DatabaseManager::instance().database()); QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"( query.prepare(R"(
INSERT INTO accounts (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, :numbers, :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) ON CONFLICT(device_id, app_name, last_numbers)
DO UPDATE SET DO UPDATE SET
amount = excluded.amount, amount = excluded.amount,
@ -22,7 +22,6 @@ bool AccountDAO::upsertAccount(const AccountInfo &info) {
query.bindValue(":device_id", info.deviceId); query.bindValue(":device_id", info.deviceId);
query.bindValue(":app_name", info.appName); query.bindValue(":app_name", info.appName);
query.bindValue(":numbers", info.numbers);
query.bindValue(":last_numbers", info.lastNumbers); query.bindValue(":last_numbers", info.lastNumbers);
query.bindValue(":amount", info.amount); query.bindValue(":amount", info.amount);
query.bindValue(":update_time", DateUtils::toUtcString(info.updateTime)); 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.deviceId = query.value("device_id").toString();
acc.appName = query.value("app_name").toString(); acc.appName = query.value("app_name").toString();
acc.lastNumbers = query.value("last_numbers").toString(); acc.lastNumbers = query.value("last_numbers").toString();
acc.numbers = query.value("numbers").toString();
acc.amount = query.value("amount").toDouble(); acc.amount = query.value("amount").toDouble();
acc.updateTime = DateUtils::fromUtcString(query.value("update_time").toString()); acc.updateTime = DateUtils::fromUtcString(query.value("update_time").toString());
acc.status = accountStatusFromString(query.value("status").toString()); acc.status = accountStatusFromString(query.value("status").toString());
@ -63,7 +61,7 @@ QList<AccountInfo> AccountDAO::getAccounts(const QString &deviceId, const QStrin
QSqlQuery query(DatabaseManager::instance().database()); QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"( 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 FROM accounts
WHERE device_id = :device_id AND app_name = :app_name 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()); QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"( 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 FROM accounts
WHERE id = :id WHERE id = :id
LIMIT 1 LIMIT 1
@ -110,7 +108,7 @@ AccountInfo AccountDAO::findAppAccount(const QString &deviceId, const QString &a
QSqlQuery query(DatabaseManager::instance().database()); QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"( 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 FROM accounts
WHERE app_name = :appName AND last_numbers = :lastNumber AND device_id = :deviceId WHERE app_name = :appName AND last_numbers = :lastNumber AND device_id = :deviceId
LIMIT 1 LIMIT 1

View File

@ -40,7 +40,7 @@ int EventDAO::insertEvent(const EventInfo &event) {
query.bindValue(":amount", event.amount); query.bindValue(":amount", event.amount);
query.bindValue(":phone", event.phone); query.bindValue(":phone", event.phone);
query.bindValue(":bank_name", event.bankName); query.bindValue(":bank_name", event.bankName);
query.bindValue(":timestamp", DateUtils::toUtcString(DateUtils::getUtcNow())); query.bindValue(":timestamp", event.timestamp);
if (!query.exec()) { if (!query.exec()) {
qCritical() << "Ошибка при вставке event:" << query.lastError().text(); qCritical() << "Ошибка при вставке event:" << query.lastError().text();

View File

@ -11,7 +11,7 @@
#include "automation_script/Script.h" #include "automation_script/Script.h"
#include <iostream> #include <iostream>
#include <cstdlib> #include <QStyleFactory>
#include <thread> #include <thread>
#include <chrono> #include <chrono>
#include <QFile> #include <QFile>
@ -191,35 +191,38 @@ void messageHandler(
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
QApplication app(argc, argv); QApplication app(argc, argv);
logFile.setFileName("application.log"); // Выбрать стиль Fusion
if (!logFile.open(QIODevice::Append | QIODevice::Text)) { app.setStyle(QStyleFactory::create("Fusion"));
qWarning() << "Не удалось открыть лог-файл для записи";
}
// Устанавливаем свой обработчик
qInstallMessageHandler(messageHandler);
auto *thread = new QThread; // logFile.setFileName("application.log");
auto *worker = new EventHandler; // if (!logFile.open(QIODevice::Append | QIODevice::Text)) {
worker->moveToThread(thread); // qWarning() << "Не удалось открыть лог-файл для записи";
QObject::connect(thread, &QThread::started, worker, &EventHandler::start); // }
QObject::connect(worker, &EventHandler::finished, thread, &QThread::quit); // // Устанавливаем свой обработчик
QObject::connect(worker, &EventHandler::finished, worker, &QObject::deleteLater); // qInstallMessageHandler(messageHandler);
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); //
thread->start(); // auto *thread = new QThread;
// auto *worker = new EventHandler;
auto *paymentThread = new QThread; // worker->moveToThread(thread);
auto *networkService = new NetworkService; // QObject::connect(thread, &QThread::started, worker, &EventHandler::start);
// QObject::connect(worker, &EventHandler::finished, thread, &QThread::quit);
// FIXME доставить десктоп ид из настроеек // QObject::connect(worker, &EventHandler::finished, worker, &QObject::deleteLater);
networkService->setDeviceData("DT88bokcwQ", ""); // QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
networkService->moveToThread(paymentThread); // thread->start();
QObject::connect(paymentThread, &QThread::started, networkService, &NetworkService::start); //
QObject::connect(networkService, &NetworkService::finished, paymentThread, &QThread::quit); // auto *paymentThread = new QThread;
QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater); // auto *networkService = new NetworkService;
QObject::connect(paymentThread, &QThread::finished, paymentThread, &QThread::deleteLater); //
paymentThread->start(); // // FIXME доставить десктоп ид из настроеек
// networkService->setDeviceData("DT88bokcwQ", "");
setupWorkerAndThread(app); // 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; MainWindow window;
window.show(); window.show();

View File

@ -31,7 +31,6 @@ CREATE TABLE IF NOT EXISTS accounts
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT, device_id TEXT,
app_name TEXT, app_name TEXT,
numbers TEXT,
last_numbers TEXT, last_numbers TEXT,
description TEXT, description TEXT,
amount REAL, amount REAL,

View File

@ -26,7 +26,6 @@ struct AccountInfo {
QString deviceId; QString deviceId;
QString appName; QString appName;
QString lastNumbers; QString lastNumbers;
QString numbers;
double amount = 0.0; double amount = 0.0;
QDateTime updateTime; QDateTime updateTime;
AccountStatus status = AccountStatus::Active; 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.id != -1) lines << " id: " + QString::number(info.id); else emptyFields << "id";
if (!info.deviceId.isEmpty()) lines << " deviceId: " + info.deviceId; else emptyFields << "deviceId"; if (!info.deviceId.isEmpty()) lines << " deviceId: " + info.deviceId; else emptyFields << "deviceId";
if (!info.appName.isEmpty()) lines << " appName: " + info.appName; else emptyFields << "appName"; 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.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.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"; if (info.updateTime.isValid()) lines << " updateTime: " + info.updateTime.toString(Qt::ISODate); else emptyFields << "updateTime";

View File

@ -8,6 +8,7 @@
#include <QThread> #include <QThread>
#include <QSettings> #include <QSettings>
#include <QTimer> #include <QTimer>
#include <QTimeZone>
#include "dao/AccountDAO.h" #include "dao/AccountDAO.h"
#include "dao/EventDAO.h" #include "dao/EventDAO.h"
@ -144,6 +145,7 @@ void NetworkService::getPayments() {
EventInfo eventInfo; EventInfo eventInfo;
eventInfo.status = EventStatus::Wait; eventInfo.status = EventStatus::Wait;
eventInfo.externalId = obj["id"].toInt(); eventInfo.externalId = obj["id"].toInt();
eventInfo.timestamp = QDateTime::fromMSecsSinceEpoch(obj["timestamp"].toDouble(), QTimeZone::utc());
eventInfo.amount = obj["amount"].toDouble(); eventInfo.amount = obj["amount"].toDouble();
eventInfo.bankName = obj["bankName"].toString(); eventInfo.bankName = obj["bankName"].toString();
eventInfo.phone = obj["phone"].toString(); eventInfo.phone = obj["phone"].toString();

17
tutorials/test.md Normal file
View File

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

16
tutorials/test2.md Normal file
View File

@ -0,0 +1,16 @@
# Руководство: Запуск приложения TutorialWindow
Этот файл объясняет, как собрать и запустить ваше Qt6-приложение с окном TutorialWindow.
---
## 1. Требования
- Qt 6 (рекомендуется версия 6.2 или выше)
- C++17-совместимый компилятор (GCC, Clang, MSVC)
- CMake (версия ≥ 3.16) или qmake
---
## 2. Структура проекта

16
tutorials/test3.md Normal file
View File

@ -0,0 +1,16 @@
# Руководство: Запуск приложения TutorialWindow
Этот файл объясняет, как собрать и запустить ваше Qt6-приложение с окном TutorialWindow.
---
## 1. Требования
- Qt 6 (рекомендуется версия 6.2 или выше)
- C++17-совместимый компилятор (GCC, Clang, MSVC)
- CMake (версия ≥ 3.16) или qmake
---
## 2. Структура проекта

View File

@ -1,15 +1,16 @@
#include "MainWindow.h" #include "MainWindow.h"
#include "AccountWindow.h" #include "bank/AccountWindow.h"
#include <QLabel> #include <QLabel>
#include <QScrollArea> #include <QScrollArea>
#include <QMenuBar> #include <QMenuBar>
#include <QStackedWidget> #include <QStackedWidget>
#include "TutorialWindow.h"
#include "db/DeviceInfo.h" #include "db/DeviceInfo.h"
#include "DeviceWidget.h" #include "device/DeviceWidget.h"
#include "dao/DeviceDAO.h" #include "dao/DeviceDAO.h"
#include "widget/FlowLayout.h" #include "widget/common/FlowLayout.h"
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
// Инициализируем таймер обновления // Инициализируем таймер обновления
@ -20,12 +21,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
setCentralWidget(stackedWidget); setCentralWidget(stackedWidget);
createDevicePage(); createDevicePage();
stackedWidget->addWidget(devicePage); stackedWidget->addWidget(m_devicesWindow);
stackedWidget->setCurrentWidget(devicePage); stackedWidget->setCurrentWidget(m_devicesWindow);
// FIXME переделать на ленивое создание // FIXME переделать на ленивое создание
accountPage = new AccountWindow(this); m_accountsWindow = new AccountWindow(this);
stackedWidget->addWidget(accountPage); stackedWidget->addWidget(m_accountsWindow);
// stackedWidget->setCurrentWidget(accountPage); // stackedWidget->setCurrentWidget(accountPage);
resize(800, 600); resize(800, 600);
@ -34,25 +35,43 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
QMenu *menu = menuBar()->addMenu("Меню"); QMenu *menu = menuBar()->addMenu("Меню");
auto *devicesPageAction = new QAction("Активные устройства", this); auto *devicesPageAction = new QAction("Активные устройства", this);
auto *accountPageAction = new QAction("Аккаунты", this); auto *accountPageAction = new QAction("Аккаунты", this);
auto *tutorialPageAction = new QAction("Инструкция", this);
menu->addAction(devicesPageAction); menu->addAction(devicesPageAction);
menu->addAction(accountPageAction); menu->addAction(accountPageAction);
menu->addAction(tutorialPageAction);
connect(devicesPageAction, &QAction::triggered, this, [=]() { connect(devicesPageAction, &QAction::triggered, this, [=]() {
if (!devicePage) { if (!m_devicesWindow) {
createDevicePage(); createDevicePage();
} }
stackedWidget->setCurrentWidget(devicePage); stackedWidget->setCurrentWidget(m_devicesWindow);
}); });
connect(accountPageAction, &QAction::triggered, this, [=]() { connect(accountPageAction, &QAction::triggered, this, [=]() {
stackedWidget->setCurrentWidget(accountPage); stackedWidget->setCurrentWidget(m_accountsWindow);
deleteDevicePage(); // освобождаем ресурсы 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() { void MainWindow::createDevicePage() {
if (devicePage) { if (m_devicesWindow) {
return; return;
} }
@ -73,22 +92,22 @@ void MainWindow::createDevicePage() {
scrollArea->setWidget(contentWidget); scrollArea->setWidget(contentWidget);
scrollArea->setWidgetResizable(true); scrollArea->setWidgetResizable(true);
devicePage = scrollArea; m_devicesWindow = scrollArea;
stackedWidget->addWidget(devicePage); stackedWidget->addWidget(m_devicesWindow);
timer->start(1000); timer->start(1000);
} }
void MainWindow::deleteDevicePage() { void MainWindow::deleteDevicePage() {
if (!devicePage) { if (!m_devicesWindow) {
return; return;
} }
timer->stop(); timer->stop();
stackedWidget->removeWidget(devicePage); stackedWidget->removeWidget(m_devicesWindow);
devicePage->deleteLater(); m_devicesWindow->deleteLater();
devicePage = nullptr; m_devicesWindow = nullptr;
flowLayout = nullptr; flowLayout = nullptr;
} }

View File

@ -3,24 +3,31 @@
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QStackedWidget> #include <QStackedWidget>
#include <QTimer> #include <QTimer>
#include "TutorialWindow.h"
#include "bank/AccountWindow.h"
#include "db/DeviceInfo.h" #include "db/DeviceInfo.h"
#include "widget/FlowLayout.h" #include "widget/common/FlowLayout.h"
class MainWindow final : public QMainWindow { class MainWindow final : public QMainWindow {
Q_OBJECT Q_OBJECT
public: public:
explicit MainWindow(QWidget *parent = nullptr); explicit MainWindow(QWidget *parent = nullptr);
void loadDevices(); void loadDevices();
private: private:
QWidget* central; QWidget *central;
FlowLayout* flowLayout; FlowLayout *flowLayout;
QTimer* timer; QTimer *timer;
QStackedWidget* stackedWidget; QStackedWidget *stackedWidget;
QWidget* devicePage = nullptr; QWidget *m_devicesWindow = nullptr;
QWidget* accountPage = nullptr; QWidget *m_accountsWindow = nullptr;
QWidget *m_tutorialWindow = nullptr;
void createDevicePage(); void createDevicePage();
void deleteDevicePage(); void deleteDevicePage();
}; };

63
views/TutorialWindow.cpp Normal file
View File

@ -0,0 +1,63 @@
#include "TutorialWindow.h"
#include <QVBoxLayout>
#include <QDir>
#include <QFile>
#include <QTextStream>
#include <QScrollArea>
#include <QCoreApplication>
#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);
}

9
views/TutorialWindow.h Normal file
View File

@ -0,0 +1,9 @@
#pragma once
#include <QWidget>
class TutorialWindow final : public QWidget {
Q_OBJECT
public:
explicit TutorialWindow(QWidget *parent = nullptr);
};

View File

@ -0,0 +1,85 @@
#include "CollapsibleSection.h"
#include <QVBoxLayout>
#include <QTextBrowser>
#include <QEvent>
#include <QDesktopServices>
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<int>(h) + 5);
}
void CollapsibleSection::onToggled(const bool checked) const {
m_toggleButton->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow);
m_contentBrowser->setVisible(checked);
if (checked) {
updateContentHeight();
}
}

View File

@ -0,0 +1,28 @@
#pragma once
#include <QWidget>
#include <QToolButton>
#include <QTextBrowser>
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;
};