1
0
forked from BRT/arc

Refactor scripts and add enhanced error handling

Revised scripts for payments and history retrieval, introducing improved error management with screenshots for debugging. Added a new LoginAndCheckAccountsScript for account validation, streamlined multi-threading, and updated application logic for clarity and reliability.
This commit is contained in:
slava 2025-05-30 22:16:25 +07:00
parent 01aa8bca5e
commit 4fe6e399bc
28 changed files with 389 additions and 241 deletions

View File

@ -92,21 +92,30 @@ bool AdbUtils::tryToKillApplication(const QString &deviceId, const QString &pack
// idx - временный лог
// adb -s 6edc4a47 shell 'uiautomator dump /sdcard/uidump.xml' && adb -s 6edc4a47 pull /sdcard/uidump.xml step_1.xml && adb -s 6edc4a47 shell 'rm /sdcard/uidump.xml'
QString AdbUtils::getScreenDumpAsXml(const QString &deviceId, const int idx) {
QString AdbUtils::getScreenDumpAsXml(const QString &deviceId, const int idx, const bool screenshot) {
// TODO убрать логирование в xml файл
// "adb -s %1 pull /sdcard/uidump.xml step_%2.xml && "
QProcess process;
QString xml = QString(
QString xml;
if (screenshot) {
xml = QString(
"adb -s %1 shell 'uiautomator dump /sdcard/uidump.xml' && "
"adb -s %1 pull /sdcard/uidump.xml step_%2.xml && "
"adb -s %1 shell 'cat /sdcard/uidump.xml' && "
"adb -s %1 shell 'rm /sdcard/uidump.xml'"
).arg(deviceId, QString::number(idx));
} else {
xml = QString(
"adb -s %1 shell 'uiautomator dump /sdcard/uidump.xml' && "
"adb -s %1 shell 'cat /sdcard/uidump.xml' && "
"adb -s %1 shell 'rm /sdcard/uidump.xml'"
// ).arg(deviceId, QString::number(idx));
).arg(deviceId);
}
process.start("sh", {"-c", xml});
process.waitForFinished();
if (false) {
if (screenshot) {
// TODO делаем скрин, убрать потом
// adb -s 6edc4a47 shell 'screencap -p /sdcard/screen.png' && adb -s 6edc4a47 pull /sdcard/screen.png screen_step_1.png && adb -s 6edc4a47 shell 'rm /sdcard/screen.png'
QProcess processScreenshot;
@ -136,7 +145,7 @@ bool AdbUtils::takeScreenshot(const QString &deviceId, const QString &name) {
QProcess process;
QString screenshot = QString(
"adb -s %1 shell 'screencap -p /sdcard/screen.png' && "
"adb -s %1 pull /sdcard/screen.png screen_%2.png && "
"adb -s %1 pull /sdcard/screen.png %2.png && "
"adb -s %1 shell 'rm /sdcard/screen.png'"
).arg(deviceId, name);
process.start("sh", {"-c", screenshot});
@ -144,6 +153,19 @@ bool AdbUtils::takeScreenshot(const QString &deviceId, const QString &name) {
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
}
bool AdbUtils::takeXmlDump(const QString &deviceId, const QString &name) {
QProcess process;
QString xml = QString(
"adb -s %1 shell 'uiautomator dump /sdcard/uidump.xml' && "
"adb -s %1 pull /sdcard/uidump.xml %2.xml && "
"adb -s %1 shell 'cat /sdcard/uidump.xml' && "
"adb -s %1 shell 'rm /sdcard/uidump.xml'"
).arg(deviceId, name);
process.start("sh", {"-c", xml});
process.waitForFinished();
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
}
bool AdbUtils::makeTap(const QString &deviceId, const int x, const int y) {
QProcess process;
process.start("adb", {
@ -235,7 +257,7 @@ QSet<QString> AdbUtils::getListApps(const QString &deviceId) {
QSet<QString> apps;
const QStringList lines = output.split('\n');
for (int i = 1; i < lines.size(); ++i) {
for (int i = 0; i < lines.size(); ++i) {
QString line = lines.at(i).trimmed().replace("package:", "");
if (line.isEmpty()) {

View File

@ -17,10 +17,12 @@ public:
static bool tryToKillApplication(const QString &deviceId, const QString &packageName);
static QString getScreenDumpAsXml(const QString &deviceId, int idx);
static QString getScreenDumpAsXml(const QString &deviceId, int idx, bool screenshot = false);
static bool takeScreenshot(const QString &deviceId, const QString &name);
static bool takeXmlDump(const QString &deviceId, const QString &name);
static bool makeTap(const QString &deviceId, int x, int y);
static bool enableAdbIMEKeyboard(const QString &deviceId);

View File

@ -39,7 +39,7 @@ bool CommonScript::inputPinCode(const QString &deviceId, const QList<Node> &pinc
for (const Node &node: pincode) {
const bool completePincode = AdbUtils::makeTap(deviceId, node.x(), node.y());
if (!completePincode) {
return false;
AdbUtils::makeTap(deviceId, node.x(), node.y());
}
QThread::msleep(QRandomGenerator::global()->bounded(100, 901));
}
@ -68,27 +68,16 @@ bool CommonScript::runAppAndGoToHomeScreen(
// Даем минимальное время на запуск
QThread::msleep(4500);
int counter = 0;
int restarted = 0;
int totalCounter = 0;
bool findAndInputPincode = false;
while (true) {
counter++;
totalCounter++;
// 3 раза уже перезапускали приложение, завершаем попытки
if (restarted > 2) {
AdbUtils::takeScreenshot(deviceId, "3_times_try_start");
// Если более 10 раз мы пытаемся перейти на домашнюю
if (counter > 10) {
AdbUtils::takeScreenshot(deviceId, "too_many_attempts");
return false;
}
// Если более 30 секунд мы пытаемся перейти на домашнюю, перезапускаем
if (counter > 30) {
AdbUtils::tryToKillApplication(deviceId, packageName);
QThread::msleep(4500);
++restarted;
counter = 0;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
if (xmlScreenParser.isHomeScreen()) {
@ -115,6 +104,10 @@ bool CommonScript::runAppAndGoToHomeScreen(
if (!findAndInputPincode) {
if (const QList<Node> pincode = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
pincode.size() == 4) {
// чтобы пробудить экран
if (Node textPinCodeNode = xmlScreenParser.findTextNode("Введите ПИН"); !textPinCodeNode.isEmpty()) {
AdbUtils::makeTap(deviceId, textPinCodeNode.x(), textPinCodeNode.y());
}
inputPinCode(deviceId, pincode);
// После ввода пин-код нужно сделать задержку, пока подгрузка идет, чтобы снова не спарсить
findAndInputPincode = true;
@ -124,12 +117,6 @@ bool CommonScript::runAppAndGoToHomeScreen(
}
QThread::msleep(1000);
// Если что-то пойдет не по плану...
if (totalCounter > 100) {
AdbUtils::takeScreenshot(deviceId, "too_many_attempts");
return false;
}
}
}
@ -161,7 +148,7 @@ bool CommonScript::goToAllProducts(
// 1. Если нет кнопки видимой, свайпаем. Кликаем на "Все продукты"
for (int i = 0; i < 5; ++i) {
Node allProductsBtn = xmlScreenParser.findButtonNode("Все продукты", false);
if (allProductsBtn.x() > 0 && allProductsBtn.y() > 0) {
if (allProductsBtn.x() > 0 && allProductsBtn.y() > 0 && allProductsBtn.y() < height - 200) {
AdbUtils::makeTap(deviceId, allProductsBtn.x(), allProductsBtn.y());
QThread::msleep(1000);
break;
@ -221,7 +208,7 @@ void CommonScript::postAccountsData(const QList<QPair<AccountInfo, Node> > &acco
for (const QPair<AccountInfo, Node> &pair: accounts) {
QJsonObject obj;
AccountInfo account = pair.first;
obj["appName"] = account.appName;
obj["appName"] = account.appCode;
obj["lastNumbers"] = account.lastNumbers;
obj["amount"] = account.amount;
obj["description"] = account.description;
@ -229,7 +216,7 @@ void CommonScript::postAccountsData(const QList<QPair<AccountInfo, Node> > &acco
list.append(obj);
}
// FIXME доставить десктоп ид из настроеек
worker->setDeviceData("DT88bokcwQ", accounts.first().first.deviceId);
worker->setDeviceId(accounts.first().first.deviceId);
worker->setPayload(QJsonDocument(list).toJson());
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &NetworkService::postAccountsData);
@ -258,7 +245,7 @@ void CommonScript::postNewTransactionData(const AccountInfo &account, const Tran
obj["bankTransactionId"] = transaction.bankTrExternalId;
// FIXME доставить десктоп ид из настроеек
worker->setDeviceData("DT88bokcwQ", account.deviceId);
worker->setDeviceId(account.deviceId);
worker->addExtra("lastNumbers", account.lastNumbers);
worker->addExtra("transactionId", transaction.id);
worker->setPayload(QJsonDocument(obj).toJson());
@ -287,7 +274,7 @@ void CommonScript::updateTransactionData(
obj["updatedDescription"] = oldDesc;
// FIXME доставить десктоп ид из настроеек
worker->setDeviceData("DT88bokcwQ", account.deviceId);
worker->setDeviceId(account.deviceId);
worker->addExtra("lastNumbers", account.lastNumbers);
worker->setPayload(QJsonDocument(obj).toJson());
worker->moveToThread(thread);

View File

@ -32,17 +32,17 @@ QList<TransactionInfo> findSavedTransaction(
for (const TransactionInfo &info: transactions) {
QDateTime timestamp = info.timestamp;
QString shortDescription = parsed.shortDescription;
QString description = parsed.description;
// если тот же день
if (dateTime.date() == timestamp.date() && parsed.amount == info.amount) {
// Перевод Надежда Юрьевна К** в Альфа-Банк через СБП, по номеру телефона +79641815...
if (shortDescription.contains("Перевод")) {
if (shortDescription.contains(info.name)) {
if (description.contains("Перевод")) {
if (description.contains(info.name)) {
txs.append(info);
}
} else {
// Перевод по карте
if (shortDescription == info.shortDescription) {
if (description == info.description) {
txs.append(info);
}
}
@ -56,7 +56,7 @@ int countSimilar(const TransactionInfo &transaction, const QList<TransactionInfo
int count = 0;
for (const TransactionInfo &info: transactions) {
if (info.amount == transaction.amount
&& info.shortDescription == transaction.shortDescription
&& info.description == transaction.description
&& info.bankTime == transaction.bankTime) {
++count;
}
@ -179,8 +179,6 @@ bool GetLastDaysHistoryScript::updateAndPostTransactionData(
void GetLastDaysHistoryScript::doStart() {
return;
// берем все транзакции из БД
QList<TransactionInfo> localTransactions = TransactionDAO::getTransactionsWithinHours(m_account.id, 48);
const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId);
@ -191,7 +189,7 @@ void GetLastDaysHistoryScript::doStart() {
QList<QPair<AccountInfo, Node> > accounts = xmlScreenParser.parseAccountsInfo();
for (auto &[account, node]: accounts) {
account.deviceId = m_account.deviceId;
account.appName = m_account.appName;
account.appCode = m_account.appCode;
if (!AccountDAO::upsertAccount(account)) {
qDebug() << "Not updated: " << account.lastNumbers;
}

View File

@ -185,7 +185,7 @@ void HomeScreenScript::start() {
// .. пробуем кликать, но если не кликается ???
QList<TransactionInfo> transactions = xmlScreenParser.parseTodayAndYesterdayHistory();
for (const TransactionInfo &info: transactions) {
QString text = QString("%1 %2").arg(info.shortDescription, getPriceAsString(info.amount));
QString text = QString("%1 %2").arg(info.description, getPriceAsString(info.amount));
qDebug().noquote().nospace() << convertTransactionToString(info);
// Node trButton = xmlScreenParser.findButtonNode(text, true);

View File

@ -0,0 +1,61 @@
#include "LoginAndCheckAccountsScript.h"
#include "adb/AdbUtils.h"
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/DeviceDAO.h"
LoginAndCheckAccountsScript::LoginAndCheckAccountsScript(
QString deviceId,
QString appCode,
QObject *parent
) : CommonScript(parent),
m_deviceId(std::move(deviceId)), m_appCode(std::move(appCode)) {
}
LoginAndCheckAccountsScript::~LoginAndCheckAccountsScript() = default;
void LoginAndCheckAccountsScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode);
if (device.id.isEmpty() || app.id == -1) {
qCritical() << "Device or app not found: " << m_deviceId << " " << m_appCode;
emit finished();
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, device.screenWidth, device.screenHeight)) {
qWarning() << "Cant open the home screen: " << m_deviceId << " " << m_appCode;
if (!ApplicationDAO::updatePinCodeStatus(app.id, "no", "Не прошла проверка [дата]")) {
qWarning() << "Cant update the pincode: " << m_deviceId << " " << m_appCode;
}
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finished();
return;
} else {
if (!ApplicationDAO::updatePinCodeStatus(app.id, "yes", "")) {
qWarning() << "Cant update the pincode: " << m_deviceId << " " << m_appCode;
}
if (goToAllProducts(device.id, device.screenWidth, device.screenHeight)) {
QList<QPair<AccountInfo, Node> > accounts = xmlScreenParser.parseAccountsInfo();
for (auto &[account, node]: accounts) {
account.deviceId = m_deviceId;
account.appCode = m_appCode;
if (!AccountDAO::upsertAccount(account)) {
qDebug() << "Not updated: " << account.lastNumbers;
}
}
postAccountsData(accounts);
} else {
qWarning() << "Cant go to all products: " << m_deviceId << " " << m_appCode;
}
}
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finished();
}

View File

@ -0,0 +1,22 @@
#pragma once
#include "CommonScript.h"
class LoginAndCheckAccountsScript final : public CommonScript {
Q_OBJECT
public:
~LoginAndCheckAccountsScript() override;
explicit LoginAndCheckAccountsScript(
QString deviceId,
QString aooCode,
QObject *parent = nullptr
);
protected:
void doStart() override;
private:
const QString m_deviceId;
const QString m_appCode;
};

View File

@ -40,6 +40,7 @@ void PayByPhoneScript::makePayment(
AdbUtils::makeTap(deviceId, payBtn.x(), payBtn.y());
QThread::msleep(1000);
} else {
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_pay_btn_not_found");
m_error = "Кнопка 'Оплатить' не нашлась";
qDebug() << m_error;
return;
@ -54,11 +55,13 @@ void PayByPhoneScript::makePayment(
AdbUtils::makeTap(deviceId, payByPhoneBtn.x(), payByPhoneBtn.y());
QThread::msleep(1000);
} else {
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_not_found_pay_by_phone");
m_error = "Не смогли найти пункт 'По номеру телефона В РСХБ и через'";
qDebug() << m_error;
return;
}
} else {
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_go_to_payments");
m_error = "Не смогли перейти на экран 'Платежи и переводы'";
qDebug() << m_error;
return;
@ -75,6 +78,7 @@ void PayByPhoneScript::makePayment(
AdbUtils::inputText(deviceId, m_phone);
QThread::msleep(1000);
} else {
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_find_pay_by_phone");
m_error = "Не смогли найти пункт 'Перевод по телефону'";
qDebug() << m_error;
return;
@ -87,6 +91,7 @@ void PayByPhoneScript::makePayment(
AdbUtils::makeTap(deviceId, payOtherBankBtn.x(), payOtherBankBtn.y());
QThread::msleep(1000);
} else {
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_tap_on_bank");
m_error = "Не смогли нажать на первичный выбор банка";
qDebug() << m_error;
return;
@ -158,16 +163,19 @@ void PayByPhoneScript::makePayment(
AdbUtils::makeTap(deviceId, continuePayBtn.x(), continuePayBtn.y());
QThread::msleep(1000);
} else {
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_tap_on_ready");
m_error = "Не смогли нажать 'Готово'";
qDebug() << m_error;
return;
}
} else {
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_tap_on_continue");
m_error = "Не смогли нажать 'Продолжить'";
qDebug() << m_error;
return;
}
} else {
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_find_bank_title");
m_error = "Не смогли найти заголовок: " + payByPhoneScreenTitleText;
qDebug() << m_error;
return;
@ -200,6 +208,7 @@ void PayByPhoneScript::makePayment(
}
if (!isRshbBank && !inputedPinCode) {
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_input_pin_code");
m_error = "Не смогли ввести пин-код!'";
qDebug() << m_error;
return;
@ -245,6 +254,9 @@ void PayByPhoneScript::makePayment(
}
postNewTransactionData(m_account, transaction);
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_finish_transactions_" + transaction.id);
AdbUtils::takeXmlDump(deviceId, "pay_by_phone_finish_transactions_" + transaction.id);
break;
} else {
qWarning() << "--- moreDetailBtn not found, i: " << i;
@ -260,7 +272,7 @@ void PayByPhoneScript::makePayment(
void PayByPhoneScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId);
const ApplicationInfo app = ApplicationDAO::getApplication(m_account.deviceId, m_account.appName);
const ApplicationInfo app = ApplicationDAO::getApplication(m_account.deviceId, m_account.appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found....";
@ -272,7 +284,7 @@ void PayByPhoneScript::doStart() {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_account.deviceId, ++m_counter));
if (!xmlScreenParser.isHomeScreen()) {
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, device.screenWidth, device.screenHeight)) {
m_error = "Не смогли дойти до домашней страницы....";
m_error = "Не смогли дойти до домашней страницы";
qDebug() << m_error;
emit finishedWithResult(m_error);
return;
@ -280,11 +292,10 @@ void PayByPhoneScript::doStart() {
}
if (goToAllProducts(device.id, device.screenWidth, device.screenHeight)) {
qDebug() << "======== 1. All products";
QList<QPair<AccountInfo, Node> > accounts = xmlScreenParser.parseAccountsInfo();
for (auto &[account, node]: accounts) {
account.deviceId = m_account.deviceId;
account.appName = m_account.appName;
account.appCode = m_account.appCode;
if (!AccountDAO::upsertAccount(account)) {
qDebug() << "Not updated: " << account.lastNumbers;
}
@ -295,16 +306,27 @@ void PayByPhoneScript::doStart() {
for (auto &[account, node]: accounts) {
// FIXME будем считать что числа последние везде уникальные
if (m_account.lastNumbers == account.lastNumbers) {
qDebug() << "======== 2. Tap by card";
if (account.amount >= m_amount) {
AdbUtils::makeTap(device.id, node.x(), node.y());
QThread::msleep(1000);
} else {
AdbUtils::takeScreenshot(device.id, "pay_by_phone_not_enough_money_" + QString::number(m_account.id));
m_error = "Not enough money, id: " + QString::number(m_account.id)
+ ", amount: " + QString::number(account.amount)
+ ", trx amount: " + QString::number(m_amount);
qCritical() << m_error;
emit finishedWithResult(m_error);
return;
}
break;
}
}
qDebug() << "======== 3. Make payment";
makePayment(device.id, app.pinCode, device.screenWidth, device.screenHeight);
// AdbUtils::tryToKillApplication(device.id, app.package); TODO - надо ли это???
} else {
AdbUtils::takeScreenshot(device.id, "pay_by_phone_cant_open_all_products_" + QString::number(m_account.id));
AdbUtils::tryToKillApplication(device.id, app.package);
m_error = "Не смогли открыть 'Все продукты'";
qDebug() << m_error;
}

View File

@ -660,7 +660,7 @@ QList<TransactionInfo> ScreenXmlParser::parseTodayAndYesterdayHistory() {
TransactionInfo info;
info.amount = amount;
info.bankTime = date.toString("dd.MM.yyyy");
info.shortDescription = shortDesc;
info.description = shortDesc;
transactions.append(info);
}

Binary file not shown.

View File

@ -21,7 +21,7 @@ bool AccountDAO::upsertAccount(const AccountInfo &info) {
)");
query.bindValue(":device_id", info.deviceId);
query.bindValue(":app_name", info.appName);
query.bindValue(":app_name", info.appCode);
query.bindValue(":last_numbers", info.lastNumbers);
query.bindValue(":amount", info.amount);
query.bindValue(":update_time", DateUtils::toUtcString(info.updateTime));
@ -47,7 +47,7 @@ AccountInfo parseAccount(const QSqlQuery &query) {
AccountInfo acc;
acc.id = query.value("id").toInt();
acc.deviceId = query.value("device_id").toString();
acc.appName = query.value("app_name").toString();
acc.appCode = query.value("app_name").toString();
acc.lastNumbers = query.value("last_numbers").toString();
acc.amount = query.value("amount").toDouble();
acc.updateTime = DateUtils::fromUtcString(query.value("update_time").toString());

View File

@ -78,7 +78,6 @@ bool ApplicationDAO::update(
)");
query.bindValue(":id", appId);
query.bindValue(":pin_code", pinCode);
query.bindValue(":comment", comment);
query.bindValue(":pin_code", pinCode);
query.bindValue(":status", status);
@ -90,6 +89,32 @@ bool ApplicationDAO::update(
return true;
}
bool ApplicationDAO::updatePinCodeStatus(
const int &appId,
const QString &pinCodeStatus,
const QString &comment
) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
UPDATE applications
SET pin_code_checked = :pin_code_checked, comment = :comment, status = :status
WHERE id = :id
)");
query.bindValue(":id", appId);
query.bindValue(":comment", comment);
query.bindValue(":pin_code_checked", pinCodeStatus);
query.bindValue(":status", pinCodeStatus == "yes" ? "active" : "off");
if (!query.exec()) {
qWarning() << "Ошибка при обновлении PIN-кода приложения:" << query.lastError().text();
return false;
}
return true;
}
ApplicationInfo parseApplication(const QSqlQuery &query) {
ApplicationInfo app;
app.id = query.value("id").toInt();

View File

@ -8,8 +8,18 @@ public:
[[nodiscard]] static bool updatePinCode(const QString &appId, const QString &pinCode);
[[nodiscard]] static bool update(const int &appId, const QString &pinCode, const QString &comment,
const QString &status);
[[nodiscard]] static bool update(
const int &appId,
const QString &pinCode,
const QString &comment,
const QString &status
);
[[nodiscard]] static bool updatePinCodeStatus(
const int &appId,
const QString &pinCodeStatus,
const QString &comment
);
[[nodiscard]] static ApplicationInfo getApplication(const QString &deviceId, const QString &appName);

View File

@ -40,7 +40,7 @@ int EventDAO::insertEvent(const EventInfo &event) {
query.bindValue(":amount", event.amount);
query.bindValue(":phone", event.phone);
query.bindValue(":bank_name", event.bankName);
query.bindValue(":timestamp", event.timestamp);
query.bindValue(":timestamp", DateUtils::toUtcString(event.timestamp));
if (!query.exec()) {
qCritical() << "Ошибка при вставке event:" << query.lastError().text();
@ -52,33 +52,46 @@ int EventDAO::insertEvent(const EventInfo &event) {
bool EventDAO::updateEvent(
const int id,
const EventStatus &status
const EventStatus &status,
const QString &comment
) {
QSqlQuery query(DatabaseManager::instance().database());
if (status == EventStatus::InProgress) {
query.prepare(R"(
UPDATE events SET
status = :status,
update_time = :update_time
WHERE id = :id
)");
query.bindValue(":update_time", DateUtils::toUtcString(DateUtils::getUtcNow()));
}
if (status == EventStatus::Complete) {
query.prepare(R"(
UPDATE events SET
status = :status,
complete_time = :complete_time
WHERE id = :id
)");
query.bindValue(":complete_time", DateUtils::toUtcString(DateUtils::getUtcNow()));
}
if (status == EventStatus::Error) {
query.prepare(R"(
UPDATE events SET
status = :status,
update_time = :update_time,
complete_time = :complete_time
comment = :comment
WHERE id = :id
)");
query.bindValue(":update_time", DateUtils::toUtcString(DateUtils::getUtcNow()));
query.bindValue(":comment", comment);
}
query.bindValue(":id", id);
query.bindValue(":status", eventStatusToString(status));
if (status == EventStatus::Wait) {
query.bindValue(":update_time", DateUtils::toUtcString(DateUtils::getUtcNow()));
} else {
query.bindValue(":update_time", QVariant(QMetaType(QMetaType::QString)));
}
if (status == EventStatus::Complete) {
query.bindValue(":complete_time", DateUtils::toUtcString(DateUtils::getUtcNow()));
} else {
query.bindValue(":complete_time", QVariant(QMetaType(QMetaType::QString)));
}
if (!query.exec()) {
qCritical() << "Ошибка при обновлении event:" << query.lastError().text();
return false;

View File

@ -8,7 +8,8 @@ public:
[[nodiscard]] static bool updateEvent(
int id,
const EventStatus &status
const EventStatus &status,
const QString &comment
);
[[nodiscard]] static QList<EventInfo> getAllEvents(const EventStatus &status);

View File

@ -40,12 +40,12 @@ bool TransactionDAO::insertTransactionIfNotExists(const TransactionInfo &info) {
query.prepare(R"(
INSERT INTO transactions (
account_id, amount, fee, status, phone, type,
description, updated_description, short_description, updated_short_description,
description, updated_description,
bank_name, bank_time, name, bank_tr_external_id,
update_time, complete_time, timestamp
) VALUES (
:account_id, :amount, :fee, :status, :phone, :type,
:description, :updated_description, :short_description, :updated_short_description,
:description, :updated_description,
:bank_name, :bank_time, :name, :bank_tr_external_id,
:update_time, :complete_time, :timestamp
)
@ -60,8 +60,6 @@ bool TransactionDAO::insertTransactionIfNotExists(const TransactionInfo &info) {
query.bindValue(":name", info.name);
query.bindValue(":description", info.description);
query.bindValue(":updated_description", info.updatedDescription);
query.bindValue(":short_description", info.shortDescription);
query.bindValue(":updated_short_description", info.updatedShortDescription);
query.bindValue(":bank_name", info.bankName);
query.bindValue(":bank_time", info.bankTime);
query.bindValue(":bank_tr_external_id", info.bankTrExternalId);
@ -82,12 +80,12 @@ int TransactionDAO::insertTransaction(const TransactionInfo &info) {
query.prepare(R"(
INSERT INTO transactions (
account_id, amount, fee, status, phone, type,
description, updated_description, short_description, updated_short_description,
description, updated_description,
bank_name, bank_time, name, bank_tr_external_id,
update_time, complete_time
) VALUES (
:account_id, :amount, :fee, :status, :phone, :type,
:description, :updated_description, :short_description, :updated_short_description,
:description, :updated_description,
:bank_name, :bank_time, :name, :bank_tr_external_id,
:update_time, :complete_time
)
@ -102,8 +100,6 @@ int TransactionDAO::insertTransaction(const TransactionInfo &info) {
query.bindValue(":name", info.name);
query.bindValue(":description", info.description);
query.bindValue(":updated_description", info.updatedDescription);
query.bindValue(":short_description", info.shortDescription);
query.bindValue(":updated_short_description", info.updatedShortDescription);
query.bindValue(":bank_name", info.bankName);
query.bindValue(":bank_time", info.bankTime);
query.bindValue(":bank_tr_external_id", info.bankTrExternalId);
@ -138,8 +134,6 @@ TransactionInfo parseTransaction(const QSqlQuery &query) {
tx.name = query.value("name").toString();
tx.description = query.value("description").toString();
tx.updatedDescription = query.value("updated_description").toString();
tx.shortDescription = query.value("short_description").toString();
tx.updatedShortDescription = query.value("updated_short_description").toString();
tx.bankName = query.value("bank_name").toString();
tx.bankTime = query.value("bank_time").toString();
tx.bankTrExternalId = query.value("bank_tr_external_id").toString();
@ -188,8 +182,6 @@ bool TransactionDAO::updateTransaction(const TransactionInfo &info) {
phone = :phone,
description = :description,
updated_description = :updated_description,
short_description = :short_description,
updated_short_description = :updated_short_description,
bank_name = :bank_name,
bank_time = :bank_time,
name = :name,
@ -209,8 +201,6 @@ bool TransactionDAO::updateTransaction(const TransactionInfo &info) {
query.bindValue(":name", info.name);
query.bindValue(":description", info.description);
query.bindValue(":updated_description", info.updatedDescription);
query.bindValue(":short_description", info.shortDescription);
query.bindValue(":updated_short_description", info.updatedShortDescription);
query.bindValue(":bank_name", info.bankName);
query.bindValue(":bank_time", info.bankTime);
query.bindValue(":bank_tr_external_id", info.bankTrExternalId);

133
main.cpp
View File

@ -21,8 +21,8 @@
#include "rshb/HomeScreenScript.h"
#include "rshb/PayByPhoneScript.h"
void startDeviceScreener(QCoreApplication &app) {
auto *thread = new QThread;
void startDeviceScreener(const QCoreApplication &app) {
auto *thread = new QThread();
auto *deviceScreenerWorker = new DeviceScreener;
// Перемещаем worker в новый поток thread.
@ -51,94 +51,32 @@ void startDeviceScreener(QCoreApplication &app) {
thread->start();
}
void setupWorkerAndThreadHistory(QCoreApplication &app) {
const QString appName = "rshb";
const QString lastNumber = "7183";
const QString deviceId = "6edc4a47";
AccountInfo account = AccountDAO::findAppAccount(deviceId, appName, lastNumber);
void startEventHandler(const QCoreApplication &app) {
auto *thread = new QThread;
auto *deviceScreenerWorker = new GetLastDaysHistoryScript(account);
deviceScreenerWorker->moveToThread(thread);
QObject::connect(thread, &QThread::started, deviceScreenerWorker, &GetLastDaysHistoryScript::start);
QObject::connect(deviceScreenerWorker, &GetLastDaysHistoryScript::finished, thread, &QThread::quit);
QObject::connect(deviceScreenerWorker, &GetLastDaysHistoryScript::finished, deviceScreenerWorker,
&QObject::deleteLater);
auto *eventHandler = new EventHandler;
eventHandler->moveToThread(thread);
QObject::connect(thread, &QThread::started, eventHandler, &EventHandler::start);
QObject::connect(eventHandler, &EventHandler::finished, thread, &QThread::quit);
QObject::connect(eventHandler, &EventHandler::finished, eventHandler, &QObject::deleteLater);
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
QObject::connect(&app, &QCoreApplication::aboutToQuit, [=]() {
deviceScreenerWorker->stop();
eventHandler->stop();
});
thread->start();
}
void setupScriptAutoPaymentAndThread(QCoreApplication &app) {
const QString appName = "rshb";
const QString lastNumber = "7183";
const QString phoneNumber = "79641815146";
const QString deviceId = "6edc4a47";
const QString bankName = "Альфа-Банк";
// const QString bankName = "Сбербанк";
const double amount = 320;
const AccountInfo account = AccountDAO::findAppAccount(deviceId, appName, lastNumber);
if (account.id != -1) {
// Функция для подключения потоков к завершению приложения
auto connectStopOnQuit = [&](QObject *worker, QThread *thread) {
void startNetworkService(const QCoreApplication &app) {
auto *paymentThread = new QThread;
auto *networkService = new NetworkService;
networkService->moveToThread(paymentThread);
QObject::connect(paymentThread, &QThread::started, networkService, &NetworkService::start);
QObject::connect(networkService, &NetworkService::finished, paymentThread, &QThread::quit);
QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater);
QObject::connect(paymentThread, &QThread::finished, paymentThread, &QThread::deleteLater);
QObject::connect(&app, &QCoreApplication::aboutToQuit, [=]() {
if (thread->isRunning()) {
qDebug() << "Завершаем поток:" << thread;
thread->quit();
thread->wait(); // Ожидаем завершения потока
}
worker->deleteLater();
thread->deleteLater();
networkService->stop();
});
};
// Первый поток
auto *thread = new QThread;
auto *scriptWorker = new PayByPhoneScript(account, phoneNumber, bankName, amount);
scriptWorker->moveToThread(thread);
// Запуск первого потока
QObject::connect(thread, &QThread::started, scriptWorker, &PayByPhoneScript::start);
QObject::connect(scriptWorker, &PayByPhoneScript::finished, thread, &QThread::quit);
QObject::connect(scriptWorker, &PayByPhoneScript::finished, scriptWorker, &QObject::deleteLater);
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
// Обработка результата первого потока
QObject::connect(scriptWorker, &PayByPhoneScript::finishedWithResult, &app,
[account, connectStopOnQuit](const QString &result) {
qDebug() << "Получен результат: [" << result << "]";
// Создаем второй поток после завершения первого
auto *thread2 = new QThread;
auto *scriptWorker2 = new GetLastDaysHistoryScript(account);
scriptWorker2->moveToThread(thread2);
// Запуск второго потока
QObject::connect(thread2, &QThread::started, scriptWorker2,
&GetLastDaysHistoryScript::start);
QObject::connect(scriptWorker2, &GetLastDaysHistoryScript::finished, thread2,
&QThread::quit);
QObject::connect(scriptWorker2, &GetLastDaysHistoryScript::finished, scriptWorker2,
&QObject::deleteLater);
QObject::connect(thread2, &QThread::finished, thread2, &QThread::deleteLater);
// Остановка второго потока при завершении приложения
connectStopOnQuit(scriptWorker2, thread2);
thread2->start();
});
// Остановка первого потока при завершении приложения
connectStopOnQuit(scriptWorker, thread);
// Запуск первого потока
thread->start();
}
paymentThread->start();
}
static QFile logFile;
@ -191,7 +129,7 @@ int main(int argc, char *argv[]) {
QApplication app(argc, argv);
// Выбрать стиль Fusion
app.setStyle(QStyleFactory::create("Fusion"));
QApplication::setStyle(QStyleFactory::create("Fusion"));
logFile.setFileName("application.log");
if (!logFile.open(QIODevice::Append | QIODevice::Text)) {
@ -199,29 +137,16 @@ int main(int argc, char *argv[]) {
}
// Устанавливаем свой обработчик
qInstallMessageHandler(messageHandler);
//
// auto *thread = new QThread;
// auto *worker = new EventHandler;
// worker->moveToThread(thread);
// QObject::connect(thread, &QThread::started, worker, &EventHandler::start);
// QObject::connect(worker, &EventHandler::finished, thread, &QThread::quit);
// QObject::connect(worker, &EventHandler::finished, worker, &QObject::deleteLater);
// QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
// thread->start();
//
// auto *paymentThread = new QThread;
// auto *networkService = new NetworkService;
//
// // FIXME доставить десктоп ид из настроеек
// networkService->setDeviceData("DT88bokcwQ", "");
// networkService->moveToThread(paymentThread);
// QObject::connect(paymentThread, &QThread::started, networkService, &NetworkService::start);
// QObject::connect(networkService, &NetworkService::finished, paymentThread, &QThread::quit);
// QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater);
// QObject::connect(paymentThread, &QThread::finished, paymentThread, &QThread::deleteLater);
// paymentThread->start();
//
// 7927296033 - Амир
// 79829976987 - Егор
// 79641815146 - Мама Альфа
// 79199196105 - Папа рсхб
// startNetworkService(app);
startDeviceScreener(app);
startEventHandler(app);
MainWindow window;
window.show();

View File

@ -80,6 +80,7 @@ CREATE TABLE IF NOT EXISTS events
amount REAL,
phone TEXT,
bank_name TEXT,
comment TEXT,
status TEXT,

View File

@ -24,7 +24,7 @@ inline AccountStatus accountStatusFromString(const QString &str) {
struct AccountInfo {
int id = -1;
QString deviceId; // code
QString appName;
QString appCode;
QString lastNumbers;
double amount = 0.0;
QDateTime updateTime;
@ -40,7 +40,7 @@ inline QString convertAccountToString(const AccountInfo &info) {
if (info.id != -1) lines << " id: " + QString::number(info.id); else emptyFields << "id";
if (!info.deviceId.isEmpty()) lines << " deviceId: " + info.deviceId; else emptyFields << "deviceId";
if (!info.appName.isEmpty()) lines << " appName: " + info.appName; else emptyFields << "appName";
if (!info.appCode.isEmpty()) lines << " appCode: " + info.appCode; else emptyFields << "appCode";
if (!info.lastNumbers.isEmpty()) lines << " lastNumbers: " + info.lastNumbers; else emptyFields << "lastNumbers";
if (info.amount != 0.0) lines << " amount: " + QString::number(info.amount, 'f', 2); else emptyFields << "amount";
if (info.updateTime.isValid()) lines << " updateTime: " + info.updateTime.toString(Qt::ISODate); else emptyFields << "updateTime";

View File

@ -7,6 +7,7 @@ enum class EventStatus {
Wait,
InProgress,
Complete,
Error,
Unknown
};
@ -14,6 +15,7 @@ enum class EventStatus {
inline QString eventStatusToString(const EventStatus type) {
switch (type) {
case EventStatus::Wait: return "wait";
case EventStatus::Error: return "error";
case EventStatus::Complete: return "complete";
case EventStatus::InProgress: return "iPprogress";
default: return "unknown";
@ -21,6 +23,7 @@ inline QString eventStatusToString(const EventStatus type) {
}
inline EventStatus eventStatusFromString(const QString &str) {
if (str == "error") return EventStatus::Error;
if (str == "wait") return EventStatus::Wait;
if (str == "complete") return EventStatus::Complete;
if (str == "iPprogress") return EventStatus::InProgress;
@ -36,6 +39,7 @@ struct EventInfo {
double amount = 0;
QString phone;
QString bankName;
QString comment;
EventStatus status = EventStatus::Unknown;

View File

@ -55,8 +55,6 @@ struct TransactionInfo {
QString phone;
QString name;
QString shortDescription;
QString updatedShortDescription;
QString description;
QString updatedDescription;
@ -83,8 +81,6 @@ inline QString convertTransactionToString(const TransactionInfo &tx) {
if (tx.status != TransactionStatus::Unknown) lines << " status: " + transactionStatusToString(tx.status); else emptyFields << "status";
if (tx.type != TransactionType::Unknown) lines << " type: " + transactionTypeToString(tx.type); else emptyFields << "type";
if (!tx.phone.isEmpty()) lines << " phone: " + tx.phone; else emptyFields << "phone";
if (!tx.shortDescription.isEmpty()) lines << " shortDescription: " + tx.shortDescription; else emptyFields << "shortDescription";
if (!tx.updatedShortDescription.isEmpty()) lines << " updatedShortDescription: " + tx.updatedShortDescription; else emptyFields << "updatedShortDescription";
if (!tx.description.isEmpty()) lines << " description: " + tx.description; else emptyFields << "description";
if (!tx.updatedDescription.isEmpty()) lines << " updatedDescription: " + tx.updatedDescription; else emptyFields << "updatedDescription";
if (!tx.bankName.isEmpty()) lines << " bankName: " + tx.bankName; else emptyFields << "bankName";

View File

@ -126,7 +126,7 @@ void DeviceScreener::start() {
}
if (!deviceInfos.empty()) {
postDevicesData(deviceInfos);
// postDevicesData(deviceInfos); FIXME
}
if (!DeviceDAO::markDevicesOfflineExcept(onlineIds)) {

View File

@ -49,7 +49,7 @@ void EventHandler::startThreadForKey(const QString &key) {
if (event.id == -1) {
return;
}
if (!EventDAO::updateEvent(event.id, EventStatus::InProgress)) {
if (!EventDAO::updateEvent(event.id, EventStatus::InProgress, "")) {
qCritical() << "Cant update event: " << event.id;
return;
}
@ -66,10 +66,32 @@ void EventHandler::startThreadForKey(const QString &key) {
connect(worker, &PayByPhoneScript::finished, worker, &QObject::deleteLater);
connect(thr, &QThread::finished, thr, &QObject::deleteLater);
const int eventId = event.id;
// При нормальном завершении — перезапустить новый поток
connect(worker, &PayByPhoneScript::finished, this,
[this, key]() {
// qDebug() << "Thread finished for key" << key << "- restarting.";
// connect(worker, &PayByPhoneScript::finished, this,
// [this, key, eventId]() {
// if (!EventDAO::updateEvent(eventId, EventStatus::Complete, "")) {
// qCritical() << "Cant update event: " << eventId;
// }
// if (const auto tm = m_timers.take(key)) {
// tm->stop();
// tm->deleteLater();
// }
// m_threads.remove(key);
// });
connect(worker, &PayByPhoneScript::finishedWithResult, this,
[this, key, eventId](const QString &result) {
if (!result.isEmpty()) {
if (!EventDAO::updateEvent(eventId, EventStatus::Error, result)) {
qCritical() << "Cant update event: " << eventId;
}
} else {
if (!EventDAO::updateEvent(eventId, EventStatus::Complete, "")) {
qCritical() << "Cant update event: " << eventId;
}
}
if (const auto tm = m_timers.take(key)) {
tm->stop();
tm->deleteLater();

View File

@ -22,6 +22,7 @@ NetworkService::NetworkService(QObject *parent) : QObject(parent) {
m_urlTransactionUpdate = settings.value("net/transactionUpdate").toString();
m_urlTransactionInsert = settings.value("net/transactionInsert").toString();
m_manager = new QNetworkAccessManager(this);
m_desktopId = "DT88bokcwQ";
}
NetworkService::~NetworkService() = default;

View File

@ -14,8 +14,7 @@ public:
void setPayload(const QByteArray &json) { m_json = json; }
void setDeviceData(const QString &desktopId, const QString &deviceId) {
m_desktopId = desktopId;
void setDeviceId(const QString &deviceId) {
m_deviceId = deviceId;
}

View File

@ -11,11 +11,13 @@
#include <QMessageBox>
#include <QLabel>
#include <QDialog>
#include <QThread>
#include "dao/ApplicationDAO.h"
#include "db/ApplicationInfo.h"
#include "common/PaddedItemDelegate.h"
#include "device/EditBankDialog.h"
#include "rshb/LoginAndCheckAccountsScript.h"
#include "widget/common/CollapsibleSection.h"
QTableWidgetItem *BankSettingsWindow::createTableWidget(const QString &text) {
@ -30,7 +32,7 @@ QString parseStatus(const ApplicationInfo &app) {
}
if (app.status == "active") {
if (!app.pinCodeChecked) {
return app.pinCode.isEmpty() ? "нет пин-код" : "ин-код не проверен";
return app.pinCode.isEmpty() ? "нет пин-код" : "пин-код не проверен";
}
return "работает";
} else if (app.status == "off") {
@ -40,6 +42,18 @@ QString parseStatus(const ApplicationInfo &app) {
}
}
void BankSettingsWindow::startCheckPinCode(const QString &appCode) {
const auto thread = new QThread(nullptr);
auto *worker = new LoginAndCheckAccountsScript(m_deviceId, appCode, nullptr);
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &LoginAndCheckAccountsScript::start);
connect(worker, &LoginAndCheckAccountsScript::finished, thread, &QThread::quit);
connect(worker, &LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
thread->start();
}
void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) {
QList<ApplicationInfo> applications = ApplicationDAO::getApplicationsByDeviceId(m_deviceId);
@ -121,7 +135,7 @@ void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) {
pinCodeBtn->setProperty("btnClass", "pincode");
tableWidget->setCellWidget(k, 5, pinCodeBtn);
connect(pinCodeBtn, &QPushButton::clicked, this, [this,tableWidget, app]() {
connect(pinCodeBtn, &QPushButton::clicked, this, [this, app]() {
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Проверка пин-код"));
msgBox.setText("Сейчас запустится скрипт проверки пин-код в приложении: " + app.name);
@ -133,6 +147,7 @@ void BankSettingsWindow::reloadContent(QTableWidget *tableWidget) {
msgBox.exec();
if (msgBox.clickedButton() == deleteBtn) {
startCheckPinCode(app.code);
}
});
} else {

View File

@ -14,5 +14,7 @@ private:
QTableWidgetItem *createTableWidget(const QString &text);
void startCheckPinCode(const QString &appCode);
void reloadContent(QTableWidget *tableWidget);
};

View File

@ -8,6 +8,7 @@
#include <QPointer>
#include <QPropertyAnimation>
#include <QGraphicsScene>
#include <QMessageBox>
#include "bank/BankSettingsWindow.h"
#include "dao/ApplicationDAO.h"
#include "db/ApplicationInfo.h"
@ -64,21 +65,52 @@ void DeviceWidget::buildBankRow(const ApplicationInfo &app, QWidget *rowWidget,
const QString color = getColorByStatus(app);
circle->setStyleSheet(
"margin: 0px;"
"background-color: " +color + "; "
"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;"
);
// 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;"
// );
connect(btn1, &QPushButton::clicked, this, [app]() {
QString newStatus = app.status == "active" ? "off" : "active";
QMessageBox msgBox(nullptr);
msgBox.setWindowTitle(tr("Подтверждение"));
msgBox.setText(
"Вы уверены, что хотите "
+ QString(newStatus == "off" ? "отключить от работы" : "включить")
+ " данный банк: "
+ app.name + "?");
QPushButton *deleteBtn = msgBox.addButton(tr(newStatus == "off" ? "Отключить" : "Включить"),
QMessageBox::AcceptRole);
QPushButton *closeBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole);
msgBox.setDefaultButton(closeBtn);
msgBox.setModal(true);
msgBox.exec();
if (msgBox.clickedButton() == deleteBtn) {
if (!ApplicationDAO::update(app.id, app.pinCode, app.comment, newStatus)) {
qDebug() << "Cant update bank data";
} else {
}
} else {
// просто закрыли — ничего не делаем
}
});
hbox->addWidget(btn1);
}
auto *btn2 = new QPushButton(parent);
QIcon btn2Icon(QCoreApplication::applicationDirPath() + "/images/cogwheel.png");
@ -108,11 +140,9 @@ void DeviceWidget::buildBankRow(const ApplicationInfo &app, QWidget *rowWidget,
setOpenBanksSettings(btn2);
// собираем
hbox->addWidget(icon);
hbox->addWidget(label,1);
hbox->addWidget(label, 1);
hbox->addWidget(circle);
// hbox->addWidget(btn1);
hbox->addWidget(btn2);
// hbox->addStretch();