mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-29 19:00:16 +08:00
fix(update): 统一忽略更新配置到用户目录
This commit is contained in:
105
docs/superpowers/plans/2026-04-15-update-ignore.md
Normal file
105
docs/superpowers/plans/2026-04-15-update-ignore.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Update Ignore Configuration Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Move update-ignore persistence to user config, add ignore and unignore controls to the Electron update center, and make the legacy Qt updater plus root notifier honor the same `pkg|newVersion` rules.
|
||||
|
||||
**Architecture:** Keep the existing text config format and IPC channels. Change the default config path in the Electron backend, expose ignore actions in the renderer store and item component, align the Qt updater with the same new-version key semantics, and teach the notifier to discover user config files without trusting root `HOME`.
|
||||
|
||||
**Tech Stack:** TypeScript, Vue 3, Electron IPC, Vitest, Qt/C++, POSIX shell
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify: `electron/main/backend/update-center/ignore-config.ts`
|
||||
Responsibility: switch the default ignore config path to the user config directory and keep exact `pkg|version` matching.
|
||||
- Modify: `electron/main/backend/update-center/service.ts`
|
||||
Responsibility: apply ignored sorting after refresh.
|
||||
- Modify: `src/modules/updateCenter.ts`
|
||||
Responsibility: expose ignore and unignore actions to the renderer.
|
||||
- Modify: `src/components/update-center/UpdateCenterItem.vue`
|
||||
Responsibility: render ignore and unignore controls for each item.
|
||||
- Modify: `src/components/update-center/UpdateCenterList.vue`
|
||||
Responsibility: bubble ignore and unignore item events upward.
|
||||
- Modify: `src/components/UpdateCenterModal.vue`
|
||||
Responsibility: wire ignore and unignore item events to the store.
|
||||
- Modify: `src/__tests__/unit/update-center/ignore-config.test.ts`
|
||||
Responsibility: prove the new default path resolves to the user config directory.
|
||||
- Modify: `src/__tests__/unit/update-center/store.test.ts`
|
||||
Responsibility: prove ignore and unignore call the preload bridge and refresh state.
|
||||
- Modify: `src/__tests__/unit/update-center/UpdateCenterModal.test.ts`
|
||||
Responsibility: prove ignore-state actions render correctly.
|
||||
- Modify: `spark-update-tool/src/ignoreconfig.cpp`
|
||||
Responsibility: move the Qt config path to the user config directory.
|
||||
- Modify: `spark-update-tool/src/ignoreconfig.h`
|
||||
Responsibility: support exact unignore by package plus version.
|
||||
- Modify: `spark-update-tool/src/appdelegate.cpp`
|
||||
Responsibility: emit the target new version when ignoring or unignoring.
|
||||
- Modify: `spark-update-tool/src/appdelegate.h`
|
||||
Responsibility: update the unignore signal signature.
|
||||
- Modify: `spark-update-tool/src/mainwindow.cpp`
|
||||
Responsibility: match ignored state against new versions and remove exact entries.
|
||||
- Modify: `spark-update-tool/src/mainwindow.h`
|
||||
Responsibility: update slot signatures.
|
||||
- Modify: `tool/update-upgrade/ss-update-notifier.sh`
|
||||
Responsibility: locate user config files from a root service context and filter exact `pkg|newVersion` matches.
|
||||
|
||||
## Task 1: Electron Ignore Path And Renderer Actions
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `electron/main/backend/update-center/ignore-config.ts`
|
||||
- Modify: `electron/main/backend/update-center/service.ts`
|
||||
- Modify: `src/modules/updateCenter.ts`
|
||||
- Modify: `src/components/update-center/UpdateCenterItem.vue`
|
||||
- Modify: `src/components/update-center/UpdateCenterList.vue`
|
||||
- Modify: `src/components/UpdateCenterModal.vue`
|
||||
- Modify: `src/__tests__/unit/update-center/ignore-config.test.ts`
|
||||
- Modify: `src/__tests__/unit/update-center/store.test.ts`
|
||||
- Modify: `src/__tests__/unit/update-center/UpdateCenterModal.test.ts`
|
||||
|
||||
- [ ] Write failing tests for the new user config path, ignore/unignore store methods, and item actions.
|
||||
- [ ] Run `npx vitest run src/__tests__/unit/update-center/ignore-config.test.ts src/__tests__/unit/update-center/store.test.ts src/__tests__/unit/update-center/UpdateCenterModal.test.ts` and confirm they fail for the expected reasons.
|
||||
- [ ] Implement the minimal backend path change, item sorting, renderer store methods, and modal wiring.
|
||||
- [ ] Re-run the same Vitest command and confirm it passes.
|
||||
|
||||
## Task 2: Legacy Qt Updater Alignment
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `spark-update-tool/src/ignoreconfig.cpp`
|
||||
- Modify: `spark-update-tool/src/ignoreconfig.h`
|
||||
- Modify: `spark-update-tool/src/appdelegate.cpp`
|
||||
- Modify: `spark-update-tool/src/appdelegate.h`
|
||||
- Modify: `spark-update-tool/src/mainwindow.cpp`
|
||||
- Modify: `spark-update-tool/src/mainwindow.h`
|
||||
|
||||
- [ ] Change Qt config path resolution to `QStandardPaths::ConfigLocation/spark-store/ignored_apps.conf`.
|
||||
- [ ] Switch ignore and unignore to use `packageName + newVersion` exact entries.
|
||||
- [ ] Build-check the Qt target if a local build command is available.
|
||||
|
||||
## Task 3: Root Notifier User Config Discovery
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `tool/update-upgrade/ss-update-notifier.sh`
|
||||
|
||||
- [ ] Add shell helpers to detect a desktop user home when possible.
|
||||
- [ ] Add fallback scanning across `/home/*/.config/spark-store/ignored_apps.conf`.
|
||||
- [ ] Merge all discovered config files into one ignore set.
|
||||
- [ ] Filter updates by exact `pkg|newVersion` instead of package-only.
|
||||
- [ ] Run `bash -n tool/update-upgrade/ss-update-notifier.sh` and confirm syntax is valid.
|
||||
|
||||
## Task 4: Verification And Commit
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: tracked files from Tasks 1-3
|
||||
|
||||
- [ ] Run `npx vitest run src/__tests__/unit/update-center/ignore-config.test.ts src/__tests__/unit/update-center/store.test.ts src/__tests__/unit/update-center/UpdateCenterModal.test.ts`.
|
||||
- [ ] Run `npm run lint`.
|
||||
- [ ] Run `npm run build`.
|
||||
- [ ] Run `bash -n tool/update-upgrade/ss-update-notifier.sh`.
|
||||
- [ ] Review the final diff.
|
||||
- [ ] Create a commit with a message in repository style.
|
||||
135
docs/superpowers/specs/2026-04-15-update-ignore-design.md
Normal file
135
docs/superpowers/specs/2026-04-15-update-ignore-design.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# 更新忽略配置迁移设计
|
||||
|
||||
## 背景
|
||||
|
||||
Electron 更新中心已经具备忽略状态的数据通路,但默认仍写入 `/etc/spark-store/ignored_apps.conf`。老 Qt 更新器也沿用同一路径。新架构下更新器不再以 root 身份启动,因此 GUI 无法稳定写入 `/etc`。与此同时,`ss-update-notifier.sh` 以 root systemd 服务运行,若直接使用 `~/` 会错误落到 `/root`。
|
||||
|
||||
本次改动的目标是在不重做更新链路的前提下,把“忽略更新”统一改为用户级配置,并让 Electron、老 Qt 更新器和 notifier 对同一份规则生效。
|
||||
|
||||
## 目标
|
||||
|
||||
1. 忽略配置统一迁移到用户目录 `~/.config/spark-store/ignored_apps.conf`。
|
||||
2. Electron 更新中心支持显式忽略和取消忽略操作。
|
||||
3. 老 Qt 更新器改为读写同一份用户级忽略配置。
|
||||
4. `ss-update-notifier.sh` 在 root systemd 环境下也能读取用户级忽略配置。
|
||||
5. 忽略规则同时作用于 Spark 与 APM 更新项。
|
||||
6. 忽略规则按 `pkgname|version` 精确匹配,被忽略的旧版本在后续出现新版本时应重新提醒。
|
||||
|
||||
## 非目标
|
||||
|
||||
1. 不兼容旧的 `/etc/spark-store/ignored_apps.conf`。
|
||||
2. 不改变更新下载、安装和迁移逻辑。
|
||||
3. 不把忽略配置升级为 JSON 或数据库格式。
|
||||
4. 不修改 AmberPM 侧的 `amber-pm-upgrade-notifier`。
|
||||
|
||||
## 方案概览
|
||||
|
||||
本次实现由三部分组成:
|
||||
|
||||
1. Electron 主进程把忽略配置路径切到用户目录,渲染层补齐“忽略 / 取消忽略”入口,并把已忽略项排在后面展示。
|
||||
2. 老 Qt 更新器的 `IgnoreConfig` 改为使用 `QStandardPaths::ConfigLocation` 下的 `spark-store/ignored_apps.conf`,同时将忽略键统一为“包名 + 新版本”。
|
||||
3. `ss-update-notifier.sh` 新增用户配置定位与扫描逻辑,在 root systemd 环境下优先识别活动桌面用户,失败时回退扫描 `/home/*/.config/spark-store/ignored_apps.conf` 并合并忽略集合。
|
||||
|
||||
## 配置文件设计
|
||||
|
||||
### 路径
|
||||
|
||||
- 统一路径:`~/.config/spark-store/ignored_apps.conf`
|
||||
- Electron 通过当前进程用户的 home 解析该路径。
|
||||
- Qt 通过 `QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)` 解析该路径。
|
||||
- notifier 不直接依赖 `~/`,而是根据目标 home 拼出 `<home>/.config/spark-store/ignored_apps.conf`。
|
||||
|
||||
### 格式
|
||||
|
||||
继续沿用现有纯文本格式,每行一条:
|
||||
|
||||
```text
|
||||
pkgname|version
|
||||
```
|
||||
|
||||
其中 `version` 统一表示“待更新到的新版本”,而不是当前已安装版本。
|
||||
|
||||
### 匹配语义
|
||||
|
||||
1. 仅当 `pkgname` 与 `version` 同时匹配时,视为被忽略。
|
||||
2. 忽略规则不区分 `spark` / `apm` 来源。相同包名与目标版本的更新,在两侧都应被同一条规则命中。
|
||||
3. 某版本被忽略后,未来出现更高版本时,不自动继承忽略状态。
|
||||
|
||||
## Electron 更新中心
|
||||
|
||||
### 主进程
|
||||
|
||||
`electron/main/backend/update-center/ignore-config.ts` 保持文本解析逻辑不变,只修改默认配置路径到用户目录。
|
||||
|
||||
`electron/main/backend/update-center/service.ts` 的默认读写也改用新路径,并在刷新结果上做一次稳定排序:
|
||||
|
||||
1. 正常更新项在前。
|
||||
2. 已忽略项在后。
|
||||
3. 同组内保持原有顺序,避免不必要的 UI 抖动。
|
||||
|
||||
### 渲染层交互
|
||||
|
||||
更新中心列表项新增两个互斥操作:
|
||||
|
||||
1. 未忽略项显示“忽略”按钮。
|
||||
2. 已忽略项显示“取消忽略”按钮。
|
||||
|
||||
交互规则:
|
||||
|
||||
1. 点击“忽略”后调用 `window.updateCenter.ignore({ packageName, newVersion })`。
|
||||
2. 点击“取消忽略”后调用 `window.updateCenter.unignore({ packageName, newVersion })`。
|
||||
3. 主进程刷新完成后,渲染层使用推送或返回的新快照更新列表。
|
||||
4. 已忽略项继续不可勾选,也不会加入“更新选中”任务。
|
||||
|
||||
## 老 Qt 更新器
|
||||
|
||||
### 配置路径
|
||||
|
||||
`IgnoreConfig` 不再尝试写 `/etc`,改为:
|
||||
|
||||
1. 使用 `QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)`。
|
||||
2. 在其下创建 `spark-store/ignored_apps.conf`。
|
||||
|
||||
### 忽略键统一
|
||||
|
||||
Qt 当前交互里,忽略按钮传的是当前版本,检查时也匹配当前版本。这会导致与 Electron 的“目标版本忽略”语义不一致。
|
||||
|
||||
本次统一改为:
|
||||
|
||||
1. 点击“忽略”时写入 `packageName + newVersion`。
|
||||
2. 刷新列表时,用 `packageName + newVersion` 判断是否忽略。
|
||||
|
||||
取消忽略也改为按包名 + 版本删除对应条目,避免误删同包历史忽略记录。
|
||||
|
||||
## `ss-update-notifier.sh`
|
||||
|
||||
### 读取忽略配置
|
||||
|
||||
脚本新增两个步骤:
|
||||
|
||||
1. 尝试定位最可能的桌面用户 home。
|
||||
2. 如果无法可靠定位,则扫描 `/home/*/.config/spark-store/ignored_apps.conf`。
|
||||
|
||||
扫描模式下需要把所有命中的配置文件合并成一个忽略集合,再参与过滤。
|
||||
|
||||
### 过滤规则
|
||||
|
||||
脚本当前只按包名过滤,本次改为按 `pkgname|newVersion` 精确过滤:
|
||||
|
||||
1. 从 `ss-do-upgrade-worker.sh upgradable-list` 读取 `PKG_NAME PKG_NEW_VER PKG_CUR_VER`。
|
||||
2. 构造键 `PKG_NAME|PKG_NEW_VER`。
|
||||
3. 若忽略集合中存在该键,则跳过通知计数。
|
||||
|
||||
### 与通知用户识别解耦
|
||||
|
||||
通知发送仍然尽量复用现有“找活动用户然后 `sudo -u` 发送”的策略,但“读取忽略配置”与“给谁发通知”必须解耦:
|
||||
|
||||
1. 即使没有可靠的当前登录用户,也应先完成忽略过滤。
|
||||
2. 只有在最终需要发送通知时,再尝试解析实际桌面用户。
|
||||
|
||||
## 验证范围
|
||||
|
||||
1. Electron 单元测试覆盖新路径常量、忽略排序与忽略按钮交互。
|
||||
2. Electron 手动验证更新中心忽略 / 取消忽略流程。
|
||||
3. Qt 手动验证忽略后重新打开更新器仍保留状态。
|
||||
4. 手动执行 `ss-update-notifier.sh`,验证 root 环境下能命中用户级忽略配置且按版本精确过滤。
|
||||
@@ -1,9 +1,16 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname } from "node:path";
|
||||
import { join } from "node:path";
|
||||
|
||||
import type { UpdateCenterItem } from "./types";
|
||||
|
||||
export const LEGACY_IGNORE_CONFIG_PATH = "/etc/spark-store/ignored_apps.conf";
|
||||
export const IGNORE_CONFIG_PATH = join(
|
||||
homedir(),
|
||||
".config",
|
||||
"spark-store",
|
||||
"ignored_apps.conf",
|
||||
);
|
||||
|
||||
const LEGACY_IGNORE_SEPARATOR = "|";
|
||||
|
||||
@@ -77,3 +84,15 @@ export const applyIgnoredEntries = (
|
||||
createIgnoreKey(item.pkgname, item.nextVersion),
|
||||
),
|
||||
}));
|
||||
|
||||
export const sortIgnoredItems = (
|
||||
items: UpdateCenterItem[],
|
||||
): UpdateCenterItem[] => {
|
||||
return [...items].sort((left, right) => {
|
||||
if (left.ignored === right.ignored) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return left.ignored === true ? 1 : -1;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
import {
|
||||
LEGACY_IGNORE_CONFIG_PATH,
|
||||
IGNORE_CONFIG_PATH,
|
||||
applyIgnoredEntries,
|
||||
createIgnoreKey,
|
||||
loadIgnoredEntries,
|
||||
saveIgnoredEntries,
|
||||
sortIgnoredItems,
|
||||
} from "./ignore-config";
|
||||
import {
|
||||
createUpdateCenterQueue,
|
||||
@@ -136,11 +137,11 @@ export const createUpdateCenterService = (
|
||||
const listeners = new Set<(snapshot: UpdateCenterServiceState) => void>();
|
||||
const loadIgnored =
|
||||
options.loadIgnoredEntries ??
|
||||
(() => loadIgnoredEntries(LEGACY_IGNORE_CONFIG_PATH));
|
||||
(() => loadIgnoredEntries(IGNORE_CONFIG_PATH));
|
||||
const saveIgnored =
|
||||
options.saveIgnoredEntries ??
|
||||
((entries: ReadonlySet<string>) =>
|
||||
saveIgnoredEntries(LEGACY_IGNORE_CONFIG_PATH, entries));
|
||||
saveIgnoredEntries(IGNORE_CONFIG_PATH, entries));
|
||||
|
||||
const applyWarning = (message: string): void => {
|
||||
queue.finishRefresh([message]);
|
||||
@@ -163,7 +164,9 @@ export const createUpdateCenterService = (
|
||||
try {
|
||||
const ignoredEntries = await loadIgnored();
|
||||
const loadedItems = normalizeLoadedItems(await options.loadItems());
|
||||
const items = applyIgnoredEntries(loadedItems.items, ignoredEntries);
|
||||
const items = sortIgnoredItems(
|
||||
applyIgnoredEntries(loadedItems.items, ignoredEntries),
|
||||
);
|
||||
queue.setItems(items);
|
||||
queue.finishRefresh(loadedItems.warnings);
|
||||
return emit();
|
||||
|
||||
@@ -324,7 +324,8 @@ bool AppDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
|
||||
QRect unignoreButtonRect(option.rect.right() - 80, option.rect.top() + (option.rect.height() - 30) / 2, 70, 30);
|
||||
if (unignoreButtonRect.contains(mouseEvent->pos())) {
|
||||
// 发送取消忽略信号
|
||||
emit unignoreApp(packageName);
|
||||
QString newVersion = index.data(Qt::UserRole + 3).toString();
|
||||
emit unignoreApp(packageName, newVersion);
|
||||
return true;
|
||||
}
|
||||
return true; // 消耗其他事件,不允许其他交互
|
||||
@@ -369,8 +370,8 @@ bool AppDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
|
||||
// 检查是否点击了忽略按钮
|
||||
QRect ignoreButtonRect(rect.right() - 160, rect.top() + (rect.height() - 30) / 2, 70, 30);
|
||||
if (ignoreButtonRect.contains(mouseEvent->pos())) {
|
||||
QString currentVersion = index.data(Qt::UserRole + 2).toString();
|
||||
emit ignoreApp(packageName, currentVersion);
|
||||
QString newVersion = index.data(Qt::UserRole + 3).toString();
|
||||
emit ignoreApp(packageName, newVersion);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ signals:
|
||||
void updateDisplay(const QString &packageName);
|
||||
void updateFinished(bool success); //传递是否完成更新
|
||||
void ignoreApp(const QString &packageName, const QString &version); // 新增:忽略应用信号
|
||||
void unignoreApp(const QString &packageName); // 新增:取消忽略应用信号
|
||||
void unignoreApp(const QString &packageName, const QString &version); // 新增:取消忽略应用信号
|
||||
|
||||
private slots:
|
||||
void updateSpinner(); // 新增槽函数
|
||||
|
||||
@@ -4,40 +4,19 @@
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QDebug>
|
||||
#include <unistd.h> // for geteuid
|
||||
|
||||
IgnoreConfig::IgnoreConfig(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
// 设置配置文件路径
|
||||
QString configDir;
|
||||
QByteArray sudoUserHomeEnv = qgetenv("SUDO_USER_HOME");
|
||||
|
||||
// // 检查是否以 root 权限运行
|
||||
// if (geteuid() == 0) {
|
||||
// // 首先检查是否有 SUDO_USER_HOME 环境变量(表示是通过 pkexec 提权的普通用户)
|
||||
// QByteArray sudoUserHomeEnv = qgetenv("SUDO_USER_HOME");
|
||||
// if (!sudoUserHomeEnv.isEmpty()) {
|
||||
// // 通过 pkexec 提权的普通用户,使用原用户的配置目录
|
||||
// QString sudoUserHomePath = QString::fromLocal8Bit(sudoUserHomeEnv);
|
||||
// configDir = sudoUserHomePath + "/.config";
|
||||
// } else {
|
||||
// // 获取实际的 HOME 目录来判断是真正的 root 用户还是其他方式提权的用户
|
||||
// QByteArray homeEnv = qgetenv("HOME");
|
||||
// QString homePath = QString::fromLocal8Bit(homeEnv);
|
||||
if (!sudoUserHomeEnv.isEmpty()) {
|
||||
configDir = QString::fromLocal8Bit(sudoUserHomeEnv) + "/.config";
|
||||
} else {
|
||||
configDir = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation);
|
||||
}
|
||||
|
||||
// if (homePath == "/root") {
|
||||
// // 真正的 root 用户,使用 /root/.config
|
||||
// configDir = "/root/.config";
|
||||
// } else {
|
||||
// // 其他方式提权的用户,使用 HOME 目录下的配置
|
||||
// configDir = homePath + "/.config";
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// // 普通用户,使用标准配置目录
|
||||
// configDir = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation);
|
||||
// }
|
||||
configDir = "/etc/";
|
||||
QDir dir(configDir);
|
||||
if (!dir.exists()) {
|
||||
dir.mkpath(".");
|
||||
@@ -64,17 +43,9 @@ void IgnoreConfig::addIgnoredApp(const QString &packageName, const QString &vers
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
void IgnoreConfig::removeIgnoredApp(const QString &packageName)
|
||||
void IgnoreConfig::removeIgnoredApp(const QString &packageName, const QString &version)
|
||||
{
|
||||
// 移除所有该包名的版本
|
||||
auto it = m_ignoredApps.begin();
|
||||
while (it != m_ignoredApps.end()) {
|
||||
if (it->first == packageName) {
|
||||
it = m_ignoredApps.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
m_ignoredApps.remove(qMakePair(packageName, version));
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ public:
|
||||
void addIgnoredApp(const QString &packageName, const QString &version);
|
||||
|
||||
// 移除忽略的应用
|
||||
void removeIgnoredApp(const QString &packageName);
|
||||
void removeIgnoredApp(const QString &packageName, const QString &version);
|
||||
|
||||
// 检查应用是否被忽略
|
||||
bool isAppIgnored(const QString &packageName, const QString &version) const;
|
||||
|
||||
@@ -238,10 +238,10 @@ void MainWindow::checkUpdates()
|
||||
for (const auto &item : updateInfo) {
|
||||
QJsonObject obj = item.toObject();
|
||||
QString packageName = obj["package"].toString();
|
||||
QString currentVersion = obj["current_version"].toString();
|
||||
QString newVersion = obj["new_version"].toString();
|
||||
|
||||
// 检查应用是否被忽略
|
||||
if (m_ignoreConfig->isAppIgnored(packageName, currentVersion)) {
|
||||
if (m_ignoreConfig->isAppIgnored(packageName, newVersion)) {
|
||||
// 标记为忽略状态
|
||||
obj["ignored"] = true;
|
||||
ignoredApps.append(obj);
|
||||
@@ -468,9 +468,9 @@ void MainWindow::onIgnoreApp(const QString &packageName, const QString &version)
|
||||
}
|
||||
|
||||
// 新增:处理取消忽略应用的槽函数
|
||||
void MainWindow::onUnignoreApp(const QString &packageName) {
|
||||
void MainWindow::onUnignoreApp(const QString &packageName, const QString &version) {
|
||||
// 从忽略配置中移除应用
|
||||
m_ignoreConfig->removeIgnoredApp(packageName);
|
||||
m_ignoreConfig->removeIgnoredApp(packageName, version);
|
||||
|
||||
// 更新模型中应用的状态
|
||||
QJsonArray updatedApps;
|
||||
|
||||
@@ -43,6 +43,6 @@ private slots:
|
||||
void handleUpdateFinished(bool success); // 新增:处理更新完成的槽函数
|
||||
void handleSelectionChanged(); // 新增:处理选择变化的槽函数
|
||||
void onIgnoreApp(const QString &packageName, const QString &version); // 新增:处理忽略应用的槽函数
|
||||
void onUnignoreApp(const QString &packageName); // 新增:处理取消忽略应用
|
||||
void onUnignoreApp(const QString &packageName, const QString &version); // 新增:处理取消忽略应用
|
||||
};
|
||||
#endif // MAINWINDOW_H
|
||||
@@ -69,10 +69,14 @@ const createStore = (
|
||||
selectedTaskKeys,
|
||||
snapshot,
|
||||
filteredItems: computed(() => snapshot.value.items),
|
||||
allSelected: computed(() => false),
|
||||
someSelected: computed(() => selectedTaskKeys.value.size > 0),
|
||||
bind: vi.fn(),
|
||||
unbind: vi.fn(),
|
||||
open: vi.fn(),
|
||||
refresh: vi.fn(),
|
||||
ignoreItem: vi.fn(),
|
||||
unignoreItem: vi.fn(),
|
||||
toggleSelection: vi.fn(),
|
||||
getSelectedItems: vi.fn(() =>
|
||||
snapshot.value.items.filter(
|
||||
@@ -87,7 +91,7 @@ const createStore = (
|
||||
};
|
||||
|
||||
describe("UpdateCenterModal", () => {
|
||||
it("renders source tags, running state, warnings, migration marker, and close confirmation", () => {
|
||||
it("renders source tags, running state, warnings, and migration marker", () => {
|
||||
const store = createStore();
|
||||
|
||||
render(UpdateCenterModal, {
|
||||
@@ -104,24 +108,6 @@ describe("UpdateCenterModal", () => {
|
||||
expect(screen.getByText("更新过程中请勿关闭商店")).toBeTruthy();
|
||||
expect(screen.getByText("下载中")).toBeTruthy();
|
||||
expect(screen.getByText("42%")).toBeTruthy();
|
||||
expect(screen.getByText(/确定关闭/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("close confirmation exposes a confirm-close path", async () => {
|
||||
const onConfirmClose = vi.fn();
|
||||
const store = createStore();
|
||||
|
||||
render(UpdateCenterModal, {
|
||||
props: {
|
||||
show: true,
|
||||
store,
|
||||
onConfirmClose,
|
||||
},
|
||||
});
|
||||
|
||||
await fireEvent.click(screen.getByRole("button", { name: "确认关闭" }));
|
||||
|
||||
expect(onConfirmClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("renders ignored items as disabled instead of normal selectable actions", () => {
|
||||
@@ -148,7 +134,34 @@ describe("UpdateCenterModal", () => {
|
||||
});
|
||||
|
||||
expect(screen.getByText("已忽略")).toBeTruthy();
|
||||
expect(screen.getByRole("checkbox")).toBeDisabled();
|
||||
expect(screen.getAllByRole("checkbox").at(-1)).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "取消忽略" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders ignore action for normal items", () => {
|
||||
const store = createStore({
|
||||
items: [
|
||||
createItem({
|
||||
taskKey: "aptss:spark-weather",
|
||||
packageName: "spark-weather",
|
||||
displayName: "Spark Weather",
|
||||
source: "aptss",
|
||||
ignored: false,
|
||||
}),
|
||||
],
|
||||
tasks: [],
|
||||
warnings: [],
|
||||
hasRunningTasks: false,
|
||||
});
|
||||
|
||||
render(UpdateCenterModal, {
|
||||
props: {
|
||||
show: true,
|
||||
store,
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByRole("button", { name: "忽略更新" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders migration confirmation when requested", () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
@@ -6,7 +7,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { UpdateCenterItem } from "../../../../electron/main/backend/update-center/types";
|
||||
import {
|
||||
LEGACY_IGNORE_CONFIG_PATH,
|
||||
IGNORE_CONFIG_PATH,
|
||||
applyIgnoredEntries,
|
||||
createIgnoreKey,
|
||||
loadIgnoredEntries,
|
||||
@@ -15,9 +16,9 @@ import {
|
||||
} from "../../../../electron/main/backend/update-center/ignore-config";
|
||||
|
||||
describe("update-center ignore config", () => {
|
||||
it("round-trips the legacy package|version format", async () => {
|
||||
expect(LEGACY_IGNORE_CONFIG_PATH).toBe(
|
||||
"/etc/spark-store/ignored_apps.conf",
|
||||
it("round-trips the package|version format at the user config path", async () => {
|
||||
expect(IGNORE_CONFIG_PATH).toBe(
|
||||
join(homedir(), ".config", "spark-store", "ignored_apps.conf"),
|
||||
);
|
||||
|
||||
const entries = new Set([
|
||||
|
||||
@@ -24,6 +24,8 @@ const createSnapshot = (overrides = {}) => ({
|
||||
describe("updateCenter store", () => {
|
||||
const open = vi.fn();
|
||||
const refresh = vi.fn();
|
||||
const ignore = vi.fn();
|
||||
const unignore = vi.fn();
|
||||
const start = vi.fn();
|
||||
const onState = vi.fn();
|
||||
const offState = vi.fn();
|
||||
@@ -31,6 +33,8 @@ describe("updateCenter store", () => {
|
||||
beforeEach(() => {
|
||||
open.mockReset();
|
||||
refresh.mockReset();
|
||||
ignore.mockReset();
|
||||
unignore.mockReset();
|
||||
start.mockReset();
|
||||
onState.mockReset();
|
||||
offState.mockReset();
|
||||
@@ -41,8 +45,8 @@ describe("updateCenter store", () => {
|
||||
value: {
|
||||
open,
|
||||
refresh,
|
||||
ignore: vi.fn(),
|
||||
unignore: vi.fn(),
|
||||
ignore,
|
||||
unignore,
|
||||
start,
|
||||
cancel: vi.fn(),
|
||||
getState: vi.fn(),
|
||||
@@ -132,6 +136,25 @@ describe("updateCenter store", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards ignore and unignore actions with the package and target version", async () => {
|
||||
const snapshot = createSnapshot();
|
||||
open.mockResolvedValue(snapshot);
|
||||
const store = createUpdateCenterStore();
|
||||
|
||||
await store.open();
|
||||
await store.ignoreItem("spark-weather", "2.0.0");
|
||||
await store.unignoreItem("spark-weather", "2.0.0");
|
||||
|
||||
expect(ignore).toHaveBeenCalledWith({
|
||||
packageName: "spark-weather",
|
||||
newVersion: "2.0.0",
|
||||
});
|
||||
expect(unignore).toHaveBeenCalledWith({
|
||||
packageName: "spark-weather",
|
||||
newVersion: "2.0.0",
|
||||
});
|
||||
});
|
||||
|
||||
it("assigns update-center download ids from a separate range", async () => {
|
||||
downloads.value = [
|
||||
{
|
||||
@@ -178,8 +201,8 @@ describe("updateCenter store", () => {
|
||||
|
||||
store.requestClose();
|
||||
|
||||
expect(store.isOpen.value).toBe(true);
|
||||
expect(store.showCloseConfirm.value).toBe(true);
|
||||
expect(store.isOpen.value).toBe(false);
|
||||
expect(store.showCloseConfirm.value).toBe(false);
|
||||
});
|
||||
|
||||
it("applies pushed snapshots from the main process", () => {
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
:tasks="store.snapshot.value.tasks"
|
||||
:selected-task-keys="store.selectedTaskKeys.value"
|
||||
@toggle-selection="emit('toggle-selection', $event)"
|
||||
@ignore-item="store.ignoreItem"
|
||||
@unignore-item="store.unignoreItem"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -63,6 +63,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
v-if="item.ignored === true"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-2xl border border-slate-300/80 px-3 py-2 text-sm font-semibold text-slate-600 transition hover:bg-slate-50 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
|
||||
aria-label="取消忽略"
|
||||
@click.stop="$emit('unignore-item')"
|
||||
>
|
||||
<i class="fas fa-rotate-left"></i>
|
||||
取消忽略
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-2xl border border-amber-300/80 px-3 py-2 text-sm font-semibold text-amber-700 transition hover:bg-amber-50 dark:border-amber-500/40 dark:text-amber-300 dark:hover:bg-amber-500/10"
|
||||
aria-label="忽略更新"
|
||||
@click.stop="$emit('ignore-item')"
|
||||
>
|
||||
<i class="fas fa-eye-slash"></i>
|
||||
忽略更新
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="showProgress" class="space-y-2">
|
||||
<div
|
||||
class="h-2 overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800"
|
||||
@@ -96,6 +119,8 @@ const iconIndex = ref(0);
|
||||
|
||||
defineEmits<{
|
||||
(e: "toggle-selection"): void;
|
||||
(e: "ignore-item"): void;
|
||||
(e: "unignore-item"): void;
|
||||
}>();
|
||||
|
||||
const normalizeIconSrc = (icon: string): string => {
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
:task="taskMap.get(item.taskKey)"
|
||||
:selected="selectedTaskKeys.has(item.taskKey)"
|
||||
@toggle-selection="$emit('toggle-selection', item.taskKey)"
|
||||
@ignore-item="$emit('ignore-item', item.packageName, item.newVersion)"
|
||||
@unignore-item="
|
||||
$emit('unignore-item', item.packageName, item.newVersion)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,6 +43,8 @@ const props = defineProps<{
|
||||
|
||||
defineEmits<{
|
||||
(e: "toggle-selection", taskKey: string): void;
|
||||
(e: "ignore-item", packageName: string, newVersion: string): void;
|
||||
(e: "unignore-item", packageName: string, newVersion: string): void;
|
||||
}>();
|
||||
|
||||
const taskMap = computed(() => {
|
||||
|
||||
@@ -30,6 +30,8 @@ export interface UpdateCenterStore {
|
||||
unbind: () => void;
|
||||
open: () => Promise<void>;
|
||||
refresh: () => Promise<void>;
|
||||
ignoreItem: (packageName: string, newVersion: string) => Promise<void>;
|
||||
unignoreItem: (packageName: string, newVersion: string) => Promise<void>;
|
||||
toggleSelection: (taskKey: string) => void;
|
||||
toggleSelectAll: () => void;
|
||||
getSelectedItems: () => UpdateCenterItem[];
|
||||
@@ -139,6 +141,20 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
||||
applySnapshot(nextSnapshot);
|
||||
};
|
||||
|
||||
const ignoreItem = async (
|
||||
packageName: string,
|
||||
newVersion: string,
|
||||
): Promise<void> => {
|
||||
await window.updateCenter.ignore({ packageName, newVersion });
|
||||
};
|
||||
|
||||
const unignoreItem = async (
|
||||
packageName: string,
|
||||
newVersion: string,
|
||||
): Promise<void> => {
|
||||
await window.updateCenter.unignore({ packageName, newVersion });
|
||||
};
|
||||
|
||||
const toggleSelection = (taskKey: string): void => {
|
||||
const item = snapshot.value.items.find(
|
||||
(entry) => entry.taskKey === taskKey,
|
||||
@@ -260,6 +276,8 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
||||
unbind,
|
||||
open,
|
||||
refresh,
|
||||
ignoreItem,
|
||||
unignoreItem,
|
||||
toggleSelection,
|
||||
toggleSelectAll,
|
||||
getSelectedItems,
|
||||
|
||||
@@ -7,8 +7,12 @@ load_transhell_debug
|
||||
|
||||
# 发送通知
|
||||
function notify-send() {
|
||||
# Detect user using the display
|
||||
local user=$(who | awk '{print $1}' | head -n 1)
|
||||
local user
|
||||
user=$(detect-notify-user)
|
||||
|
||||
if [ -z "$user" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Detect uid of the user
|
||||
local uid=$(id -u $user)
|
||||
@@ -16,6 +20,72 @@ function notify-send() {
|
||||
sudo -u $user DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${uid}/bus notify-send "$@"
|
||||
}
|
||||
|
||||
function detect-notify-user() {
|
||||
local user
|
||||
|
||||
user=$(who | awk '{print $1}' | head -n 1)
|
||||
if [ -n "$user" ]; then
|
||||
echo "$user"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v loginctl >/dev/null 2>&1; then
|
||||
user=$(loginctl list-sessions --no-legend 2>/dev/null | awk 'NR == 1 {print $3}')
|
||||
if [ -n "$user" ]; then
|
||||
echo "$user"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
function load-ignored-apps() {
|
||||
declare -gA ignored_apps=()
|
||||
local config_paths=()
|
||||
declare -A seen_config_paths=()
|
||||
local user
|
||||
local user_home
|
||||
local config_path
|
||||
|
||||
user=$(detect-notify-user)
|
||||
if [ -n "$user" ]; then
|
||||
user_home=$(getent passwd "$user" | cut -d: -f6)
|
||||
if [ -n "$user_home" ] && [ -d "$user_home" ]; then
|
||||
config_path="$user_home/.config/spark-store/ignored_apps.conf"
|
||||
if [ -f "$config_path" ] && [ -z "${seen_config_paths["$config_path"]}" ]; then
|
||||
config_paths+=("$config_path")
|
||||
seen_config_paths["$config_path"]=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
local home_dir
|
||||
for home_dir in /home/*; do
|
||||
if [ ! -d "$home_dir" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
config_path="$home_dir/.config/spark-store/ignored_apps.conf"
|
||||
if [ -f "$config_path" ] && [ -z "${seen_config_paths["$config_path"]}" ]; then
|
||||
config_paths+=("$config_path")
|
||||
seen_config_paths["$config_path"]=1
|
||||
fi
|
||||
done
|
||||
|
||||
local pkg_name
|
||||
local pkg_version
|
||||
for config_path in "${config_paths[@]}"; do
|
||||
while IFS='|' read -r pkg_name pkg_version || [ -n "$pkg_name" ]; do
|
||||
pkg_name=$(printf '%s' "$pkg_name" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
pkg_version=$(printf '%s' "$pkg_version" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
if [ -n "$pkg_name" ] && [ -n "$pkg_version" ]; then
|
||||
ignored_apps["$pkg_name|$pkg_version"]=1
|
||||
fi
|
||||
done < "$config_path"
|
||||
done
|
||||
}
|
||||
|
||||
# 检测网络链接畅通
|
||||
function network-check() {
|
||||
# 超时时间
|
||||
@@ -80,17 +150,7 @@ if [ "$update_app_number" -le 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 读取忽略列表到数组
|
||||
declare -A ignored_apps
|
||||
if [ -f "/etc/spark-store/ignored_apps.conf" ]; then
|
||||
while IFS='|' read -r pkg_name pkg_version || [ -n "$pkg_name" ]; do
|
||||
# 去除前后空白字符
|
||||
pkg_name=$(echo "$pkg_name" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
if [ -n "$pkg_name" ]; then
|
||||
ignored_apps["$pkg_name"]=1
|
||||
fi
|
||||
done < "/etc/spark-store/ignored_apps.conf"
|
||||
fi
|
||||
load-ignored-apps
|
||||
|
||||
# 获取用户选择的要更新的应用
|
||||
PKG_LIST="$(/opt/durapps/spark-store/bin/update-upgrade/ss-do-upgrade-worker.sh upgradable-list)"
|
||||
@@ -118,7 +178,7 @@ for line in $PKG_LIST; do
|
||||
fi
|
||||
|
||||
# 检测是否在忽略列表中
|
||||
if [ -n "${ignored_apps[$PKG_NAME]}" ]; then
|
||||
if [ -n "${ignored_apps["$PKG_NAME|$PKG_NEW_VER"]}" ]; then
|
||||
let update_app_number=$update_app_number-1
|
||||
continue
|
||||
fi
|
||||
@@ -135,8 +195,8 @@ update_transhell
|
||||
# TODO: 除了apt-mark hold之外额外有一个禁止检查列表
|
||||
# 如果不想提示就不提示
|
||||
|
||||
user=$(who | awk '{print $1}' | head -n 1)
|
||||
if [ -e "/home/$user/.config/spark-union/spark-store/ssshell-config-do-not-show-upgrade-notify" ]; then
|
||||
user=$(detect-notify-user)
|
||||
if [ -n "$user" ] && [ -e "/home/$user/.config/spark-union/spark-store/ssshell-config-do-not-show-upgrade-notify" ]; then
|
||||
echo "他不想站在世界之巅,好吧"
|
||||
echo "Okay he don't want to be at the top of the world, okay"
|
||||
exit
|
||||
|
||||
Reference in New Issue
Block a user