将更新器合入到星火商店中

This commit is contained in:
momen 2025-07-04 04:08:09 +08:00
parent 8f708aa8b2
commit e69deb4b95
29 changed files with 1847 additions and 0 deletions

62
spark-update-tool/.gitignore vendored Normal file
View File

@ -0,0 +1,62 @@
build
.vscode
.cache
CMakeLists.txt.user
CMakeLists.txt.user.*
obj-x86_64-linux-gnu
# C++ objects and libs
*.slo
*.lo
*.o
*.a
*.la
*.lai
*.so
*.dll
*.dylib
# Qt-es
object_script.*.Release
object_script.*.Debug
*_plugin_import.cpp
/.qmake.cache
/.qmake.stash
*.pro.user
*.pro.user.*
*.qbs.user
*.qbs.user.*
*.moc
moc_*.cpp
moc_*.h
qrc_*.cpp
ui_*.h
*.qmlc
*.jsc
Makefile*
*build-*
# Qt unit tests
target_wrapper.*
# Qt qm files
translations/*.qm
# QtCreator
*.autosave
# QtCreator Qml
*.qmlproject.user
*.qmlproject.user.*
# QtCreator CMake
CMakeLists.txt.user*
build
# Debian dpkg-buildpackage
debian/*.debhelper*
debian/files
debian/*.substvars
debian/spark-update-tool
.vscode/*
src/spark-update-tool

View File

@ -0,0 +1,58 @@
cmake_minimum_required(VERSION 3.16)
project(spark-update-tool VERSION 0.1 LANGUAGES CXX)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt5 REQUIRED COMPONENTS Widgets Network Concurrent)
find_package(Qt5 REQUIRED COMPONENTS Widgets Network Concurrent Core Gui)
#
set(PROJECT_SOURCES
src/main.cpp
src/mainwindow.cpp
src/mainwindow.h
src/mainwindow.ui
src/aptssupdater.h src/aptssupdater.cpp
src/icons.qrc
src/appdelegate.h src/appdelegate.cpp
src/applistmodel.h src/applistmodel.cpp
src/downloadmanager.h src/downloadmanager.cpp
)
add_executable(spark-update-tool
${PROJECT_SOURCES}
)
target_link_libraries(spark-update-tool PRIVATE
Qt5::Widgets
Qt5::Network
Qt5::Concurrent
Qt5::Core
Qt5::Gui
)
set_target_properties(spark-update-tool PROPERTIES
# ${BUNDLE_ID_OPTION} #
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
)
include(GNUInstallDirs)
install(TARGETS spark-update-tool
BUNDLE DESTINATION .
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

View File

@ -0,0 +1,29 @@
### 星火软件更新器
#### 简介
欢迎使用星火软件更新器!本工具可帮助您便捷地更新 Linux 计算机上的各类程序。
本版本专为仅包含 Qt5 的 Linux 发行版设计。
**请在 root 权限下运行本程序。**
#### 当前支持的 Linux 发行版
- [x] GXDE OS
- [x] Ubuntu
- [x] deepin
- [ ] Kylin
#### 功能清单
| 功能模块 | 描述 |
|------------------|--------------------------------------|
| 应用名识别 | 基于 `ss-do-upgrade.sh` 部分代码实现 |
| 应用包大小识别 | 通过 dpkg 获取包大小信息 |
| 获取应用图标 | 利用 QDesktopServices 实现 |
| 支持 ACE 兼容环境| |
| 多线程下载 | 基于 aptss 方案 |
#### 联系与反馈
如有问题或建议欢迎联系momen@momen.world

View File

@ -0,0 +1,5 @@
spark-update-tool (1.0.0) unstable; urgency=low
* Initial release.
-- momen <vmomenv@gmail.com> Wed, 18 Jun 2025 00:00:00 +0000

View File

@ -0,0 +1 @@
9

View File

@ -0,0 +1,13 @@
Source: spark-update-tool
Section: utils
Priority: optional
Maintainer: momen <vmomenv@gmail.com>
Build-Depends: debhelper (>= 9)
Standards-Version: 3.9.6
Homepage: https://gitee.com/spark-store-project/Spark-Update-Tool
Package: spark-update-tool
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}
Description: Spark Update Tool
星火应用商店更新组件。This package provides the Spark Update Tool. It includes features for checking for updates, downloading, and applying them seamlessly.

View File

@ -0,0 +1,7 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: spark-update-tool
Source: https://gitee.com/spark-store-project/Spark-Update-Tool
Files: *
Copyright: 2025, momen <vmomenv@gmail.com>
License: GPL-3.0+

View File

@ -0,0 +1,3 @@
build/spark-update-tool /usr/bin/
debian/spark-update-tool.desktop /usr/share/applications
resources/128*128/spark-update-tool.png /usr/share/icons/hicolor/128x128/apps

View File

@ -0,0 +1,11 @@
#!/bin/sh
set -e
case "$1" in
purge)
rm -rf /usr/share/spark-update-tool
;;
esac
exit 0

7
spark-update-tool/debian/rules Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/make -f
%:
dh $@
override_dh_auto_configure:
dh_auto_configure -- -DCMAKE_INSTALL_PREFIX=/usr

View File

@ -0,0 +1 @@
3.0 (native)

View File

@ -0,0 +1,9 @@
[Desktop Entry]
Version=1.0
Name=Spark Update Tool
Comment=A Qt-based application for managing and updating software
Exec=spark-update-tool
Icon=spark-update-tool
Terminal=false
Type=Application
Categories=Utility;

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="12" height="16" viewBox="0 0 12 16"><defs><clipPath id="master_svg0_1_074"><rect x="0.75" y="2" width="10.5" height="12" rx="0"/></clipPath></defs><g><rect x="0" y="0" width="12" height="16" rx="0" fill="#000000" fill-opacity="0" style="mix-blend-mode:passthrough"/><g clip-path="url(#master_svg0_1_074)"><g transform="matrix(1,0,0,-1,0,24.015625)"><g><path d="M4.71094,12.2187505Q4.94531,12.0078125,5.25,12.0078125Q5.55469,12.0078125,5.78906,12.2187505L10.2891,16.7187525Q10.5,16.9531225,10.5,17.2578125Q10.5,17.5625025,10.2891,17.7968725Q10.0547,18.0078125,9.75,18.0078125Q9.44531,18.0078125,9.21094,17.7968725L5.25,13.8125025L1.28906,17.7968725Q1.05469,18.0078125,0.75,18.0078125Q0.445312,18.0078125,0.210938,17.7968725Q0,17.5625025,0,17.2578125Q0,16.9531225,0.210938,16.7187525L4.71094,12.2187505Z" fill="#4B5563" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1009 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,321 @@
#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;
emit updateDisplay(packageName);
qDebug() << (success ? "下载完成:" : "下载失败:") << packageName;
if (success) {
enqueueInstall(packageName);
}
}
});
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();
// 若未检测到“软件包已安装”,此处兜底
if (!m_downloads[packageName].isInstalled) {
m_downloads[packageName].isInstalling = false;
}
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);
}

View 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();
};

View 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();
}

View 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

View 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;
}

View 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

View 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);
}
}

View 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

View 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>

View 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();
}

View File

@ -0,0 +1,229 @@
#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();
initStyle();
});
// 启动异步任务
watcher->setFuture(QtConcurrent::run([this](){
runAptssUpgrade();
}));
}
//初始化控件样式
void MainWindow::initStyle()
{
//设置窗口标题
this->setWindowTitle("软件更新中心");
//查询框样式
ui->plainTextEdit->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;
}
)");
ui->plainTextEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
ui->plainTextEdit->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_model->setUpdateData(updateInfo);
for (const auto &item : updateInfo) {
QJsonObject obj = item.toObject();
qDebug() << "模型设置的包名:" << obj["package"].toString();
qDebug() << "模型设置的下载 URL:" << obj["download_url"].toString(); // 检查模型数据
}
}
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;
}

View File

@ -0,0 +1,33 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "aptssupdater.h"
#include "applistmodel.h"
#include "appdelegate.h"
#include <QListView>
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 指针
};
#endif // MAINWINDOW_H

View 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::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="plainTextEdit">
<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::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::LeftToRight</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::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>