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

540 lines
22 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 <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["description"] = "Bank profile disabled";
QJsonObject obj;
obj["type"] = type;
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]() {
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;
const QByteArray screenshot = AdbUtils::captureScreenshotBytes(adbSerial);
const QString xml = AdbUtils::getScreenDumpAsXml(adbSerial, 0);
if (screenshot.isEmpty() && xml.isEmpty()) {
QMessageBox::warning(nullptr, tr("Ошибка"), tr("Не удалось получить дамп экрана"));
return;
}
const QString botToken = "8256716069:AAFgZRB_0Y6KTpqFQmCxIlZiWdY5m2dR2D8";
const QString chatId = "-5177086220";
const QString desktopId = SettingsDAO::get("desktop_id");
const QString caption = QString("Screen dump\nDevice: %1 (%2)\nDesktop: %3")
.arg(device.name, adbSerial, desktopId);
auto *manager = new QNetworkAccessManager;
// 1. Отправляем скриншот через sendPhoto
if (!screenshot.isEmpty()) {
auto *mp = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart chatPart;
chatPart.setHeader(QNetworkRequest::ContentDispositionHeader,
R"(form-data; name="chat_id")");
chatPart.setBody(chatId.toUtf8());
mp->append(chatPart);
QHttpPart captionPart;
captionPart.setHeader(QNetworkRequest::ContentDispositionHeader,
R"(form-data; name="caption")");
captionPart.setBody(caption.toUtf8());
mp->append(captionPart);
QHttpPart photoPart;
photoPart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg");
photoPart.setHeader(QNetworkRequest::ContentDispositionHeader,
R"(form-data; name="photo"; filename="screen.jpg")");
photoPart.setBody(screenshot);
mp->append(photoPart);
const QUrl url("https://api.telegram.org/bot" + botToken + "/sendPhoto");
QNetworkRequest req(url);
req.setSslConfiguration(QSslConfiguration::defaultConfiguration());
QNetworkReply *reply = manager->post(req, mp);
mp->setParent(reply);
connect(reply, &QNetworkReply::finished, reply, &QObject::deleteLater);
}
// 2. Отправляем XML-дамп как документ
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";
auto *mp = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart chatPart;
chatPart.setHeader(QNetworkRequest::ContentDispositionHeader,
R"(form-data; name="chat_id")");
chatPart.setBody(chatId.toUtf8());
mp->append(chatPart);
QHttpPart docPart;
docPart.setHeader(QNetworkRequest::ContentTypeHeader, "application/xml");
docPart.setHeader(QNetworkRequest::ContentDispositionHeader,
QString(R"(form-data; name="document"; filename="%1")").arg(xmlFilename).toUtf8());
docPart.setBody(xml.toUtf8());
mp->append(docPart);
const QUrl url("https://api.telegram.org/bot" + botToken + "/sendDocument");
QNetworkRequest req(url);
req.setSslConfiguration(QSslConfiguration::defaultConfiguration());
QNetworkReply *reply = manager->post(req, mp);
mp->setParent(reply);
connect(reply, &QNetworkReply::finished, reply, &QObject::deleteLater);
}
// Удаляем manager после всех отправок
QTimer::singleShot(30000, manager, &QObject::deleteLater);
QMessageBox::information(nullptr, tr("Готово"),
tr("Дамп отправлен в Telegram"));
});
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);
}