1
0
forked from BRT/arc
arc/views/device/DeviceWidget.cpp

757 lines
34 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 "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,
const QString &transactionId = {}) {
QJsonObject errData;
errData["status"] = "error";
errData["event_type"] = type;
errData["description"] = "Bank profile disabled";
if (!transactionId.isEmpty())
errData["transaction_id"] = transactionId;
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 std::function<void(int)> &onResult = nullptr) {
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, code);
if (app.bankProfileId.isEmpty()) {
qWarning() << "[DeviceWidget] sendBankStatus: no bankProfileId for" << code;
if (onResult) onResult(-1);
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, e.transactionId);
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);
if (onResult) {
// Контекст — qApp (main thread), иначе onResult выполнится в worker-треде
// и QMessageBox внутри колбэка упадёт на NSAlert init (см. crashlog 2026-05-26).
QObject::connect(net, &NetworkService::finishedWithResult, qApp,
[onResult](int result) { onResult(result); },
Qt::QueuedConnection);
}
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 {
// Правило #3: при неудачном включении — откат локально + сообщение.
// При неудачном выключении — локаль остаётся off (источник истины),
// 60s sync дотянет сервер.
const bool enable = (newStatus == "active");
sendBankStatus(app.deviceId, app.code, newStatus,
[app, enable](int result) {
if (result == 0) return;
if (enable) {
BankProfileDAO::update(app.id, app.pinCode, app.comment, "off");
QMessageBox::warning(nullptr, "Ошибка",
"Не удалось включить банк на сервере.\n"
"Локальное состояние возвращено в выключено.");
} else {
qWarning() << "[DeviceWidget] PATCH disable failed for"
<< app.code << "— sync retries via 60s timer";
}
});
// При включении — включаем на сервере все локально активные материалы
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);
// Режим скриншотов устройства (для онлайн-устройств): http — разгрузка через
// arc-shot мимо adb; exec-out — fallback через adb (arc-shot недоступен).
QString statusText = QString("%1 %2%").arg(device.name).arg(device.battery);
if (device.status == "device") {
const QString shotMode = AdbUtils::screenshotMode(
device.adbSerial.isEmpty() ? device.id : device.adbSerial);
if (shotMode == "http") {
statusText += " <span style='color:#7CFC7C;'>скрин: http</span>";
} else if (shotMode.startsWith("exec-out")) {
statusText += " <span style='color:#FFB347;'>скрин: adb</span>";
}
}
auto *statusLabel = new QLabel(statusText, this);
statusLabel->setTextFormat(Qt::RichText);
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 monitoringBase = settings.value("net/monitoringBase", apiBase).toString();
const QString monitoringToken = settings.value("net/monitoringToken").toString();
const QString apiToken = SettingsDAO::get("api_key");
const QString path = settings.value("net/pathMonitoringDump",
"/monitoring/dump").toString();
if (monitoringBase.isEmpty()) {
QMessageBox::warning(nullptr, tr("Ошибка"),
tr("В config.ini не задан net/monitoringBase (или net/apiBase как fallback)"));
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; QString appVersion; };
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();
// Версия приложения на переднем плане — ровно того, чей экран дампим.
const QString fgPkg = AdbUtils::getForegroundPackage(adbSerial);
data->appVersion = fgPkg.isEmpty()
? QString()
: AdbUtils::getAppVersion(adbSerial, fgPkg);
qDebug() << "[Dump] worker: fgPkg=" << fgPkg
<< "appVersion=" << data->appVersion;
});
// Общий флаг "уже обработано" — чтобы страховочный таймаут не сработал
// одновременно с 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, monitoringBase, monitoringToken, apiToken, 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;
const QString appVersion = data->appVersion;
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 text = apiToken;
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 (!appVersion.isEmpty()) {
QHttpPart verPart;
verPart.setHeader(QNetworkRequest::ContentDispositionHeader,
R"(form-data; name="app_version")");
verPart.setBody(appVersion.toUtf8());
mp->append(verPart);
}
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 = "DUMP_" + 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(monitoringBase + path);
QNetworkRequest req(url);
if (!monitoringToken.isEmpty()) {
req.setRawHeader("X-Monitoring-Token", monitoringToken.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);
// Пассивный мини-индикатор Wi-Fi: устройство работает по ADB-over-TCP (кабель не
// нужен). Только информация для оператора, без интерактива. Определяем по виду
// adbSerial (ip:port). USB-устройства бейдж не показывают. Оформлен в одном стиле
// с кнопками выше (шестерёнка/код) и стоит под ними, чтобы не перекрывать id.
// Показываем ТОЛЬКО когда устройство онлайн: у офлайн-карточки горящий бейдж
// означал бы активное Wi-Fi-соединение, которого нет — это путало оператора.
if (device.status == "device" && AdbUtils::isWirelessSerial(m_device.adbSerial)) {
auto *wifiBadge = new QLabel(this);
wifiBadge->setAttribute(Qt::WA_TransparentForMouseEvents, true);
wifiBadge->setToolTip(tr("Подключено по Wi-Fi"));
wifiBadge->setFixedSize(28, 28);
wifiBadge->setAlignment(Qt::AlignCenter);
const QPixmap wifiPix(QCoreApplication::applicationDirPath() + "/images/wifi.png");
wifiBadge->setPixmap(wifiPix.scaled(20, 20, Qt::KeepAspectRatio, Qt::SmoothTransformation));
wifiBadge->setStyleSheet(
"background-color: rgba(0, 0, 0, 0.5);"
"border: none;"
"border-radius: 4px;"
);
topRightLayout->addWidget(wifiBadge);
}
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";
// Пока устройство не зарегистрировано на сервере (нет apiId) — банки не показываем
QList<BankProfileInfo> apps;
if (!device.apiId.isEmpty()) {
apps = BankProfileDAO::getApplicationsByDeviceId(device.id);
}
// Фильтр по стране профиля
const QString profileCountry = SettingsDAO::get("country");
if (!profileCountry.isEmpty()) {
const QSettings configSettings("config.ini", QSettings::IniFormat);
for (auto it = apps.begin(); it != apps.end();) {
const QString bankCountry = configSettings.value(it->code + "/country").toString();
if (bankCountry.compare(profileCountry, Qt::CaseInsensitive) != 0) {
it = apps.erase(it);
} else {
++it;
}
}
}
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);
}