mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-09-20 21:50:11 +08:00
feat(update-center): 支持被系统锁定(hold)包的强制安装与更新中心体验优化
- 标记 apt-mark hold 的 spark(aptss) 源包,默认禁用勾选并提示开启强制安装 - 更新中心发起更新时透传 forceHeld,后端 Spark 本地 deb 走 shell-caller force-ssinstall 分支(单次免密 pkexec:unhold→ssinstall→hold),避免二次提权 - 无可选项时禁用全选并提示"无可更新项";更新选中后从列表中移除已启动项 - DownloadDetail 紧凑布局避免整页滚动;Toolbar 全选与更新选中按钮同行 - 继承 PKGNAME_PATTERN 与 metalinkUrl 白名单(https+*.spark-app.store+拒..)纵深防御 - apm 源无 hold 概念,forceHeld 在 apm 分支忽略(正确行为)
This commit is contained in:
@@ -219,9 +219,15 @@ window.ipcRenderer.on("install-log", (_event, log: InstallLog) => {
|
||||
window.ipcRenderer.on("install-complete", (_event, log: DownloadResult) => {
|
||||
const downloadObj = downloads.value.find((d) => d.id === log.id);
|
||||
if (downloadObj) {
|
||||
// 更新中心来源的任务用「更新」语义,其余(普通安装)用「安装」语义,
|
||||
// 避免把安装结果误报为「下载完成」。
|
||||
const isUpdate = (downloadObj.source ?? "").toLowerCase().includes("update");
|
||||
const doneLabel = isUpdate ? "更新完成" : "安装完成";
|
||||
const failVerb = isUpdate ? "更新失败" : "安装失败";
|
||||
|
||||
if (log.success) {
|
||||
downloadObj.status = "completed";
|
||||
downloadObj.logs.push({ time: Date.now(), message: "下载完成" });
|
||||
downloadObj.logs.push({ time: Date.now(), message: doneLabel });
|
||||
} else {
|
||||
downloadObj.status = "failed";
|
||||
// 将失败原因写入日志,避免 UI 停在"正在获取 Metalink"等中间日志后突兀结束
|
||||
@@ -232,7 +238,10 @@ window.ipcRenderer.on("install-complete", (_event, log: DownloadResult) => {
|
||||
} catch {
|
||||
reason = log.message || reason;
|
||||
}
|
||||
downloadObj.logs.push({ time: Date.now(), message: `下载失败: ${reason}` });
|
||||
downloadObj.logs.push({
|
||||
time: Date.now(),
|
||||
message: `${failVerb}: ${reason}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -24,10 +24,12 @@ export interface UpdateCenterStore {
|
||||
showMigrationConfirm: Ref<boolean>;
|
||||
searchQuery: Ref<string>;
|
||||
selectedTaskKeys: Ref<Set<string>>;
|
||||
forcedTaskKeys: Ref<Set<string>>;
|
||||
snapshot: Ref<UpdateCenterSnapshot>;
|
||||
filteredItems: ComputedRef<UpdateCenterItem[]>;
|
||||
allSelected: ComputedRef<boolean>;
|
||||
someSelected: ComputedRef<boolean>;
|
||||
selectableCount: ComputedRef<number>;
|
||||
bind: () => void;
|
||||
unbind: () => void;
|
||||
open: (storeFilter?: StoreFilter) => Promise<void>;
|
||||
@@ -35,6 +37,7 @@ export interface UpdateCenterStore {
|
||||
ignoreItem: (packageName: string, newVersion: string) => Promise<void>;
|
||||
unignoreItem: (packageName: string, newVersion: string) => Promise<void>;
|
||||
toggleSelection: (taskKey: string) => void;
|
||||
toggleForce: (taskKey: string) => void;
|
||||
toggleSelectAll: () => void;
|
||||
getSelectedItems: () => UpdateCenterItem[];
|
||||
closeNow: () => void;
|
||||
@@ -60,21 +63,32 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
||||
const showMigrationConfirm = ref(false);
|
||||
const searchQuery = ref("");
|
||||
const selectedTaskKeys = ref(new Set<string>());
|
||||
// 强制安装集合:仅当前会话内有效(刷新/重开更新中心即重置),不持久化。
|
||||
// 仅对被系统锁定(held)的项有意义:开启后该 held 项可被单独选中并升级。
|
||||
const forcedTaskKeys = ref(new Set<string>());
|
||||
const snapshot = ref<UpdateCenterSnapshot>(EMPTY_SNAPSHOT);
|
||||
let lastStoreFilter: StoreFilter = "both";
|
||||
|
||||
// 判断某 item 是否可被选中:未被忽略,且(未被锁定 或 已开启强制安装)
|
||||
const isSelectable = (item: UpdateCenterItem): boolean => {
|
||||
if (item.ignored === true) return false;
|
||||
if (item.held === true && !forcedTaskKeys.value.has(item.taskKey)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const resetSessionState = (): void => {
|
||||
showCloseConfirm.value = false;
|
||||
showMigrationConfirm.value = false;
|
||||
searchQuery.value = "";
|
||||
selectedTaskKeys.value = new Set();
|
||||
forcedTaskKeys.value = new Set();
|
||||
};
|
||||
|
||||
const applySnapshot = (nextSnapshot: UpdateCenterSnapshot): void => {
|
||||
const selectableTaskKeys = new Set(
|
||||
nextSnapshot.items
|
||||
.filter((item) => item.ignored !== true)
|
||||
.map((item) => item.taskKey),
|
||||
nextSnapshot.items.filter(isSelectable).map((item) => item.taskKey),
|
||||
);
|
||||
selectedTaskKeys.value = new Set(
|
||||
[...selectedTaskKeys.value].filter((taskKey) =>
|
||||
@@ -85,7 +99,7 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
||||
};
|
||||
|
||||
const selectableItems = computed(() =>
|
||||
snapshot.value.items.filter((item) => item.ignored !== true),
|
||||
snapshot.value.items.filter(isSelectable),
|
||||
);
|
||||
|
||||
const filteredItems = computed(() => {
|
||||
@@ -93,10 +107,11 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
||||
const matched = snapshot.value.items.filter((item) =>
|
||||
matchesSearch(item, query),
|
||||
);
|
||||
// 已忽略项沉底:非忽略在前、已忽略在后,各自保持原有顺序
|
||||
// 已忽略项沉底:可选(含已强制的锁定项)在前、不可选(已忽略 / 未强制的锁定项)在后,
|
||||
// 各自保持原有顺序
|
||||
return [
|
||||
...matched.filter((item) => item.ignored !== true),
|
||||
...matched.filter((item) => item.ignored === true),
|
||||
...matched.filter(isSelectable),
|
||||
...matched.filter((item) => !isSelectable(item)),
|
||||
];
|
||||
});
|
||||
|
||||
@@ -116,6 +131,9 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
||||
);
|
||||
});
|
||||
|
||||
// 可选项数量:被忽略 / 被锁定未强制的项不计入。用于全选按钮可用性判断。
|
||||
const selectableCount = computed(() => selectableItems.value.length);
|
||||
|
||||
const handleState = (nextSnapshot: UpdateCenterSnapshot): void => {
|
||||
applySnapshot(nextSnapshot);
|
||||
};
|
||||
@@ -240,7 +258,7 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
||||
const item = snapshot.value.items.find(
|
||||
(entry) => entry.taskKey === taskKey,
|
||||
);
|
||||
if (!item || item.ignored === true) {
|
||||
if (!item || !isSelectable(item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -254,6 +272,32 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
||||
selectedTaskKeys.value = nextSelection;
|
||||
};
|
||||
|
||||
// 切换「强制安装」开关(仅对被系统锁定的包有意义)。
|
||||
// 关闭强制时,若该包已被选中则同步取消其选中态。
|
||||
const toggleForce = (taskKey: string): void => {
|
||||
const item = snapshot.value.items.find(
|
||||
(entry) => entry.taskKey === taskKey,
|
||||
);
|
||||
if (!item || item.held !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextForced = new Set(forcedTaskKeys.value);
|
||||
if (nextForced.has(taskKey)) {
|
||||
nextForced.delete(taskKey);
|
||||
// 取消强制后该包不可再被选中,移出选中集
|
||||
if (selectedTaskKeys.value.has(taskKey)) {
|
||||
const nextSelection = new Set(selectedTaskKeys.value);
|
||||
nextSelection.delete(taskKey);
|
||||
selectedTaskKeys.value = nextSelection;
|
||||
}
|
||||
} else {
|
||||
nextForced.add(taskKey);
|
||||
}
|
||||
|
||||
forcedTaskKeys.value = nextForced;
|
||||
};
|
||||
|
||||
const toggleSelectAll = (): void => {
|
||||
const selectable = selectableItems.value;
|
||||
if (allSelected.value) {
|
||||
@@ -266,7 +310,7 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
||||
const getSelectedItems = (): UpdateCenterItem[] => {
|
||||
return snapshot.value.items.filter(
|
||||
(item) =>
|
||||
selectedTaskKeys.value.has(item.taskKey) && item.ignored !== true,
|
||||
selectedTaskKeys.value.has(item.taskKey) && isSelectable(item),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -328,6 +372,7 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
||||
startTasks.push({
|
||||
taskKey: item.taskKey,
|
||||
id: downloadId,
|
||||
forceHeld: item.held === true,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -351,10 +396,12 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
||||
showMigrationConfirm,
|
||||
searchQuery,
|
||||
selectedTaskKeys,
|
||||
forcedTaskKeys,
|
||||
snapshot,
|
||||
filteredItems,
|
||||
allSelected,
|
||||
someSelected,
|
||||
selectableCount,
|
||||
bind,
|
||||
unbind,
|
||||
open,
|
||||
@@ -362,6 +409,7 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
||||
ignoreItem,
|
||||
unignoreItem,
|
||||
toggleSelection,
|
||||
toggleForce,
|
||||
toggleSelectAll,
|
||||
getSelectedItems,
|
||||
closeNow,
|
||||
|
||||
Reference in New Issue
Block a user