409 lines
18 KiB
C++
409 lines
18 KiB
C++
#include "MainWindow.h"
|
||
#include "bank/AccountWindow.h"
|
||
|
||
#include <QLabel>
|
||
#include <QPixmap>
|
||
#include <QResizeEvent>
|
||
#include <QScrollArea>
|
||
#include <QMenuBar>
|
||
#include <QStackedWidget>
|
||
|
||
#include <QMessageBox>
|
||
#include <QStatusBar>
|
||
#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->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 (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));
|
||
}
|
||
}
|