mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-05-30 01:31:06 +08:00
fix(sources): hide unavailable update and management entries
This commit is contained in:
+69
-7
@@ -20,6 +20,7 @@
|
||||
:active-category="activeCategory"
|
||||
:category-counts="categoryCounts"
|
||||
:theme-mode="themeMode"
|
||||
:spark-available="sparkAvailable"
|
||||
:apm-available="apmAvailable"
|
||||
:store-filter="storeFilter"
|
||||
@toggle-theme="toggleTheme"
|
||||
@@ -120,6 +121,7 @@
|
||||
:error="installedError"
|
||||
:active-origin="activeInstalledOrigin"
|
||||
:store-filter="storeFilter"
|
||||
:spark-available="sparkAvailable"
|
||||
:apm-available="apmAvailable"
|
||||
@close="closeInstalledModal"
|
||||
@refresh="refreshInstalledApps"
|
||||
@@ -192,6 +194,12 @@ import {
|
||||
rankAppsBySearch,
|
||||
} from "./modules/appSearch";
|
||||
import { handleInstall, handleRetry } from "./modules/processInstall";
|
||||
import {
|
||||
getAllowedInstalledOrigin,
|
||||
getEffectiveStoreFilter,
|
||||
getDefaultInstalledOrigin,
|
||||
isOriginEnabled,
|
||||
} from "./modules/storeFilter";
|
||||
import { createUpdateCenterStore } from "./modules/updateCenter";
|
||||
import type {
|
||||
App,
|
||||
@@ -264,10 +272,18 @@ const showUninstallModal = ref(false);
|
||||
const uninstallTargetApp: Ref<App | null> = ref(null);
|
||||
const showAboutModal = ref(false);
|
||||
const showSettingsModal = ref(false);
|
||||
const sparkAvailable = ref(false);
|
||||
const apmAvailable = ref(false);
|
||||
|
||||
/** 启动参数 --no-apm => 仅 Spark;--no-spark => 仅 APM;由主进程 IPC 提供 */
|
||||
const storeFilter = ref<"spark" | "apm" | "both">("both");
|
||||
const availableSources = computed(() => ({
|
||||
spark: sparkAvailable.value,
|
||||
apm: apmAvailable.value,
|
||||
}));
|
||||
const effectiveStoreFilter = computed(() =>
|
||||
getEffectiveStoreFilter(storeFilter.value, availableSources.value),
|
||||
);
|
||||
|
||||
// 计算属性
|
||||
const baseApps = computed(() => {
|
||||
@@ -761,7 +777,11 @@ const handleList = () => {
|
||||
|
||||
const openUpdateModal = async () => {
|
||||
try {
|
||||
await updateCenterStore.open();
|
||||
if (!effectiveStoreFilter.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateCenterStore.open(effectiveStoreFilter.value);
|
||||
} catch (error) {
|
||||
logger.error(`打开更新中心失败: ${error}`);
|
||||
}
|
||||
@@ -791,11 +811,21 @@ const confirmMigrationStart = async () => {
|
||||
};
|
||||
|
||||
const openInstalledModal = () => {
|
||||
showInstalledModal.value = true;
|
||||
// 如果没有 APM 可用,默认切换到 Spark 应用管理
|
||||
if (!apmAvailable.value && activeInstalledOrigin.value === "apm") {
|
||||
activeInstalledOrigin.value = "spark";
|
||||
const defaultOrigin = getDefaultInstalledOrigin(
|
||||
storeFilter.value,
|
||||
availableSources.value,
|
||||
);
|
||||
if (!defaultOrigin) {
|
||||
return;
|
||||
}
|
||||
|
||||
showInstalledModal.value = true;
|
||||
activeInstalledOrigin.value =
|
||||
getAllowedInstalledOrigin(
|
||||
storeFilter.value,
|
||||
activeInstalledOrigin.value,
|
||||
availableSources.value,
|
||||
) ?? defaultOrigin;
|
||||
refreshInstalledApps();
|
||||
};
|
||||
|
||||
@@ -804,7 +834,12 @@ const closeInstalledModal = () => {
|
||||
};
|
||||
|
||||
const handleSwitchOrigin = (origin: "apm" | "spark") => {
|
||||
activeInstalledOrigin.value = origin;
|
||||
activeInstalledOrigin.value =
|
||||
getAllowedInstalledOrigin(
|
||||
storeFilter.value,
|
||||
origin,
|
||||
availableSources.value,
|
||||
) ?? activeInstalledOrigin.value;
|
||||
refreshInstalledApps();
|
||||
};
|
||||
|
||||
@@ -812,7 +847,24 @@ const refreshInstalledApps = async () => {
|
||||
installedLoading.value = true;
|
||||
installedError.value = "";
|
||||
try {
|
||||
const origin = activeInstalledOrigin.value;
|
||||
const origin = getAllowedInstalledOrigin(
|
||||
storeFilter.value,
|
||||
activeInstalledOrigin.value,
|
||||
availableSources.value,
|
||||
);
|
||||
if (!origin) {
|
||||
installedApps.value = [];
|
||||
installedError.value = "当前系统不可用应用管理功能";
|
||||
return;
|
||||
}
|
||||
|
||||
activeInstalledOrigin.value = origin;
|
||||
|
||||
if (!isOriginEnabled(storeFilter.value, origin)) {
|
||||
installedApps.value = [];
|
||||
installedError.value = `当前启动模式已禁用 ${origin === "spark" ? "Spark" : "APM"} 软件管理`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Spark 优化:只检查远端商店目录中的应用,避免全量扫描
|
||||
let pkgnameList: string[] | undefined;
|
||||
@@ -1151,11 +1203,21 @@ onMounted(async () => {
|
||||
// 从主进程获取启动参数(--no-apm / --no-spark),再加载数据
|
||||
storeFilter.value = await window.ipcRenderer.invoke("get-store-filter");
|
||||
|
||||
if (storeFilter.value !== "apm") {
|
||||
sparkAvailable.value = await window.ipcRenderer.invoke(
|
||||
"check-spark-available",
|
||||
);
|
||||
}
|
||||
|
||||
// 检查 apm 是否可用
|
||||
if (storeFilter.value !== "spark") {
|
||||
apmAvailable.value = await window.ipcRenderer.invoke("check-apm-available");
|
||||
}
|
||||
|
||||
activeInstalledOrigin.value =
|
||||
getDefaultInstalledOrigin(storeFilter.value, availableSources.value) ??
|
||||
"spark";
|
||||
|
||||
await loadCategories();
|
||||
|
||||
// 分类目录加载后,并行加载主页数据和所有应用列表
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { render, screen } from "@testing-library/vue";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import AppSidebar from "@/components/AppSidebar.vue";
|
||||
|
||||
const renderSidebar = (
|
||||
overrides: Partial<InstanceType<typeof AppSidebar>["$props"]> = {},
|
||||
) => {
|
||||
return render(AppSidebar, {
|
||||
props: {
|
||||
categories: {},
|
||||
activeCategory: "all",
|
||||
categoryCounts: { all: 0 },
|
||||
themeMode: "auto",
|
||||
storeFilter: "both",
|
||||
sparkAvailable: true,
|
||||
apmAvailable: true,
|
||||
...overrides,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe("AppSidebar", () => {
|
||||
it("shows management and update entries when at least one source is usable", () => {
|
||||
renderSidebar({ sparkAvailable: true, apmAvailable: false });
|
||||
|
||||
expect(screen.getByText("应用管理")).toBeTruthy();
|
||||
expect(screen.getByText("软件更新")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("hides management and update entries when both sources are unavailable", () => {
|
||||
renderSidebar({ sparkAvailable: false, apmAvailable: false });
|
||||
|
||||
expect(screen.queryByText("应用管理")).toBeNull();
|
||||
expect(screen.queryByText("软件更新")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -35,6 +35,7 @@ describe("InstalledAppsModal", () => {
|
||||
error: "",
|
||||
activeOrigin: "spark",
|
||||
storeFilter: "both",
|
||||
sparkAvailable: true,
|
||||
apmAvailable: true,
|
||||
},
|
||||
});
|
||||
@@ -54,6 +55,7 @@ describe("InstalledAppsModal", () => {
|
||||
error: "",
|
||||
activeOrigin: "spark",
|
||||
storeFilter: "both",
|
||||
sparkAvailable: true,
|
||||
apmAvailable: true,
|
||||
},
|
||||
});
|
||||
@@ -71,6 +73,7 @@ describe("InstalledAppsModal", () => {
|
||||
error: "",
|
||||
activeOrigin: "spark",
|
||||
storeFilter: "both",
|
||||
sparkAvailable: true,
|
||||
apmAvailable: true,
|
||||
},
|
||||
});
|
||||
@@ -92,6 +95,7 @@ describe("InstalledAppsModal", () => {
|
||||
error: "",
|
||||
activeOrigin: "spark",
|
||||
storeFilter: "both",
|
||||
sparkAvailable: true,
|
||||
apmAvailable: true,
|
||||
},
|
||||
});
|
||||
@@ -113,6 +117,7 @@ describe("InstalledAppsModal", () => {
|
||||
error: "",
|
||||
activeOrigin: "spark",
|
||||
storeFilter: "both",
|
||||
sparkAvailable: true,
|
||||
apmAvailable: true,
|
||||
},
|
||||
});
|
||||
@@ -129,6 +134,7 @@ describe("InstalledAppsModal", () => {
|
||||
error: "",
|
||||
activeOrigin: "spark",
|
||||
storeFilter: "both",
|
||||
sparkAvailable: true,
|
||||
apmAvailable: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
getEffectiveStoreFilter,
|
||||
getAllowedInstalledOrigin,
|
||||
getDefaultInstalledOrigin,
|
||||
isOriginEnabled,
|
||||
isOriginUsable,
|
||||
} from "@/modules/storeFilter";
|
||||
|
||||
describe("storeFilter helpers", () => {
|
||||
it("reports whether an origin is enabled by the current store filter", () => {
|
||||
expect(isOriginEnabled("both", "spark")).toBe(true);
|
||||
expect(isOriginEnabled("both", "apm")).toBe(true);
|
||||
expect(isOriginEnabled("spark", "spark")).toBe(true);
|
||||
expect(isOriginEnabled("spark", "apm")).toBe(false);
|
||||
expect(isOriginEnabled("apm", "apm")).toBe(true);
|
||||
expect(isOriginEnabled("apm", "spark")).toBe(false);
|
||||
});
|
||||
|
||||
it("chooses the default installed origin from the active store filter", () => {
|
||||
expect(getDefaultInstalledOrigin("spark", { spark: true, apm: true })).toBe(
|
||||
"spark",
|
||||
);
|
||||
expect(getDefaultInstalledOrigin("apm", { spark: true, apm: true })).toBe(
|
||||
"apm",
|
||||
);
|
||||
expect(getDefaultInstalledOrigin("both", { spark: true, apm: true })).toBe(
|
||||
"apm",
|
||||
);
|
||||
expect(getDefaultInstalledOrigin("both", { spark: true, apm: false })).toBe(
|
||||
"spark",
|
||||
);
|
||||
expect(
|
||||
getDefaultInstalledOrigin("both", { spark: false, apm: false }),
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it("redirects disallowed installed origins to an allowed one", () => {
|
||||
expect(
|
||||
getAllowedInstalledOrigin("spark", "apm", { spark: true, apm: true }),
|
||||
).toBe("spark");
|
||||
expect(
|
||||
getAllowedInstalledOrigin("apm", "spark", { spark: true, apm: true }),
|
||||
).toBe("apm");
|
||||
expect(
|
||||
getAllowedInstalledOrigin("both", "apm", { spark: true, apm: false }),
|
||||
).toBe("spark");
|
||||
expect(
|
||||
getAllowedInstalledOrigin("both", "spark", { spark: false, apm: false }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("computes the effective runtime store filter from source availability", () => {
|
||||
expect(getEffectiveStoreFilter("both", { spark: true, apm: true })).toBe(
|
||||
"both",
|
||||
);
|
||||
expect(getEffectiveStoreFilter("both", { spark: true, apm: false })).toBe(
|
||||
"spark",
|
||||
);
|
||||
expect(getEffectiveStoreFilter("both", { spark: false, apm: true })).toBe(
|
||||
"apm",
|
||||
);
|
||||
expect(getEffectiveStoreFilter("both", { spark: false, apm: false })).toBe(
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
it("only treats enabled and installed origins as usable", () => {
|
||||
expect(isOriginUsable("both", "spark", { spark: true, apm: false })).toBe(
|
||||
true,
|
||||
);
|
||||
expect(isOriginUsable("both", "apm", { spark: true, apm: false })).toBe(
|
||||
false,
|
||||
);
|
||||
expect(isOriginUsable("spark", "apm", { spark: true, apm: true })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,9 @@ const APTSS_WEATHER_PRINT_URIS_KEY =
|
||||
const APTSS_NOTES_PRINT_URIS_KEY =
|
||||
"bash -lc /usr/bin/apt download spark-notes --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/aptss.list -o Dir::Etc::sourceparts=/dev/null";
|
||||
|
||||
const WHICH_APTSS_KEY = "which aptss";
|
||||
const WHICH_APM_KEY = "which apm";
|
||||
|
||||
const loadUpdateCenterModule = async (
|
||||
remoteStore: Record<string, RemoteStoreResponse>,
|
||||
) => {
|
||||
@@ -106,6 +109,22 @@ afterEach(() => {
|
||||
describe("update-center load items", () => {
|
||||
it("enriches apm migration items with download metadata and remote fallback icons", async () => {
|
||||
const commandResults = new Map<string, CommandResult>([
|
||||
[
|
||||
WHICH_APTSS_KEY,
|
||||
{
|
||||
code: 0,
|
||||
stdout: "/usr/bin/aptss\n",
|
||||
stderr: "",
|
||||
},
|
||||
],
|
||||
[
|
||||
WHICH_APM_KEY,
|
||||
{
|
||||
code: 0,
|
||||
stdout: "/usr/bin/apm\n",
|
||||
stderr: "",
|
||||
},
|
||||
],
|
||||
[
|
||||
APTSS_LIST_UPGRADABLE_KEY,
|
||||
{
|
||||
@@ -217,6 +236,14 @@ describe("update-center load items", () => {
|
||||
const result = await loadUpdateCenterItems(async (command, args) => {
|
||||
const key = `${command} ${args.join(" ")}`;
|
||||
|
||||
if (key === WHICH_APTSS_KEY) {
|
||||
return { code: 0, stdout: "/usr/bin/aptss\n", stderr: "" };
|
||||
}
|
||||
|
||||
if (key === WHICH_APM_KEY) {
|
||||
return { code: 127, stdout: "", stderr: "apm: command not found" };
|
||||
}
|
||||
|
||||
if (key === APTSS_LIST_UPGRADABLE_KEY) {
|
||||
return {
|
||||
code: 0,
|
||||
@@ -279,10 +306,7 @@ describe("update-center load items", () => {
|
||||
sha512: "beadfeed",
|
||||
},
|
||||
]);
|
||||
expect(result.warnings).toEqual([
|
||||
"apm upgradable query failed: apm: command not found",
|
||||
"apm installed query failed: apm: command not found",
|
||||
]);
|
||||
expect(result.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it("retries category lookup after an earlier fetch failure in the same process", async () => {
|
||||
@@ -292,6 +316,14 @@ describe("update-center load items", () => {
|
||||
const runCommand = async (command: string, args: string[]) => {
|
||||
const key = `${command} ${args.join(" ")}`;
|
||||
|
||||
if (key === WHICH_APTSS_KEY) {
|
||||
return { code: 0, stdout: "/usr/bin/aptss\n", stderr: "" };
|
||||
}
|
||||
|
||||
if (key === WHICH_APM_KEY) {
|
||||
return { code: 127, stdout: "", stderr: "apm: command not found" };
|
||||
}
|
||||
|
||||
if (key === APTSS_LIST_UPGRADABLE_KEY) {
|
||||
return {
|
||||
code: 0,
|
||||
@@ -387,6 +419,14 @@ describe("update-center load items", () => {
|
||||
const result = await loadUpdateCenterItems(async (command, args) => {
|
||||
const key = `${command} ${args.join(" ")}`;
|
||||
|
||||
if (key === WHICH_APTSS_KEY) {
|
||||
return { code: 0, stdout: "/usr/bin/aptss\n", stderr: "" };
|
||||
}
|
||||
|
||||
if (key === WHICH_APM_KEY) {
|
||||
return { code: 127, stdout: "", stderr: "apm: command not found" };
|
||||
}
|
||||
|
||||
if (key === APTSS_LIST_UPGRADABLE_KEY) {
|
||||
return {
|
||||
code: 0,
|
||||
@@ -440,9 +480,122 @@ describe("update-center load items", () => {
|
||||
sha512: "beadfeed",
|
||||
},
|
||||
]);
|
||||
expect(result.warnings).toEqual([
|
||||
"apm upgradable query failed: apm: command not found",
|
||||
"apm installed query failed: apm: command not found",
|
||||
expect(result.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it("skips aptss commands when the store filter disables Spark", async () => {
|
||||
const { loadUpdateCenterItems } = await loadUpdateCenterModule({
|
||||
"https://erotica.spark-app.store/amd64-apm/categories.json": {
|
||||
tools: { zh: "Tools" },
|
||||
},
|
||||
"https://erotica.spark-app.store/amd64-apm/tools/applist.json": [
|
||||
{ Name: "Spark Clock", Pkgname: "spark-clock" },
|
||||
],
|
||||
});
|
||||
|
||||
const runCommand = vi.fn(async (command: string, args: string[]) => {
|
||||
const key = `${command} ${args.join(" ")}`;
|
||||
|
||||
if (key === WHICH_APM_KEY) {
|
||||
return { code: 0, stdout: "/usr/bin/apm\n", stderr: "" };
|
||||
}
|
||||
|
||||
if (key === "apm list --upgradable") {
|
||||
return {
|
||||
code: 0,
|
||||
stdout: "spark-clock/main 2.0.0 amd64 [upgradable from: 1.0.0]",
|
||||
stderr: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (key === "apm list --installed") {
|
||||
return {
|
||||
code: 0,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
key ===
|
||||
"bash -lc amber-pm-debug /usr/bin/apt -c /opt/durapps/spark-store/bin/apt-fast-conf/aptss-apt.conf download spark-clock --print-uris"
|
||||
) {
|
||||
return {
|
||||
code: 0,
|
||||
stdout:
|
||||
"'https://example.invalid/spark-clock_2.0.0_amd64.deb' spark-clock_2.0.0_amd64.deb 1234 SHA512:feedface",
|
||||
stderr: "",
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected command ${key}`);
|
||||
});
|
||||
|
||||
await loadUpdateCenterItems(runCommand, "apm");
|
||||
|
||||
expect(runCommand).not.toHaveBeenCalledWith(
|
||||
"bash",
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining("apt list --upgradable"),
|
||||
]),
|
||||
);
|
||||
expect(runCommand).not.toHaveBeenCalledWith(
|
||||
"dpkg-query",
|
||||
expect.any(Array),
|
||||
);
|
||||
});
|
||||
|
||||
it("skips apm commands when the store filter disables APM", async () => {
|
||||
const { loadUpdateCenterItems } = await loadUpdateCenterModule({
|
||||
"https://erotica.spark-app.store/amd64-store/categories.json": {
|
||||
office: { zh: "Office" },
|
||||
},
|
||||
"https://erotica.spark-app.store/amd64-store/office/applist.json": [
|
||||
{ Name: "Spark Notes", Pkgname: "spark-notes" },
|
||||
],
|
||||
});
|
||||
|
||||
const runCommand = vi.fn(async (command: string, args: string[]) => {
|
||||
const key = `${command} ${args.join(" ")}`;
|
||||
|
||||
if (key === WHICH_APTSS_KEY) {
|
||||
return { code: 0, stdout: "/usr/bin/aptss\n", stderr: "" };
|
||||
}
|
||||
|
||||
if (key === APTSS_LIST_UPGRADABLE_KEY) {
|
||||
return {
|
||||
code: 0,
|
||||
stdout: "spark-notes/stable 2.0.0 amd64 [upgradable from: 1.0.0]",
|
||||
stderr: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (key === DPKG_QUERY_INSTALLED_KEY) {
|
||||
return {
|
||||
code: 0,
|
||||
stdout: "spark-notes\tinstall ok installed\n",
|
||||
stderr: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (key === APTSS_NOTES_PRINT_URIS_KEY) {
|
||||
return {
|
||||
code: 0,
|
||||
stdout:
|
||||
"'https://example.invalid/spark-notes_2.0.0_amd64.deb' spark-notes_2.0.0_amd64.deb 654321 SHA512:beadfeed",
|
||||
stderr: "",
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected command ${key}`);
|
||||
});
|
||||
|
||||
await loadUpdateCenterItems(runCommand, "spark");
|
||||
|
||||
expect(runCommand).not.toHaveBeenCalledWith("apm", [
|
||||
"list",
|
||||
"--upgradable",
|
||||
]);
|
||||
expect(runCommand).not.toHaveBeenCalledWith("apm", ["list", "--installed"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -157,8 +157,8 @@ describe("update-center/ipc", () => {
|
||||
await cancelHandler?.({}, "aptss:spark-weather");
|
||||
|
||||
expect(getStateHandler?.()).toEqual(snapshot);
|
||||
expect(service.open).toHaveBeenCalledTimes(1);
|
||||
expect(service.refresh).toHaveBeenCalledTimes(1);
|
||||
expect(service.open).toHaveBeenCalledWith("both");
|
||||
expect(service.refresh).toHaveBeenCalledWith("both");
|
||||
expect(service.ignore).toHaveBeenCalledWith({
|
||||
packageName: "spark-weather",
|
||||
newVersion: "2.0.0",
|
||||
@@ -176,6 +176,51 @@ describe("update-center/ipc", () => {
|
||||
expect(send).toHaveBeenCalledWith("update-center-state", snapshot);
|
||||
});
|
||||
|
||||
it("forwards store filter payloads to open and refresh", async () => {
|
||||
const handle = vi.fn();
|
||||
const snapshot: UpdateCenterServiceState = {
|
||||
items: [],
|
||||
tasks: [],
|
||||
warnings: [],
|
||||
hasRunningTasks: false,
|
||||
};
|
||||
const service = {
|
||||
open: vi.fn().mockResolvedValue(snapshot),
|
||||
refresh: vi.fn().mockResolvedValue(snapshot),
|
||||
ignore: vi.fn().mockResolvedValue(undefined),
|
||||
unignore: vi.fn().mockResolvedValue(undefined),
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
cancel: vi.fn().mockResolvedValue(undefined),
|
||||
getState: vi.fn().mockReturnValue(snapshot),
|
||||
subscribe: vi.fn(() => () => undefined),
|
||||
};
|
||||
|
||||
registerUpdateCenterIpc({ handle }, service);
|
||||
|
||||
const openHandler = handle.mock.calls.find(
|
||||
([channel]: [string]) => channel === "update-center-open",
|
||||
)?.[1] as
|
||||
| ((
|
||||
event: unknown,
|
||||
storeFilter?: "spark" | "apm" | "both",
|
||||
) => Promise<UpdateCenterServiceState>)
|
||||
| undefined;
|
||||
const refreshHandler = handle.mock.calls.find(
|
||||
([channel]: [string]) => channel === "update-center-refresh",
|
||||
)?.[1] as
|
||||
| ((
|
||||
event: unknown,
|
||||
storeFilter?: "spark" | "apm" | "both",
|
||||
) => Promise<UpdateCenterServiceState>)
|
||||
| undefined;
|
||||
|
||||
await openHandler?.({}, "apm");
|
||||
await refreshHandler?.({}, "spark");
|
||||
|
||||
expect(service.open).toHaveBeenCalledWith("apm");
|
||||
expect(service.refresh).toHaveBeenCalledWith("spark");
|
||||
});
|
||||
|
||||
it("service subscribers receive state updates after refresh start and ignore", async () => {
|
||||
let ignoredEntries = new Set<string>();
|
||||
const send = vi.fn();
|
||||
|
||||
@@ -61,9 +61,9 @@ describe("updateCenter store", () => {
|
||||
open.mockResolvedValue(snapshot);
|
||||
const store = createUpdateCenterStore();
|
||||
|
||||
await store.open();
|
||||
await store.open("apm");
|
||||
|
||||
expect(open).toHaveBeenCalledTimes(1);
|
||||
expect(open).toHaveBeenCalledWith("apm");
|
||||
expect(store.isOpen.value).toBe(true);
|
||||
expect(store.snapshot.value).toEqual(snapshot);
|
||||
expect(store.filteredItems.value).toEqual(snapshot.items);
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
|
||||
<div class="border-t border-slate-200 pt-4 dark:border-slate-800">
|
||||
<button
|
||||
v-if="storeFilter !== 'spark'"
|
||||
v-if="canManageApps"
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 rounded-2xl border border-transparent px-4 py-3 text-left text-sm font-medium text-slate-600 transition hover:border-brand/30 hover:bg-brand/5 hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||
@click="$emit('list')"
|
||||
@@ -98,6 +98,7 @@
|
||||
<span>应用管理</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canOpenUpdateCenter"
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 rounded-2xl border border-transparent px-4 py-3 text-left text-sm font-medium text-slate-600 transition hover:border-brand/30 hover:bg-brand/5 hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||
@click="$emit('update')"
|
||||
@@ -110,15 +111,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import ThemeToggle from "./ThemeToggle.vue";
|
||||
import amberLogo from "../assets/imgs/spark-store.svg";
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
categories: Record<string, any>;
|
||||
activeCategory: string;
|
||||
categoryCounts: Record<string, number>;
|
||||
themeMode: "light" | "dark" | "auto";
|
||||
sparkAvailable: boolean;
|
||||
apmAvailable: boolean;
|
||||
storeFilter: "spark" | "apm" | "both";
|
||||
}>();
|
||||
@@ -135,6 +138,15 @@ const toggleTheme = () => {
|
||||
emit("toggle-theme");
|
||||
};
|
||||
|
||||
const canManageApps = computed(() => {
|
||||
return (
|
||||
(props.storeFilter !== "apm" && props.sparkAvailable) ||
|
||||
(props.storeFilter !== "spark" && props.apmAvailable)
|
||||
);
|
||||
});
|
||||
|
||||
const canOpenUpdateCenter = canManageApps;
|
||||
|
||||
const selectCategory = (category: string) => {
|
||||
emit("select-category", category);
|
||||
};
|
||||
|
||||
@@ -29,10 +29,11 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
v-if="storeFilter === 'both'"
|
||||
v-if="showOriginSwitcher"
|
||||
class="flex items-center rounded-2xl border border-slate-200/70 p-1 dark:border-slate-800/70"
|
||||
>
|
||||
<button
|
||||
v-if="apmEnabled"
|
||||
type="button"
|
||||
class="rounded-xl px-4 py-1.5 text-sm font-semibold transition"
|
||||
:class="
|
||||
@@ -46,6 +47,7 @@
|
||||
APM 软件
|
||||
</button>
|
||||
<button
|
||||
v-if="sparkEnabled"
|
||||
type="button"
|
||||
class="rounded-xl px-4 py-1.5 text-sm font-semibold transition"
|
||||
:class="
|
||||
@@ -185,7 +187,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from "vue";
|
||||
import { computed, reactive } from "vue";
|
||||
import { App } from "../global/typedefinition";
|
||||
import { APM_STORE_BASE_URL } from "../global/storeConfig";
|
||||
|
||||
@@ -209,13 +211,14 @@ const canOpenDetail = (app: App) => {
|
||||
);
|
||||
};
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
show: boolean;
|
||||
apps: App[];
|
||||
loading: boolean;
|
||||
error: string;
|
||||
activeOrigin: "apm" | "spark";
|
||||
storeFilter: "spark" | "apm" | "both";
|
||||
sparkAvailable: boolean;
|
||||
apmAvailable: boolean;
|
||||
}>();
|
||||
|
||||
@@ -233,4 +236,16 @@ const onOverlayWheel = (e: WheelEvent) => {
|
||||
if (target.closest(".overflow-y-auto, .overflow-auto")) return;
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const sparkEnabled = computed(() => {
|
||||
return props.storeFilter !== "apm" && props.sparkAvailable;
|
||||
});
|
||||
|
||||
const apmEnabled = computed(() => {
|
||||
return props.storeFilter !== "spark" && props.apmAvailable;
|
||||
});
|
||||
|
||||
const showOriginSwitcher = computed(() => {
|
||||
return sparkEnabled.value && apmEnabled.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -26,6 +26,8 @@ export type DownloadItemStatus =
|
||||
|
||||
export type StoreMode = "spark" | "apm" | "hybrid";
|
||||
|
||||
export type StoreFilter = "spark" | "apm" | "both";
|
||||
|
||||
export interface DownloadItem {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -178,8 +180,8 @@ export interface UpdateCenterSnapshot {
|
||||
}
|
||||
|
||||
export interface UpdateCenterBridge {
|
||||
open: () => Promise<UpdateCenterSnapshot>;
|
||||
refresh: () => Promise<UpdateCenterSnapshot>;
|
||||
open: (storeFilter?: StoreFilter) => Promise<UpdateCenterSnapshot>;
|
||||
refresh: (storeFilter?: StoreFilter) => Promise<UpdateCenterSnapshot>;
|
||||
ignore: (payload: {
|
||||
packageName: string;
|
||||
newVersion: string;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { StoreFilter } from "@/global/typedefinition";
|
||||
|
||||
export interface SourceAvailability {
|
||||
spark: boolean;
|
||||
apm: boolean;
|
||||
}
|
||||
|
||||
export const isOriginEnabled = (
|
||||
storeFilter: StoreFilter,
|
||||
origin: "spark" | "apm",
|
||||
): boolean => {
|
||||
return storeFilter === "both" || storeFilter === origin;
|
||||
};
|
||||
|
||||
export const getDefaultInstalledOrigin = (
|
||||
storeFilter: StoreFilter,
|
||||
availability: SourceAvailability,
|
||||
): "spark" | "apm" | null => {
|
||||
if (storeFilter === "spark") {
|
||||
return availability.spark ? "spark" : null;
|
||||
}
|
||||
|
||||
if (storeFilter === "apm") {
|
||||
return availability.apm ? "apm" : null;
|
||||
}
|
||||
|
||||
if (availability.apm) {
|
||||
return "apm";
|
||||
}
|
||||
|
||||
if (availability.spark) {
|
||||
return "spark";
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getEffectiveStoreFilter = (
|
||||
storeFilter: StoreFilter,
|
||||
availability: SourceAvailability,
|
||||
): StoreFilter | null => {
|
||||
if (storeFilter === "spark") {
|
||||
return availability.spark ? "spark" : null;
|
||||
}
|
||||
|
||||
if (storeFilter === "apm") {
|
||||
return availability.apm ? "apm" : null;
|
||||
}
|
||||
|
||||
if (availability.spark && availability.apm) {
|
||||
return "both";
|
||||
}
|
||||
|
||||
if (availability.spark) {
|
||||
return "spark";
|
||||
}
|
||||
|
||||
if (availability.apm) {
|
||||
return "apm";
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const isOriginUsable = (
|
||||
storeFilter: StoreFilter,
|
||||
origin: "spark" | "apm",
|
||||
availability: SourceAvailability,
|
||||
): boolean => {
|
||||
return isOriginEnabled(storeFilter, origin) && availability[origin];
|
||||
};
|
||||
|
||||
export const getAllowedInstalledOrigin = (
|
||||
storeFilter: StoreFilter,
|
||||
requestedOrigin: "spark" | "apm",
|
||||
availability: SourceAvailability,
|
||||
): "spark" | "apm" | null => {
|
||||
if (isOriginUsable(storeFilter, requestedOrigin, availability)) {
|
||||
return requestedOrigin;
|
||||
}
|
||||
|
||||
return getDefaultInstalledOrigin(storeFilter, availability);
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
UpdateCenterSnapshot,
|
||||
DownloadItem,
|
||||
UpdateCenterStartTask,
|
||||
StoreFilter,
|
||||
} from "@/global/typedefinition";
|
||||
import { downloads, getNextUpdateDownloadId } from "@/global/downloadStatus";
|
||||
import { APM_STORE_BASE_URL } from "@/global/storeConfig";
|
||||
@@ -28,8 +29,8 @@ export interface UpdateCenterStore {
|
||||
someSelected: ComputedRef<boolean>;
|
||||
bind: () => void;
|
||||
unbind: () => void;
|
||||
open: () => Promise<void>;
|
||||
refresh: () => Promise<void>;
|
||||
open: (storeFilter?: StoreFilter) => Promise<void>;
|
||||
refresh: (storeFilter?: StoreFilter) => Promise<void>;
|
||||
ignoreItem: (packageName: string, newVersion: string) => Promise<void>;
|
||||
unignoreItem: (packageName: string, newVersion: string) => Promise<void>;
|
||||
toggleSelection: (taskKey: string) => void;
|
||||
@@ -129,15 +130,15 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
||||
isBound = false;
|
||||
};
|
||||
|
||||
const open = async (): Promise<void> => {
|
||||
const open = async (storeFilter: StoreFilter = "both"): Promise<void> => {
|
||||
resetSessionState();
|
||||
const nextSnapshot = await window.updateCenter.open();
|
||||
const nextSnapshot = await window.updateCenter.open(storeFilter);
|
||||
applySnapshot(nextSnapshot);
|
||||
isOpen.value = true;
|
||||
};
|
||||
|
||||
const refresh = async (): Promise<void> => {
|
||||
const nextSnapshot = await window.updateCenter.refresh();
|
||||
const refresh = async (storeFilter: StoreFilter = "both"): Promise<void> => {
|
||||
const nextSnapshot = await window.updateCenter.refresh(storeFilter);
|
||||
applySnapshot(nextSnapshot);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user