1
0
forked from BRT/arc
arc/views/MainWindow.cpp
slava d480cc51b0 Enhance input field detection and retry logic across payment scripts:
- Add retry loops for input fields in `PayByPhoneScript` and `PayByCardScript` to improve reliability.
- Update window title in `MainWindow` with version info from `config.ini`.
2026-04-24 16:09:36 +07:00

387 lines
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "MainWindow.h"
#include "bank/AccountWindow.h"
#include <QLabel>
#include <QPixmap>
#include <QResizeEvent>
#include <QScrollArea>
#include <QMenuBar>
#include <QStackedWidget>
#include <QMessageBox>
#include <QThread>
#include "TutorialWindow.h"
#include "bank/BankListWindow.h"
#include "event/EventWindow.h"
#include "log/LogWindow.h"
#include "log/FileLogWindow.h"
#include "dao/GeneralLogDAO.h"
#include "net/NetworkService.h"
#include "db/DeviceInfo.h"
#include "device/DeviceWidget.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/SettingsDAO.h"
#include <QTimer>
#include <QCoreApplication>
#include <QSettings>
#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);
connect(timer, &QTimer::timeout, this, &MainWindow::loadDevices);
stackedWidget = new QStackedWidget(this);
setCentralWidget(stackedWidget);
createDevicePage();
stackedWidget->addWidget(m_devicesWindow);
stackedWidget->setCurrentWidget(m_devicesWindow);
// FIXME переделать на ленивое создание
// m_accountsWindow = new AccountWindow(this);
// stackedWidget->addWidget(m_accountsWindow);
// stackedWidget->setCurrentWidget(accountPage);
resize(800, 600);
// Работа с меню
QMenu *menu = menuBar()->addMenu("Меню");
auto *devicesPageAction = 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(eventAction);
menu->addAction(tutorialPageAction);
menu->addAction(fileLogAction);
connect(devicesPageAction, &QAction::triggered, this, [=]() {
if (!m_devicesWindow) {
createDevicePage();
}
stackedWidget->setCurrentWidget(m_devicesWindow);
});
// 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->sendLogToTelegram();
}
});
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");
});
}
}
void MainWindow::createDevicePage() {
if (m_devicesWindow) {
return;
}
auto *container = new QWidget(this);
auto *containerLayout = new QVBoxLayout(container);
containerLayout->setContentsMargins(0, 0, 0, 0);
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<DeviceInfo> devices = DeviceDAO::getAllDevices(false);
for (const auto &device: devices) {
flowLayout->addWidget(new DeviceWidget(device));
}
contentWidget->setLayout(flowLayout);
static_cast<QScrollArea*>(m_scrollArea)->setWidget(contentWidget);
static_cast<QScrollArea*>(m_scrollArea)->setWidgetResizable(true);
containerLayout->addWidget(m_scrollArea);
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 (!m_devicesWindow) {
return;
}
timer->stop();
stackedWidget->removeWidget(m_devicesWindow);
m_devicesWindow->deleteLater();
m_devicesWindow = nullptr;
flowLayout = nullptr;
}
/**
* Однако, в нем вы вызываете delete для элементов, что изменяет память.
* Для того, чтобы не нарушать константность, этот метод должен быть неконстантным (без const в сигнатуре).
*/
void MainWindow::loadDevices() {
const auto devices = DeviceDAO::getAllDevices(false);
// Очистить старые виджеты
QLayoutItem *item;
while ((item = flowLayout->takeAt(0)) != nullptr) {
if (item->widget()) {
item->widget()->deleteLater();
}
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::updateLogPanel() {
if (!m_logList) return;
const QList<GeneralLogEntry> 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<BankProfileInfo> 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::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));
}
}