1
0
forked from BRT/arc
arc/views/device/DeviceWidget.cpp
trnsmkot c79acee002 Add OCR utilities and backend integration for enhanced screen dump analysis:
- 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.
2026-04-30 19:17:17 +07:00

658 lines
28 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) {
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);
}