- Introduce `OcrUtils` with fuzzy-matching logic for text recognition on screenshots. - Implement dual-pass OCR to handle light-on-dark and dark-on-light text cases. - Replace hardcoded "Подтверждаю" button tap logic with OCR-based detection and fallback geometry. - Update `TaskDumpRecorder` to utilize `NetworkService` for uploading screen dumps, images, and metadata to the backend. - Add support for `/monitoring/dump` endpoint in `NetworkService`. - Enhance UI functionality with new error handling and progress feedback during dump creation and upload.
658 lines
28 KiB
C++
658 lines
28 KiB
C++
#include "DeviceWidget.h"
|
||
#include <QVBoxLayout>
|
||
#include <QLabel>
|
||
#include <QPixmap>
|
||
#include <QImage>
|
||
#include <QPushButton>
|
||
#include <QCoreApplication>
|
||
#include <QJsonDocument>
|
||
#include <QJsonObject>
|
||
#include <QPointer>
|
||
#include <QPropertyAnimation>
|
||
#include <QGraphicsScene>
|
||
#include <QMessageBox>
|
||
|
||
#include "bank/AccountSettingsWindow.h"
|
||
#include "bank/BankSetupWizard.h"
|
||
#include "dao/BankProfileDAO.h"
|
||
#include "bank/BankSettingsWindow.h"
|
||
#include "DeviceSettingsWindow.h"
|
||
#include "dao/BankProfileDAO.h"
|
||
#include "dao/MaterialDAO.h"
|
||
#include "dao/DeviceDAO.h"
|
||
#include "dao/EventDAO.h"
|
||
#include "dao/SettingsDAO.h"
|
||
#include "db/BankProfileInfo.h"
|
||
#include "net/NetworkService.h"
|
||
#include "adb/AdbUtils.h"
|
||
#include <QThread>
|
||
#include <QNetworkAccessManager>
|
||
#include <QNetworkRequest>
|
||
#include <QNetworkReply>
|
||
#include <QHttpMultiPart>
|
||
#include <QBuffer>
|
||
#include <QSslSocket>
|
||
#include <QSslConfiguration>
|
||
#include <QSettings>
|
||
#include <QProgressDialog>
|
||
#include <QTimer>
|
||
#include <QDebug>
|
||
#include <QTimer>
|
||
#include <QRegularExpression>
|
||
#include <QDateTime>
|
||
|
||
|
||
QString getColorByStatus(const BankProfileInfo &app) {
|
||
if (!app.install) {
|
||
return "black";
|
||
}
|
||
if (app.status == "active") {
|
||
if (!app.pinCodeChecked) {
|
||
return "orange";
|
||
}
|
||
return "green";
|
||
} else if (app.status == "off") {
|
||
return "red";
|
||
} else {
|
||
return "red";
|
||
}
|
||
}
|
||
|
||
static void sendTaskErrorToServer(const QString &type, const QString &materialId, const QString &bankName) {
|
||
QJsonObject errData;
|
||
errData["status"] = "error";
|
||
errData["event_type"] = type;
|
||
errData["description"] = "Bank profile disabled";
|
||
|
||
QJsonObject obj;
|
||
obj["type"] = "ERROR";
|
||
obj["material_id"] = materialId;
|
||
obj["bank_name"] = bankName;
|
||
obj["data"] = errData;
|
||
|
||
auto *thread = new QThread;
|
||
auto *net = new NetworkService;
|
||
net->moveToThread(thread);
|
||
net->setPayload(QJsonDocument(obj).toJson());
|
||
QObject::connect(thread, &QThread::started, net, &NetworkService::postTaskResult);
|
||
QObject::connect(net, &NetworkService::finished, thread, &QThread::quit);
|
||
QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
||
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||
thread->start();
|
||
}
|
||
|
||
void sendBankStatus(const QString &deviceId, const QString &code, const QString &status) {
|
||
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, code);
|
||
if (app.bankProfileId.isEmpty()) {
|
||
qWarning() << "[DeviceWidget] sendBankStatus: no bankProfileId for" << code;
|
||
return;
|
||
}
|
||
|
||
// При выключении — отменяем ожидающие задачи
|
||
if (status != "active") {
|
||
const QList<EventInfo> cancelled = EventDAO::cancelWaitingByBank(deviceId, code);
|
||
for (const EventInfo &e : cancelled) {
|
||
sendTaskErrorToServer(eventTypeToString(e.type), e.externalId, e.bankName);
|
||
EventDAO::markSentToServer(e.id);
|
||
}
|
||
}
|
||
|
||
const bool enable = (status == "active");
|
||
auto *thread = new QThread;
|
||
auto *net = new NetworkService;
|
||
net->moveToThread(thread);
|
||
net->addExtra("bank_profile_id", app.bankProfileId);
|
||
net->addExtra("enable", enable);
|
||
QObject::connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile);
|
||
QObject::connect(net, &NetworkService::finished, thread, &QThread::quit);
|
||
QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
||
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||
thread->start();
|
||
}
|
||
|
||
void DeviceWidget::buildBankRow(const BankProfileInfo &app, QWidget *rowWidget, QWidget *parent, bool isOffline) {
|
||
rowWidget->setContentsMargins(0, 0, 0, 0);
|
||
rowWidget->setStyleSheet(
|
||
"background-color: rgba(0, 0, 0, 0.6);" // полупрозрачный фон
|
||
"border-radius: 4px;" // скруглённые углы, если нужно
|
||
);
|
||
|
||
// вешаем на контейнер ваш QHBoxLayout
|
||
auto *hbox = new QHBoxLayout(rowWidget);
|
||
hbox->setContentsMargins(2, 1, 1, 1);
|
||
hbox->setSpacing(0);
|
||
|
||
auto *icon = new QLabel(parent);
|
||
QPixmap pixmap(QString("%1/images/%2_icon.png").arg(QCoreApplication::applicationDirPath(), app.code));
|
||
icon->setPixmap(pixmap);
|
||
icon->setPixmap(pixmap.scaled(18, 18, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||
icon->setStyleSheet(
|
||
"background-color: transparent;"
|
||
"margin: 0px;"
|
||
);
|
||
|
||
|
||
QString labelText;
|
||
if (!app.fullName.isEmpty()) {
|
||
labelText = app.fullName;
|
||
if (!app.phone.isEmpty()) {
|
||
labelText += "\n" + app.phone;
|
||
}
|
||
} else {
|
||
labelText = app.name;
|
||
}
|
||
auto *label = new QLabel(labelText, parent);
|
||
label->setStyleSheet(
|
||
"background-color: transparent;"
|
||
"margin: 0px;"
|
||
"font-size: 10px;"
|
||
"color: white;"
|
||
);
|
||
|
||
auto *circle = new QLabel(parent);
|
||
circle->setFixedSize(12, 12);
|
||
const QString color = getColorByStatus(app);
|
||
circle->setStyleSheet(
|
||
"margin: 0px;"
|
||
"background-color: " + color + "; "
|
||
"border-radius: 6px;"
|
||
);
|
||
// собираем
|
||
if (app.install && app.pinCodeChecked && (app.status == "active" || app.status == "off")) {
|
||
auto *btn1 = new QPushButton(parent);
|
||
bool turnOn = app.pinCodeChecked && app.status == "active";
|
||
QIcon btnIcon(
|
||
QCoreApplication::applicationDirPath() + (turnOn ? "/images/turn_off.png" : "/images/turn_on.png"));
|
||
btn1->setIcon(btnIcon);
|
||
btn1->setIconSize(QSize(16, 16));
|
||
btn1->setFlat(true);
|
||
btn1->setStyleSheet(
|
||
"margin: 0px;"
|
||
"background-color: transparent;"
|
||
"border: none;"
|
||
);
|
||
|
||
if (isOffline) {
|
||
btn1->setEnabled(false);
|
||
}
|
||
connect(btn1, &QPushButton::clicked, this, [app]() {
|
||
const QString newStatus = app.status == "active" ? "off" : "active";
|
||
QMessageBox msgBox(nullptr);
|
||
msgBox.setWindowTitle(newStatus == "off" ? "Отключить банк?" : "Включить банк?");
|
||
msgBox.setText(
|
||
"Вы уверены, что хотите "
|
||
+ QString(newStatus == "off" ? "отключить от работы" : "включить")
|
||
+ " данный банк: "
|
||
+ app.name + "?");
|
||
QPushButton *confirmBtn = msgBox.addButton(newStatus == "off" ? "Отключить" : "Включить",
|
||
QMessageBox::AcceptRole);
|
||
QPushButton *cancelBtn = msgBox.addButton("Отмена", QMessageBox::RejectRole);
|
||
msgBox.setDefaultButton(cancelBtn);
|
||
msgBox.setModal(true);
|
||
msgBox.exec();
|
||
|
||
if (msgBox.clickedButton() == confirmBtn) {
|
||
// При включении — проверяем что есть хотя бы одна активная карта
|
||
if (newStatus == "active") {
|
||
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(app.deviceId, app.code);
|
||
bool hasActive = false;
|
||
for (const auto &m : materials) {
|
||
if (m.status == MaterialStatus::Active) { hasActive = true; break; }
|
||
}
|
||
if (!hasActive) {
|
||
QMessageBox::warning(nullptr, "Невозможно включить",
|
||
"Ни одна карта не активна.\nВключите хотя бы одну карту перед включением профиля.");
|
||
return;
|
||
}
|
||
}
|
||
if (!BankProfileDAO::update(app.id, app.pinCode, app.comment, newStatus)) {
|
||
qDebug() << "Cant update bank data";
|
||
} else {
|
||
sendBankStatus(app.deviceId, app.code, newStatus);
|
||
// При включении — включаем на сервере все локально активные материалы
|
||
if (newStatus == "active") {
|
||
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(app.deviceId, app.code);
|
||
for (const auto &mat : materials) {
|
||
if (mat.status == MaterialStatus::Active && !mat.materialId.isEmpty()) {
|
||
auto *thr = new QThread;
|
||
auto *ns = new NetworkService;
|
||
ns->moveToThread(thr);
|
||
ns->addExtra("material_id", mat.materialId);
|
||
ns->addExtra("enable", true);
|
||
QObject::connect(thr, &QThread::started, ns, &NetworkService::toggleMaterial);
|
||
QObject::connect(ns, &NetworkService::finished, thr, &QThread::quit);
|
||
QObject::connect(ns, &NetworkService::finished, ns, &QObject::deleteLater);
|
||
QObject::connect(thr, &QThread::finished, thr, &QThread::deleteLater);
|
||
thr->start();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
hbox->addWidget(btn1);
|
||
}
|
||
|
||
auto *btn2 = new QPushButton(parent);
|
||
QIcon btn2Icon(QCoreApplication::applicationDirPath() + "/images/cogwheel.png");
|
||
btn2->setIcon(btn2Icon);
|
||
btn2->setIconSize(QSize(16, 16));
|
||
btn2->setFlat(true);
|
||
btn2->setStyleSheet(
|
||
"margin: 0px;"
|
||
"background-color: transparent;"
|
||
"border: none;"
|
||
);
|
||
// в конструкторе окна или сразу после создания btn2:
|
||
connect(btn2, &QPushButton::pressed, this, [btn2] {
|
||
auto anim = new QPropertyAnimation(btn2, "geometry");
|
||
anim->setDuration(120);
|
||
QRect orig = btn2->geometry();
|
||
QRect smaller = orig.adjusted(orig.width() * 0.05,
|
||
orig.height() * 0.05,
|
||
-orig.width() * 0.05,
|
||
-orig.height() * 0.05);
|
||
anim->setStartValue(orig);
|
||
anim->setKeyValueAt(0.5, smaller);
|
||
anim->setEndValue(orig);
|
||
anim->setEasingCurve(QEasingCurve::InOutQuad);
|
||
anim->start(QAbstractAnimation::DeleteWhenStopped);
|
||
});
|
||
|
||
setOpenBanksSettings(btn2, app);
|
||
|
||
hbox->addWidget(icon);
|
||
hbox->addWidget(label, 1);
|
||
hbox->addWidget(circle);
|
||
hbox->addWidget(btn2);
|
||
|
||
// hbox->addStretch();
|
||
}
|
||
|
||
void DeviceWidget::setOpenBanksSettings(QPushButton *button, const BankProfileInfo &app) {
|
||
QString deviceId = m_device.id;
|
||
QString deviceName = m_device.name;
|
||
connect(button, &QPushButton::clicked, this, [deviceId, deviceName, app]() {
|
||
const BankProfileInfo dbApp = BankProfileDAO::getApplication(deviceId, app.code);
|
||
if (dbApp.id < 0 || (!dbApp.pinCodeChecked && dbApp.bankProfileId.isEmpty())) {
|
||
auto *wizard = new BankSetupWizard(nullptr, deviceId, dbApp.id >= 0 ? dbApp : app);
|
||
wizard->setAttribute(Qt::WA_DeleteOnClose);
|
||
wizard->show();
|
||
} else {
|
||
static QPointer<AccountSettingsWindow> bankSettingsWindow;
|
||
if (!bankSettingsWindow) {
|
||
bankSettingsWindow = new AccountSettingsWindow(nullptr, deviceId, deviceName, dbApp);
|
||
bankSettingsWindow->setWindowFlags(bankSettingsWindow->windowFlags() | Qt::Window);
|
||
bankSettingsWindow->setAttribute(Qt::WA_DeleteOnClose);
|
||
}
|
||
bankSettingsWindow->show();
|
||
bankSettingsWindow->raise();
|
||
bankSettingsWindow->activateWindow();
|
||
}
|
||
});
|
||
}
|
||
|
||
DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(parent), m_device(device) {
|
||
auto *layout = new QGridLayout(this);
|
||
layout->setContentsMargins(2, 2, 2, 2);
|
||
layout->setSpacing(0);
|
||
this->setStyleSheet("background-color: lightgray;padding: 2px;");
|
||
|
||
auto *imageLabel = new QLabel(this);
|
||
if (!device.image.isEmpty()) {
|
||
QImage img;
|
||
img.loadFromData(device.image, "JPG");
|
||
imageLabel->setPixmap(QPixmap::fromImage(img).scaled(300, 450, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||
} else {
|
||
imageLabel->setFixedSize(300, 450);
|
||
imageLabel->setStyleSheet("background-color: #555;");
|
||
}
|
||
imageLabel->setAlignment(Qt::AlignCenter);
|
||
layout->addWidget(imageLabel, 0, 0);
|
||
|
||
auto *statusLabel = new QLabel(QString("%1 %2%").arg(device.name).arg(device.battery), this);
|
||
statusLabel->setWordWrap(true);
|
||
statusLabel->setMaximumWidth(300);
|
||
statusLabel->setStyleSheet(
|
||
"background-color: rgba(0, 0, 0, 0.5);"
|
||
"color: white;"
|
||
"font-size: 10px;"
|
||
);
|
||
layout->addWidget(statusLabel, 0, 0, Qt::AlignTop | Qt::AlignLeft);
|
||
|
||
// Кнопки настроек и удаления (справа сверху)
|
||
auto *topRightWidget = new QWidget(this);
|
||
topRightWidget->setStyleSheet("background: transparent;");
|
||
auto *topRightLayout = new QVBoxLayout(topRightWidget);
|
||
topRightLayout->setContentsMargins(0, 0, 0, 0);
|
||
topRightLayout->setSpacing(2);
|
||
|
||
auto *settingsBtn = new QPushButton(this);
|
||
settingsBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/cogwheel.png"));
|
||
settingsBtn->setIconSize(QSize(20, 20));
|
||
settingsBtn->setFixedSize(28, 28);
|
||
settingsBtn->setFlat(true);
|
||
settingsBtn->setStyleSheet(
|
||
"background-color: rgba(0, 0, 0, 0.5);"
|
||
"border: none;"
|
||
"border-radius: 4px;"
|
||
);
|
||
connect(settingsBtn, &QPushButton::clicked, this, [this]() {
|
||
auto *window = new DeviceSettingsWindow(nullptr, m_device.id, m_device.name);
|
||
window->setAttribute(Qt::WA_DeleteOnClose);
|
||
window->show();
|
||
window->raise();
|
||
window->activateWindow();
|
||
});
|
||
|
||
auto *deleteBtn = new QPushButton(this);
|
||
deleteBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/delete.png"));
|
||
deleteBtn->setIconSize(QSize(20, 20));
|
||
deleteBtn->setFixedSize(28, 28);
|
||
deleteBtn->setFlat(true);
|
||
deleteBtn->setStyleSheet(
|
||
"background-color: rgba(0, 0, 0, 0.5);"
|
||
"border: none;"
|
||
"border-radius: 4px;"
|
||
);
|
||
connect(deleteBtn, &QPushButton::clicked, this, [this]() {
|
||
const QString deviceId = m_device.id;
|
||
const QString deviceName = m_device.name;
|
||
QPointer<DeviceWidget> guard(this);
|
||
|
||
QMessageBox msgBox(nullptr);
|
||
msgBox.setWindowTitle(tr("Удалить устройство?"));
|
||
msgBox.setText(
|
||
"Будет удалено устройство " + deviceName +
|
||
" и все связанные данные: банки, аккаунты, транзакции и события.");
|
||
QPushButton *confirmBtn = msgBox.addButton(tr("Удалить"), QMessageBox::AcceptRole);
|
||
QPushButton *cancelBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole);
|
||
msgBox.setDefaultButton(cancelBtn);
|
||
msgBox.setModal(true);
|
||
msgBox.exec();
|
||
|
||
if (msgBox.clickedButton() == confirmBtn) {
|
||
auto *thread = new QThread;
|
||
auto *net = new NetworkService;
|
||
net->setDeviceId(deviceId);
|
||
net->moveToThread(thread);
|
||
QObject::connect(thread, &QThread::started, net, &NetworkService::deleteDeviceFromServer);
|
||
QObject::connect(net, &NetworkService::finished, thread, &QThread::quit);
|
||
QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater);
|
||
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||
thread->start();
|
||
}
|
||
});
|
||
|
||
auto *codeBtn = new QPushButton(this);
|
||
codeBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/code.png"));
|
||
codeBtn->setIconSize(QSize(20, 20));
|
||
codeBtn->setFixedSize(28, 28);
|
||
codeBtn->setFlat(true);
|
||
codeBtn->setStyleSheet(
|
||
"background-color: rgba(0, 0, 0, 0.5);"
|
||
"border: none;"
|
||
"border-radius: 4px;"
|
||
);
|
||
connect(codeBtn, &QPushButton::clicked, this, [this, codeBtn]() {
|
||
if (m_device.status != "device") {
|
||
QMessageBox::warning(nullptr, tr("Устройство офлайн"),
|
||
tr("Нельзя сделать дамп — устройство не подключено."));
|
||
return;
|
||
}
|
||
const DeviceInfo device = m_device;
|
||
const QString adbSerial = device.adbSerial.isEmpty() ? device.id : device.adbSerial;
|
||
|
||
QSettings settings(QCoreApplication::applicationDirPath() + "/config.ini",
|
||
QSettings::IniFormat);
|
||
const QString apiBase = settings.value("net/apiBase").toString();
|
||
const QString path = settings.value("net/pathMonitoringDump",
|
||
"/monitoring/dump").toString();
|
||
if (apiBase.isEmpty()) {
|
||
QMessageBox::warning(nullptr, tr("Ошибка"),
|
||
tr("В config.ini не задан net/apiBase"));
|
||
return;
|
||
}
|
||
|
||
codeBtn->setEnabled(false);
|
||
qDebug() << "[Dump] start for device" << adbSerial;
|
||
|
||
auto *progress = new QProgressDialog(tr("Сбор дампа..."), QString(), 0, 0, nullptr);
|
||
progress->setWindowTitle(tr("Дамп"));
|
||
progress->setWindowModality(Qt::ApplicationModal);
|
||
progress->setCancelButton(nullptr);
|
||
progress->setMinimumDuration(0);
|
||
progress->setAutoClose(false);
|
||
progress->setAutoReset(false);
|
||
progress->setWindowFlags(progress->windowFlags()
|
||
& ~Qt::WindowCloseButtonHint
|
||
& ~Qt::WindowContextHelpButtonHint);
|
||
progress->show();
|
||
|
||
// ADB-захват блокирует поток на несколько секунд — выносим в воркер,
|
||
// чтобы прогресс-диалог не замораживался.
|
||
struct DumpData { QByteArray screenshot; QString xml; };
|
||
auto *data = new DumpData;
|
||
auto *worker = QThread::create([data, adbSerial]() {
|
||
qDebug() << "[Dump] worker: capturing screenshot";
|
||
data->screenshot = AdbUtils::captureScreenshotBytes(adbSerial);
|
||
qDebug() << "[Dump] worker: screenshot size=" << data->screenshot.size();
|
||
qDebug() << "[Dump] worker: capturing xml";
|
||
data->xml = AdbUtils::getRawScreenDumpAsXml(adbSerial);
|
||
qDebug() << "[Dump] worker: xml size=" << data->xml.size();
|
||
});
|
||
|
||
// Общий флаг "уже обработано" — чтобы страховочный таймаут не сработал
|
||
// одновременно с finished. Шарится через shared_ptr, чтобы пережить лямбды.
|
||
auto handled = std::make_shared<bool>(false);
|
||
QPointer<QPushButton> btnGuard(codeBtn);
|
||
QPointer<QProgressDialog> progressGuard(progress);
|
||
|
||
auto closeAndReenable = [progressGuard, btnGuard]() {
|
||
if (progressGuard) {
|
||
progressGuard->reset();
|
||
progressGuard->hide();
|
||
progressGuard->deleteLater();
|
||
}
|
||
if (btnGuard) btnGuard->setEnabled(true);
|
||
};
|
||
|
||
// Страховочный таймаут на весь процесс (ADB + upload). Если что-то висит —
|
||
// диалог гарантированно закроется, и пользователь получит ошибку.
|
||
// Важно: владелец таймера и приёмник колбэков — qApp, а НЕ `this`.
|
||
// DeviceWidget пересоздаётся DeviceScreener-ом при каждом опросе устройств,
|
||
// и если повесить connect(this, ...), при пересборке списка соединение
|
||
// тихо рвётся, колбэк не запускается, диалог живёт вечно.
|
||
auto *safetyTimer = new QTimer;
|
||
safetyTimer->setSingleShot(true);
|
||
QObject::connect(safetyTimer, &QTimer::timeout, qApp,
|
||
[handled, closeAndReenable, worker, safetyTimer]() {
|
||
if (*handled) return;
|
||
*handled = true;
|
||
qWarning() << "[Dump] timeout — aborting";
|
||
if (worker && worker->isRunning()) {
|
||
worker->terminate();
|
||
worker->wait(2000);
|
||
}
|
||
if (worker) worker->deleteLater();
|
||
safetyTimer->deleteLater();
|
||
closeAndReenable();
|
||
QMessageBox::warning(nullptr, QObject::tr("Таймаут"),
|
||
QObject::tr("Сбор/отправка дампа заняли больше 90 секунд"));
|
||
});
|
||
safetyTimer->start(90000);
|
||
|
||
QObject::connect(worker, &QThread::finished, qApp,
|
||
[worker, data, device, adbSerial, apiBase, path,
|
||
handled, closeAndReenable, safetyTimer, progressGuard]() {
|
||
qDebug() << "[Dump] worker finished";
|
||
if (*handled) {
|
||
delete data;
|
||
worker->deleteLater();
|
||
return;
|
||
}
|
||
|
||
const QByteArray screenshot = data->screenshot;
|
||
const QString xml = data->xml;
|
||
delete data;
|
||
worker->deleteLater();
|
||
|
||
if (screenshot.isEmpty() && xml.isEmpty()) {
|
||
*handled = true;
|
||
safetyTimer->stop();
|
||
safetyTimer->deleteLater();
|
||
closeAndReenable();
|
||
QMessageBox::warning(nullptr, QObject::tr("Ошибка"),
|
||
QObject::tr("Не удалось получить дамп экрана"));
|
||
return;
|
||
}
|
||
|
||
if (progressGuard) progressGuard->setLabelText(QObject::tr("Отправка дампа..."));
|
||
qDebug() << "[Dump] uploading: screenshot=" << screenshot.size()
|
||
<< "xml=" << xml.size();
|
||
|
||
const QString desktopId = SettingsDAO::get("desktop_id");
|
||
const QString text = QString("Screen dump\nDevice: %1 (%2)\nDesktop: %3")
|
||
.arg(device.name, adbSerial, desktopId);
|
||
|
||
auto *mp = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||
|
||
QHttpPart textPart;
|
||
textPart.setHeader(QNetworkRequest::ContentDispositionHeader,
|
||
R"(form-data; name="text")");
|
||
textPart.setBody(text.toUtf8());
|
||
mp->append(textPart);
|
||
|
||
if (!xml.isEmpty()) {
|
||
QString safeName = device.name;
|
||
safeName.replace(QRegularExpression(R"([^\w\-.])"), "_");
|
||
const QString ts = QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm-ss");
|
||
const QString xmlFilename = safeName + "_" + ts + ".xml";
|
||
|
||
QHttpPart filePart;
|
||
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/xml");
|
||
filePart.setHeader(QNetworkRequest::ContentDispositionHeader,
|
||
QString(R"(form-data; name="file"; filename="%1")").arg(xmlFilename).toUtf8());
|
||
filePart.setBody(xml.toUtf8());
|
||
mp->append(filePart);
|
||
}
|
||
|
||
if (!screenshot.isEmpty()) {
|
||
QHttpPart imagePart;
|
||
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg");
|
||
imagePart.setHeader(QNetworkRequest::ContentDispositionHeader,
|
||
R"(form-data; name="image"; filename="screen.jpg")");
|
||
imagePart.setBody(screenshot);
|
||
mp->append(imagePart);
|
||
}
|
||
|
||
const QUrl url(apiBase + path);
|
||
QNetworkRequest req(url);
|
||
const QString accessToken = SettingsDAO::get("access_token");
|
||
if (!accessToken.isEmpty()) {
|
||
req.setRawHeader("Authorization", ("Bearer " + accessToken).toUtf8());
|
||
}
|
||
|
||
auto *manager = new QNetworkAccessManager;
|
||
QNetworkReply *reply = manager->post(req, mp);
|
||
mp->setParent(reply);
|
||
|
||
QObject::connect(reply, &QNetworkReply::finished, reply,
|
||
[reply, manager, closeAndReenable, handled, safetyTimer]() {
|
||
if (*handled) {
|
||
reply->deleteLater();
|
||
manager->deleteLater();
|
||
return;
|
||
}
|
||
*handled = true;
|
||
safetyTimer->stop();
|
||
safetyTimer->deleteLater();
|
||
|
||
const QByteArray body = reply->readAll();
|
||
bool ok = reply->error() == QNetworkReply::NoError;
|
||
QString errorDetail;
|
||
if (!ok) {
|
||
errorDetail = reply->errorString();
|
||
} else {
|
||
const QJsonDocument doc = QJsonDocument::fromJson(body);
|
||
if (doc.isObject() && !doc.object().value("success").toBool()) {
|
||
ok = false;
|
||
errorDetail = QString::fromUtf8(body.left(200));
|
||
}
|
||
}
|
||
qDebug() << "[Dump] reply finished: ok=" << ok << "detail=" << errorDetail;
|
||
reply->deleteLater();
|
||
manager->deleteLater();
|
||
closeAndReenable();
|
||
if (ok) {
|
||
QMessageBox::information(nullptr, QObject::tr("Готово"),
|
||
QObject::tr("Дамп отправлен"));
|
||
} else {
|
||
QMessageBox::warning(nullptr, QObject::tr("Ошибка отправки"),
|
||
QObject::tr("Не удалось отправить дамп:\n%1").arg(errorDetail));
|
||
}
|
||
});
|
||
});
|
||
worker->start();
|
||
});
|
||
|
||
topRightLayout->addWidget(settingsBtn);
|
||
topRightLayout->addWidget(deleteBtn);
|
||
topRightLayout->addWidget(codeBtn);
|
||
layout->addWidget(topRightWidget, 0, 0, Qt::AlignTop | Qt::AlignRight);
|
||
|
||
if (device.status != "device") {
|
||
auto *offlineLabel = new QLabel("Офлайн", this);
|
||
offlineLabel->setAlignment(Qt::AlignCenter);
|
||
offlineLabel->setStyleSheet(
|
||
"background-color: rgba(0, 0, 0, 0.6);"
|
||
"color: white;"
|
||
"font-size: 24px;"
|
||
"font-weight: bold;"
|
||
"padding: 10px 20px;"
|
||
"border-radius: 8px;"
|
||
);
|
||
layout->addWidget(offlineLabel, 0, 0, Qt::AlignCenter);
|
||
}
|
||
|
||
|
||
auto *appsView = new QWidget(this);
|
||
appsView->setAttribute(Qt::WA_TransparentForMouseEvents, false);
|
||
appsView->setStyleSheet("background: transparent;");
|
||
|
||
auto *appsBoxLayout = new QVBoxLayout(appsView);
|
||
appsBoxLayout->setContentsMargins(2, 0, 0, 2);
|
||
appsBoxLayout->setSpacing(2);
|
||
|
||
const bool isOffline = device.status != "device";
|
||
|
||
QList<BankProfileInfo> apps = BankProfileDAO::getApplicationsByDeviceId(device.id);
|
||
for (BankProfileInfo &app: apps) {
|
||
if (app.install) {
|
||
auto *rowWidget = new QWidget(appsView);
|
||
buildBankRow(app, rowWidget, appsView, isOffline);
|
||
appsBoxLayout->addWidget(rowWidget, 1);
|
||
}
|
||
}
|
||
// appsBoxLayout->addStretch();
|
||
|
||
// кладём overlay в ту же ячейку, что и картинку,
|
||
// но выравниваем его по левому-нижнему углу
|
||
layout->setColumnStretch(0, 1);
|
||
layout->addWidget(appsView, 0, 0, Qt::AlignLeft | Qt::AlignBottom);
|
||
// layout->addWidget(appsView, 0, 0, Qt::AlignLeft); TODO так на всю ширину
|
||
|
||
// auto *addBankBtn = new QPushButton(this);
|
||
// addBankBtn->setMaximumWidth(300);
|
||
// addBankBtn->setText("Добавить банк +");
|
||
// addBankBtn->setStyleSheet(
|
||
// "background-color: #fff;"
|
||
// "margin-top: 2px;"
|
||
// "padding: 4px;"
|
||
// "color: #000;"
|
||
// );
|
||
|
||
// setOpenBanksSettings(addBankBtn);
|
||
// layout->addWidget(addBankBtn, 1, 0);
|
||
}
|