mirror of
https://gitee.com/spark-store-project/spark-store
synced 2025-12-14 04:42:03 +08:00
Add 'spark-update-tool/' from commit 'e5217b3829f4732887140066e6c3d33a120dc9da'
git-subtree-dir: spark-update-tool git-subtree-mainline:286f34f3f8git-subtree-split:e5217b3829
This commit is contained in:
322
spark-update-tool/src/appdelegate.cpp
Normal file
322
spark-update-tool/src/appdelegate.cpp
Normal file
@@ -0,0 +1,322 @@
|
||||
#include "appdelegate.h"
|
||||
#include <QIcon>
|
||||
#include <QDebug>
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
#include <QPainter>
|
||||
#include <QMouseEvent>
|
||||
|
||||
AppDelegate::AppDelegate(QObject *parent)
|
||||
: QStyledItemDelegate(parent), m_downloadManager(new DownloadManager(this)), m_installProcess(nullptr) {
|
||||
connect(m_downloadManager, &DownloadManager::downloadFinished, this,
|
||||
[this](const QString &packageName, bool success) {
|
||||
if (m_downloads.contains(packageName)) {
|
||||
m_downloads[packageName].isDownloading = false;
|
||||
// 不要提前设置 isInstalled
|
||||
emit updateDisplay(packageName);
|
||||
qDebug() << (success ? "下载完成:" : "下载失败:") << packageName;
|
||||
if (success) {
|
||||
enqueueInstall(packageName); // 安装完成后再设置 isInstalled
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_downloadManager, &DownloadManager::downloadProgress, this,
|
||||
[this](const QString &packageName, int progress) {
|
||||
if (m_downloads.contains(packageName)) {
|
||||
m_downloads[packageName].progress = progress;
|
||||
qDebug()<<progress;
|
||||
emit updateDisplay(packageName); // 实时刷新进度条
|
||||
}
|
||||
});
|
||||
m_spinnerTimer.start();
|
||||
}
|
||||
|
||||
void AppDelegate::setModel(QAbstractItemModel *model) {
|
||||
m_model = model;
|
||||
}
|
||||
|
||||
void AppDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
|
||||
painter->save();
|
||||
|
||||
if (option.state & QStyle::State_Selected)
|
||||
painter->fillRect(option.rect, option.palette.highlight());
|
||||
else
|
||||
painter->fillRect(option.rect, QColor("#F3F4F6"));
|
||||
|
||||
QFont boldFont = option.font;
|
||||
boldFont.setBold(true);
|
||||
QFont normalFont = option.font;
|
||||
|
||||
QString name = index.data(Qt::DisplayRole).toString();
|
||||
QString currentVersion = index.data(Qt::UserRole + 2).toString();
|
||||
QString newVersion = index.data(Qt::UserRole + 3).toString();
|
||||
QString iconPath = index.data(Qt::UserRole + 4).toString();
|
||||
QString size = index.data(Qt::UserRole + 5).toString();
|
||||
QString description = index.data(Qt::UserRole + 6).toString();
|
||||
|
||||
QRect rect = option.rect;
|
||||
int margin = 10, spacing = 6, iconSize = 40;
|
||||
|
||||
QRect iconRect(rect.left() + margin, rect.top() + (rect.height() - iconSize) / 2, iconSize, iconSize);
|
||||
QIcon(iconPath).paint(painter, iconRect);
|
||||
|
||||
int textX = iconRect.right() + margin;
|
||||
int textWidth = rect.width() - textX - 100;
|
||||
|
||||
QRect nameRect(textX, rect.top() + margin, textWidth, 20);
|
||||
painter->setFont(boldFont);
|
||||
painter->setPen(QColor("#333333"));
|
||||
painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter, name);
|
||||
|
||||
QRect versionRect(textX, nameRect.bottom() + spacing, textWidth, 20);
|
||||
painter->setFont(normalFont);
|
||||
painter->setPen(QColor("#888888"));
|
||||
painter->drawText(versionRect, Qt::AlignLeft | Qt::AlignVCenter,
|
||||
QString("当前版本: %1 → 新版本: %2").arg(currentVersion, newVersion));
|
||||
|
||||
QRect descRect(textX, versionRect.bottom() + spacing, textWidth, 40);
|
||||
painter->setFont(normalFont);
|
||||
painter->setPen(QColor("#AAAAAA"));
|
||||
painter->drawText(descRect, Qt::TextWordWrap,
|
||||
QString("包大小:%1 MB").arg(QString::number(size.toDouble() / (1024 * 1024), 'f', 2)));
|
||||
|
||||
QString packageName = index.data(Qt::UserRole + 1).toString();
|
||||
bool isDownloading = m_downloads.contains(packageName) && m_downloads[packageName].isDownloading;
|
||||
int progress = m_downloads.value(packageName, DownloadInfo{0, false}).progress;
|
||||
bool isInstalled = m_downloads.value(packageName).isInstalled;
|
||||
bool isInstalling = m_downloads.value(packageName).isInstalling;
|
||||
|
||||
if (isDownloading) {
|
||||
QRect progressRect(rect.right() - 270, rect.top() + (rect.height() - 20) / 2, 150, 20);
|
||||
QStyleOptionProgressBar progressBarOption;
|
||||
progressBarOption.rect = progressRect;
|
||||
progressBarOption.minimum = 0;
|
||||
progressBarOption.maximum = 100;
|
||||
progressBarOption.progress = progress;
|
||||
progressBarOption.text = QString("%1%").arg(progress);
|
||||
progressBarOption.textVisible = true;
|
||||
QApplication::style()->drawControl(QStyle::CE_ProgressBar, &progressBarOption, painter);
|
||||
|
||||
// 修改后的取消按钮绘制代码
|
||||
QRect buttonRect(rect.right() - 80, rect.top() + (rect.height() - 30) / 2, 70, 30);
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(QColor("#ff4444")); // 红色背景
|
||||
painter->drawRoundedRect(buttonRect, 4, 4); // 圆角矩形
|
||||
|
||||
painter->setPen(Qt::white); // 白色文字
|
||||
painter->setFont(option.font);
|
||||
painter->drawText(buttonRect, Qt::AlignCenter, "取消");
|
||||
} else if (isInstalling) {
|
||||
// 安装中:显示转圈和文字
|
||||
QRect spinnerRect(option.rect.right() - 80, option.rect.top() + (option.rect.height() - 30) / 2, 30, 30);
|
||||
int angle = (m_spinnerTimer.elapsed() / 10) % 360;
|
||||
QPen pen(QColor("#2563EB"), 3);
|
||||
painter->setPen(pen);
|
||||
painter->setRenderHint(QPainter::Antialiasing, true);
|
||||
QRectF arcRect = spinnerRect.adjusted(3, 3, -3, -3);
|
||||
painter->drawArc(arcRect, angle * 16, 120 * 16); // 120度弧
|
||||
|
||||
QRect textRect(option.rect.right() - 120, option.rect.top() + (option.rect.height() - 30) / 2, 110, 30);
|
||||
painter->setPen(QColor("#2563EB"));
|
||||
painter->setFont(option.font);
|
||||
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, "正在安装中");
|
||||
} else {
|
||||
QRect buttonRect(option.rect.right() - 80, option.rect.top() + (option.rect.height() - 30) / 2, 70, 30);
|
||||
painter->setPen(Qt::NoPen);
|
||||
if (isInstalled) {
|
||||
painter->setBrush(QColor("#10B981"));
|
||||
painter->drawRoundedRect(buttonRect, 4, 4);
|
||||
painter->setPen(Qt::white);
|
||||
painter->drawText(buttonRect, Qt::AlignCenter, "已安装");
|
||||
} else if (m_downloads.contains(packageName) && !m_downloads[packageName].isDownloading) {
|
||||
// 下载完成,按钮绿色,样式不变
|
||||
painter->setBrush(QColor("#10B981"));
|
||||
painter->drawRoundedRect(buttonRect, 4, 4);
|
||||
painter->setPen(Qt::white);
|
||||
painter->drawText(buttonRect, Qt::AlignCenter, "下载完成");
|
||||
// 不需要特殊处理样式,交互在 editorEvent 控制
|
||||
} else {
|
||||
painter->setBrush(QColor("#e9effd"));
|
||||
painter->drawRoundedRect(buttonRect, 4, 4);
|
||||
painter->setPen(QColor("#2563EB"));
|
||||
painter->drawText(buttonRect, Qt::AlignCenter, "更新");
|
||||
}
|
||||
}
|
||||
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
QSize AppDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const {
|
||||
return QSize(option.rect.width(), 110);
|
||||
}
|
||||
|
||||
bool AppDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
|
||||
const QStyleOptionViewItem &option, const QModelIndex &index) {
|
||||
if (event->type() == QEvent::MouseButtonRelease) {
|
||||
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
|
||||
QRect rect = option.rect;
|
||||
QString packageName = index.data(Qt::UserRole + 1).toString();
|
||||
|
||||
if (m_downloads.contains(packageName) && m_downloads[packageName].isDownloading) {
|
||||
QRect cancelButtonRect(rect.right() - 70, rect.top() + (rect.height() - 20) / 2, 60, 20);
|
||||
if (cancelButtonRect.contains(mouseEvent->pos())) {
|
||||
m_downloadManager->cancelDownload(packageName);
|
||||
m_downloads.remove(packageName);
|
||||
emit updateDisplay(packageName);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
QRect buttonRect(rect.right() - 80, rect.top() + (rect.height() - 30) / 2, 70, 30);
|
||||
if (buttonRect.contains(mouseEvent->pos())) {
|
||||
// 判断是否为“下载完成”状态,如果是则不响应点击
|
||||
if (m_downloads.contains(packageName) && !m_downloads[packageName].isDownloading) {
|
||||
// “下载完成”状态,按钮失效,点击无效
|
||||
return false;
|
||||
}
|
||||
QString downloadUrl = index.data(Qt::UserRole + 7).toString();
|
||||
QString outputPath = QString("%1/%2.metalink").arg(QDir::tempPath(), packageName);
|
||||
|
||||
m_downloads[packageName] = {0, true};
|
||||
m_downloadManager->startDownload(packageName, downloadUrl, outputPath);
|
||||
emit updateDisplay(packageName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return QStyledItemDelegate::editorEvent(event, model, option, index);
|
||||
}
|
||||
|
||||
void AppDelegate::startDownloadForAll() {
|
||||
if (!m_model) return;
|
||||
for (int row = 0; row < m_model->rowCount(); ++row) {
|
||||
QModelIndex index = m_model->index(row, 0);
|
||||
QString packageName = index.data(Qt::UserRole + 1).toString();
|
||||
if (m_downloads.contains(packageName) && (m_downloads[packageName].isDownloading || m_downloads[packageName].isInstalled))
|
||||
continue; // 跳过正在下载或已安装的
|
||||
QString downloadUrl = index.data(Qt::UserRole + 7).toString();
|
||||
QString outputPath = QString("%1/%2.metalink").arg(QDir::tempPath(), packageName);
|
||||
m_downloads[packageName] = {0, true, false};
|
||||
m_downloadManager->startDownload(packageName, downloadUrl, outputPath);
|
||||
emit updateDisplay(packageName);
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:安装队列相关实现
|
||||
void AppDelegate::enqueueInstall(const QString &packageName) {
|
||||
m_installQueue.enqueue(packageName);
|
||||
if (!m_isInstalling) {
|
||||
startNextInstall();
|
||||
}
|
||||
}
|
||||
|
||||
void AppDelegate::startNextInstall() {
|
||||
if (m_installQueue.isEmpty()) {
|
||||
m_isInstalling = false;
|
||||
m_installingPackage.clear();
|
||||
return;
|
||||
}
|
||||
m_isInstalling = true;
|
||||
QString packageName = m_installQueue.dequeue();
|
||||
m_installingPackage = packageName;
|
||||
m_downloads[packageName].isInstalling = true;
|
||||
emit updateDisplay(packageName);
|
||||
|
||||
// 查找 /tmp 下以包名开头的 .deb 文件
|
||||
QDir tempDir(QDir::tempPath());
|
||||
QStringList debs = tempDir.entryList(QStringList() << QString("%1_*.deb").arg(packageName), QDir::Files);
|
||||
QString debPath;
|
||||
if (!debs.isEmpty()) {
|
||||
debPath = tempDir.absoluteFilePath(debs.first());
|
||||
} else {
|
||||
debs = tempDir.entryList(QStringList() << QString("%1*.deb").arg(packageName), QDir::Files);
|
||||
if (!debs.isEmpty()) {
|
||||
debPath = tempDir.absoluteFilePath(debs.first());
|
||||
}
|
||||
}
|
||||
|
||||
if (debPath.isEmpty()) {
|
||||
qWarning() << "未找到deb文件,包名:" << packageName;
|
||||
m_downloads[packageName].isInstalling = false;
|
||||
emit updateDisplay(packageName);
|
||||
m_installingPackage.clear();
|
||||
startNextInstall();
|
||||
return;
|
||||
}
|
||||
|
||||
m_installProcess = new QProcess(this);
|
||||
|
||||
// 新增:准备安装日志文件
|
||||
QString logPath = QString("/tmp/%1_install.log").arg(packageName);
|
||||
QFile *logFile = new QFile(logPath, m_installProcess);
|
||||
if (logFile->open(QIODevice::Append | QIODevice::Text)) {
|
||||
// 设置权限为777
|
||||
QFile::setPermissions(logPath, QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner |
|
||||
QFile::ReadGroup | QFile::WriteGroup | QFile::ExeGroup |
|
||||
QFile::ReadOther | QFile::WriteOther | QFile::ExeOther);
|
||||
connect(m_installProcess, &QProcess::readyReadStandardOutput, this, [this, packageName, logFile]() {
|
||||
QByteArray out = m_installProcess->readAllStandardOutput();
|
||||
logFile->write(out);
|
||||
logFile->flush();
|
||||
QString text = QString::fromLocal8Bit(out);
|
||||
qDebug().noquote() << text;
|
||||
// 检查“软件包已安装”关键字
|
||||
if (text.contains(QStringLiteral("软件包已安装"))) {
|
||||
m_downloads[packageName].isInstalling = false;
|
||||
m_downloads[packageName].isInstalled = true;
|
||||
emit updateDisplay(packageName);
|
||||
}
|
||||
});
|
||||
connect(m_installProcess, &QProcess::readyReadStandardError, this, [this, logFile]() {
|
||||
QByteArray err = m_installProcess->readAllStandardError();
|
||||
logFile->write(err);
|
||||
logFile->flush();
|
||||
qDebug().noquote() << QString::fromLocal8Bit(err);
|
||||
});
|
||||
connect(m_installProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||
this, [this, packageName, logFile](int exitCode, QProcess::ExitStatus status) {
|
||||
if (logFile) logFile->close();
|
||||
m_downloads[packageName].isInstalling = false;
|
||||
if (exitCode == 0) {
|
||||
m_downloads[packageName].isInstalled = true; // 安装成功
|
||||
}
|
||||
emit updateDisplay(packageName);
|
||||
m_installProcess->deleteLater();
|
||||
m_installProcess = nullptr;
|
||||
m_installingPackage.clear();
|
||||
startNextInstall();
|
||||
});
|
||||
} else {
|
||||
// 日志文件无法打开时,仍然要连接原有信号
|
||||
connect(m_installProcess, &QProcess::readyReadStandardOutput, this, [this, packageName]() {
|
||||
QByteArray out = m_installProcess->readAllStandardOutput();
|
||||
QString text = QString::fromLocal8Bit(out);
|
||||
qDebug().noquote() << text;
|
||||
if (text.contains(QStringLiteral("软件包已安装"))) {
|
||||
m_downloads[packageName].isInstalling = false;
|
||||
m_downloads[packageName].isInstalled = true;
|
||||
emit updateDisplay(packageName);
|
||||
}
|
||||
});
|
||||
connect(m_installProcess, &QProcess::readyReadStandardError, this, [this]() {
|
||||
QByteArray err = m_installProcess->readAllStandardError();
|
||||
qDebug().noquote() << QString::fromLocal8Bit(err);
|
||||
});
|
||||
connect(m_installProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||
this, [this, packageName](int /*exitCode*/, QProcess::ExitStatus /*status*/) {
|
||||
emit updateDisplay(packageName);
|
||||
m_installProcess->deleteLater();
|
||||
m_installProcess = nullptr;
|
||||
m_installingPackage.clear();
|
||||
startNextInstall();
|
||||
});
|
||||
}
|
||||
|
||||
// 注意参数顺序:deb路径在前,--no-create-desktop-entry在后
|
||||
QStringList args;
|
||||
args << debPath << "--no-create-desktop-entry";
|
||||
m_installProcess->start("ssinstall", args);
|
||||
}
|
||||
46
spark-update-tool/src/appdelegate.h
Normal file
46
spark-update-tool/src/appdelegate.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QHash>
|
||||
#include <QQueue>
|
||||
#include <QProcess>
|
||||
#include <QElapsedTimer>
|
||||
#include "downloadmanager.h"
|
||||
|
||||
struct DownloadInfo {
|
||||
int progress = 0;
|
||||
bool isDownloading = false;
|
||||
bool isInstalled = false;
|
||||
bool isInstalling = false; // 新增:标记是否正在安装
|
||||
};
|
||||
|
||||
class AppDelegate : public QStyledItemDelegate {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AppDelegate(QObject *parent = nullptr);
|
||||
|
||||
void setModel(QAbstractItemModel *model);
|
||||
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
bool editorEvent(QEvent *event, QAbstractItemModel *model,
|
||||
const QStyleOptionViewItem &option, const QModelIndex &index) override;
|
||||
void startDownloadForAll();
|
||||
|
||||
signals:
|
||||
void updateDisplay(const QString &packageName);
|
||||
|
||||
private:
|
||||
DownloadManager *m_downloadManager;
|
||||
QHash<QString, DownloadInfo> m_downloads;
|
||||
QAbstractItemModel *m_model = nullptr;
|
||||
|
||||
QQueue<QString> m_installQueue;
|
||||
bool m_isInstalling = false;
|
||||
QProcess *m_installProcess = nullptr;
|
||||
QString m_installingPackage; // 当前正在安装的包名
|
||||
QElapsedTimer m_spinnerTimer; // 用于转圈动画
|
||||
|
||||
void enqueueInstall(const QString &packageName);
|
||||
void startNextInstall();
|
||||
};
|
||||
62
spark-update-tool/src/applistmodel.cpp
Normal file
62
spark-update-tool/src/applistmodel.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#include "applistmodel.h"
|
||||
|
||||
AppListModel::AppListModel(QObject *parent) : QAbstractListModel(parent) {}
|
||||
|
||||
int AppListModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
return m_data.size();
|
||||
}
|
||||
|
||||
QVariant AppListModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() >= m_data.size())
|
||||
return QVariant();
|
||||
|
||||
const QVariantMap &map = m_data.at(index.row()); // 直接访问 QVariantMap
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:
|
||||
return map.value("name");
|
||||
case Qt::UserRole + 1: // 包名
|
||||
return map.value("package");
|
||||
case Qt::UserRole + 2: // 当前版本
|
||||
return map.value("current_version");
|
||||
case Qt::UserRole + 3: // 新版本
|
||||
return map.value("new_version");
|
||||
case Qt::UserRole + 4: // 图标路径
|
||||
return map.value("icon");
|
||||
case Qt::UserRole + 5: // 文件大小
|
||||
return map.value("size");
|
||||
case Qt::UserRole + 6: // 描述
|
||||
return map.value("description");
|
||||
case Qt::UserRole + 7: // 下载 URL
|
||||
return map.value("download_url"); // 返回下载 URL
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
void AppListModel::setUpdateData(const QJsonArray &updateInfo)
|
||||
{
|
||||
beginResetModel();
|
||||
m_data.clear(); // 清空 QList<QVariantMap>
|
||||
|
||||
for (const auto &item : updateInfo) {
|
||||
QJsonObject obj = item.toObject();
|
||||
QVariantMap map;
|
||||
map["package"] = obj["package"].toString();
|
||||
map["name"] = obj["name"].toString();
|
||||
map["current_version"] = obj["current_version"].toString();
|
||||
map["new_version"] = obj["new_version"].toString();
|
||||
map["icon"] = obj["icon"].toString();
|
||||
map["size"] = obj["size"].toString();
|
||||
map["download_url"] = obj["download_url"].toString(); // 确保设置下载 URL
|
||||
m_data.append(map); // 添加到 QList<QVariantMap>
|
||||
|
||||
qDebug() << "设置到模型的包名:" << map["package"].toString();
|
||||
qDebug() << "设置到模型的下载 URL:" << map["download_url"].toString(); // 检查设置的数据
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
26
spark-update-tool/src/applistmodel.h
Normal file
26
spark-update-tool/src/applistmodel.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#ifndef APPLISTMODEL_H
|
||||
#define APPLISTMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
// 添加 QJsonObject 头文件
|
||||
#include <QJsonObject>
|
||||
|
||||
class AppListModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AppListModel(QObject *parent = nullptr);
|
||||
|
||||
// 重写方法
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
// 设置更新数据
|
||||
void setUpdateData(const QJsonArray &data);
|
||||
|
||||
private:
|
||||
QList<QVariantMap> m_data; // 修改类型为 QList<QVariantMap>
|
||||
};
|
||||
|
||||
#endif // APPLISTMODEL_H
|
||||
398
spark-update-tool/src/aptssupdater.cpp
Normal file
398
spark-update-tool/src/aptssupdater.cpp
Normal file
@@ -0,0 +1,398 @@
|
||||
#include "aptssupdater.h"
|
||||
#include <QProcess>
|
||||
#include <QTextStream>
|
||||
#include <QRegularExpression>
|
||||
#include <QFile>
|
||||
#include <qdebug.h>
|
||||
|
||||
aptssUpdater::aptssUpdater(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
packageName = getUpdateablePackages();
|
||||
}
|
||||
|
||||
QStringList aptssUpdater::getUpdateablePackages()
|
||||
{
|
||||
QStringList packageDetails;
|
||||
QProcess process;
|
||||
QString command = R"(env LANGUAGE=en_US /usr/bin/apt -c /opt/durapps/spark-store/bin/apt-fast-conf/aptss-apt.conf list --upgradable -o Dir::Etc::sourcelist="/opt/durapps/spark-store/bin/apt-fast-conf/sources.list.d/sparkstore.list" -o Dir::Etc::sourceparts="/dev/null" -o APT::Get::List-Cleanup="0" | awk 'NR>1')";
|
||||
|
||||
process.start("bash", QStringList() << "-c" << command);
|
||||
if (!process.waitForFinished()) {
|
||||
qWarning() << "Process failed to finish.";
|
||||
return packageDetails;
|
||||
}
|
||||
|
||||
QString output = process.readAllStandardOutput();
|
||||
QStringList lines = output.split('\n', Qt::SkipEmptyParts);
|
||||
|
||||
// 创建临时文件
|
||||
QTemporaryFile tempFile;
|
||||
tempFile.setAutoRemove(false);
|
||||
if (tempFile.open()) {
|
||||
QTextStream stream(&tempFile);
|
||||
|
||||
for (const QString &line : lines) {
|
||||
QRegularExpression regex(R"(([\w\-\+\.]+)/\S+\s+([^\s]+)\s+\S+\s+\[upgradable from: ([^\]]+)\])");
|
||||
QRegularExpressionMatch match = regex.match(line);
|
||||
if (match.hasMatch()) {
|
||||
QString name = match.captured(1);
|
||||
QString newVersion = match.captured(2);
|
||||
QString oldVersion = match.captured(3);
|
||||
|
||||
// 检查版本是否相同,相同则跳过
|
||||
if (newVersion == oldVersion) {
|
||||
qDebug() << "跳过版本相同的包:" << name << "(" << oldVersion << "→" << newVersion << ")";
|
||||
continue;
|
||||
}
|
||||
|
||||
// 写入内存列表
|
||||
packageDetails << QString("%1: %2 → %3").arg(name, oldVersion, newVersion);
|
||||
|
||||
// 写入临时文件(原始数据)
|
||||
stream << name << "|" << oldVersion << "|" << newVersion << "\n";
|
||||
}
|
||||
}
|
||||
tempFile.close();
|
||||
m_tempFilePath = tempFile.fileName();
|
||||
qDebug()<< "临时文件路径:" << m_tempFilePath;
|
||||
|
||||
} else {
|
||||
qWarning() << "无法创建临时文件";
|
||||
}
|
||||
|
||||
return packageDetails;
|
||||
}
|
||||
|
||||
|
||||
QStringList aptssUpdater::getPackageSizes()
|
||||
{
|
||||
QStringList packageDetails;
|
||||
QProcess process;
|
||||
|
||||
// 获取可更新包名列表
|
||||
QStringList updateablePackages;
|
||||
for (const QString &pkgInfo : packageName) {
|
||||
updateablePackages << pkgInfo.section(":", 0, 0).trimmed();
|
||||
}
|
||||
|
||||
foreach (const QString &packageName, updateablePackages) {
|
||||
// 构建新命令(包含包名参数)
|
||||
QString command = QString("apt download %1 --print-uris -c /opt/durapps/spark-store/bin/apt-fast-conf/aptss-apt.conf "
|
||||
"-o Dir::Etc::sourcelist=\"/opt/durapps/spark-store/bin/apt-fast-conf/sources.list.d/sparkstore.list\" "
|
||||
"-o Dir::Etc::sourceparts=\"/dev/null\"").arg(packageName);
|
||||
|
||||
process.start("bash", QStringList() << "-c" << command);
|
||||
if (!process.waitForFinished()) {
|
||||
qWarning() << "获取包信息失败:" << packageName;
|
||||
continue;
|
||||
}
|
||||
|
||||
QString output = process.readAllStandardOutput();
|
||||
// 使用正则匹配所有信息
|
||||
// 调整正则表达式匹配分组
|
||||
QRegularExpression regex(R"('([^']+)'\s+(\S+)\s+(\d+)\s+SHA512:([^\s]+))"); // 分组1:URL 分组2:文件名 分组3:大小 分组4:SHA512
|
||||
QRegularExpressionMatch match = regex.match(output);
|
||||
|
||||
if (match.hasMatch()) {
|
||||
QString url = match.captured(1);
|
||||
QString fileName = match.captured(2);
|
||||
QString size = match.captured(3);
|
||||
QString sha512 = match.captured(4);
|
||||
|
||||
// 调整字段顺序:包名 | 大小 | URL | SHA512
|
||||
packageDetails << QString("%1: %2|%3|%4").arg(packageName, size, url, sha512);
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "完整包信息:" << packageDetails;
|
||||
return packageDetails;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
QStringList aptssUpdater::getDesktopAppNames()
|
||||
{
|
||||
QStringList appNames;
|
||||
QProcess dpkgProcess;
|
||||
|
||||
// 获取当前系统语言环境
|
||||
QString lang = QLocale().name().replace("_", "-");
|
||||
|
||||
// 遍历所有可更新包(复用已有的临时文件数据)
|
||||
QStringList packages = packageName;
|
||||
|
||||
foreach (const QString &package, packages) {
|
||||
QString packageName = package.split(":")[0];
|
||||
QString finalName = packageName; // 默认使用包名
|
||||
|
||||
// 获取包文件列表
|
||||
dpkgProcess.start("dpkg", QStringList() << "-L" << packageName);
|
||||
dpkgProcess.waitForFinished();
|
||||
QStringList files = QString(dpkgProcess.readAllStandardOutput()).split('\n', Qt::SkipEmptyParts);
|
||||
|
||||
// 先检查常规应用目录
|
||||
QStringList regularDesktopFiles = files.filter("/usr/share/applications/");
|
||||
QString regularAppName;
|
||||
if (!regularDesktopFiles.isEmpty()) {
|
||||
checkDesktopFiles(regularDesktopFiles, regularAppName, lang, packageName);
|
||||
}
|
||||
|
||||
// 如果常规目录没有找到,再检查特殊目录
|
||||
if (regularAppName.isEmpty()) {
|
||||
QStringList specialDesktopFiles = files.filter(QRegularExpression(QString("/opt/apps/%1/entries/applications").arg(packageName)));
|
||||
QString specialAppName;
|
||||
if (!specialDesktopFiles.isEmpty()) {
|
||||
checkDesktopFiles(specialDesktopFiles, specialAppName, lang, packageName);
|
||||
if (!specialAppName.isEmpty()) {
|
||||
finalName = specialAppName;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
finalName = regularAppName;
|
||||
}
|
||||
|
||||
// 输出格式为[软件名|包名]
|
||||
appNames << QString("[%1|%2]").arg(finalName, packageName);
|
||||
}
|
||||
qDebug()<< "应用名称列表:" << appNames;
|
||||
return appNames;
|
||||
}
|
||||
|
||||
|
||||
bool aptssUpdater::checkDesktopFiles(const QStringList &desktopFiles, QString &appName, const QString &lang, const QString &packageName)
|
||||
{
|
||||
QString lastValidName;
|
||||
QRegularExpression noDisplayRe("^NoDisplay=(true|True)");
|
||||
QRegularExpression nameRe("^Name\\[?" + lang + "?\\]?=(.*)");
|
||||
QRegularExpression nameOrigRe("^Name=(.*)");
|
||||
|
||||
foreach (const QString &filePath, desktopFiles) {
|
||||
if (!filePath.endsWith(".desktop")) continue;
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) continue;
|
||||
|
||||
bool skip = false;
|
||||
QString currentName;
|
||||
|
||||
QTextStream in(&file);
|
||||
while (!in.atEnd()) {
|
||||
QString line = in.readLine().trimmed();
|
||||
|
||||
// 检查NoDisplay属性
|
||||
if (line.startsWith("NoDisplay=")) {
|
||||
if (noDisplayRe.match(line).hasMatch()) {
|
||||
skip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 优先匹配本地化名称
|
||||
if (currentName.isEmpty()) {
|
||||
QRegularExpressionMatch match = nameRe.match(line);
|
||||
if (match.hasMatch()) {
|
||||
currentName = match.captured(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 匹配原始名称
|
||||
match = nameOrigRe.match(line);
|
||||
if (match.hasMatch()) {
|
||||
currentName = match.captured(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!skip && !currentName.isEmpty()) {
|
||||
lastValidName = currentName;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理最终的有效名称
|
||||
if (!lastValidName.isEmpty()) {
|
||||
appName = lastValidName; // 直接赋值而不是使用<<
|
||||
return true;
|
||||
}
|
||||
|
||||
// 回退到包名
|
||||
appName = packageName;
|
||||
return false;
|
||||
}
|
||||
|
||||
QStringList aptssUpdater::getPackageIcons()
|
||||
{
|
||||
QStringList packageIcons;
|
||||
QProcess dpkgProcess;
|
||||
|
||||
// 遍历所有可更新包
|
||||
QStringList packages = packageName;
|
||||
|
||||
foreach (const QString &package, packages) {
|
||||
QString packageName = package.split(":")[0];
|
||||
QString iconPath = ":/resources/default_icon.svg"; // 默认图标
|
||||
|
||||
// 获取包文件列表
|
||||
dpkgProcess.start("dpkg", QStringList() << "-L" << packageName);
|
||||
dpkgProcess.waitForFinished();
|
||||
QStringList files = QString(dpkgProcess.readAllStandardOutput()).split('\n', Qt::SkipEmptyParts);
|
||||
|
||||
// 查找.desktop文件
|
||||
QStringList desktopFiles = files.filter(QRegularExpression("/(usr/share|opt/apps)/.*\\.desktop$"));
|
||||
|
||||
// 从.desktop文件中提取图标
|
||||
foreach (const QString &desktopFile, desktopFiles) {
|
||||
QFile file(desktopFile);
|
||||
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
QTextStream in(&file);
|
||||
while (!in.atEnd()) {
|
||||
QString line = in.readLine().trimmed();
|
||||
if (line.startsWith("Icon=")) {
|
||||
QString iconName = line.mid(5).trimmed();
|
||||
|
||||
// 处理相对图标名(如Icon=vscode)
|
||||
if (!iconName.contains('/')) {
|
||||
// 查找标准图标路径
|
||||
QStringList iconPaths = {
|
||||
QString("/usr/share/pixmaps/%1.png").arg(iconName),
|
||||
QString("/usr/share/icons/hicolor/48x48/apps/%1.png").arg(iconName),
|
||||
QString("/usr/share/icons/hicolor/scalable/apps/%1.svg").arg(iconName),
|
||||
QString("/opt/apps/%1/entries/icons/hicolor/48x48/apps/%2.png").arg(packageName, iconName)
|
||||
};
|
||||
|
||||
foreach (const QString &path, iconPaths) {
|
||||
if (QFile::exists(path)) {
|
||||
iconPath = path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 已经是绝对路径
|
||||
if (QFile::exists(iconName)) {
|
||||
iconPath = iconName;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 如果.desktop中没有找到图标,尝试直接查找包中的图标文件
|
||||
if (iconPath == ":/resources/default_icon.svg") {
|
||||
QStringList iconFiles = files.filter(QRegularExpression("/(usr/share/pixmaps|usr/share/icons|opt/apps/.*/entries/icons)/.*\\.(png|svg)$"));
|
||||
if (!iconFiles.isEmpty()) {
|
||||
iconPath = iconFiles.first();
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "包名:" << packageName << "图标路径:" << iconPath;
|
||||
packageIcons << QString("%1: %2").arg(packageName, iconPath);
|
||||
}
|
||||
|
||||
return packageIcons;
|
||||
}
|
||||
|
||||
|
||||
QJsonArray aptssUpdater::getUpdateInfoAsJson()
|
||||
{
|
||||
QJsonArray jsonArray;
|
||||
|
||||
// 获取所有需要的信息
|
||||
QStringList sizes = getPackageSizes();
|
||||
QStringList names = getDesktopAppNames();
|
||||
QStringList icons = getPackageIcons();
|
||||
|
||||
// 创建包名到各种信息的映射
|
||||
QHash<QString, QHash<QString, QString>> packageInfo;
|
||||
|
||||
// 解析包版本信息
|
||||
for (const QString &pkg : packageName) {
|
||||
QStringList parts = pkg.split(": ");
|
||||
if (parts.size() >= 2) {
|
||||
QString packageName = parts[0];
|
||||
QStringList versions = parts[1].split(" → ");
|
||||
if (versions.size() == 2) {
|
||||
packageInfo[packageName]["current_version"] = versions[0];
|
||||
packageInfo[packageName]["new_version"] = versions[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析包详细信息(新增部分)
|
||||
for (const QString &sizeInfo : sizes) {
|
||||
QStringList parts = sizeInfo.split(": ");
|
||||
if (parts.size() == 2) {
|
||||
QString packageName = parts[0];
|
||||
QStringList details = parts[1].split("|");
|
||||
if (details.size() == 3) { // 现在包含大小|URL|SHA512
|
||||
packageInfo[packageName]["size"] = details[0];
|
||||
packageInfo[packageName]["url"] = details[1];
|
||||
packageInfo[packageName]["sha512"] = details[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析应用名称信息
|
||||
for (const QString &nameInfo : names) {
|
||||
if (nameInfo.startsWith("[") && nameInfo.endsWith("]")) {
|
||||
QString content = nameInfo.mid(1, nameInfo.length() - 2);
|
||||
QStringList parts = content.split("|");
|
||||
if (parts.size() == 2) {
|
||||
QString displayName = parts[0];
|
||||
QString packageName = parts[1];
|
||||
packageInfo[packageName]["display_name"] = displayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析图标信息
|
||||
for (const QString &iconInfo : icons) {
|
||||
QStringList parts = iconInfo.split(": ");
|
||||
if (parts.size() == 2) {
|
||||
QString packageName = parts[0];
|
||||
packageInfo[packageName]["icon"] = parts[1].trimmed();
|
||||
}
|
||||
}
|
||||
|
||||
// 构建JSON数组
|
||||
for (const QString &packageName : packageInfo.keys()) {
|
||||
QJsonObject jsonObj;
|
||||
jsonObj["package"] = packageName;
|
||||
|
||||
// 使用显示名称(如果有),否则使用包名
|
||||
if (packageInfo[packageName].contains("display_name")) {
|
||||
jsonObj["name"] = packageInfo[packageName]["display_name"];
|
||||
} else {
|
||||
jsonObj["name"] = packageName;
|
||||
}
|
||||
|
||||
jsonObj["current_version"] = packageInfo[packageName]["current_version"];
|
||||
jsonObj["new_version"] = packageInfo[packageName]["new_version"];
|
||||
jsonObj["icon"] = packageInfo[packageName]["icon"];
|
||||
jsonObj["ignored"] = false; // 默认不忽略
|
||||
|
||||
// 如果有大小信息也加入
|
||||
if (packageInfo[packageName].contains("size")) {
|
||||
jsonObj["size"] = packageInfo[packageName]["size"];
|
||||
}
|
||||
|
||||
// 在构建JSON对象时添加新字段(在jsonObj中添加):
|
||||
if (packageInfo[packageName].contains("url")) {
|
||||
jsonObj["download_url"] = packageInfo[packageName]["url"];
|
||||
qDebug() << "生成的下载 URL:" << packageInfo[packageName]["url"]; // 检查生成的 URL
|
||||
} else {
|
||||
qWarning() << "未找到下载 URL,包名:" << packageName;
|
||||
jsonObj["download_url"] = ""; // 设置为空字符串以避免崩溃
|
||||
}
|
||||
jsonObj["sha512"] = packageInfo[packageName]["sha512"];
|
||||
jsonArray.append(jsonObj);
|
||||
}
|
||||
qDebug()<<jsonArray;
|
||||
return jsonArray;
|
||||
}
|
||||
|
||||
|
||||
28
spark-update-tool/src/aptssupdater.h
Normal file
28
spark-update-tool/src/aptssupdater.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#ifndef APTSSUPDATER_H
|
||||
#define APTSSUPDATER_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QStringList>
|
||||
#include <QTemporaryFile>
|
||||
#include <QLocale>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
class aptssUpdater : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit aptssUpdater(QWidget *parent = nullptr);
|
||||
|
||||
QStringList getUpdateablePackages(); // 查询可更新包列表及更新内容
|
||||
QStringList getPackageSizes(); // 获取每个包的大小
|
||||
QStringList getDesktopAppNames(); // 获取桌面应用名称列表
|
||||
QStringList getPackageIcons(); // 获取包图标列表
|
||||
QJsonArray getUpdateInfoAsJson(); // 获取更新信息的 JSON 格式
|
||||
QString m_tempFilePath;
|
||||
signals:
|
||||
private:
|
||||
bool checkDesktopFiles(const QStringList &desktopFiles, QString &appName, const QString &lang, const QString &packageName);
|
||||
QStringList packageName;
|
||||
};
|
||||
|
||||
#endif // APTSSUPDATER_H
|
||||
117
spark-update-tool/src/downloadmanager.cpp
Normal file
117
spark-update-tool/src/downloadmanager.cpp
Normal file
@@ -0,0 +1,117 @@
|
||||
#include "downloadmanager.h"
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QRegularExpression>
|
||||
#include <QDebug>
|
||||
|
||||
DownloadManager::DownloadManager(QObject *parent) : QObject(parent)
|
||||
{
|
||||
cleanupTempFiles();
|
||||
}
|
||||
|
||||
void DownloadManager::startDownload(const QString &packageName, const QString &url, const QString &outputPath)
|
||||
{
|
||||
if (m_processes.contains(packageName)) {
|
||||
qWarning() << packageName << " is already downloading.";
|
||||
return;
|
||||
}
|
||||
|
||||
QString metalinkUrl = url + ".metalink";
|
||||
QFileInfo fileInfo(outputPath);
|
||||
|
||||
QStringList arguments = {
|
||||
"--enable-rpc=false",
|
||||
"--console-log-level=warn",
|
||||
"--summary-interval=1",
|
||||
"--allow-overwrite=true",
|
||||
"--dir=" + fileInfo.absolutePath(),
|
||||
"--out=" + fileInfo.fileName(),
|
||||
metalinkUrl
|
||||
};
|
||||
|
||||
QProcess *process = new QProcess(this);
|
||||
m_processes.insert(packageName, process);
|
||||
|
||||
// 新增:准备日志文件
|
||||
QString logPath = QString("/tmp/%1_download.log").arg(packageName);
|
||||
QFile *logFile = new QFile(logPath, process);
|
||||
if (logFile->open(QIODevice::Append | QIODevice::Text)) {
|
||||
// 设置权限为777
|
||||
QFile::setPermissions(logPath, QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner |
|
||||
QFile::ReadGroup | QFile::WriteGroup | QFile::ExeGroup |
|
||||
QFile::ReadOther | QFile::WriteOther | QFile::ExeOther);
|
||||
connect(process, &QProcess::readyReadStandardOutput, this, [this, packageName, process, logFile]() {
|
||||
while (process->canReadLine()) {
|
||||
QString line = QString::fromUtf8(process->readLine()).trimmed();
|
||||
// 写入日志
|
||||
logFile->write(line.toUtf8() + '\n');
|
||||
logFile->flush();
|
||||
QRegularExpression regex(R"(\((\d+)%\))");
|
||||
QRegularExpressionMatch match = regex.match(line);
|
||||
if (match.hasMatch()) {
|
||||
int progress = match.captured(1).toInt();
|
||||
emit downloadProgress(packageName, progress);
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(process, &QProcess::readyReadStandardError, this, [process, logFile]() {
|
||||
QByteArray err = process->readAllStandardError();
|
||||
logFile->write(err);
|
||||
logFile->flush();
|
||||
});
|
||||
}
|
||||
|
||||
connect(process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||
this, [this, packageName, outputPath, logFile](int exitCode, QProcess::ExitStatus status) {
|
||||
bool success = (exitCode == 0 && status == QProcess::NormalExit);
|
||||
if (!success) {
|
||||
qWarning() << "Download failed for" << packageName << "exit code:" << exitCode;
|
||||
}
|
||||
|
||||
removeAria2Files(outputPath); // 清理残留 .aria2
|
||||
emit downloadFinished(packageName, success);
|
||||
|
||||
if (logFile) logFile->close();
|
||||
|
||||
QProcess *proc = m_processes.take(packageName);
|
||||
if (proc) proc->deleteLater();
|
||||
});
|
||||
|
||||
process->start("aria2c", arguments);
|
||||
}
|
||||
|
||||
void DownloadManager::cancelDownload(const QString &packageName)
|
||||
{
|
||||
if (!m_processes.contains(packageName)) return;
|
||||
|
||||
QProcess *process = m_processes.take(packageName);
|
||||
if (process) {
|
||||
process->kill(); // 立即终止
|
||||
process->waitForFinished(3000); // 最多等待3秒
|
||||
process->deleteLater();
|
||||
}
|
||||
|
||||
emit downloadFinished(packageName, false); // 显式通知取消
|
||||
|
||||
}
|
||||
|
||||
void DownloadManager::removeAria2Files(const QString &filePath)
|
||||
{
|
||||
QString ariaFile = filePath + ".aria2";
|
||||
QFile::remove(ariaFile);
|
||||
}
|
||||
|
||||
bool DownloadManager::isDownloading(const QString &packageName) const
|
||||
{
|
||||
return m_processes.contains(packageName);
|
||||
}
|
||||
|
||||
void DownloadManager::cleanupTempFiles()
|
||||
{
|
||||
QDir tempDir(QDir::tempPath());
|
||||
QStringList leftovers = tempDir.entryList(QStringList() << "*.aria2", QDir::Files);
|
||||
for (const QString &f : leftovers) {
|
||||
tempDir.remove(f);
|
||||
}
|
||||
}
|
||||
28
spark-update-tool/src/downloadmanager.h
Normal file
28
spark-update-tool/src/downloadmanager.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#ifndef DOWNLOADMANAGER_H
|
||||
#define DOWNLOADMANAGER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QMap>
|
||||
#include <QProcess>
|
||||
|
||||
class DownloadManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DownloadManager(QObject *parent = nullptr);
|
||||
void startDownload(const QString &packageName, const QString &url, const QString &outputPath);
|
||||
void cancelDownload(const QString &packageName);
|
||||
bool isDownloading(const QString &packageName) const;
|
||||
|
||||
signals:
|
||||
void downloadProgress(const QString &packageName, int progress);
|
||||
void downloadFinished(const QString &packageName, bool success);
|
||||
|
||||
private:
|
||||
void cleanupTempFiles();
|
||||
void removeAria2Files(const QString &filePath);
|
||||
|
||||
QMap<QString, QProcess*> m_processes;
|
||||
};
|
||||
|
||||
#endif // DOWNLOADMANAGER_H
|
||||
8
spark-update-tool/src/icons.qrc
Normal file
8
spark-update-tool/src/icons.qrc
Normal file
@@ -0,0 +1,8 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>../resources/down_arrow.svg</file>
|
||||
<file>../resources/default_icon.svg</file>
|
||||
<file>../resources/spark-update-tool.svg</file>
|
||||
<file>../resources/128*128/spark-update-tool.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
86
spark-update-tool/src/main.cpp
Normal file
86
spark-update-tool/src/main.cpp
Normal file
@@ -0,0 +1,86 @@
|
||||
#include "mainwindow.h"
|
||||
#include <QApplication>
|
||||
#include <QProcess>
|
||||
#include <QMessageBox>
|
||||
#include <unistd.h> // for geteuid
|
||||
#include <cstdlib> // for getenv
|
||||
#include <QDebug> // For debugging output
|
||||
|
||||
bool isRoot() {
|
||||
return geteuid() == 0;
|
||||
}
|
||||
|
||||
bool elevateToRoot() {
|
||||
QString program = QCoreApplication::applicationFilePath();
|
||||
qDebug() << "Current application path:" << program;
|
||||
|
||||
QByteArray display = qgetenv("DISPLAY");
|
||||
QByteArray xauthority = qgetenv("XAUTHORITY");
|
||||
|
||||
QStringList args;
|
||||
args << "env"
|
||||
<< "DISPLAY=" + display
|
||||
<< "XAUTHORITY=" + xauthority
|
||||
<< program;
|
||||
|
||||
QProcess process;
|
||||
process.setProgram("pkexec");
|
||||
process.setArguments(args);
|
||||
|
||||
qDebug() << "Attempting to elevate using pkexec with arguments:" << args;
|
||||
|
||||
process.start();
|
||||
if (!process.waitForStarted(5000)) {
|
||||
qDebug() << "Failed to start pkexec.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 阻塞等待提权进程退出(比如主程序窗口关闭)
|
||||
if (!process.waitForFinished(-1)) { // 等待直到新进程退出
|
||||
qDebug() << "pkexec process waitForFinished failed.";
|
||||
return false;
|
||||
}
|
||||
|
||||
int exitCode = process.exitCode();
|
||||
QProcess::ExitStatus exitStatus = process.exitStatus();
|
||||
|
||||
qDebug() << "pkexec exit code:" << exitCode;
|
||||
qDebug() << "pkexec exit status:" << exitStatus;
|
||||
qDebug() << "pkexec stderr:" << process.readAllStandardError();
|
||||
|
||||
return (exitStatus == QProcess::NormalExit && exitCode == 0);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
// 必须在 QGuiApplication 实例创建之前调用
|
||||
// QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
|
||||
|
||||
QApplication a(argc, argv);
|
||||
a.setWindowIcon(QIcon(":/resources/128*128/spark-update-tool.png"));
|
||||
if (!isRoot()) {
|
||||
qDebug() << "Not running as root. Attempting to elevate...";
|
||||
if (!elevateToRoot()) {
|
||||
qDebug() << "Elevation failed or pkexec command was not executed successfully.";
|
||||
QMessageBox::critical(nullptr,
|
||||
"权限不足",
|
||||
"提权失败!\n\n您的系统可能不支持 `pkexec` 或 `polkit` 配置不正确,"
|
||||
"或者您取消了授权。\n\n请尝试使用 `sudo` 命令运行此程序:"
|
||||
"\n\n在终端中输入:\n`sudo " + QCoreApplication::applicationName() + "`");
|
||||
return 0; // 提权失败,退出程序
|
||||
} else {
|
||||
// 如果 elevateToRoot 返回 true,说明 pkexec 命令本身执行成功
|
||||
// 但这并不意味着原始程序以 root 权限启动了
|
||||
// 因为 elevateToRoot 启动的是一个新的进程,当前进程应该退出
|
||||
// 否则会并行运行两个程序实例
|
||||
qDebug() << "pkexec command executed successfully (new process likely started). Exiting current process.";
|
||||
return 0; // 当前非root进程退出
|
||||
}
|
||||
} else {
|
||||
qDebug() << "Running as root.";
|
||||
}
|
||||
|
||||
MainWindow w;
|
||||
w.show();
|
||||
return a.exec();
|
||||
}
|
||||
268
spark-update-tool/src/mainwindow.cpp
Normal file
268
spark-update-tool/src/mainwindow.cpp
Normal file
@@ -0,0 +1,268 @@
|
||||
#include "mainwindow.h"
|
||||
#include "./ui_mainwindow.h"
|
||||
#include <QProcess>
|
||||
#include <QMessageBox>
|
||||
#include <QProgressDialog>
|
||||
#include <QtConcurrent> // 新增
|
||||
#include <QFutureWatcher> // 新增
|
||||
#include <QIcon>
|
||||
#include <qicon.h>
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent)
|
||||
, ui(new Ui::MainWindow)
|
||||
, m_model(new AppListModel(this))
|
||||
, m_delegate(new AppDelegate(this))
|
||||
{
|
||||
QIcon icon(":/resources/128*128/spark-update-tool.png");
|
||||
setWindowIcon(icon);
|
||||
QProgressDialog *progressDialog = new QProgressDialog("正在与服务器通信,获取更新信息中...", QString(), 0, 0, this);
|
||||
progressDialog->setWindowModality(Qt::ApplicationModal);
|
||||
progressDialog->setCancelButton(nullptr);
|
||||
progressDialog->setWindowTitle("请稍候");
|
||||
progressDialog->setMinimumDuration(0);
|
||||
progressDialog->setWindowFlags(progressDialog->windowFlags() & ~Qt::WindowCloseButtonHint); // 禁用关闭按钮
|
||||
progressDialog->show();
|
||||
//异步执行runAptssUpgrade
|
||||
QFutureWatcher<void> *watcher = new QFutureWatcher<void>(this);
|
||||
connect(watcher, &QFutureWatcher<void>::finished, this, [=]() {
|
||||
progressDialog->close();
|
||||
progressDialog->deleteLater();
|
||||
watcher->deleteLater();
|
||||
ui->setupUi(this);
|
||||
QIcon icon(":/resources/128*128/spark-update-tool.png");
|
||||
setWindowIcon(icon);
|
||||
// 创建 QListView 并设置父控件为 ui->appWidget
|
||||
listView = new QListView(ui->appWidget);
|
||||
listView->setModel(m_model);
|
||||
listView->setItemDelegate(m_delegate);
|
||||
|
||||
// 新增:确保 delegate 拥有 model 指针
|
||||
m_delegate->setModel(m_model);
|
||||
|
||||
// 设置 QListView 填充 ui->appWidget
|
||||
QVBoxLayout *layout = new QVBoxLayout(ui->appWidget);
|
||||
layout->addWidget(listView);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
connect(m_delegate, &AppDelegate::updateDisplay, this, [=](const QString &packageName) {
|
||||
for (int i = 0; i < m_model->rowCount(); ++i) {
|
||||
QModelIndex index = m_model->index(i);
|
||||
if (index.data(Qt::UserRole + 1).toString() == packageName) {
|
||||
m_model->dataChanged(index, index); // 刷新该行
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 新增:点击“更新全部”按钮批量下载
|
||||
connect(ui->updatePushButton, &QPushButton::clicked, this, [=](){
|
||||
qDebug()<<"更新全部按钮被点击";
|
||||
m_delegate->startDownloadForAll();
|
||||
});
|
||||
|
||||
checkUpdates();
|
||||
// 新增:监听搜索框文本变化
|
||||
connect(ui->searchPlainTextEdit, &QPlainTextEdit::textChanged, this, [=]() {
|
||||
QString keyword = ui->searchPlainTextEdit->toPlainText();
|
||||
filterAppsByKeyword(keyword);
|
||||
});
|
||||
initStyle();
|
||||
|
||||
// 确保搜索框内容为空,placeholder 能显示
|
||||
ui->searchPlainTextEdit->clear();
|
||||
});
|
||||
|
||||
// 启动异步任务
|
||||
watcher->setFuture(QtConcurrent::run([this](){
|
||||
runAptssUpgrade();
|
||||
}));
|
||||
QScreen *screen = QGuiApplication::screenAt(QCursor::pos());
|
||||
if (!screen) screen = QGuiApplication::primaryScreen();
|
||||
QRect screenGeometry = screen->geometry();
|
||||
int x = screenGeometry.x() + (screenGeometry.width() - this->width()) / 2;
|
||||
int y = screenGeometry.y() + (screenGeometry.height() - this->height()) / 2;
|
||||
this->move(x, y);
|
||||
}
|
||||
//初始化控件样式
|
||||
void MainWindow::initStyle()
|
||||
{
|
||||
//设置窗口标题
|
||||
this->setWindowTitle("软件更新中心");
|
||||
|
||||
//查询框样式
|
||||
ui->searchPlainTextEdit->setStyleSheet(R"(
|
||||
QPlainTextEdit {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 4px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
font-size: 9px;
|
||||
line-height: 1.4;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
QPlainTextEdit[placeholderText]:empty {
|
||||
color: #9CA3AF;
|
||||
}
|
||||
)");
|
||||
|
||||
ui->searchPlainTextEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
ui->searchPlainTextEdit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
|
||||
//筛选框样式
|
||||
ui->FilterComboBox->setStyleSheet(R"(
|
||||
QComboBox {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 4px;
|
||||
color: #4B5563;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
QComboBox::drop-down {
|
||||
border: none;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
QComboBox::down-arrow {
|
||||
image: url(:/resources/down_arrow.svg);
|
||||
width: 12px;
|
||||
height: 16px;
|
||||
}
|
||||
QComboBox QAbstractItemView {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
color: #4B5563;
|
||||
selection-background-color: #F3F4F6;
|
||||
selection-color: #111827;
|
||||
}
|
||||
)");
|
||||
|
||||
//更新软件按钮样式
|
||||
ui->updatePushButton->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #2563EB;
|
||||
color: #FFFFFF;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
padding: 6px 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #1D4ED8; /* 深一点的 hover 效果,可选 */
|
||||
}
|
||||
|
||||
QPushButton:pressed {
|
||||
background-color: #1E40AF; /* 按下效果,可选 */
|
||||
}
|
||||
|
||||
QPushButton:disabled {
|
||||
background-color: #A5B4FC;
|
||||
color: #F9FAFB;
|
||||
}
|
||||
)");
|
||||
|
||||
//设置背景填充颜色
|
||||
ui->backgroundWidget->setStyleSheet(R"(
|
||||
QWidget {
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 12px;
|
||||
}
|
||||
)");
|
||||
|
||||
//设置主背景颜色
|
||||
this->setStyleSheet("background-color: #F8FAFC;");
|
||||
|
||||
// 添加滚动条样式
|
||||
this->setStyleSheet(R"(
|
||||
QScrollBar:vertical {
|
||||
background: #F3F4F6;
|
||||
width: 8px;
|
||||
margin: 0px;
|
||||
}
|
||||
QScrollBar::handle:vertical {
|
||||
background: #D1D5DB;
|
||||
border-radius: 4px;
|
||||
min-height: 30px;
|
||||
}
|
||||
QScrollBar::handle:vertical:hover {
|
||||
background: #9CA3AF;
|
||||
}
|
||||
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
|
||||
background: none;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
QScrollBar:horizontal {
|
||||
background: #F3F4F6;
|
||||
height: 8px;
|
||||
margin: 0px;
|
||||
}
|
||||
QScrollBar::handle:horizontal {
|
||||
background: #D1D5DB;
|
||||
border-radius: 4px;
|
||||
min-width: 30px;
|
||||
}
|
||||
QScrollBar::handle:horizontal:hover {
|
||||
background: #9CA3AF;
|
||||
}
|
||||
)");
|
||||
}
|
||||
void MainWindow::checkUpdates()
|
||||
{
|
||||
aptssUpdater updater;
|
||||
QJsonArray updateInfo = updater.getUpdateInfoAsJson();
|
||||
m_allApps = updateInfo; // 保存所有应用数据
|
||||
m_model->setUpdateData(updateInfo);
|
||||
|
||||
for (const auto &item : updateInfo) {
|
||||
QJsonObject obj = item.toObject();
|
||||
qDebug() << "模型设置的包名:" << obj["package"].toString();
|
||||
qDebug() << "模型设置的下载 URL:" << obj["download_url"].toString(); // 检查模型数据
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:根据关键字过滤应用
|
||||
void MainWindow::filterAppsByKeyword(const QString &keyword)
|
||||
{
|
||||
if (keyword.trimmed().isEmpty()) {
|
||||
m_model->setUpdateData(m_allApps);
|
||||
return;
|
||||
}
|
||||
QJsonArray filtered;
|
||||
for (const auto &item : m_allApps) {
|
||||
QJsonObject obj = item.toObject();
|
||||
// 可根据需要匹配更多字段
|
||||
QString name = obj.value("name").toString();
|
||||
QString package = obj.value("package").toString();
|
||||
if (name.contains(keyword, Qt::CaseInsensitive) ||
|
||||
package.contains(keyword, Qt::CaseInsensitive)) {
|
||||
filtered.append(item);
|
||||
}
|
||||
}
|
||||
m_model->setUpdateData(filtered);
|
||||
}
|
||||
|
||||
void MainWindow::runAptssUpgrade()
|
||||
{
|
||||
QProcess process;
|
||||
QStringList args;
|
||||
args << "sudo" <<"aptss" << "ssupdate";
|
||||
process.start("sudo", args);
|
||||
if (!process.waitForStarted(5000)) {
|
||||
QMessageBox::warning(this, "升级失败", "无法启动 sudo aptss ssupdate");
|
||||
return;
|
||||
}
|
||||
process.write("n\n");
|
||||
process.closeWriteChannel();
|
||||
process.waitForFinished(-1);
|
||||
if (process.exitCode() != 0) {
|
||||
QMessageBox::warning(this, "升级失败", "执行 sudo aptss ssupdate 失败,请检查系统环境。");
|
||||
}
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
36
spark-update-tool/src/mainwindow.h
Normal file
36
spark-update-tool/src/mainwindow.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#ifndef MAINWINDOW_H
|
||||
#define MAINWINDOW_H
|
||||
|
||||
#include <QMainWindow>
|
||||
#include "aptssupdater.h"
|
||||
#include "applistmodel.h"
|
||||
#include "appdelegate.h"
|
||||
#include <QListView>
|
||||
#include <QJsonArray> // 添加头文件
|
||||
#include <QScreen>
|
||||
QT_BEGIN_NAMESPACE
|
||||
namespace Ui {
|
||||
class MainWindow;
|
||||
}
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow();
|
||||
|
||||
private:
|
||||
Ui::MainWindow *ui;
|
||||
void checkUpdates();
|
||||
void initStyle();
|
||||
void runAptssUpgrade();
|
||||
AppListModel *m_model;
|
||||
AppDelegate *m_delegate;
|
||||
QListView *listView; // 声明 QListView 指针
|
||||
QJsonArray m_allApps; // 新增:保存所有应用数据
|
||||
void filterAppsByKeyword(const QString &keyword); // 新增:搜索过滤函数声明
|
||||
};
|
||||
#endif // MAINWINDOW_H
|
||||
256
spark-update-tool/src/mainwindow.ui
Normal file
256
spark-update-tool/src/mainwindow.ui
Normal file
@@ -0,0 +1,256 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1440</width>
|
||||
<height>858</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>MainWindow</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QWidget" name="titleWidget" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Noto Sans Vai</family>
|
||||
<pointsize>22</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>软件更新中心</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>1245</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QWidget" name="backgroundWidget" native="true">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_4" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_3" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>320</width>
|
||||
<height>38</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>320</width>
|
||||
<height>38</height>
|
||||
</size>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="searchPlainTextEdit">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>38</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="plainText">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="tabStopDistance">
|
||||
<double>81.000000000000000</double>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>搜索软件...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>214</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>214</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
<widget class="QComboBox" name="FilterComboBox">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>102</width>
|
||||
<height>38</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>102</width>
|
||||
<height>38</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>102</width>
|
||||
<height>38</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font/>
|
||||
</property>
|
||||
<property name="layoutDirection">
|
||||
<enum>Qt::LayoutDirection::LeftToRight</enum>
|
||||
</property>
|
||||
<property name="sizeAdjustPolicy">
|
||||
<enum>QComboBox::SizeAdjustPolicy::AdjustToContentsOnFirstShow</enum>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>按名称</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
<widget class="QPushButton" name="updatePushButton">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>118</x>
|
||||
<y>0</y>
|
||||
<width>96</width>
|
||||
<height>40</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>更新全部</string>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="appWidget" native="true">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1440</width>
|
||||
<height>23</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
Reference in New Issue
Block a user